Khớp nối thấp và sự gắn kết chặt chẽ


11

Tất nhiên nó phụ thuộc vào tình hình. Nhưng khi một đối tượng đòn bẩy thấp hơn hoặc hệ thống giao tiếp với một hệ thống cấp cao hơn, các cuộc gọi lại hoặc sự kiện nên được ưu tiên để giữ một con trỏ đến đối tượng cấp cao hơn?

Ví dụ, chúng ta có một worldlớp có một biến thành viên vector<monster> monsters. Khi monsterlớp giao tiếp với world, tôi nên sử dụng hàm gọi lại sau đó hay tôi nên có một con trỏ tới worldlớp bên trong monsterlớp?


Ngoài cách đánh vần trong tiêu đề câu hỏi, nó không thực sự được đặt theo dạng câu hỏi. Tôi nghĩ rằng việc đọc lại nó có thể giúp củng cố những gì bạn đang hỏi ở đây, vì tôi không nghĩ bạn có thể nhận được câu trả lời hữu ích cho câu hỏi này ở dạng hiện tại. Và đó cũng không phải là một câu hỏi về thiết kế trò chơi, đó là một câu hỏi về cấu trúc lập trình (không chắc điều đó có nghĩa là thẻ thiết kế có phù hợp hay không, không thể nhớ chúng ta đã đưa ra 'thiết kế phần mềm' và thẻ)
MrCranky

Câu trả lời:


10

Có ba cách chính mà một lớp có thể nói chuyện với nhau mà không bị liên kết chặt chẽ với nó:

  1. Thông qua chức năng gọi lại.
  2. Thông qua một hệ thống sự kiện.
  3. Thông qua một giao diện.

Ba người có liên quan chặt chẽ với nhau. Một hệ thống sự kiện theo nhiều cách chỉ là một danh sách các cuộc gọi lại. Một cuộc gọi lại ít nhiều là một giao diện với một phương thức duy nhất.

Trong C ++, tôi hiếm khi sử dụng các cuộc gọi lại:

  1. C ++ không hỗ trợ tốt cho các cuộc gọi lại giữ thiscon trỏ của họ , vì vậy thật khó để sử dụng các cuộc gọi lại trong mã hướng đối tượng.

  2. Một cuộc gọi lại về cơ bản là một giao diện một phương thức không thể mở rộng. Theo thời gian, tôi thấy rằng hầu như tôi luôn luôn cần nhiều hơn một phương thức để xác định giao diện đó và một cuộc gọi lại hiếm khi đủ.

Trong trường hợp này, tôi có thể làm một giao diện. Trong câu hỏi của bạn, bạn không thực sự đánh vần những gì monsterthực sự cần phải giao tiếp world. Đoán thử, tôi sẽ làm một cái gì đó như:

class IWorld {
public:
  virtual Monster* getNearbyMonster(const Position & position) = 0;
  virtual Item*    getItemAt(const Position & position) = 0;
};

class Monster {
public:
  void update(IWorld * world) {
    // Do stuff...
  }
}

class World : public IWorld {
public:
  virtual Monster* getNearbyMonster(const Position & position) {
    // ...
  }

  virtual Item*    getItemAt(const Position & position) {
    // ...
  }

  // Lots of other stuff that Monster should not have access to...
}

Ý tưởng ở đây là bạn chỉ đưa vào IWorld(đó là một tên nhảm nhí) mức tối thiểu Monstercần phải truy cập. Quan điểm của nó về thế giới nên càng hẹp càng tốt.


1
+1 Đại biểu (cuộc gọi lại) thường trở nên nhiều hơn khi thời gian trôi qua. Đưa ra một giao diện cho những con quái vật để chúng có thể có được thứ gì đó theo ý kiến ​​của tôi.
Michael Coleman

12

