Đây có phải là mùi mã để lưu trữ các đối tượng chung trong một thùng chứa và sau đó lấy đối tượng và hạ thấp các đối tượng từ container không?


34

Ví dụ: tôi có một trò chơi, có một số công cụ để tăng khả năng của Người chơi:

Tool.h

class Tool{
public:
    std::string name;
};

Và một số công cụ:

Kiếm.h

class Sword : public Tool{
public:
    Sword(){
        this->name="Sword";
    }
    int attack;
};

Khiên.h

class Shield : public Tool{
public:
    Shield(){
        this->name="Shield";
    }
    int defense;
};

MagicCloth.h

class MagicCloth : public Tool{
public:
    MagicCloth(){
        this->name="MagicCloth";
    }
    int attack;
    int defense;
};

Và sau đó một người chơi có thể giữ một số công cụ để tấn công:

class Player{
public:
    int attack;
    int defense;
    vector<Tool*> tools;
    void attack(){
        //original attack and defense
        int currentAttack=this->attack;
        int currentDefense=this->defense;
        //calculate attack and defense affected by tools
        for(Tool* tool : tools){
            if(tool->name=="Sword"){
                Sword* sword=(Sword*)tool;
                currentAttack+=sword->attack;
            }else if(tool->name=="Shield"){
                Shield* shield=(Shield*)tool;
                currentDefense+=shield->defense;
            }else if(tool->name=="MagicCloth"){
                MagicCloth* magicCloth=(MagicCloth*)tool;
                currentAttack+=magicCloth->attack;
                currentDefense+=magicCloth->shield;
            }
        }
        //some other functions to start attack
    }
};

Tôi nghĩ rằng rất khó để thay thế if-elsebằng các phương thức ảo trong các công cụ, bởi vì mỗi công cụ có các thuộc tính khác nhau và mỗi công cụ ảnh hưởng đến tấn công và phòng thủ của người chơi, trong đó việc cập nhật tấn công và phòng thủ của người chơi cần được thực hiện bên trong đối tượng Người chơi.

Nhưng tôi không hài lòng với thiết kế này, vì nó chứa sự thất vọng, với một if-elsetuyên bố dài . Thiết kế này có cần phải được "sửa chữa" không? Nếu vậy, tôi có thể làm gì để sửa nó?


4
Một kỹ thuật OOP tiêu chuẩn để loại bỏ các bài kiểm tra cho một lớp con cụ thể (và các lần phát tiếp theo) là tạo một hoặc trong trường hợp này có thể hai, phương thức ảo trong lớp cơ sở để sử dụng thay cho chuỗi if và phôi. Điều này có thể được sử dụng loại bỏ hoàn toàn if và ủy thác thao tác cho các lớp con để thực hiện. Bạn cũng sẽ không phải chỉnh sửa các câu lệnh if mỗi khi bạn thêm một lớp con mới.
Erik Eidt

2
Cũng xem xét công văn đôi.
Boris the Spider

Tại sao không thêm một thuộc tính vào lớp Công cụ của bạn chứa một từ điển các loại thuộc tính (nghĩa là tấn công, phòng thủ) và một giá trị được gán cho nó. Các cuộc tấn công, phòng thủ có thể được liệt kê các giá trị. Sau đó, bạn chỉ có thể gọi giá trị từ chính Công cụ bằng hằng số liệt kê.
dùng1740075

8
Tôi sẽ chỉ để lại ở đây: ericlippert.com/2015/04/27/wizards-and-warriors-part-one
Bạn

1
Cũng xem mẫu Khách truy cập.
JDługosz

Câu trả lời:


63

Vâng, nó là một mùi mã (trong rất nhiều trường hợp).

Tôi nghĩ rằng rất khó để thay thế if-other bằng các phương thức ảo trong các công cụ

Trong ví dụ của bạn, khá đơn giản để thay thế if / other bằng các phương thức ảo:

class Tool{
 public:
   virtual int GetAttack() const=0;
   virtual int GetDefense() const=0;
};

class Sword : public Tool{
    // ...
 public:
   virtual int GetAttack() const {return attack;}
   virtual int GetDefense() const{return 0;}
};

Bây giờ không cần thêm cho ifkhối của bạn , người gọi chỉ có thể sử dụng nó như

       currentAttack+=tool->GetAttack();
       currentDefense+=tool->GetDefense();

Tất nhiên, đối với các tình huống phức tạp hơn, một giải pháp như vậy không phải lúc nào cũng rõ ràng (nhưng gần như bất cứ lúc nào cũng có thể). Nhưng nếu bạn gặp tình huống không biết cách giải quyết vụ việc bằng các phương thức ảo, bạn có thể hỏi lại một câu hỏi mới ở đây về "Lập trình viên" (hoặc, nếu nó trở thành ngôn ngữ hoặc cách triển khai cụ thể, trên Stackoverflow).


4
hoặc, đối với vấn đề đó, trên gamedev.stackexchange.com
Kromster nói hỗ trợ Monica

7
Bạn thậm chí sẽ không cần khái niệm Swordtheo cách này trong cơ sở mã của bạn. Bạn chỉ có thể new Tool("sword", swordAttack, swordDefense)từ ví dụ một tệp JSON.
AmazingDreams

7
@AmazedDreams: đó là chính xác (đối với các phần của mã chúng ta thấy ở đây), nhưng tôi đoán OP đã đơn giản hóa mã thực sự của mình cho câu hỏi của mình để tập trung vào khía cạnh mà anh ấy muốn thảo luận.
Doc Brown

3
Điều này không tốt hơn nhiều so với mã gốc (tốt, nó là một chút). Bất kỳ công cụ nào có thuộc tính bổ sung không thể được tạo mà không thêm phương thức bổ sung. Tôi nghĩ trong trường hợp này người ta nên ưu tiên sáng tác hơn thừa kế. Vâng, hiện tại chỉ có tấn công và phòng thủ, nhưng điều đó không cần phải theo cách đó.
Polygnome

1
@DocBrown Đúng vậy, mặc dù nó trông giống như một game nhập vai trong đó một nhân vật có một số chỉ số được sửa đổi bằng các công cụ hoặc các vật phẩm được trang bị. Tôi sẽ tạo một cái chung Toolvới tất cả các vector<Tool*>công cụ sửa đổi có thể, điền vào một số thứ được đọc từ một tệp dữ liệu, sau đó chỉ cần lặp lại chúng và sửa đổi các số liệu thống kê như bạn làm bây giờ. Bạn sẽ gặp rắc rối khi bạn muốn một vật phẩm mang lại, ví dụ như tiền thưởng 10% cho cuộc tấn công. Có lẽ a tool->modify(playerStats)là một lựa chọn khác.
AmazingDreams

23

Vấn đề chính với mã của bạn là, bất cứ khi nào bạn giới thiệu bất kỳ mục mới nào, bạn không chỉ phải viết và cập nhật mã của mục đó, bạn còn phải sửa đổi trình phát của mình (hoặc bất cứ nơi nào mục được sử dụng), điều này làm cho toàn bộ phức tạp hơn nhiều

