Làm thế nào để tránh các nhà quản lý của người Viking trong mã của tôi


26

Tôi hiện đang thiết kế lại Hệ thống thực thể của mình , cho C ++ và tôi có rất nhiều Người quản lý. Trong thiết kế của tôi, tôi có các lớp này, để gắn kết thư viện của tôi với nhau. Tôi đã nghe thấy rất nhiều điều tồi tệ khi nói đến các lớp "người quản lý", có lẽ tôi không đặt tên cho các lớp học của mình một cách thích hợp. Tuy nhiên, tôi không có ý tưởng gì khác để đặt tên cho chúng.

Hầu hết các trình quản lý, trong thư viện của tôi, bao gồm các lớp này (mặc dù nó thay đổi một chút):

  • Container - một container cho các đối tượng trong trình quản lý
  • Các thuộc tính - thuộc tính cho các đối tượng trong trình quản lý

Trong thiết kế mới cho thư viện của tôi, tôi có các lớp cụ thể này, để gắn kết thư viện của tôi với nhau.

  • ElementManager - quản lý các thành phần trong Hệ thống thực thể

    • Thành phần liên kết
    • Thành phần
    • Cảnh * - tham chiếu đến Cảnh (xem bên dưới)
  • SystemManager - quản lý các hệ thống trong Hệ thống thực thể

    • SystemContainer
    • Cảnh * - tham chiếu đến Cảnh (xem bên dưới)
  • EntityManager - quản lý các thực thể trong Hệ thống thực thể

    • EntityPool - nhóm thực thể
    • EntityAttribution - các thuộc tính của một thực thể (điều này sẽ chỉ có thể truy cập được đối với các lớp ElementContainer và System)
    • Cảnh * - tham chiếu đến Cảnh (xem bên dưới)
  • Cảnh - liên kết tất cả các nhà quản lý với nhau

    • Thành phần quản lý
    • Quản lý hệ thống
    • Quản lý thực thể

Tôi đã nghĩ đến việc chỉ cần đặt tất cả các thùng chứa / nhóm trong lớp Cảnh.

I E

Thay vì điều này:

Scene scene; // create a Scene

// NOTE:
// I technically could wrap this line in a createEntity() call in the Scene class
Entity entity = scene.getEntityManager().getPool().create();

Nó sẽ là thế này:

Scene scene; // create a Scene

Entity entity = scene.getEntityPool().create();

Nhưng, tôi không chắc chắn. Nếu tôi làm cái sau, điều đó có nghĩa là tôi sẽ có rất nhiều đối tượng và phương thức được khai báo bên trong lớp Cảnh của tôi.

GHI CHÚ:

  1. Một hệ thống thực thể đơn giản là một thiết kế được sử dụng cho các trò chơi. Nó bao gồm 3 phần chính: các thành phần, thực thể và hệ thống. Các thành phần chỉ là dữ liệu, có thể được "thêm" vào các thực thể, để các thực thể được phân biệt. Một thực thể được đại diện bởi một số nguyên. Các hệ thống chứa logic cho một thực thể, với các thành phần cụ thể.
  2. Lý do tôi thay đổi thiết kế cho thư viện của mình là vì tôi nghĩ nó có thể thay đổi khá nhiều, hiện tại tôi không thích cảm giác / dòng chảy của nó.

Bạn có thể giải thích những điều tồi tệ bạn đã nghe về những người quản lý và cách họ liên quan đến bạn không?
MrFox


@MrFox Về cơ bản tôi đã nghe những gì Dunk đã đề cập và tôi có rất nhiều lớp * Trình quản lý trong thư viện của mình, thứ mà tôi muốn loại bỏ.
Miguel.martin

Điều này cũng có thể hữu ích: Medium.com/@wrong.about/ Kẻ
Zapadlo

Bạn yếu tố một khung hữu ích trong một ứng dụng làm việc. Gần như không thể viết một khung hữu ích cho các ứng dụng tưởng tượng.
kevin cline

Câu trả lời:


19