Không sử dụng chức năng gọi lại để ẩn chức năng bạn đang gọi. Nếu bạn đã đặt một chức năng gọi lại và chức năng gọi lại đó sẽ có một và chỉ một chức năng được gán cho nó, thì bạn hoàn toàn không thực sự phá vỡ khớp nối. Bạn chỉ che giấu nó bằng một lớp trừu tượng khác. Bạn không đạt được bất cứ điều gì (ngoài thời gian có thể biên dịch), nhưng bạn đang mất đi sự rõ ràng.

Tôi sẽ không gọi nó là một cách thực hành tốt nhất, nhưng đó là một mô hình phổ biến để có các thực thể được chứa bởi một cái gì đó để có một con trỏ tới cha mẹ của chúng.

Điều đó đang được nói, có thể đáng để bạn sử dụng mẫu giao diện để cung cấp cho quái vật của bạn một tập hợp con hạn chế về chức năng mà chúng có thể gọi trên thế giới.


+1 Theo tôi, cách cho con quái vật là một cách hạn chế để kêu gọi cha mẹ là một cách tốt đẹp.
Michael Coleman

7

Nói chung, tôi cố gắng và tránh các liên kết hai chiều, nhưng nếu tôi phải có chúng, tôi chắc chắn rằng có một phương pháp để tạo ra chúng và một để phá vỡ chúng, để bạn không bao giờ gặp phải mâu thuẫn.

Thường thì bạn có thể tránh liên kết hai chiều hoàn toàn bằng cách truyền dữ liệu khi cần thiết. Một cấu trúc lại tầm thường là làm cho nó thay vì để con quái vật giữ liên kết với thế giới, bạn vượt qua thế giới bằng cách tham khảo các phương pháp quái vật cần nó. Vẫn tốt hơn là chỉ truyền vào một giao diện cho các bit của thế giới mà con quái vật thực sự cần, có nghĩa là con quái vật không dựa vào sự triển khai cụ thể của thế giới. Điều này tương ứng với Nguyên tắc phân chia giao diệnNguyên tắc đảo ngược phụ thuộc , nhưng không bắt đầu đưa ra sự trừu tượng dư thừa mà đôi khi bạn có thể nhận được với các sự kiện, tín hiệu + vị trí, v.v.

Theo một cách nào đó, bạn có thể lập luận rằng sử dụng một cuộc gọi lại là một giao diện nhỏ rất chuyên dụng, và điều đó là tốt. Bạn phải quyết định xem bạn có thể đạt được mục tiêu của mình một cách có ý nghĩa hơn thông qua một tập hợp các phương thức trong một đối tượng giao diện hoặc một số phương thức trong các cuộc gọi lại khác nhau.


3

Tôi cố gắng tránh việc chứa các đối tượng gọi container của họ vì tôi thấy nó dẫn đến sự nhầm lẫn, nó trở nên quá dễ dàng để biện minh, nó sẽ bị lạm dụng và nó tạo ra sự phụ thuộc không thể quản lý được.

Theo tôi, giải pháp lý tưởng là các lớp cấp cao hơn đủ thông minh để quản lý các lớp cấp thấp hơn. Ví dụ, thế giới biết xác định liệu một vụ va chạm giữa quái vật và hiệp sĩ xảy ra mà không biết về người khác cũng tốt hơn tôi so với quái vật hỏi thế giới nếu nó va chạm với một hiệp sĩ.

Một lựa chọn khác trong trường hợp của bạn, tôi có lẽ sẽ tìm ra lý do tại sao lớp quái vật cần biết về đẳng cấp thế giới và rất có thể bạn sẽ thấy rằng có một thứ gì đó trong lớp thế giới có thể được chia thành một lớp của chính nó. ý nghĩa cho lớp quái vật để biết về.


2

Rõ ràng là bạn sẽ không đi xa mà không có sự kiện, nhưng thậm chí trước khi bắt đầu viết (và thiết kế) một hệ thống sự kiện, bạn nên hỏi bạn câu hỏi thực sự: tại sao quái vật lại giao tiếp với đẳng cấp thế giới? Có nên thực sự?

Hãy để một tình huống "cổ điển", một con quái vật tấn công người chơi.

