Khi một vài lớp cần truy cập vào cùng một dữ liệu, dữ liệu nên được khai báo ở đâu?


39

Tôi có một trò chơi phòng thủ tháp 2D cơ bản trong C ++.

Mỗi bản đồ là một lớp riêng biệt kế thừa từ GameState. Bản đồ ủy quyền logic và vẽ mã cho từng đối tượng trong trò chơi và thiết lập dữ liệu như đường dẫn bản đồ. Trong mã giả, phần logic có thể trông giống như thế này:

update():
  for each creep in creeps:
    creep.update()
  for each tower in towers:
    tower.update()
  for each missile in missiles:
    missile.update()

Các vật thể (creep, tháp và tên lửa) được lưu trữ trong các vectơ con trỏ. Các tòa tháp phải có quyền truy cập vào các vectơ và các vectơ của tên lửa để tạo ra các tên lửa mới và xác định mục tiêu.

Câu hỏi là: tôi khai báo các vectơ ở đâu? Họ có nên là thành viên của lớp Map và chuyển qua làm đối số cho hàm tower.update () không? Hoặc tuyên bố trên toàn cầu? Hoặc có những giải pháp khác tôi đang thiếu hoàn toàn?

Khi một vài lớp cần truy cập vào cùng một dữ liệu, dữ liệu nên được khai báo ở đâu?


1
Các thành viên toàn cầu được coi là "xấu xí" nhưng nhanh nhẹn và giúp phát triển dễ dàng hơn, nếu đó là một trò chơi nhỏ, điều đó không thành vấn đề (IMHO). Bạn cũng có thể tạo một lớp bên ngoài xử lý logic ( tại sao các tháp cần các vectơ này) và có quyền truy cập vào tất cả các vectơ.
Jonathan Connell

-1 nếu điều này có liên quan đến lập trình trò chơi, thì ăn pizza cũng vậy. Lấy cho mình một số sách thiết kế phần mềm tốt
Maik Semder

9
@Maik: Thiết kế phần mềm không liên quan đến lập trình trò chơi như thế nào? Chỉ vì nó cũng áp dụng cho các lĩnh vực lập trình khác không làm cho nó lạc đề.
BlueRaja - Daniel Pflughoeft

@BlueRaja danh sách các mẫu thiết kế phần mềm phù hợp hơn trên SO, đó là những gì nó có sẵn cho tất cả. GD.SE dành cho lập trình trò chơi, không phải thiết kế phần mềm
Maik Semder

Câu trả lời:


53

