Làm thế nào để giải quyết sự phụ thuộc lẫn nhau trong mã C ++ của tôi?


10

Trong dự án C ++ của tôi, tôi có hai lớp ParticleContact. Trong Particlelớp, tôi có một biến thành viên std::vector<Contact> contactschứa tất cả các liên hệ của một Particleđối tượng và các hàm thành viên tương ứng getContacts()addContact(Contact cont). Do đó, trong "Particle.h", tôi bao gồm "Contact.h".

Trong Contactlớp, tôi muốn thêm mã vào hàm tạo cho lệnh Contactđó sẽ gọi Particle::addContact(Contact cont), để nó contactsđược cập nhật cho cả các Particleđối tượng mà Contactđối tượng đang được thêm vào. Vì vậy, tôi sẽ phải đưa "Particle.h" vào "Contact.cpp".

Câu hỏi của tôi là liệu điều này có được chấp nhận / thực hành mã hóa tốt hay không và nếu không, cách nào tốt hơn để thực hiện những gì tôi đang cố gắng đạt được (chỉ cần đặt, tự động cập nhật danh sách các liên hệ cho một hạt cụ thể bất cứ khi nào có liên hệ mới được tạo ra).


Các lớp này sẽ được liên kết với nhau bởi một Networklớp sẽ có các hạt N ( std::vector<Particle> particles) và Nc contact ( std::vector<Contact> contacts). Nhưng tôi muốn có thể có các hàm như particles[0].getContacts()- có ổn không khi có các hàm như vậy trong Particlelớp trong trường hợp này, hoặc có một "cấu trúc" liên kết tốt hơn trong C ++ cho mục đích này (của hai lớp liên quan được sử dụng trong lớp khác) .


Tôi có thể cần một sự thay đổi quan điểm ở đây trong cách tôi đang tiếp cận điều này. Do hai lớp được kết nối bởi một Networkđối tượng lớp, nên tổ chức mã / lớp điển hình có thông tin kết nối được kiểm soát hoàn toàn bởi Networkđối tượng (trong đó một đối tượng Hạt không nên biết về các liên hệ của nó và do đó, nó không nên có getContacts()thành viên chức năng). Sau đó, để biết những liên hệ nào mà một hạt cụ thể có, tôi sẽ cần lấy thông tin đó thông qua Networkđối tượng (ví dụ: sử dụng network.getContacts(Particle particle)).

Nó sẽ ít điển hình hơn (thậm chí có thể không khuyến khích) thiết kế lớp C ++ cho một đối tượng Hạt để có kiến ​​thức đó, (có nghĩa là có nhiều cách để truy cập thông tin đó - thông qua đối tượng Mạng hoặc đối tượng Hạt, có vẻ thuận tiện hơn )?


4
Dưới đây là bài nói chuyện từ cppcon 2017 - Ba lớp tiêu đề: youtu.be/su9ittf-ozk
Robert Andrzejuk

3
Các câu hỏi có chứa các từ như "tốt nhất", "tốt hơn" và "chấp nhận được" là không thể trả lời được trừ khi bạn có thể nêu các tiêu chí đánh giá cụ thể của mình.
Robert Harvey

Cảm ơn bạn đã chỉnh sửa, mặc dù việc thay đổi từ ngữ của bạn thành "điển hình" chỉ khiến nó trở thành một câu hỏi phổ biến. Có nhiều lý do tại sao mã hóa được thực hiện bằng cách này hay cách khác, và trong khi sự phổ biến có thể là một dấu hiệu cho thấy một kỹ thuật là "tốt" (đối với một số định nghĩa của "tốt"), nó cũng có thể là một dấu hiệu của việc nuôi cấy hàng hóa.
Robert Harvey