Quái vật đang tấn công: thế giới có thể xác định rất rõ tình huống mà một anh hùng bên cạnh một con quái vật, và bảo quái vật tấn công. Vì vậy, chức năng trong quái vật sẽ là:

void Monster::attack(LivingCreature l)
{
  // Call to combat system
}

Nhưng thế giới (những người đã biết quái vật) không cần phải biết về quái vật. Trên thực tế, quái vật có thể bỏ qua sự tồn tại của Thế giới đẳng cấp, có lẽ tốt hơn.

Điều tương tự khi quái vật đang di chuyển (Tôi để các hệ thống phụ lấy sinh vật và xử lý tính toán / ý định di chuyển cho nó, quái vật chỉ là một túi dữ liệu, nhưng nhiều người sẽ nói đây không phải là OOP thực sự).

Quan điểm của tôi: tất nhiên các sự kiện (hoặc gọi lại) là tuyệt vời, nhưng chúng không phải là câu trả lời duy nhất cho mọi vấn đề bạn sẽ gặp phải.


1

Bất cứ khi nào tôi có thể, tôi cố gắng hạn chế giao tiếp giữa các đối tượng theo mô hình yêu cầu và trả lời. Có một thứ tự ngụ ý một phần trên các đối tượng trong chương trình của tôi sao cho giữa hai đối tượng A và B bất kỳ, có thể có cách để A gọi trực tiếp hoặc gián tiếp một phương thức của B hoặc để B gọi trực tiếp hoặc gián tiếp một phương thức của A , nhưng A và B không bao giờ có thể gọi phương thức của nhau. Đôi khi, tất nhiên, bạn muốn có giao tiếp ngược với người gọi phương thức. Có một vài cách tôi thích để làm điều này, và cả hai đều không phải là cuộc gọi lại.

Một cách là bao gồm nhiều thông tin hơn trong giá trị trả về của lệnh gọi phương thức, có nghĩa là mã máy khách sẽ quyết định phải làm gì với nó sau khi thủ tục trả lại quyền điều khiển cho nó.

Một cách khác là gọi một đối tượng con chung. Nghĩa là, nếu A gọi một phương thức trên B và B cần truyền đạt một số thông tin cho A, B gọi một phương thức trên C, trong đó A và B đều có thể gọi C, nhưng C không thể gọi A hoặc B. Đối tượng A sẽ là chịu trách nhiệm nhận thông tin từ C sau khi B trả lại quyền kiểm soát cho A. Lưu ý rằng điều này thực sự không khác biệt cơ bản so với cách đầu tiên tôi đề xuất. Đối tượng A vẫn chỉ có thể truy xuất thông tin từ giá trị trả về; không có phương thức nào của đối tượng A được gọi bởi B hoặc C. Một biến thể của thủ thuật này là truyền C làm tham số cho phương thức, nhưng các hạn chế về mối quan hệ của C với A và B vẫn được áp dụng.

Bây giờ, câu hỏi quan trọng là tại sao tôi khăng khăng làm mọi thứ theo cách này. Có ba lý do chính:

  • Nó giữ cho các đối tượng của tôi liên kết lỏng lẻo hơn. Các đối tượng của tôi có thể gói gọn các đối tượng khác, nhưng chúng sẽ không bao giờ phụ thuộc vào ngữ cảnh của người gọi và bối cảnh sẽ không bao giờ phụ thuộc vào các đối tượng được đóng gói.
  • Nó giữ cho dòng điều khiển của tôi dễ dàng lý do về. Thật tuyệt khi có thể giả định rằng mã duy nhất có thể thay đổi trạng thái bên selftrong trong khi một phương thức thực thi là một phương thức và không có phương thức nào khác. Đây là cùng một loại lý luận có thể khiến người ta đặt các trường hợp đột biến lên các đối tượng đồng thời.
  • Nó bảo vệ bất biến trên dữ liệu được đóng gói của các đối tượng của tôi. Các phương thức công khai được phép phụ thuộc vào các bất biến và các bất biến đó có thể bị vi phạm nếu một phương thức có thể được gọi bên ngoài trong khi phương thức khác đang thực thi.

