Một cách tốt để thể hiện mối quan hệ nhiều-nhiều giữa hai lớp là gì?


10

Giả sử tôi có hai loại đối tượng, A và B. Mối quan hệ giữa chúng là nhiều đối tượng, nhưng cả hai đều không phải là chủ sở hữu của đối tượng kia.

Cả hai trường hợp A và B cần phải biết về kết nối; nó không chỉ là một cách.

Vì vậy, chúng ta có thể làm điều này:

class A
{
    ...

    private: std::vector<B *> Bs;
}

class B
{
    private: std::vector<A *> As;
}

Câu hỏi của tôi là: nơi tôi đặt các chức năng để tạo và phá hủy các kết nối?

Nó có nên là A :: Đính kèm (B), sau đó cập nhật A :: Bs và B :: dưới dạng vectơ?

Hoặc nên là B :: Đính kèm (A), có vẻ hợp lý như nhau.

Không ai trong số họ cảm thấy đúng. Nếu tôi ngừng làm việc với mã và quay lại sau một tuần, tôi chắc chắn tôi sẽ không thể nhớ lại nếu tôi nên làm A.Attach (B) hoặc B.Attach (A).

Có lẽ nó nên là một chức năng như thế này:

CreateConnection(A, B);

Nhưng việc tạo ra một hàm toàn cầu dường như cũng không mong muốn, vì đó là một hàm đặc biệt chỉ làm việc với các lớp A và B.

Một câu hỏi khác: nếu tôi gặp phải vấn đề / yêu cầu này thường xuyên, bằng cách nào đó tôi có thể đưa ra giải pháp chung cho nó không? Có lẽ một lớp TwoWayConnection mà tôi có thể rút ra hoặc sử dụng trong các lớp có chung mối quan hệ này?

Một số cách tốt để xử lý tình huống này là gì ... Tôi biết cách xử lý tình huống "C sở hữu D" một-nhiều khá tốt, nhưng cách này là khó khăn hơn.

Chỉnh sửa: Chỉ để làm cho nó rõ ràng hơn, câu hỏi này không liên quan đến vấn đề sở hữu. Cả A và B đều thuộc sở hữu của một số đối tượng Z khác và Z xử lý tất cả các vấn đề về quyền sở hữu. Tôi chỉ quan tâm đến cách tạo / xóa liên kết nhiều-nhiều giữa A và B.


Tôi sẽ tạo một Z :: Đính kèm (A, B), vì vậy, nếu Z sở hữu hai loại đối tượng, "cảm thấy đúng", để anh ta tạo kết nối. Trong ví dụ (không tốt) của tôi, Z sẽ là kho lưu trữ cho cả A và B. Tôi không thể thấy một cách khác nếu không có ví dụ điển hình.
Machado

1
Nói chính xác hơn, A và B có thể có chủ sở hữu khác nhau. Có lẽ A thuộc sở hữu của Z, trong khi B thuộc sở hữu của X, lần lượt thuộc sở hữu của Z. Như bạn có thể thấy, sau đó nó thực sự trở nên hỗn độn khi cố gắng quản lý liên kết ở nơi khác. A và B cần có thể truy cập nhanh vào danh sách các cặp được liên kết.
Dmitri Shuralyov

Nếu bạn tò mò về tên thật của A và B trong tình huống của tôi, thì chúng là PointerGestureRecognizer. Con trỏ được sở hữu và quản lý bởi lớp InputManager. GestureRecognors được sở hữu bởi các phiên bản Widget, được sở hữu bởi một phiên bản Màn hình được sở hữu bởi một phiên bản Ứng dụng. Các con trỏ được gán cho GestureRecognators để chúng có thể cung cấp dữ liệu đầu vào thô cho chúng, nhưng GestureRecognifier cần phải biết có bao nhiêu con trỏ hiện đang được liên kết với chúng (để phân biệt 1 ngón tay với 2 cử chỉ ngón tay, v.v.).
Dmitri Shuralyov

Bây giờ tôi nghĩ về điều đó nhiều hơn, có vẻ như tôi chỉ đang cố gắng thực hiện nhận dạng cử chỉ dựa trên khung (chứ không phải dựa trên con trỏ). Vì vậy, tôi cũng có thể chuyển các sự kiện sang cử chỉ khung hình tại một thời điểm chứ không phải là con trỏ một lúc. Hừm.
Dmitri Shuralyov