@RobertHarvey Tôi đã loại bỏ "tốt hơn" và "xấu" trong phần cuối cùng của tôi. Tôi cho rằng tôi đang yêu cầu cách tiếp cận điển hình (thậm chí được ưa chuộng / khuyến khích) khi bạn có một Networkđối tượng lớp có chứa Particlecác đối tượng và Contactđối tượng. Với kiến ​​thức cơ bản đó, sau đó tôi có thể cố gắng đánh giá xem nó có phù hợp với nhu cầu cụ thể của mình hay không, vẫn đang được khám phá / phát triển khi tôi đi cùng trong dự án.
AnInquiringMind

@RobertHarvey Tôi cho rằng tôi đủ mới để viết các dự án C ++ hoàn toàn từ đầu rằng tôi ổn với việc học những gì "điển hình" và "phổ biến". Hy vọng rằng tôi sẽ đạt được cái nhìn sâu sắc vào một lúc nào đó để có thể nhận ra lý do tại sao một triển khai khác thực sự tốt hơn, nhưng bây giờ, tôi chỉ muốn chắc chắn rằng tôi không tiếp cận điều này theo cách hoàn toàn xương!
AnInquiringMind

Câu trả lời:


17

Có hai phần trong câu hỏi của bạn.

Phần đầu tiên là tổ chức các tệp tiêu đề C ++ và các tệp nguồn. Điều này được giải quyết bằng cách sử dụng khai báo chuyển tiếp và tách khai báo lớp (đặt chúng vào tệp tiêu đề) và thân phương thức (đặt chúng vào tệp nguồn). Hơn nữa, trong một số trường hợp, người ta có thể áp dụng thành ngữ Pimpl ("con trỏ để thực hiện") để giải quyết các trường hợp khó hơn. Sử dụng con trỏ sở hữu chung ( shared_ptr), con trỏ sở hữu đơn ( unique_ptr) và con trỏ không sở hữu (con trỏ thô, tức là "dấu hoa thị") theo thông lệ tốt nhất.

Phần thứ hai là làm thế nào để mô hình hóa các đối tượng có liên quan đến nhau dưới dạng biểu đồ . Các biểu đồ chung không phải là DAG (biểu đồ chu kỳ có hướng) không có cách thể hiện quyền sở hữu giống như cây. Thay vào đó, các nút và kết nối đều là siêu dữ liệu thuộc về một đối tượng đồ thị. Trong trường hợp này, không thể mô hình hóa mối quan hệ kết nối nút dưới dạng tập hợp. Các nút không "kết nối" riêng; các kết nối không "nút" riêng. Thay vào đó, chúng là các hiệp hội và cả các nút và kết nối được "sở hữu" bởi biểu đồ. Biểu đồ cung cấp các phương thức truy vấn và thao tác hoạt động trên các nút và kết nối.


Cảm ơn vì sự trả lời! Tôi thực sự có một lớp Mạng sẽ có các hạt N và các tiếp điểm Nc. Nhưng tôi muốn có thể có các hàm như particles[0].getContacts()- bạn có gợi ý trong đoạn cuối của bạn rằng tôi không nên có các hàm như vậy trong Particlelớp không, hoặc cấu trúc hiện tại có ổn không vì chúng vốn có liên quan / liên kết với nhau Network? Có một "cấu trúc" liên kết tốt hơn trong C ++ trong trường hợp này không?
AnInquiringMind

1
Nói chung, Mạng có trách nhiệm biết về mối quan hệ giữa các đối tượng. Ví dụ, nếu bạn sử dụng danh sách kề, thì hạt network.particle[p]sẽ có tương ứng network.contacts[p]với các chỉ số của các tiếp điểm của nó. Mặt khác, Mạng và Hạt bằng cách nào đó cả hai đều theo dõi cùng một thông tin.
Vô dụng