Theo nguyên tắc chung, tôi nghĩ nó luôn hơi tanh, khi bạn không thể dựa vào phân lớp / thừa kế thông thường và phải tự làm ầm lên.

Tôi có thể nghĩ về hai cách tiếp cận có thể làm cho toàn bộ điều linh hoạt hơn:

  • Như những người khác đã đề cập, di chuyển attackdefensecác thành viên lớp cơ sở và chỉ cần khởi tạo họ 0. Điều này cũng có thể tăng gấp đôi khi kiểm tra xem bạn có thực sự có thể xoay vật phẩm để tấn công hay sử dụng nó để chặn các cuộc tấn công hay không.

  • Tạo một số loại hệ thống gọi lại / sự kiện. Có nhiều cách tiếp cận khác nhau cho việc này.

    Làm thế nào về việc giữ cho nó đơn giản?

    • Bạn có thể tạo một thành viên lớp cơ sở như virtual void onEquip(Owner*) {}virtual void onUnequip(Owner*) {}.
    • Quá tải của họ sẽ được gọi và sửa đổi các số liệu thống kê khi (un-) trang bị vật phẩm, ví dụ virtual void onEquip(Owner *o) { o->modifyStat("attack", attackValue); }virtual void onUnequip(Owner *o) { o->modifyStat("attack", -attackValue); }.
    • Các số liệu thống kê có thể được truy cập theo một cách năng động, ví dụ: sử dụng một chuỗi ngắn hoặc hằng số làm khóa, do đó, bạn thậm chí có thể giới thiệu các giá trị hoặc phần thưởng cụ thể cho thiết bị mới mà bạn không nhất thiết phải xử lý trong trình phát hoặc "chủ sở hữu" của mình.
    • So với việc chỉ yêu cầu các giá trị tấn công / phòng thủ đúng lúc, điều này không chỉ giúp mọi thứ trở nên năng động hơn mà còn giúp bạn tiết kiệm các cuộc gọi không cần thiết và thậm chí cho phép bạn tạo các vật phẩm sẽ ảnh hưởng đến nhân vật của bạn vĩnh viễn.

      Ví dụ, hãy tưởng tượng một chiếc nhẫn bị nguyền rủa sẽ chỉ đặt một số chỉ số ẩn sau khi được trang bị, đánh dấu nhân vật của bạn là bị nguyền rủa vĩnh viễn.


7

Mặc dù @DocBrown đã đưa ra một câu trả lời hay, nhưng nó không đi đủ xa, imho. Trước khi bạn bắt đầu đánh giá các câu trả lời, bạn nên đánh giá nhu cầu của bạn. Bạn thực sự cần gì?

Dưới đây tôi sẽ chỉ ra hai giải pháp khả thi, cung cấp những lợi thế khác nhau cho các nhu cầu khác nhau.

Đầu tiên là rất đơn giản và được thiết kế riêng cho những gì bạn đã thể hiện:

class Tool {
    public:
        std::string name;
        int attack;
        int defense;
}

public void attack() {
    int attack = this->attack;
    int defense = this->defense;
    for (Tool* tool : tools){
        attack += tool->attack;
        defense += tool->defense;
    }
}

Điều này cho phép tuần tự hóa / giải tuần tự hóa các công cụ rất dễ dàng (ví dụ như để lưu hoặc kết nối mạng) và hoàn toàn không cần gửi ảo. Nếu mã của bạn là tất cả những gì bạn đã thể hiện và bạn không hy vọng nó sẽ phát triển nhiều thứ khác thì sẽ có nhiều công cụ khác nhau với các tên khác nhau và các số liệu thống kê đó, chỉ với số lượng khác nhau, thì đây là cách để đi.

@DocBrown đã cung cấp một giải pháp vẫn dựa vào công văn ảo và đó có thể là một lợi thế nếu bạn bằng cách nào đó chuyên môn hóa các công cụ cho các phần của mã không được hiển thị. Tuy nhiên, nếu bạn thực sự cần hoặc muốn thay đổi hành vi khác, thì tôi sẽ đề xuất giải pháp sau:

Thành phần trên thừa kế

Điều gì nếu sau này bạn muốn một công cụ sửa đổi sự nhanh nhẹn ? Hay chạy tốc độ ? Đối với tôi, có vẻ như bạn đang làm một game nhập vai. Một điều quan trọng đối với các game nhập vai là được mở rộng . Các giải pháp hiển thị cho đến bây giờ không cung cấp điều đó. Bạn sẽ phải thay đổi Toollớp và thêm các phương thức ảo mới vào nó mỗi khi bạn cần một thuộc tính mới.

Giải pháp thứ hai tôi đang trình bày là giải pháp mà tôi đã gợi ý trước đó trong một nhận xét - nó sử dụng thành phần thay vì kế thừa và tuân theo nguyên tắc "đóng để sửa đổi, mở cho phần mở rộng *. Nếu bạn quen với cách các hệ thống thực thể hoạt động, một số điều sẽ trông quen thuộc (tôi thích nghĩ về sáng tác như người anh em nhỏ hơn của ES).

Lưu ý rằng những gì tôi đang trình bày dưới đây thanh lịch hơn đáng kể trong các ngôn ngữ có thông tin loại thời gian chạy, như Java hoặc C #. Do đó, mã C ++ mà tôi đang hiển thị phải bao gồm một số "sổ sách kế toán" đơn giản là cần thiết để làm cho tác phẩm hoạt động ngay tại đây. Có lẽ ai đó có nhiều kinh nghiệm về C ++ hơn có thể đề xuất một cách tiếp cận thậm chí tốt hơn.

Đầu tiên, chúng tôi nhìn lại phía người gọi . Trong ví dụ của bạn, bạn là người gọi bên trong attackphương thức không quan tâm đến các công cụ. Điều bạn quan tâm là hai thuộc tính - điểm tấn công và phòng thủ. Bạn không thực sự quan tâm những người đến từ đâu và bạn không quan tâm đến các tính chất khác (ví dụ như tốc độ chạy, sự nhanh nhẹn).

Vì vậy, trước tiên, chúng tôi giới thiệu một lớp học mới

class Component {
    public:
        // we need this, in Java we'd simply use getClass()
        virtual std::string type() const = 0;
};

Và sau đó, chúng tôi tạo ra hai thành phần đầu tiên của chúng tôi

class Attack : public Component {
    public:
        std::string type() const override { return std::string("mygame::components::Attack"); }
        int attackValue = 0;
};

class Defense : public Component {
    public:
      std::string type() const override { return std::string("mygame::components::Defense"); }
      int defenseValue = 0;
};

Sau đó, chúng tôi tạo một Công cụ giữ một tập các thuộc tính và làm cho các thuộc tính có thể truy vấn thuộc tính.

class Tool {
private:
    std::map<std::string, Component*> components;

public:
    /** Adds a component to the tool */
    void addComponent(Component* component) { 
        components[component->type()] = component;
    };
    /** Removes a component from the tool */
    void removeComponent(Component* component) { components.erase(component->type()); };
    /** Return the component with the given type */
    Component* getComponentByType(std::string type) { 
        std::map<std::string, Component*>::iterator it = components.find(type);
        if (it != components.end()) { return it->second; }
        return nullptr;
    };
    /** Check wether a tol has a given component */
    bool hasComponent(std::string type) {
        std::map<std::string, Component*>::iterator it = components.find(type);
        return it != components.end();
    }
};