Khi bạn cần một phiên bản duy nhất của một lớp trong suốt chương trình của mình, chúng tôi gọi lớp đó là dịch vụ . Có một số phương pháp tiêu chuẩn để thực hiện dịch vụ trong các chương trình:

  • Biến toàn cầu . Đây là những dễ dàng nhất để thực hiện, nhưng thiết kế tồi tệ nhất. Nếu bạn sử dụng quá nhiều biến toàn cục, bạn sẽ nhanh chóng thấy mình viết các mô-đun dựa vào nhau quá nhiều ( khớp nối mạnh ), khiến cho dòng logic rất khó theo dõi. Biến toàn cầu không thân thiện với đa luồng. Các biến toàn cục làm cho việc theo dõi vòng đời của các đối tượng trở nên khó khăn hơn và làm lộn xộn không gian tên. Tuy nhiên, chúng là tùy chọn hiệu quả nhất, vì vậy có những lúc chúng có thể và nên được sử dụng, nhưng sử dụng chúng một cách rảnh rỗi.
  • Người độc thân . Khoảng 10-15 năm trước, độc thân là những thiết kế mẫu lớn để biết về. Tuy nhiên, ngày nay họ bị coi thường. Chúng dễ dàng hơn nhiều đối với đa luồng, nhưng bạn phải giới hạn việc sử dụng chúng ở một luồng tại một thời điểm, đây không phải lúc nào cũng là điều bạn muốn. Theo dõi cuộc sống cũng khó khăn như với các biến toàn cầu.
    Một lớp singleton điển hình sẽ trông giống như thế này:

    class MyClass
    {
    private:
        static MyClass* _instance;
        MyClass() {} //private constructor
    
    public:
        static MyClass* getInstance();
        void method();
    };
    
    ...
    
    MyClass* MyClass::_instance = NULL;
    MyClass* MyClass::getInstance()
    {
        if(_instance == NULL)
            _instance = new MyClass(); //Not thread-safe version
        return _instance;
    
        //Note that _instance is *never* deleted - 
        //it exists for the entire lifetime of the program!
    }
    
  • Phụ thuộc tiêm (DI) . Điều này chỉ có nghĩa là truyền dịch vụ dưới dạng tham số hàm tạo. Một dịch vụ phải tồn tại để chuyển nó vào một lớp, vì vậy không có cách nào để hai dịch vụ dựa vào nhau; trong 98% các trường hợp, đây là những gì bạn muốn (và đối với 2% còn lại, bạn luôn có thể tạo một setWhatever()phương thức và chuyển vào dịch vụ sau) . Do đó, DI không gặp vấn đề về khớp nối như các tùy chọn khác. Nó có thể được sử dụng với đa luồng, bởi vì mỗi luồng chỉ có thể có phiên bản riêng của mỗi dịch vụ (và chỉ chia sẻ những luồng mà nó thực sự cần). Nó cũng làm cho mã đơn vị có thể kiểm tra được, nếu bạn quan tâm đến điều đó.

    Vấn đề với tiêm phụ thuộc là nó chiếm nhiều bộ nhớ hơn; bây giờ mọi phiên bản của một lớp cần tham chiếu đến mọi dịch vụ mà nó sẽ sử dụng. Ngoài ra, nó gây khó chịu khi sử dụng khi bạn có quá nhiều dịch vụ; có các khung làm giảm thiểu vấn đề này bằng các ngôn ngữ khác, nhưng do thiếu phản xạ của C ++, các khung DI trong C ++ có xu hướng thậm chí còn hiệu quả hơn là chỉ làm thủ công.

    //Example of dependency injection
    class Tower
    {
    private:
        MissileCreationService* _missileCreator;
        CreepLocatorService* _creepLocator;
    public:
        Tower(MissileCreationService*, CreepLocatorService*);
    }
    
    //In order to create a tower, the creating-class must also have instances of
    // MissileCreationService and CreepLocatorService; thus, if we want to 
    // add a new service to the Tower constructor, we must add it to the
    // constructor of every class which creates a Tower as well!
    //This is not a problem in languages like C# and Java, where you can use
    // a framework to create an instance and inject automatically.
    

    Xem trang này (từ tài liệu cho Ninject, khung C # DI) để biết ví dụ khác.

    Tiêm phụ thuộc là giải pháp thông thường cho vấn đề này và là câu trả lời bạn sẽ thấy được đánh giá cao nhất cho các câu hỏi như thế này trên StackOverflow.com. DI là một kiểu đảo ngược của điều khiển (IoC).

  • Định vị dịch vụ . Về cơ bản, chỉ là một lớp chứa một thể hiện của mọi dịch vụ. Bạn có thể làm điều đó bằng cách sử dụng sự phản chiếu hoặc bạn chỉ có thể thêm một thể hiện mới vào nó mỗi khi bạn muốn tạo một dịch vụ mới. Bạn vẫn gặp vấn đề tương tự như trước đây - Làm thế nào để các lớp truy cập vào trình định vị này? - có thể được giải quyết theo bất kỳ cách nào ở trên, nhưng bây giờ bạn chỉ cần làm điều đó cho ServiceLocatorlớp của mình , thay vì cho hàng tá dịch vụ. Phương pháp này cũng có thể kiểm tra đơn vị, nếu bạn quan tâm đến loại điều đó.

    Bộ định vị dịch vụ là một dạng khác của Inversion of Control (IoC). Thông thường, các khung làm việc tiêm phụ thuộc tự động cũng sẽ có một bộ định vị dịch vụ.

    XNA (khung lập trình trò chơi C # của Microsoft) bao gồm một bộ định vị dịch vụ; để tìm hiểu thêm về nó, xem câu trả lời này .


Nhân tiện, IMHO các tòa tháp không nên biết về creep. Trừ khi bạn có kế hoạch chỉ đơn giản là lặp qua danh sách các creep cho mỗi tòa tháp, bạn có thể muốn thực hiện một số phân vùng không gian không cần thiết ; và loại logic đó không thuộc về lớp tháp.


Bình luận không dành cho thảo luận mở rộng; cuộc trò chuyện này đã được chuyển sang trò chuyện .
Josh

Một trong những câu trả lời hay nhất, rõ ràng nhất mà tôi từng đọc. Làm tốt. Tôi nghĩ rằng một dịch vụ luôn luôn được chia sẻ mặc dù.
Nikos

5

Cá nhân tôi sẽ sử dụng đa hình ở đây. Tại sao có một missilevectơ, một towervectơ và một creepvectơ..khi tất cả chúng đều gọi cùng một hàm; update? Tại sao không có một vectơ con trỏ đến một số lớp cơ sở Entityhay GameObject?

Tôi thấy một cách tốt để thiết kế là nghĩ 'điều này có ý nghĩa về mặt sở hữu' không? Rõ ràng một tòa tháp sở hữu một cách để tự cập nhật, nhưng bản đồ có sở hữu tất cả các đối tượng trên đó không? Nếu bạn đi toàn cầu, bạn có nói rằng không có gì sở hữu các tòa tháp và creep? Toàn cầu thường là một giải pháp tồi - nó thúc đẩy các mẫu thiết kế xấu, nhưng nó dễ làm việc hơn nhiều. Cân nhắc cân nhắc 'tôi có muốn hoàn thành việc này không?' và 'tôi có muốn một cái gì đó mà tôi có thể tái sử dụng' không?

Một cách xung quanh đây là một số hình thức của hệ thống nhắn tin. Họ towercó thể gửi một thông điệp tới map(mà nó có quyền truy cập, có lẽ là một tham chiếu đến chủ sở hữu của nó?) Rằng nó nhấn một creep, và mapsau đó nói rằng creepnó đã bị tấn công. Điều này là rất sạch sẽ và tách biệt dữ liệu.

Một cách khác là chỉ tìm kiếm bản đồ cho những gì nó muốn. Tuy nhiên, có thể có vấn đề với thứ tự cập nhật ở đây.


1
Đề nghị của bạn về đa hình không thực sự phù hợp. Tôi có chúng được lưu trữ trong các vectơ riêng biệt để tôi có thể lặp qua từng loại riêng lẻ, chẳng hạn như trong mã bản vẽ (nơi tôi sẽ muốn các đối tượng nhất định được vẽ trước) hoặc trong mã va chạm.
Juicy

Đối với mục đích của tôi, bản đồ sở hữu các thực thể, vì bản đồ ở đây tương tự với 'cấp độ'. Tôi sẽ xem xét ý tưởng của bạn về tin nhắn, cảm ơn.
Juicy

1
Trong một vấn đề hiệu suất trò chơi. Vì vậy, vectơ của cùng một thời gian đối tượng có địa phương tham chiếu tốt hơn. Ngoài ra, các đối tượng đa hình với con trỏ ảo có hiệu suất khủng khiếp vì chúng không thể được đưa vào vòng cập nhật.
Zan Lynx

0

Đây là một trường hợp trong đó lập trình hướng đối tượng nghiêm ngặt (OOP) bị phá vỡ.

Theo các nguyên tắc của OOP, bạn nên nhóm dữ liệu với hành vi liên quan bằng cách sử dụng các lớp. Nhưng bạn có một hành vi (nhắm mục tiêu) cần dữ liệu không liên quan đến nhau (tháp và creep). Trong tình huống này, nhiều lập trình viên sẽ cố gắng liên kết hành vi với một phần dữ liệu cần thiết (ví dụ: tháp xử lý nhắm mục tiêu, nhưng không biết về creep), nhưng có một tùy chọn khác: không nhóm hành vi với dữ liệu.

Thay vì làm cho hành vi nhắm mục tiêu trở thành một phương thức của lớp tháp, hãy biến nó thành một hàm miễn phí chấp nhận các tháp và creep làm đối số. Điều này có thể yêu cầu làm cho nhiều thành viên còn lại trong tháp và leo lên các lớp công khai, và điều đó ổn. Ẩn dữ liệu là hữu ích, nhưng nó là một phương tiện, không phải là kết thúc và bạn không nên làm nô lệ cho nó. Ngoài ra, các thành viên tư nhân không phải là cách duy nhất để kiểm soát quyền truy cập vào dữ liệu - nếu dữ liệu không được truyền vào một chức năng và nó không phải là toàn cầu, thì nó thực sự bị ẩn khỏi chức năng đó. Nếu sử dụng kỹ thuật này cho phép bạn tránh dữ liệu toàn cầu, bạn thực sự có thể đang cải thiện việc đóng gói.

Một ví dụ cực đoan của phương pháp này là kiến trúc hệ thống thực thể .

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.