DeadMG được phát hiện trên các chi tiết cụ thể về mã của bạn nhưng tôi cảm thấy như nó bỏ lỡ một sự làm rõ. Ngoài ra, tôi không đồng ý với một số khuyến nghị của anh ấy về việc không nắm giữ một số bối cảnh cụ thể như hầu hết phát triển trò chơi điện tử hiệu suất cao chẳng hạn. Nhưng anh ấy nói đúng rằng hầu hết các mã của bạn không hữu ích ngay bây giờ.

Như Dunk nói, các lớp Manager được gọi như thế này vì chúng "quản lý" mọi thứ. "Quản lý" giống như "dữ liệu" hoặc "làm", đó là một từ trừu tượng có thể chứa hầu hết mọi thứ.

Tôi chủ yếu ở cùng một chỗ với bạn khoảng 7 năm trước và tôi bắt đầu nghĩ rằng có một cái gì đó sai trong cách suy nghĩ của tôi bởi vì đó là rất nhiều nỗ lực để viết mã để không làm gì cả.

Điều tôi đã thay đổi để sửa lỗi này là thay đổi từ vựng tôi sử dụng trong mã. Tôi hoàn toàn tránh các từ chung chung (trừ khi đó là mã chung, nhưng hiếm khi bạn không tạo thư viện chung). Tôi tránh đặt tên bất kỳ loại "Người quản lý" hoặc "đối tượng".

Tác động là trực tiếp trong mã: nó buộc bạn phải tìm đúng từ tương ứng với trách nhiệm thực sự của loại của bạn. Nếu bạn cảm thấy kiểu này làm một số việc (giữ một chỉ mục Sách, giữ cho chúng tồn tại, tạo / hủy sách) thì bạn cần các loại khác nhau mà mỗi loại sẽ có một trách nhiệm, sau đó bạn kết hợp chúng trong mã sử dụng chúng. Đôi khi tôi cần một nhà máy, đôi khi không. Đôi khi tôi cần một sổ đăng ký, vì vậy tôi thiết lập một (sử dụng các thùng chứa tiêu chuẩn và con trỏ thông minh). Thỉnh thoảng tôi cần một hệ thống bao gồm một số hệ thống phụ khác nhau để tôi tách mọi thứ nhiều nhất có thể để mỗi phần làm một cái gì đó hữu ích.

Không bao giờ đặt tên cho một loại "người quản lý" và đảm bảo tất cả các loại của bạn có một vai trò duy nhất là đề xuất của tôi. Đôi khi có thể khó tìm thấy tên, nhưng nói chung đó là một trong những điều khó nhất trong lập trình


Nếu một cặp đôi dynamic_castđang giết chết hiệu suất của bạn, thì hãy sử dụng hệ thống loại tĩnh - đó là những gì nó làm. Đây thực sự là một bối cảnh chuyên biệt, không phải là một bối cảnh chung, phải tạo ra RTTI của riêng bạn như LLVM đã làm. Thậm chí sau đó, họ không làm điều này.
DeadMG

@DeadMG Tôi đã không nói về phần này nói riêng, nhưng hầu hết các trò chơi hiệu suất cao không thể đủ khả năng, ví dụ, phân bổ động thông qua các công cụ mới hoặc thậm chí các thứ khác tốt cho lập trình nói chung. Chúng là những con thú cụ thể cho phần cứng cụ thể mà bạn không thể khái quát, đó là lý do tại sao "nó phụ thuộc" là một câu trả lời tổng quát hơn, đặc biệt là với C ++. (Nhân tiện, không ai trong trò chơi sử dụng tính năng động, bằng cách này, nó không bao giờ hữu ích cho đến khi bạn có plugin và thậm chí sau đó nó rất hiếm).
Klaim

@DeadMG Tôi không sử dụng Dynamic_cast, tôi cũng không sử dụng thay thế cho Dynamic_cast. Tôi đã thực hiện nó trong thư viện, nhưng tôi không thể bận tâm lấy nó ra. template <typename T> class Class { ... };thực sự là để gán ID cho các lớp khác nhau (cụ thể là các thành phần tùy chỉnh và hệ thống tùy chỉnh). Việc gán ID được sử dụng để lưu trữ các thành phần / hệ thống trong một vectơ, vì bản đồ có thể không mang lại hiệu suất tôi cần cho trò chơi.
Miguel.martin