Lưu ý rằng trong ví dụ này, tôi chỉ hỗ trợ có một thành phần của mỗi loại - điều này giúp mọi việc dễ dàng hơn. Về lý thuyết bạn cũng có thể cho phép nhiều thành phần cùng loại, nhưng điều đó trở nên xấu xí rất nhanh. Một khía cạnh quan trọng: Toolhiện đã bị đóng để sửa đổi - chúng tôi sẽ không bao giờ chạm vào nguồn của Toollần nữa - nhưng mở để mở rộng - chúng tôi có thể mở rộng hành vi của Công cụ bằng cách sửa đổi những thứ khác và chỉ bằng cách chuyển các Thành phần khác vào đó.

Bây giờ chúng ta cần một cách để lấy các công cụ theo các loại thành phần. Bạn vẫn có thể sử dụng một vectơ cho các công cụ, giống như trong ví dụ mã của bạn:

class Player {
    private:
        int attack = 0; 
        int defense = 0;
        int walkSpeed;
    public:
        std::vector<Tool*> tools;
        std::vector<Tool*> getToolsByComponentType(std::string type) {
            std::vector<Tool*> retVal;
            for (Tool* tool : tools) {
                if (tool->hasComponent(type)) { 
                    retVal.push_back(tool); 
                }
            }
            return retVal;
        }

        void doAttack() {
            int attackValue = this->attack;
            int defenseValue = this->defense;

            for (Tool* tool : this->getToolsByComponentType(std::string("mygame::components::Attack"))) {
                Attack* component = (Attack*) tool->getComponentByType(std::string("mygame::components::Attack"));
                attackValue += component->attackValue;
            }
            for (Tool* tool : this->getToolsByComponentType(std::string("mygame::components::Defense"))) {
                Defense* component = (Defense*)tool->getComponentByType(std::string("mygame::components::Defense"));
                defenseValue += component->defenseValue;
            }
            std::cout << "Attack with strength " << attackValue << "! Defend with strenght " << defenseValue << "!";
        }
};

Bạn cũng có thể cấu trúc lại lớp này thành Inventorylớp của riêng bạn và lưu trữ các bảng tra cứu giúp đơn giản hóa rất nhiều công cụ truy xuất theo loại thành phần và tránh lặp đi lặp lại toàn bộ bộ sưu tập.

Những lợi thế có cách tiếp cận này? Trong attack, bạn xử lý Công cụ có hai thành phần - bạn không quan tâm đến bất cứ điều gì khác.

Hãy tưởng tượng bạn có một walkTophương pháp, và bây giờ bạn quyết định rằng đó là một ý tưởng tốt nếu một số công cụ sẽ đạt được khả năng sửa đổi tốc độ đi bộ của bạn. Không vấn đề gì!

Đầu tiên, tạo cái mới Component:

class WalkSpeed : public Component {
public:
    std::string type() const override { return std::string("mygame::components::WalkSpeed"); }
    int speedBonus;
};

Sau đó, bạn chỉ cần thêm một thể hiện của thành phần này vào công cụ bạn muốn tăng tốc độ thức dậy và thay đổi WalkTophương thức để xử lý thành phần bạn vừa tạo:

void walkTo() {
    int walkSpeed = this->walkSpeed;

    for (Tool* tool : this->getToolsByComponentType(std::string("mygame::components:WalkSpeed"))) {
        WalkSpeed* component = (WalkSpeed*)tool->getComponentByType(std::string("mygame::components::Defense"));
        walkSpeed += component->speedBonus;
        std::cout << "Walk with " << walkSpeed << std::endl;
    }
}

Lưu ý rằng chúng tôi đã thêm một số hành vi vào Công cụ của mình mà không sửa đổi lớp Công cụ.

Bạn có thể (và nên) di chuyển các chuỗi thành một biến const vĩ mô hoặc tĩnh, vì vậy bạn không phải gõ nó nhiều lần.

Nếu bạn thực hiện phương pháp này hơn nữa - ví dụ: tạo các thành phần có thể được thêm vào người chơi và tạo một Combatthành phần gắn cờ người chơi có thể tham gia chiến đấu, thì bạn cũng có thể thoát khỏi attackphương thức đó và xử lý nó bởi Thành phần hoặc được xử lý ở nơi khác.

Lợi thế của việc làm cho người chơi cũng có thể có được Thành phần, đó là, sau đó, bạn thậm chí sẽ không cần thay đổi người chơi để cho anh ta hành vi khác. Trong ví dụ của tôi, bạn có thể tạo một Movablethành phần, theo cách đó bạn không cần phải thực hiện walkTophương thức trên trình phát để khiến anh ta di chuyển. Bạn chỉ cần tạo thành phần, gắn nó vào trình phát và để người khác xử lý nó.

Bạn có thể tìm thấy một ví dụ hoàn chỉnh trong ý chính này: https://gist.github.com/NetzwergX/3a29e1b106c6bb9c7308e89dd715ee20

Giải pháp này rõ ràng là phức tạp hơn một chút sau đó những giải pháp khác đã được đăng. Nhưng tùy thuộc vào mức độ linh hoạt mà bạn muốn trở thành, bạn muốn đi bao xa, đây có thể là một cách tiếp cận rất mạnh mẽ.

Chỉnh sửa

Một số câu trả lời khác đề xuất kế thừa trực tiếp (Tạo kiếm mở rộng Công cụ, tạo Công cụ mở rộng Shield). Tôi không nghĩ rằng đây là một kịch bản mà sự kế thừa hoạt động rất tốt. Điều gì sẽ xảy ra nếu bạn quyết định rằng chặn bằng khiên theo một cách nhất định cũng có thể gây sát thương cho kẻ tấn công? Với giải pháp của tôi, bạn chỉ cần thêm một thành phần Tấn công vào một tấm khiên và nhận ra rằng không có bất kỳ thay đổi nào đối với mã của bạn. Với thừa kế, bạn sẽ có một vấn đề. vật phẩm / Công cụ trong game nhập vai là ứng cử viên chính cho sáng tác hoặc thậm chí sử dụng hệ thống thực thể ngay từ đầu.


1

Nói chung, nếu bạn có nhu cầu sử dụng if(kết hợp với yêu cầu loại thể hiện) trong bất kỳ ngôn ngữ OOP nào, đó là một dấu hiệu, rằng một cái gì đó có mùi đang diễn ra. Ít nhất, bạn nên có một cái nhìn cận cảnh hơn về các mô hình của bạn.

Tôi sẽ mô hình tên miền của bạn khác nhau.

Cho usecase của bạn một Toolcó một AttackBonusvà một DefenseBonus- mà cả hai có thể là 0trong trường hợp nó là vô ích cho chiến đấu như lông hoặc một cái gì đó như thế.

Đối với một cuộc tấn công, bạn có baserate+ bonustừ vũ khí được sử dụng. Tương tự với phòng thủ baserate+ bonus.