Tôi không chống lại tất cả các sử dụng của cuộc gọi lại. Để tuân thủ chính sách của tôi về việc không bao giờ "gọi người gọi", nếu một đối tượng A gọi một phương thức trên B và chuyển một cuộc gọi lại cho nó, cuộc gọi lại có thể không thay đổi trạng thái bên trong của A và bao gồm các đối tượng được bao bọc bởi A và các đối tượng trong bối cảnh của A. Nói cách khác, cuộc gọi lại chỉ có thể gọi các phương thức trên các đối tượng được cung cấp cho nó bởi B. Cuộc gọi lại, thực tế, nằm trong cùng các hạn chế mà B là.

Một kết thúc lỏng lẻo cuối cùng để gắn kết là tôi sẽ cho phép gọi bất kỳ chức năng thuần túy nào, bất kể thứ tự từng phần này mà tôi đã nói đến. Các hàm thuần túy khác một chút so với các phương thức ở chỗ chúng không thể thay đổi hoặc phụ thuộc vào trạng thái có thể thay đổi hoặc tác dụng phụ, do đó không phải lo lắng về các vấn đề khó hiểu.


0

Cá nhân? Tôi chỉ sử dụng một singleton.

Yeah, okay, thiết kế xấu, không hướng đối tượng, vv Bạn biết gì không? Tôi không quan tâm . Tôi đang viết một trò chơi, không phải là một chương trình giới thiệu công nghệ. Không ai sẽ chấm điểm cho tôi về mã. Mục đích là để tạo ra một trò chơi thú vị, và mọi thứ cản đường tôi sẽ dẫn đến một trò chơi ít vui hơn.

Bạn sẽ bao giờ có hai thế giới chạy cùng một lúc? Có lẽ! Có thể bạn sẽ. Nhưng trừ khi bạn có thể nghĩ về tình huống đó ngay bây giờ, bạn có thể sẽ không.

Vì vậy, giải pháp của tôi: tạo ra một thế giới đơn lẻ. Gọi chức năng trên đó. Được thực hiện với toàn bộ mớ hỗn độn. Bạn có thể chuyển một tham số bổ sung cho mọi chức năng - và không có lỗi, đó là nơi dẫn đến điều này. Hoặc bạn chỉ có thể viết mã hoạt động.

Làm theo cách này đòi hỏi một chút kỷ luật để dọn dẹp mọi thứ khi nó trở nên lộn xộn (đó là "khi", không phải "nếu") nhưng không có cách nào bạn có thể ngăn chặn mã bị lộn xộn - hoặc bạn có vấn đề về spaghetti, hoặc hàng ngàn -vấn đề trừu tượng-trừu tượng. Ít nhất theo cách này bạn không viết một lượng lớn mã không cần thiết.

Và nếu bạn quyết định không muốn độc thân nữa, việc loại bỏ nó thường khá đơn giản. Thực hiện một số công việc, vượt qua một tỷ thông số xung quanh, nhưng đó là những tham số bạn cần phải vượt qua.


2
Có lẽ tôi muốn nói ngắn gọn hơn một chút: "Đừng ngại tái cấu trúc".
Tetrad

Singletons là ác! Quả cầu tốt hơn nhiều. Tất cả các đức tính độc thân mà bạn liệt kê là giống nhau cho toàn cầu. Tôi sử dụng các con trỏ toàn cục (thực sự là các hàm toàn cục trả về các tham chiếu) cho các hệ thống con khác nhau của tôi và khởi tạo / hủy chúng trong hàm chính của tôi. Con trỏ toàn cầu tránh các vấn đề trật tự khởi tạo, treo lủng lẳng các singletons trong quá trình phá bỏ, xây dựng các singletons không tầm thường, v.v ... Tôi nhắc lại singletons là ác.
deft_code

@Tetrad, rất đồng ý. Đó là một trong những kỹ năng tốt nhất bạn có thể có.
ZorbaTHut
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.