Chà, tôi cũng không thực sự thay thế cho Dynamic_cast, chỉ là một type_id giả. Nhưng, tôi vẫn không muốn những gì type_id đạt được.
Miguel.martin

"Tìm từ đúng tương ứng với trách nhiệm thực sự của loại của bạn" - mặc dù tôi hoàn toàn đồng ý với điều này, thường thì không có một từ nào (nếu có) mà cộng đồng nhà phát triển đã đồng ý cho một loại trách nhiệm cụ thể.
bytedev

23

Vâng, tôi đã đọc qua một số mã bạn liên kết và bài đăng của bạn, và tóm tắt trung thực của tôi là phần lớn trong số đó về cơ bản là hoàn toàn vô giá trị. Lấy làm tiếc. Ý tôi là, bạn có tất cả các mã này, nhưng bạn chưa đạt được gì. Ở tất cả. Tôi sẽ phải đi vào một số độ sâu ở đây, vì vậy hãy chịu đựng tôi.

Hãy bắt đầu với ObjectFactory . Thứ nhất, con trỏ hàm. Đây là trạng thái không trạng thái, cách duy nhất hữu ích để tạo đối tượng là newvà bạn không cần một nhà máy cho việc đó. Tạo một đối tượng một cách chính xác là những gì hữu ích và các con trỏ hàm không hữu ích cho nhiệm vụ này. Và thứ hai, mọi đối tượng cần biết cách tự hủy , không phải tìm trường hợp tạo chính xác đó để phá hủy nó. Và ngoài ra, bạn phải trả về một con trỏ thông minh, không phải là một con trỏ thô, vì ngoại lệ và an toàn tài nguyên của người dùng của bạn. Toàn bộ lớp ClassRegistryData của bạn chỉ là std::function<std::unique_ptr<Base, std::function<void(Base*)>>()>, nhưng với các lỗ hổng đã nói ở trên. Nó hoàn toàn không cần tồn tại.

Sau đó, chúng tôi có AllocatorFunctor - đã có giao diện Bộ cấp phát tiêu chuẩn được cung cấp - Không đề cập đến việc chúng chỉ là trình bao bọc delete obj;return new T;- một lần nữa, đã được cung cấp và chúng sẽ không tương tác với bất kỳ bộ cấp phát bộ nhớ tùy chỉnh hoặc trạng thái nào tồn tại.

Và ClassRegistry chỉ đơn giản std::map<std::string, std::function<std::unique_ptr<Base, std::function<void(Base*)>>()>>nhưng bạn đã viết một lớp cho nó thay vì sử dụng các chức năng hiện có. Nó giống nhau, nhưng hoàn toàn không cần thiết trạng thái biến đổi toàn cầu với một loạt các macro xảo quyệt.

Điều tôi đang nói ở đây là bạn đã tạo ra một số lớp trong đó bạn có thể thay thế toàn bộ bằng chức năng được xây dựng từ các lớp Tiêu chuẩn, và sau đó làm cho chúng trở nên tồi tệ hơn bằng cách biến chúng thành trạng thái có thể thay đổi toàn cầu và liên quan đến một loạt các macro, đó là điều tồi tệ nhất bạn có thể làm.

Sau đó, chúng tôi đã xác định được . Có rất nhiều câu chuyện tương tự ở đây - std::type_infođã thực hiện công việc xác định từng loại là giá trị thời gian chạy và nó không yêu cầu quá mức nồi hơi đối với người dùng.

    template<typename T, typename Y>
    bool isSameType(const X& x, const Y& y) { return typeid(x) == typeid(y); }
    template <class Type, class T>
    bool isType(const T& obj)
    {
            return dynamic_cast<const Type*>(std::addressof(obj));
    }

Vấn đề đã được giải quyết, nhưng không có bất kỳ ưu tiên nào trong hai trăm dòng macro và không có gì. Điều này cũng đánh dấu Nhận dạng của bạn là không có gì nhưng std::vectorbạn phải sử dụng tính tham chiếu ngay cả khi quyền sở hữu duy nhất hoặc không sở hữu sẽ tốt hơn.