Do đó, bạn Toolphải có một virtualphương pháp để tính toán các đòn tấn công / phòng thủ.

tl; dr

Với một thiết kế tốt hơn, bạn có thể tránh được hacky ifs.


Đôi khi một if là cần thiết, ví dụ khi so sánh các giá trị vô hướng. Đối với chuyển đổi loại đối tượng, không quá nhiều.
Andy

Haha, nếu là một toán tử khá thiết yếu và bạn không thể chỉ nói rằng sử dụng là mùi mã.
tymtam

1
@Tymski để một số tôn trọng bạn là đúng. Tôi làm cho mình rõ ràng hơn. Tôi không bảo vệ ifchương trình ít hơn. Chủ yếu là trong các kết hợp như nếu instanceofhoặc một cái gì đó như thế. Nhưng có một vị trí, trong đó tuyên bố ifs là một mật mã và có nhiều cách để khắc phục nó. Và bạn đã đúng, đó là một toán tử thiết yếu có quyền riêng của nó.
Thomas Junk

1

Như đã viết, nó "có mùi", nhưng đó có thể chỉ là những ví dụ bạn đưa ra. Lưu trữ dữ liệu trong các thùng chứa đối tượng chung, sau đó truyền dữ liệu để có quyền truy cập vào dữ liệu không phải là mã tự động. Bạn sẽ thấy nó được sử dụng trong nhiều tình huống. Tuy nhiên, khi bạn sử dụng nó, bạn nên biết những gì bạn đang làm, cách bạn đang làm và tại sao. Khi tôi nhìn vào ví dụ, việc sử dụng các phép so sánh dựa trên chuỗi để cho tôi biết đối tượng nào là thứ làm vấp phải máy đo mùi cá nhân của tôi. Điều đó cho thấy bạn không hoàn toàn chắc chắn những gì bạn đang làm ở đây (điều này tốt, vì bạn đã có sự khôn ngoan khi đến đây với các lập trình viên. Hãy nói và nói "này, tôi không nghĩ tôi thích những gì tôi đang làm, hãy giúp đỡ tôi ra ngoài! ").

Vấn đề cơ bản với mô hình truyền dữ liệu từ các thùng chứa chung như thế này là nhà sản xuất dữ liệu và người tiêu dùng dữ liệu phải làm việc cùng nhau, nhưng có thể không rõ ràng rằng thoạt nhìn họ làm như vậy. Trong mọi ví dụ về mẫu này, có mùi hay không có mùi, đây là vấn đề cơ bản. Nó là rất tốt cho các nhà phát triển bên cạnh hoàn toàn không biết rằng bạn đang làm mô hình này và phá vỡ nó một cách tình cờ, vì vậy nếu bạn sử dụng mô hình này, bạn phải chăm sóc để giúp các nhà phát triển ra tiếp theo. Bạn phải làm cho anh ta dễ dàng hơn để không phá vỡ mã vô ý do một số chi tiết anh ta có thể không biết tồn tại.

Ví dụ, nếu tôi muốn sao chép một người chơi thì sao? Nếu tôi chỉ nhìn vào nội dung của đối tượng người chơi, nó có vẻ khá dễ dàng. Tôi chỉ có sao chép attack, defensetoolscác biến. Dễ như ăn bánh! Chà, tôi sẽ nhanh chóng phát hiện ra rằng việc bạn sử dụng các con trỏ làm cho nó khó hơn một chút (tại một số điểm, đáng để xem các con trỏ thông minh, nhưng đó là một chủ đề khác). Điều đó dễ dàng được giải quyết. Tôi sẽ chỉ tạo các bản sao mới của mỗi công cụ và đưa chúng vào toolsdanh sách mới của tôi . Rốt cuộc, Toollà một lớp học thực sự đơn giản chỉ có một thành viên. Vì vậy, tôi tạo ra một loạt các bản sao, bao gồm một bản sao của nó Sword, nhưng tôi không biết đó là một thanh kiếm, vì vậy tôi chỉ sao chép nó name. Sau đó, attack()hàm nhìn vào tên, thấy rằng đó là một "thanh kiếm", sử dụng nó và những điều tồi tệ sẽ xảy ra!

Chúng ta có thể so sánh trường hợp này với trường hợp khác trong lập trình socket, sử dụng cùng một mẫu. Tôi có thể thiết lập chức năng ổ cắm UNIX như thế này:

int sockfd = socket(AF_INET, SOCK_STREAM, 0);
sockaddr_in serv_addr;
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(portno);
serv_addr.sin_addr.s_addr = INADDR_ANY;
bind(sockfd, (struct sockaddr *) &serv_addr, sizeof(serv_addr));

Tại sao đây là mô hình tương tự? Bởi vì bindkhông chấp nhận a sockaddr_in*, nó chấp nhận chung chung hơn sockaddr*. Nếu bạn nhìn vào các định nghĩa cho các lớp đó, chúng tôi thấy rằng sockaddrchỉ có một thành viên mà gia đình chúng tôi gán cho sin_family*. Gia đình nói rằng bạn nên chọn kiểu con sockaddrnào. AF_INETcho bạn biết rằng cấu trúc địa chỉ thực sự là a sockaddr_in. Nếu đúng như vậy AF_INET6, địa chỉ sẽ là một sockaddr_in6, có các trường lớn hơn để hỗ trợ các địa chỉ IPv6 lớn hơn.

Điều này giống hệt với Toolví dụ của bạn , ngoại trừ nó sử dụng một số nguyên để chỉ định họ nào hơn là a std::string. Tuy nhiên, tôi sẽ tuyên bố rằng nó không có mùi, và cố gắng làm như vậy vì những lý do khác ngoài "đó là một cách tiêu chuẩn để làm ổ cắm, vì vậy nó không nên" ngửi. "" Rõ ràng là cùng một kiểu, đó là tại sao tôi cho rằng việc lưu trữ dữ liệu trong các đối tượng chung và truyền dữ liệu không phải là mã tự động , nhưng có một số khác biệt trong cách họ làm điều đó giúp an toàn hơn.