@ Vô dụng Vâng, đó là nơi tôi không chắc chắn về cách tiến hành. Vì vậy, bạn đang nói rằng Particleđối tượng không nên biết về các liên hệ của nó (vì vậy tôi không nên có getContacts()chức năng thành viên) và thông tin đó chỉ nên đến từ bên trong Networkđối tượng? Nó có phải là thiết kế lớp C ++ không tốt cho một Particleđối tượng để có kiến ​​thức đó (nghĩa là có nhiều cách để truy cập thông tin đó - thông qua Networkđối tượng hoặc Particleđối tượng, cách nào có vẻ thuận tiện hơn)? Cái sau dường như có ý nghĩa hơn với tôi, nhưng có lẽ tôi cần thay đổi quan điểm của mình về điều này.
AnInquiringMind

1
@PhysicsCodingEnthusiast: Vấn đề với Particleviệc biết bất cứ điều gì về Contacts hoặc Networks là nó ràng buộc bạn với một cách cụ thể để thể hiện mối quan hệ đó. Tất cả ba lớp có thể phải đồng ý. Nếu thay vào đó Networklà người duy nhất biết hoặc quan tâm, thì đó chỉ là một lớp cần thay đổi nếu bạn quyết định một đại diện khác là tốt hơn.
cHao

@cHao Được rồi, điều này có ý nghĩa. Vì vậy ParticleContactnên tách biệt hoàn toàn, và sự liên kết giữa chúng được xác định bởi Networkđối tượng. Hoàn toàn chắc chắn, đây là (có lẽ) ý nghĩa của @rwong khi anh ấy / cô ấy viết, "cả hai nút và kết nối đều được" sở hữu "bởi biểu đồ. Biểu đồ cung cấp các phương thức truy vấn và thao tác hoạt động trên các nút và kết nối." , đúng?
AnInquiringMind

5

Nếu tôi hiểu bạn đúng, cùng một đối tượng tiếp xúc thuộc về nhiều hơn một đối tượng hạt, vì nó đại diện cho một loại tiếp xúc vật lý giữa hai hoặc nhiều hạt, phải không?

Vì vậy, điều đầu tiên mà tôi nghĩ là nghi vấn là tại sao Particlecó một biến thành viên std::vector<Contact>? Nó nên là một std::vector<Contact*>hoặc std::vector<std::shared_ptr<Contact> >thay thế. addContactsau đó nên có chữ ký khác nhau như addContact(Contact *cont)hoặc addContact(std::shared_ptr<Contact> cont)thay vào đó.

Điều này khiến không cần thiết phải bao gồm "Contact.h" trong "Particle.h", một tuyên bố chuyển tiếp class Contacttrong "Particle.h" và bao gồm "Contact.h" trong "Particle.cpp" là đủ.

Sau đó, câu hỏi về các nhà xây dựng. Bạn muốn một cái gì đó như

 Contact(Particle &p1, Particle &p2)
 {
      p1.addContact(this);
      p2.addContact(this);
 }

Đúng? Thiết kế này là ổn, miễn là chương trình của bạn luôn biết các hạt liên quan tại thời điểm khi một đối tượng tiếp xúc phải được tạo.

Lưu ý, nếu bạn đi theo std::vector<Contact*>lộ trình, bạn phải đầu tư một số suy nghĩ về tuổi thọ và quyền sở hữu của các Contactđối tượng. Không có hạt nào "sở hữu" các liên hệ của nó, một liên hệ có thể sẽ phải bị xóa chỉ khi cả hai Particleđối tượng liên quan bị phá hủy. Sử dụng std::shared_ptr<Contact>thay thế sẽ giải quyết vấn đề này cho bạn tự động. Hoặc bạn để một đối tượng "bối cảnh xung quanh" nắm quyền sở hữu các hạt và địa chỉ liên hệ (như được đề xuất bởi @rwong) và quản lý vòng đời của chúng.


Tôi không thấy lợi ích của addContact(const std::shared_ptr<Contact> &cont)hơn addContact(std::shared_ptr<Contact> cont)?
Caleth