Câu trả lời:


2

Một cách là thêm một Attach()phương thức công khai và cũng là một AttachWithoutReciprocating()phương thức được bảo vệ cho mỗi lớp. Làm AB kết bạn để Attach()phương thức của họ có thể gọi cho người khác AttachWithoutReciprocating():

A::Attach(B &b) {
    Bs.push_back(&b);
    b.AttachWithoutReciprocating(*this);
}

A::AttachWithoutReciprocating(B &b) {
    Bs.push_back(&b);
}

Nếu bạn thực hiện các phương thức tương tự cho B, bạn sẽ không phải nhớ lớp nào để gọi Attach().

Tôi chắc chắn rằng bạn có thể kết hợp hành vi đó trong một MutuallyAttachablelớp mà cả hai ABkế thừa từ đó, do đó tránh lặp lại chính mình và ghi điểm thưởng vào Ngày phán xét. Nhưng ngay cả cách tiếp cận thực hiện không phức tạp cũng sẽ hoàn thành công việc.


1
Điều này nghe có vẻ chính xác như giải pháp mà tôi đã nghĩ đến ở phía sau tâm trí của tôi (nghĩa là đặt Đính kèm () vào cả A và B). Tôi cũng thực sự thích giải pháp DRY hơn (tôi thực sự không thích mã trùng lặp) khi có một lớp MutualAttachable mà bạn có thể rút ra từ bất cứ khi nào hành vi này là cần thiết (tôi thấy trước khá thường xuyên). Chỉ cần nhìn thấy giải pháp tương tự được cung cấp bởi người khác làm tôi tự tin hơn nhiều về nó, cảm ơn!
Dmitri Shuralyov

Chấp nhận điều này bởi vì IMO đó là giải pháp tốt nhất được cung cấp cho đến nay. Cảm ơn! Bây giờ tôi sẽ triển khai lớp MutualAttachable đó và báo cáo ở đây nếu tôi gặp phải bất kỳ vấn đề không lường trước nào (một số khó khăn cũng có thể xảy ra do thừa kế hiện tại mà tôi chưa có nhiều kinh nghiệm).
Dmitri Shuralyov

Mọi thứ diễn ra suôn sẻ. Tuy nhiên, theo nhận xét cuối cùng của tôi về câu hỏi ban đầu, tôi bắt đầu nhận ra rằng tôi không cần chức năng này cho những gì tôi đã hình dung ban đầu. Thay vì theo dõi các kết nối 2 chiều này, tôi sẽ chỉ truyền mỗi cặp như một tham số cho hàm cần nó. Tuy nhiên, điều này vẫn có thể có ích sau một thời gian.
Dmitri Shuralyov

Đây là MutuallyAttachablelớp tôi đã viết cho tác vụ này, nếu có ai muốn sử dụng lại nó: goo.gl/VY9RB (Pastebin) Chỉnh sửa: Mã này có thể được cải thiện bằng cách biến nó thành một lớp mẫu.
Dmitri Shuralyov

1
Tôi đã đặt MutuallyAttachable<T, U>mẫu lớp tại Gist. gist.github.com/3308058
Dmitri Shuralyov

5

Có thể cho bản thân mối quan hệ có các thuộc tính bổ sung?

Nếu vậy, thì nó phải là một lớp riêng biệt.

Nếu không, thì bất kỳ cơ chế quản lý danh sách đối ứng sẽ đủ.


Nếu đó là một lớp riêng biệt, làm thế nào A biết các thể hiện được liên kết của B và B biết các thể hiện được liên kết của A? Vào thời điểm đó, cả A và B sẽ cần phải có mối quan hệ 1-nhiều với C, phải không?
Dmitri Shuralyov

1
Cả A và B đều cần một tham chiếu đến tập hợp các thể hiện C; C phục vụ cùng một mục đích như một 'bảng cầu' trong mô hình quan hệ
Steven A. Lowe

1

Thông thường khi tôi gặp phải những tình huống như thế này cảm thấy khó xử, nhưng tôi không thể hiểu tại sao, đó là vì tôi đang cố gắng đưa thứ gì đó vào lớp sai. Hầu như luôn luôn có một cách để di chuyển một số chức năng ra khỏi lớp ABlàm cho mọi thứ rõ ràng hơn.