Khi sử dụng mẫu này, thông tin quan trọng nhất là nắm bắt việc truyền tải thông tin về phân lớp từ nhà sản xuất đến người tiêu dùng. Đây là những gì bạn đang làm với nametrường và các socket UNIX làm với sin_familytrường của chúng . Lĩnh vực đó là thông tin người tiêu dùng cần để hiểu những gì nhà sản xuất đã thực sự tạo ra. Trong tất cả các trường hợp của mẫu này, nó phải là một phép liệt kê (hoặc ít nhất, một số nguyên hoạt động như một phép liệt kê). Tại sao? Hãy suy nghĩ về những gì người tiêu dùng của bạn sẽ làm với thông tin. Họ sẽ cần phải viết ra một số iftuyên bố lớn hoặc mộtswitchcâu lệnh, như bạn đã làm, nơi họ xác định kiểu con chính xác, bỏ nó và sử dụng dữ liệu. Theo định nghĩa, chỉ có thể có một số lượng nhỏ các loại này. Bạn có thể lưu trữ nó trong một chuỗi, như bạn đã làm, nhưng điều đó có nhiều nhược điểm:

  • Chậm - std::stringthường phải thực hiện một số bộ nhớ động để giữ chuỗi. Bạn cũng phải thực hiện một so sánh toàn văn bản để khớp với tên mỗi khi bạn muốn tìm ra lớp con nào bạn có.
  • Quá linh hoạt - Có điều gì đó để nói về việc đặt ra những ràng buộc cho chính bạn khi bạn đang làm điều gì đó cực kỳ nguy hiểm. Tôi đã có những hệ thống như thế này để tìm một chuỗi con để cho nó biết loại vật thể mà nó đang nhìn. Điều này hoạt động rất tốt cho đến khi tên của một đối tượng vô tình chứa chuỗi con đó và tạo ra một lỗi khó hiểu khủng khiếp. Vì, như chúng tôi đã trình bày ở trên, chúng tôi chỉ cần một số ít trường hợp, không có lý do gì để sử dụng một công cụ bị áp đảo ồ ạt như chuỗi. Điều này dẫn đến...
  • Dễ bị lỗi - Chúng ta chỉ cần nói rằng bạn sẽ muốn tiếp tục một vụ giết người điên cuồng cố gắng gỡ lỗi tại sao mọi thứ không hoạt động khi một người tiêu dùng vô tình đặt tên của một miếng vải ma thuật MagicC1oth. Nghiêm túc mà nói, những con bọ như thế có thể mất nhiều ngày để gãi đầu trước khi bạn nhận ra chuyện gì đã xảy ra.

Một bảng liệt kê hoạt động tốt hơn nhiều. Nó nhanh, rẻ và dễ bị lỗi hơn nhiều:

class Tool {
public:
    enum TypeE {
        kSword,
        kShield,
        kMagicCloth
    };
    TypeE type;

    std::string typeName() const {
        switch(type) {
            case kSword:      return "Sword";
            case kSheild:     return "Sheild";
            case kMagicCloth: return "Magic Cloth";

            default:
                throw std::runtime_error("Invalid enum!");
        }
   }
};

Ví dụ này cũng cho thấy một switchtuyên bố liên quan đến các enum, với phần quan trọng nhất của mẫu này: một defaulttrường hợp ném. Bạn không bao giờ nên gặp tình huống đó nếu bạn làm mọi thứ một cách hoàn hảo. Tuy nhiên, nếu ai đó thêm một loại công cụ mới và bạn quên cập nhật mã của mình để hỗ trợ nó, bạn sẽ muốn một cái gì đó để bắt lỗi. Trên thực tế, tôi khuyên họ rất nhiều vì vậy bạn nên thêm chúng ngay cả khi bạn không cần chúng.

Ưu điểm lớn khác của enumlà nó cung cấp cho các nhà phát triển tiếp theo một danh sách đầy đủ các loại công cụ có hiệu lực, phải lên phía trước. Không cần phải lội qua mã để tìm lớp Sáo chuyên dụng của Bob mà anh ta sử dụng trong trận đấu trùm hoành tráng của mình.

void damageWargear(Tool* tool)
{
    switch(tool->type)
    {
        case Tool::kSword:
            static_cast<Sword*>(tool)->damageSword();
            break;
        case Tool::kShield:
            static_cast<Sword*>(tool)->damageShield();
            break;
        default:
            break; // Ignore all other objects
    }
}

Có, tôi đã đưa ra một tuyên bố mặc định "trống rỗng", chỉ để làm cho nó rõ ràng cho nhà phát triển tiếp theo những gì tôi dự kiến ​​sẽ xảy ra nếu một số loại bất ngờ mới xuất hiện theo cách của tôi.

Nếu bạn làm điều này, mô hình sẽ ít mùi hơn. Tuy nhiên, để không có mùi, điều cuối cùng bạn cần làm là xem xét các lựa chọn khác. Những diễn viên này là một số công cụ mạnh mẽ và nguy hiểm hơn mà bạn có trong tiết mục C ++. Bạn không nên sử dụng chúng trừ khi bạn có lý do chính đáng.

Một thay thế rất phổ biến là cái mà tôi gọi là "union struct" hoặc "class union". Ví dụ của bạn, điều này thực sự sẽ rất phù hợp. Để tạo một trong số này, bạn tạo một Toollớp, với một bảng liệt kê như trước đây, nhưng thay vì phân lớpTool , chúng tôi chỉ đặt tất cả các trường từ mỗi kiểu con trên đó.

class Tool {
    public:
        enum TypeE {
            kSword,
            kShield,
            kMagicCloth
        };
    TypeE type;

    int   attack;
    int   defense;
};

Bây giờ bạn không cần lớp con nào cả. Bạn chỉ cần nhìn vàotype trường để xem trường nào khác thực sự hợp lệ. Điều này là an toàn hơn và dễ hiểu hơn. Tuy nhiên, nó có nhược điểm. Có những lúc bạn không muốn sử dụng cái này:

  • Khi các đối tượng quá khác nhau - Bạn có thể kết thúc với một danh sách các lĩnh vực giặt ủi và có thể không rõ những đối tượng nào áp dụng cho từng loại đối tượng.
  • Khi hoạt động trong tình huống quan trọng về bộ nhớ - Nếu bạn cần tạo 10 công cụ, bạn có thể lười biếng với bộ nhớ. Khi bạn cần tạo ra 500 triệu công cụ, bạn sẽ bắt đầu quan tâm đến bit và byte. Cấu trúc liên minh luôn lớn hơn mức cần thiết.

Giải pháp này không được sử dụng bởi các socket UNIX vì vấn đề không giống nhau được kết hợp bởi tính kết thúc mở của API. Mục đích của các socket UNIX là tạo ra thứ gì đó mà mọi hương vị của UNIX có thể hoạt động. Mỗi hương vị có thể xác định danh sách các gia đình họ hỗ trợ, như AF_INET, và sẽ có một danh sách ngắn cho mỗi gia đình. Tuy nhiên, nếu một giao thức mới xuất hiện, nhưAF_INET6 đã làm, bạn có thể cần thêm các trường mới. Nếu bạn đã làm điều này với một cấu trúc hợp nhất, cuối cùng bạn sẽ tạo ra một phiên bản mới của cấu trúc có cùng tên, tạo ra các vấn đề không tương thích vô tận. Đây là lý do tại sao các socket UNIX chọn sử dụng mô hình đúc thay vì cấu trúc hợp. Tôi chắc rằng họ đã xem xét nó và thực tế là họ nghĩ về nó là một phần lý do tại sao nó không có mùi khi họ sử dụng nó.

Bạn cũng có thể sử dụng một liên minh thực sự. Các công đoàn tiết kiệm bộ nhớ, bằng cách chỉ lớn hơn thành viên lớn nhất, nhưng họ đi kèm với các vấn đề riêng của họ. Đây có thể không phải là một tùy chọn cho mã của bạn, nhưng nó luôn là một tùy chọn bạn nên xem xét.