@Caleth: điều này đã được thảo luận ở đây: stackoverflow.com/questions/3310737/ trộm - "const" không thực sự quan trọng ở đây, nhưng chuyển các đối tượng bằng tham chiếu (và vô hướng theo giá trị) là thành ngữ tiêu chuẩn trong C ++.
Doc Brown

2
Nhiều câu trả lời trong số đó dường như đến từ một movemô hình trước
Caleth

@Caleth: ok, để giữ cho tất cả các nitopperers hạnh phúc, tôi đã thay đổi phần khá quan trọng này trong câu trả lời của tôi.
Doc Brown

1
@PhysicsCodingEnthusiast: không, đây là điều quan trọng nhất về việc tạo particle1.getContacts()particle2.getContacts()phân phối cùng một Contactđối tượng thể hiện sự tiếp xúc vật lý giữa particle1particle2, chứ không phải hai đối tượng khác nhau. Tất nhiên, người ta có thể cố gắng thiết kế hệ thống theo cách không thành vấn đề nếu có hai Contactđối tượng có sẵn cùng một lúc đại diện cho cùng một liên hệ vật lý. Điều này sẽ liên quan đến việc làm cho Contactbất biến, nhưng bạn có chắc chắn rằng đây là những gì bạn muốn?
Doc Brown

0

Có, những gì bạn mô tả là một cách rất chấp nhận để đảm bảo rằng mọi Contacttrường hợp đều nằm trong danh sách liên hệ của a Particle.


Cảm ơn vì sự trả lời. Tôi đã đọc một số gợi ý rằng nên tránh một cặp các lớp phụ thuộc lẫn nhau (ví dụ, trong "Mô hình thiết kế và giá cả phái sinh C ++" của MS Joshi), nhưng dường như điều đó không nhất thiết phải đúng? Vì tò mò, có lẽ có một cách khác để thực hiện cập nhật tự động này mà không cần phụ thuộc lẫn nhau?
AnInquiringMind

4
@PhysicsCodingEnthusiast: Có các lớp phụ thuộc lẫn nhau tạo ra tất cả các loại khó khăn và bạn nên cố gắng tránh chúng. Nhưng đôi khi, hai lớp có liên quan chặt chẽ với nhau đến mức loại bỏ sự phụ thuộc lẫn nhau giữa chúng gây ra nhiều vấn đề hơn chính sự phụ thuộc lẫn nhau.
Bart van Ingen Schenau

0

Những gì bạn đã làm là chính xác.

Một cách khác ... Nếu mục đích là để đảm bảo rằng mọi thứ đều Contactnằm trong danh sách, thì bạn có thể:

  • tạo khối Contact(nhà xây dựng riêng),
  • chuyển tiếp khai báo Particlelớp,
  • làm cho Particlelớp một người bạn của Contact,
  • trong việc Particletạo ra một phương thức nhà máy tạo ra mộtContact

Sau đó, bạn không cần phải bao gồm particle.htrongcontact


Cảm ơn vì sự trả lời! Đó dường như là một cách hữu ích để thực hiện điều này. Chỉ cần tự hỏi, với chỉnh sửa của tôi cho câu hỏi ban đầu liên quan đến Networklớp, điều đó có thay đổi cấu trúc được đề xuất không, hay nó vẫn sẽ giống nhau?
AnInquiringMind

Sau khi bạn đã cập nhật câu hỏi của bạn, nó đang thay đổi phạm vi. ... Bây giờ bạn đang hỏi về kiến ​​trúc của ứng dụng của bạn, khi trước đó là về một vấn đề kỹ thuật.
Robert Andrzejuk

0

Một tùy chọn khác mà bạn có thể xem xét là làm cho hàm tạo Liên hệ chấp nhận tham chiếu Hạt tham chiếu. Điều này sẽ cho phép một Liên hệ tự thêm vào bất kỳ vùng chứa nào addContact(Contact).

template<class Container>
Contact(/*parameters*/, Container& container)
{
  container.addContact(*this);
}
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.