Ồ, và tôi đã đề cập rằng Các loại có chứa một loạt các typedef hoàn toàn vô dụng ? Bạn thực sự không có lợi thế gì khi sử dụng các loại nguyên liệu trực tiếp và mất đi một phần dễ đọc.

Tiếp theo là ElementContainer . Bạn không thể chọn quyền sở hữu, bạn không thể có các thùng chứa riêng biệt cho các lớp dẫn xuất của các thành phần khác nhau, bạn không thể sử dụng ngữ nghĩa trọn đời của riêng mình. Xóa mà không hối hận.

Bây giờ, ElementFilter thực sự có thể có giá trị một chút, nếu bạn tái cấu trúc nó rất nhiều. Cái gì đó như

class ComponentFilter {
    std::unordered_map<std::type_info, unsigned> types;
public:
    template<typename T> void SetTypeRequirement(unsigned count = 1) {
        types[typeid(T)] = count;
    }
    void clear() { types.clear(); }
    template<typename Iterator> bool matches(Iterator begin, Iterator end) {
        std::for_each(begin, end, [this](const std::iterator_traits<Iterator>::value_type& t) {
            types[typeid(t)]--;
        });
        return types.empty();
    }
};

Điều này không đối phó với các lớp xuất phát từ những lớp bạn đang cố gắng xử lý, nhưng cũng không phải của bạn.

Phần còn lại của điều này là khá nhiều câu chuyện tương tự. Không có gì ở đây không thể được thực hiện tốt hơn nhiều nếu không sử dụng thư viện của bạn.

Làm thế nào bạn sẽ tránh các nhà quản lý trong mã này? Xóa về cơ bản tất cả của nó.


14
Tôi phải mất một thời gian để tìm hiểu, nhưng những đoạn mã không có lỗi đáng tin cậy nhất là những đoạn không có ở đó .
Tacroy

1
Lý do tôi không sử dụng std :: type_info là vì tôi đã cố tránh RTTI. Tôi đã đề cập đến khung này nhằm vào các trò chơi, về cơ bản là lý do tôi không sử dụng typeidhoặc dynamic_cast. Cảm ơn các phản hồi mặc dù, tôi đánh giá cao nó.
Miguel.martin

Đây có phải là những lời phê bình dựa trên kiến ​​thức của bạn về các công cụ trò chơi hệ thống thực thể thành phần hay nó là một đánh giá mã C ++ chung?
Den

1
@Den Tôi nghĩ nhiều về một vấn đề thiết kế hơn bất cứ điều gì khác.
Miguel.martin

10

Vấn đề với các lớp Manager là người quản lý có thể làm bất cứ điều gì. Bạn không thể đọc tên lớp và biết những gì lớp học làm. EntityManager .... Tôi biết nó làm gì đó với Thực thể nhưng ai biết cái gì. SystemManager ... vâng, nó quản lý hệ thống. Điều đó rất hữu ích.

Tôi đoán là các lớp quản lý của bạn có thể làm rất nhiều thứ. Tôi sẽ đặt cược nếu bạn phân chia những thứ đó thành các phần chức năng có liên quan chặt chẽ với nhau, thì có lẽ bạn sẽ có thể đưa ra các tên lớp liên quan đến các phần chức năng mà bất kỳ ai cũng có thể biết chúng làm gì chỉ bằng cách nhìn vào tên lớp. Điều này sẽ làm cho việc bảo trì và hiểu thiết kế dễ dàng hơn nhiều.


1
+1 và họ không chỉ có thể làm bất cứ điều gì, trên dòng thời gian đủ dài họ sẽ làm. Mọi người xem mô tả "Người quản lý" như một lời mời để tạo ra các lớp dao bếp-chìm / swiss-army-dao.
Erik Dietrich

@Erik - À đúng, tôi nên nói thêm rằng việc có một tên lớp tốt không chỉ quan trọng bởi vì nó cho ai đó biết những gì lớp có thể làm; nhưng bằng nhau, tên lớp cũng nói những gì lớp không nên làm.
Dunk