Một giải pháp thú vị khác là boost::variant. Boost là một thư viện tuyệt vời với đầy đủ các giải pháp đa nền tảng có thể tái sử dụng. Đây có lẽ là một số mã C ++ tốt nhất từng được viết. Boost.Variant về cơ bản là phiên bản C ++ của các hiệp hội. Nó là một thùng chứa có thể chứa nhiều loại khác nhau, nhưng chỉ có một loại tại một thời điểm. Bạn có thể làm cho bạn Sword, ShieldMagicClothcác lớp học, sau đó làm công cụ là một bằng khá một chút!), Nhưng mô hình này có thể vô cùng hữu ích. Biến thể thường được sử dụng, ví dụ, trong các cây phân tích, lấy một chuỗi văn bản và phá vỡ nó bằng cách sử dụng một ngữ pháp cho các quy tắc.boost::variant<Sword, Shield, MagicCloth> , có nghĩa là nó có chứa một trong ba loại. Điều này vẫn gặp phải vấn đề tương tự với khả năng tương thích trong tương lai ngăn các socket UNIX sử dụng nó (chưa kể các socket UNIX là C, dự đoánboost

Giải pháp cuối cùng mà tôi khuyên bạn nên xem xét trước khi lao vào và sử dụng phương pháp đúc đối tượng chung là mẫu thiết kế của Khách truy cập . Khách truy cập là một mẫu thiết kế mạnh mẽ, tận dụng lợi thế của việc quan sát rằng việc gọi một chức năng ảo thực sự có hiệu quả mà bạn cần và nó thực hiện điều đó cho bạn. Bởi vì trình biên dịch làm điều đó, nó không bao giờ có thể sai. Do đó, thay vì lưu trữ một enum, Khách truy cập sử dụng một lớp cơ sở trừu tượng, có một vtable để biết loại đối tượng là gì. Sau đó, chúng tôi tạo ra một cuộc gọi gián tiếp đôi nhỏ gọn thực hiện công việc:

class Tool;
class Sword;
class Shield;
class MagicCloth;

class ToolVisitor {
public:
    virtual void visit(Sword* sword) = 0;
    virtual void visit(Shield* shield) = 0;
    virtual void visit(MagicCloth* cloth) = 0;
};

class Tool {
public:
    virtual void accept(ToolVisitor& visitor) = 0;
};

lass Sword : public Tool{
public:
    virtual void accept(ToolVisitor& visitor) { visitor.visit(*this); }
    int attack;
};
class Shield : public Tool{
public:
    virtual void accept(ToolVisitor& visitor) { visitor.visit(*this); }
    int defense;
};
class MagicCloth : public Tool{
public:
    virtual void accept(ToolVisitor& visitor) { visitor.visit(*this); }
    int attack;
    int defense;
};

Vì vậy, mô hình thần khủng khiếp này là gì? Vâng, Toolcó một chức năng ảo accept,. Nếu bạn vượt qua nó một khách truy cập, dự kiến ​​sẽ quay lại và gọi đúng visitchức năng trên khách truy cập đó cho loại. Đây là những gì visitor.visit(*this);làm trên mỗi kiểu con. Phức tạp, nhưng chúng tôi có thể hiển thị điều này với ví dụ của bạn ở trên:

class AttackVisitor : public ToolVisitor
{
public:
    int& currentAttack;
    int& currentDefense;

    AttackVisitor(int& currentAttack_, int& currentDefense_)
    : currentAttack(currentAttack_)
    , currentDefense(currentDefense_)
    { }

    virtual void visit(Sword* sword)
    {
        currentAttack += sword->attack;
    }

    virtual void visit(Shield* shield)
    {
        currentDefense += shield->defense;
    }

    virtual void visit(MagicCloth* cloth)
    {
        currentAttack += cloth->attack;
        currentDefense += cloth->defense;
    }
};

void Player::attack()
{
    int currentAttack = this->attack;
    int currentDefense = this->defense;
    AttackVisitor v(currentAttack, currentDefense);
    for (Tool* t: tools) {
        t->accept(v);
    }
    //some other functions to start attack
}

Vậy chuyện gì xảy ra ở đây? Chúng tôi tạo ra một khách truy cập sẽ thực hiện một số công việc cho chúng tôi, khi họ biết loại đối tượng mà nó truy cập. Chúng tôi sau đó lặp đi lặp lại danh sách các công cụ. Để tranh luận, giả sử đối tượng đầu tiên là một Shield, nhưng mã của chúng tôi chưa biết điều đó. Nó gọi t->accept(v), một chức năng ảo. Bởi vì đối tượng đầu tiên là một lá chắn, cuối cùng nó gọi void Shield::accept(ToolVisitor& visitor), mà gọi visitor.visit(*this);. Bây giờ, khi chúng tôi tìm kiếm visitđể gọi, chúng tôi đã biết rằng chúng tôi có Khiên (vì chức năng này đã được gọi), vì vậy chúng tôi sẽ kết thúc cuộc gọi void ToolVisitor::visit(Shield* shield)của chúng tôi AttackVisitor. Điều này bây giờ chạy mã chính xác để cập nhật quốc phòng của chúng tôi.

Khách truy cập là cồng kềnh. Thật là rắc rối đến nỗi tôi gần như nghĩ rằng nó có mùi của riêng nó. Rất dễ dàng để viết các mẫu khách truy cập xấu. Tuy nhiên, nó có một lợi thế rất lớn mà không ai khác có được. Nếu chúng ta thêm một loại công cụ mới, chúng ta phải thêm một ToolVisitor::visitchức năng mới cho nó. Ngay khi chúng tôi thực hiện việc này, mọi ToolVisitor chương trình sẽ từ chối biên dịch vì nó thiếu chức năng ảo. Điều này làm cho nó rất dễ dàng để bắt tất cả các trường hợp chúng ta bỏ lỡ một cái gì đó. Sẽ khó hơn nhiều để đảm bảo rằng nếu bạn sử dụng ifhoặc switchtuyên bố để thực hiện công việc. Những lợi thế này là đủ tốt để Khách truy cập đã tìm thấy một vị trí nhỏ đẹp trong trình tạo cảnh đồ họa 3d. Họ tình cờ cần chính xác loại hành vi mà Khách cung cấp để nó hoạt động tuyệt vời!

Trong tất cả, hãy nhớ rằng các mẫu này làm khó nhà phát triển tiếp theo. Dành thời gian để làm cho họ dễ dàng hơn và mã sẽ không có mùi!

* Về mặt kỹ thuật, nếu bạn nhìn vào thông số kỹ thuật, sockaddr có một thành viên được đặt tên sa_family. Có một số khó khăn được thực hiện ở đây ở cấp độ C không quan trọng đối với chúng tôi. Bạn có thể xem xét việc triển khai thực tế , nhưng đối với câu trả lời này tôi sẽ sử dụng sa_family sin_familyvà những người khác hoàn toàn có thể thay thế cho nhau, sử dụng bất cứ ai trực quan nhất cho văn xuôi, tin rằng thủ thuật C quan tâm đến các chi tiết không quan trọng.


Tấn công liên tiếp sẽ làm cho người chơi mạnh mẽ vô cùng trong ví dụ của bạn. Và bạn không thể mở rộng cách tiếp cận của mình mà không sửa đổi nguồn ToolVisitor. Đó là một giải pháp tuyệt vời, mặc dù.
Polygnome

@Polygnome Bạn nói đúng về ví dụ. Tôi nghĩ rằng mã trông kỳ lạ, nhưng cuộn qua tất cả các trang văn bản tôi đã bỏ qua lỗi. Sửa nó ngay. Đối với yêu cầu sửa đổi nguồn của ToolVisitor, đó là một đặc điểm thiết kế của mẫu Khách truy cập. Đó là một phước lành (như tôi đã viết) và một lời nguyền (như bạn đã viết). Xử lý trường hợp bạn muốn một phiên bản mở rộng tùy ý của điều này khó hơn nhiều và bắt đầu đào theo ý nghĩa của các biến, thay vì chỉ giá trị của chúng, và mở ra các mẫu khác, chẳng hạn như các biến và từ điển được gõ yếu và JSON.
Cort Ammon

1
Vâng, thật đáng buồn, chúng ta không biết đủ về các ưu tiên và mục tiêu của OP để đưa ra quyết định thực sự sáng suốt. Và vâng, một giải pháp hoàn toàn linh hoạt khó thực hiện hơn, tôi đã làm việc với câu trả lời của mình trong gần 3 giờ, vì C ++ của tôi khá
hoen

0

Nói chung, tôi tránh thực hiện một số lớp / kế thừa nếu nó chỉ để truyền dữ liệu. Bạn có thể dính vào một lớp duy nhất và thực hiện mọi thứ từ đó. Ví dụ của bạn, điều này là đủ

class Tool{
    public:
    //constructor, name etc.
    int GetAttack() { return attack }; //Endpoints for your Player
    int GetDefense() { return defense };
    protected:
         int attack;
         int defense;
};

Có thể, bạn đang dự đoán rằng trò chơi của bạn sẽ thực hiện một số loại kiếm, v.v. nhưng bạn sẽ có những cách khác để thực hiện điều này. Lớp nổ hiếm khi là kiến ​​trúc tốt nhất. Giữ cho nó đơn giản.


0

Như đã nêu trước đây, đây là một mùi mã nghiêm trọng. Tuy nhiên, người ta có thể coi nguồn gốc của vấn đề của bạn là sử dụng tính kế thừa thay vì thành phần trong thiết kế của bạn.

Ví dụ: với những gì bạn đã cho chúng tôi thấy, bạn rõ ràng có 3 khái niệm:

  • Mục
  • Vật phẩm có thể tấn công.
  • Vật phẩm có thể phòng thủ.

Lưu ý rằng lớp thứ tư của bạn chỉ là sự kết hợp của hai khái niệm cuối cùng. Vì vậy, tôi sẽ đề nghị sử dụng thành phần cho việc này.

Bạn cần một cấu trúc dữ liệu để thể hiện thông tin cần thiết cho cuộc tấn công. Và bạn cần một cấu trúc dữ liệu đại diện cho thông tin bạn cần cho quốc phòng. Cuối cùng, bạn cần một cấu trúc dữ liệu để thể hiện những thứ có thể có hoặc không có hoặc cả hai thuộc tính này:

class Attack
{
private:
  int attack_;

public:
  int AttackValue() const;
};

class Defense
{
private:
  int defense_

public:
  int DefenseValue() const;
};

class Tool
{
private:
  std::optional<Attack> atk_;
  std::optional<Defense> def_;

public:
  const std::optional<Attack> &GetAttack() const {return atk_;}
  const std::optional<Defense> &GetDefense() const {return def_;}
};

Ngoài ra: không sử dụng cách tiếp cận compose-always :)! Tại sao sử dụng thành phần trong trường hợp này? Tôi đồng ý rằng đó là một giải pháp thay thế, nhưng việc tạo một lớp để "đóng gói" một trường (lưu ý "") có vẻ kỳ lạ trong trường hợp này ...
AilurusFulgens