Một ứng cử viên để chuyển hiệp hội sang là mã tạo ra hiệp hội ở vị trí đầu tiên. Có lẽ thay vì gọi attach()nó chỉ đơn giản là lưu trữ một danh sách std::pair<A, B>. Nếu có nhiều nơi tạo liên kết, nó có thể được trừu tượng hóa thành một lớp.

Một ứng cử viên khác cho việc di chuyển là đối tượng sở hữu các đối tượng liên quan, Ztrong trường hợp của bạn.

Một ứng cử viên phổ biến khác để di chuyển liên kết đến là mã thường gọi các phương thức Ahoặc Bđối tượng. Ví dụ, thay vì gọi a->foo(), trong nội bộ thực hiện một cái gì đó với tất cả Bcác đối tượng được liên kết , nó đã biết các liên kết và các cuộc gọia->foo(b) cho từng đối tượng. Một lần nữa, nếu nhiều nơi gọi nó, nó có thể được trừu tượng hóa thành một lớp duy nhất.

Rất nhiều lần các đối tượng tạo, sở hữu và sử dụng liên kết xảy ra là cùng một đối tượng. Điều đó có thể làm cho việc tái cấu trúc rất dễ dàng.

Phương pháp cuối cùng là rút ra mối quan hệ từ các mối quan hệ khác. Ví dụ, anh chị em là mối quan hệ nhiều-nhiều, nhưng thay vì giữ một danh sách các chị em trong lớp anh em và ngược lại, bạn xuất phát mối quan hệ đó từ mối quan hệ của cha mẹ. Ưu điểm khác của phương pháp này là nó tạo ra một nguồn sự thật duy nhất. Không có cách nào có thể để lỗi hoặc lỗi thời gian chạy tạo ra sự khác biệt giữa danh sách anh, chị và cha, bởi vì bạn chỉ có một danh sách.

Kiểu tái cấu trúc này không phải là một công thức ma thuật hoạt động mọi lúc. Bạn vẫn phải thử nghiệm cho đến khi bạn tìm thấy một sự phù hợp tốt cho từng hoàn cảnh riêng lẻ, nhưng tôi đã thấy nó hoạt động được khoảng 95% thời gian. Những lần còn lại bạn chỉ phải sống với sự vụng về.


0

Nếu tôi đang thực hiện điều này, tôi sẽ đặt Đính kèm vào cả A và B. Bên trong phương thức Đính kèm, sau đó tôi sẽ gọi Đính kèm trên đối tượng được truyền vào. Bằng cách đó bạn có thể gọi A.Attach (B) hoặc B.Attach ( A). Cú pháp của tôi có thể không đúng, tôi đã không sử dụng c ++ trong nhiều năm:

class A {
  private: std::vector<B *> Bs;

  void Attach (B* obj) {
    // Only add obj if it's not in the vector
    if (std::find(Bs.begin(), Bs.end(), obj) == Bs.end()) {
      //Add obj to the vector
      Bs.push_back(obj);

      // Attach this class to obj
      obj->Attach(this);
    }
  }    
}

class B {
  private: std::vector<A *> As;

  void Attach (A* obj) {
    // Only add obj if it's not in the vector
    if (std::find(As.begin(), As.end(), obj) == As.end()) {
      //Add obj to the vector
      As.push_back(obj);

      // Attach this class to obj
      obj->Attach(this);
    }
  }    
}

Một phương pháp thay thế sẽ là tạo một lớp thứ 3, C quản lý một danh sách tĩnh các cặp AB.


Chỉ là một câu hỏi liên quan đến đề xuất thay thế của bạn về việc có lớp C thứ 3 để quản lý danh sách các cặp AB: Làm thế nào A và B biết các thành viên của cặp đó? Mỗi A và B sẽ cần một liên kết đến (đơn) C, sau đó yêu cầu C tìm các cặp thích hợp? B :: Foo () {std :: vector <A *> As = C.GetAs (này); / * Làm gì đó với As ... * /} Hay bạn đã hình dung ra điều gì khác? Bởi vì điều này có vẻ khủng khiếp.
Dmitri Shuralyov
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.