3

Tôi thực sự không nghĩ Managerlà quá tệ trong bối cảnh này, nhún vai. Nếu có một nơi mà tôi nghĩ Managerlà có thể tha thứ, thì đó sẽ là một hệ thống thành phần thực thể, vì nó thực sự là " quản lý " vòng đời của các thành phần, thực thể và hệ thống. Ít nhất đó là một cái tên có ý nghĩa hơn ở đây hơn là, Factoryđiều đó không có ý nghĩa nhiều trong bối cảnh này vì ECS thực sự làm nhiều hơn một chút so với việc tạo ra mọi thứ.

Tôi cho rằng bạn có thể sử dụng Database, như ComponentDatabase. Hoặc bạn chỉ có thể sử dụng Components, SystemsEntities (đây là những gì cá nhân tôi sử dụng và chủ yếu chỉ vì tôi thích các định danh ngắn gọn mặc dù nó thừa nhận truyền tải thậm chí ít thông tin hơn, có lẽ, hơn cả "Người quản lý").

Phần tôi thấy hơi vụng về là đây:

Entity entity = scene.getEntityManager().getPool().create();

Đó là rất nhiều thứ gettrước khi bạn có thể tạo ra một thực thể và có cảm giác như thiết kế đang rò rỉ chi tiết triển khai một chút nếu bạn phải biết về nhóm thực thể và "người quản lý" để tạo ra chúng. Tôi nghĩ những thứ như "nhóm" và "người quản lý" (nếu bạn dính vào tên này) phải là chi tiết triển khai của ECS, chứ không phải thứ gì đó tiếp xúc với khách hàng.

Nhưng dunno, tôi đã thấy một số công dụng rất thiếu óc tưởng tượng và chung chung của Manager, Handler, Adapter, và tên của loại này, nhưng tôi nghĩ rằng đây là một trường hợp sử dụng một cách hợp lý có thể tha thứ khi điều gọi là "quản lý" là thực sự chịu trách nhiệm về "quản lý" ( "kiểm soát? "," xử lý? ") Thời gian sống của các đối tượng, chịu trách nhiệm tạo và phá hủy và cung cấp quyền truy cập riêng cho chúng. Đó là cách sử dụng "Trình quản lý" đơn giản và trực quan nhất đối với tôi ít nhất.

Điều đó nói rằng, một chiến lược chung để tránh "Người quản lý" trong tên của bạn chỉ là lấy phần "Người quản lý" và thêm -sphần cuối. :-D Ví dụ: giả sử bạn có ThreadManagervà chịu trách nhiệm tạo chủ đề và cung cấp thông tin về họ và truy cập chúng, v.v., nhưng nó không tập hợp chủ đề nên chúng tôi không thể gọi nó ThreadPool. Vâng, trong trường hợp đó, bạn chỉ cần gọi nó Threads. Đó là những gì tôi làm dù sao khi tôi không tưởng tượng.

Tôi hơi nhớ lại khi tương đương với "Windows Explorer" được gọi là "Trình quản lý tệp", như vậy:

nhập mô tả hình ảnh ở đây

Và tôi thực sự nghĩ rằng "Trình quản lý tệp" trong trường hợp này mô tả nhiều hơn "Windows Explorer" ngay cả khi có một cái tên nào đó tốt hơn không liên quan đến "Trình quản lý". Vì vậy, ít nhất xin vui lòng đừng quá thích thú với tên của bạn và bắt đầu đặt tên cho những thứ như "ElementExplorer". "Thành phần quản lý" ít nhất mang đến cho tôi một ý tưởng hợp lý về những gì mà công việc đó làm khi một người từng làm việc với các công cụ ECS.


Đã đồng ý. Nếu một lớp được sử dụng cho các nhiệm vụ quản lý thông thường (tạo ("tuyển dụng"), phá hủy ("bắn"), giám sát và giao diện "nhân viên" / cao hơn, thì tên đó Managerlà phù hợp. Lớp có thể có vấn đề về thiết kế , nhưng tên là tại chỗ.
Justin Time 2 Tái lập lại
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.