@AilurusFulgens: Hôm nay là "một lĩnh vực". Ngày mai nó sẽ ra sao? Thiết kế này cho phép AttackDefensetrở nên phức tạp hơn mà không thay đổi giao diện Tool.
Nicol Bolas

1
Bạn vẫn không thể mở rộng Công cụ rất tốt với điều này - chắc chắn, tấn công và phòng thủ có thể phức tạp hơn, nhưng đó là nó. Nếu bạn sử dụng bố cục cho toàn bộ sức mạnh của nó, bạn có thể Toolđóng hoàn toàn để sửa đổi trong khi vẫn để nó mở rộng.
Polygnome

@Polygnome: Nếu bạn muốn trải qua sự cố khi tạo toàn bộ hệ thống thành phần tùy ý cho một trường hợp tầm thường như thế này, điều đó tùy thuộc vào bạn. Cá nhân tôi thấy không có lý do tại sao tôi muốn gia hạn Toolmà không sửa đổi nó. Và nếu tôi có quyền sửa đổi nó, thì tôi không thấy sự cần thiết của các thành phần tùy ý.
Nicol Bolas

Miễn là Công cụ nằm dưới sự kiểm soát của chính bạn, bạn có thể sửa đổi nó. Nhưng nguyên tắc "đóng để sửa đổi, mở để mở rộng" tồn tại vì lý do chính đáng (quá dài để giải thích ở đây). Tôi không nghĩ rằng đó là tầm thường, mặc dù. Nếu bạn dành thời gian thích hợp để lên kế hoạch cho một hệ thống thành phần linh hoạt cho một game nhập vai, bạn sẽ nhận được những phần thưởng to lớn trong thời gian dài. Tôi không thấy lợi ích bổ sung trong này loại thành phần trên chỉ sử dụng các lĩnh vực đồng bằng. Có thể chuyên tấn công và phòng thủ hơn nữa dường như là một kịch bản rất lý thuyết. Nhưng như tôi đã viết, nó phụ thuộc vào yêu cầu chính xác của OP.
Polygnome

0

Tại sao không tạo các phương thức trừu tượng modifyAttackmodifyDefensetrong Toollớp? Sau đó, mỗi đứa trẻ sẽ có cách thực hiện riêng và bạn gọi đây là cách thanh lịch:

for(Tool* tool : tools){
    currentAttack = tool->recalculateAttack(currentAttack);
    currentDefense = tool->recalculateDefense(currentDefense);
}
// proceed with new values for currentAttack and currentDefense

Truyền các giá trị làm tham chiếu sẽ tiết kiệm tài nguyên nếu bạn có thể:

for(Tool* tool : tools){
    tool->recalculateAttack(&currentAttack);
    tool->recalculateDefense(&currentDefense);
}
// proceed with new values for currentAttack and currentDefense

0

Nếu một người sử dụng đa hình thì sẽ luôn tốt nhất nếu tất cả các mã quan tâm đến lớp nào được sử dụng nằm trong chính lớp đó. Đây là cách tôi sẽ mã hóa nó:

class Tool{
 public:
   virtual void equipTo(Player* player) =0;
   virtual void unequipFrom(Player* player) =0;
};

class Sword : public Tool{
  public:
    int attack;
    virtual void equipTo(Player* player) {
      player->attackBonus+=this->attack;
    };
    //unequipFrom = reverse equip
};
class Shield : public Tool{
  public:
    int defense;
    virtual void equipTo(Player* player) {
      player->defenseBonus+=this->defense;
    };
    //unequipFrom = reverse equip
};
//other tools
class Player{
  public:
    int baseAttack;
    int baseDefense;
    int attackBonus;
    int defenseBonus;

    virtual void equip(Tool* tool) {
      tool->equipTo(this);
      this->tools.push_back(tool)
    };

    //unequip = reverse equip

    void attack(){
      //modified attack and defense
      int modifiedAttack = baseAttack + this->attackBonus;
      int modifiedDefense = baseDefense+ this->defenseBonus;
      //some other functions to start attack
    }
  private:
    vector<Tool*> tools;
};

Điều này có những lợi thế sau:

  • dễ dàng hơn để thêm các lớp mới: Bạn chỉ phải thực hiện tất cả các phương thức trừu tượng và phần còn lại của mã chỉ hoạt động
  • dễ dàng hơn để loại bỏ các lớp
  • dễ dàng hơn để thêm số liệu thống kê mới (các lớp không quan tâm đến chỉ số chỉ cần bỏ qua nó)

Ít nhất bạn cũng nên bao gồm một phương thức unequip () để loại bỏ phần thưởng từ người chơi.
Polygnome

0

Tôi nghĩ một cách để nhận ra những sai sót trong phương pháp này là phát triển ý tưởng của bạn đến kết luận hợp lý của nó.

Điều này trông giống như một trò chơi, vì vậy ở một số giai đoạn, bạn có thể sẽ bắt đầu lo lắng về hiệu suất và trao đổi các so sánh chuỗi đó cho một inthoặc enum. Khi danh sách các mục trở nên dài hơn, điều đó if-elsebắt đầu trở nên khá khó sử dụng, vì vậy bạn có thể xem xét tái cấu trúc nó thành một switch-case. Bạn cũng đã có một bức tường văn bản vào thời điểm này để bạn có thể chuyển hành động trong từng casethành một chức năng riêng biệt.

Khi bạn đạt đến điểm này, cấu trúc mã của bạn bắt đầu trông quen thuộc - bắt đầu giống như một vtable homebrew, được cuộn bằng tay * - cấu trúc cơ bản mà các phương thức ảo thường được triển khai. Ngoại trừ, đây là một vtable mà bạn phải tự cập nhật và tự bảo trì, mỗi lần bạn thêm hoặc sửa đổi một loại mục.

Bằng cách bám vào các chức năng ảo "thực", bạn có thể duy trì việc thực hiện từng hành vi của từng mục trong chính mục đó. Bạn có thể thêm các mục bổ sung theo cách khép kín và nhất quán hơn. Và khi bạn làm tất cả những điều này, nó là trình biên dịch sẽ đảm nhiệm việc thực hiện công văn động của bạn, chứ không phải là bạn.

Để giải quyết vấn đề cụ thể của bạn: bạn đang vật lộn để viết một cặp chức năng ảo đơn giản để cập nhật tấn công và phòng thủ vì một số vật phẩm chỉ ảnh hưởng đến tấn công và một số vật phẩm chỉ ảnh hưởng đến phòng thủ. Thủ thuật trong một trường hợp đơn giản như thế này để thực hiện cả hai hành vi, nhưng không có tác dụng trong một số trường hợp nhất định. GetDefenseBonus()có thể trở lại 0hoặc ApplyDefenseBonus(int& defence)có thể defencekhông thay đổi. Làm thế nào bạn đi về nó sẽ phụ thuộc vào cách bạn muốn xử lý các hành động khác có ảnh hưởng. Trong các trường hợp phức tạp hơn, khi có hành vi đa dạng hơn, bạn chỉ cần kết hợp hoạt động thành một phương thức duy nhất.

* (Mặc dù, chuyển đổi liên quan đến việc thực hiện điển hình)


0

Có một khối mã biết về tất cả các "công cụ" có thể không phải là thiết kế tuyệt vời (đặc biệt là vì bạn sẽ kết thúc với nhiều khối như vậy trong mã của mình); nhưng không có cơ bản Toolvới sơ khai cho tất cả các thuộc tính công cụ có thể: bây giờ Toollớp phải biết về tất cả các sử dụng có thể.

Những gì mỗi công cụ biết là những gì nó có thể đóng góp cho nhân vật sử dụng nó. Vì vậy, cung cấp một phương pháp cho tất cả các công cụ giveto(*Character owner),. Nó sẽ điều chỉnh các chỉ số của người chơi cho phù hợp mà không cần biết các công cụ khác có thể làm gì, và những gì tốt nhất, nó cũng không cần biết về các thuộc tính không liên quan của nhân vật. Ví dụ, một lá chắn thậm chí không cần phải biết về các thuộc tính attack, invisibility, healthvv Tất cả những gì cần thiết để áp dụng một công cụ dành cho những nhân vật để hỗ trợ các thuộc tính của đối tượng yêu cầu. Nếu bạn cố gắng đưa một thanh kiếm cho một con lừa và con lừa không có attacksố liệu thống kê, bạn sẽ gặp lỗi.

Các công cụ cũng nên có một remove()phương pháp, đảo ngược tác dụng của chúng đối với chủ sở hữu. Đây là một mẹo nhỏ (có thể kết thúc bằng các công cụ để lại hiệu ứng khác không khi được đưa ra và sau đó bị lấy đi), nhưng ít nhất nó được bản địa hóa cho mỗi công cụ.


-4

Không có câu trả lời nào nói rằng nó không có mùi nên tôi sẽ là người đại diện cho ý kiến ​​đó; mã này là hoàn toàn tốt! Ý kiến ​​của tôi dựa trên thực tế là đôi khi việc di chuyển dễ dàng hơn và cho phép các kỹ năng của bạn tăng dần khi bạn tạo ra nhiều công cụ mới. Bạn có thể bị mắc kẹt trong nhiều ngày để tạo ra một kiến ​​trúc hoàn hảo, nhưng có lẽ thậm chí không ai sẽ thấy nó hoạt động, bởi vì bạn chưa bao giờ hoàn thành dự án. Chúc mừng!


4
Cải thiện kỹ năng với kinh nghiệm cá nhân là tốt, chắc chắn. Nhưng cải thiện kỹ năng bằng cách hỏi những người đã có kinh nghiệm cá nhân đó, vì vậy bạn không cần phải tự mình rơi vào lỗ hổng, thông minh hơn. Chắc chắn đó là lý do mọi người đặt câu hỏi ở đây ngay từ đầu phải không?
Graham

Tôi không đồng ý. Nhưng tôi hiểu rằng trang web này là tất cả về đi sâu. Đôi khi điều đó có nghĩa là quá tầm thường. Đây là lý do tại sao tôi muốn đăng ý kiến ​​này, vì nó gắn liền với thực tế và nếu bạn đang tìm kiếm các mẹo để trở nên tốt hơn và giúp đỡ cho người mới bắt đầu, bạn sẽ bỏ lỡ toàn bộ chương này về "đủ tốt" cho người mới bắt đầu biết.
Ostmeistro
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.