Cách tốt nhất để khởi tạo tham chiếu của trẻ với cha mẹ là gì?


35

Tôi đang phát triển một mô hình đối tượng có nhiều lớp cha / con khác nhau. Mỗi đối tượng con có một tham chiếu đến đối tượng cha của nó. Tôi có thể nghĩ ra (và đã thử) một số cách để khởi tạo tham chiếu cha mẹ, nhưng tôi thấy những hạn chế đáng kể đối với mỗi phương pháp. Đưa ra các cách tiếp cận được mô tả dưới đây là tốt nhất ... hoặc thậm chí còn tốt hơn.

Tôi sẽ không đảm bảo mã bên dưới biên dịch, vì vậy hãy thử xem ý định của tôi nếu mã không đúng về mặt cú pháp.

Lưu ý rằng một số hàm tạo của lớp con tôi có các tham số (không phải là cha mẹ) mặc dù tôi không luôn hiển thị bất kỳ.

  1. Người gọi có trách nhiệm thiết lập cha mẹ và thêm vào cùng một cha mẹ.

    class Child {
      public Child(Parent parent) {Parent=parent;}
      public Parent Parent {get; private set;}
    }
    class Parent {
      // singleton child
      public Child Child {get; set;}
      //children
      private List<Child> _children = new List<Child>();
      public List<Child> Children { get {return _children;} }
    }
    

    Nhược điểm: thiết lập cha mẹ là một quá trình hai bước cho người tiêu dùng.

    var child = new Child(parent);
    parent.Children.Add(child);
    

    Nhược điểm: dễ bị lỗi. Người gọi có thể thêm con vào một cha mẹ khác so với được sử dụng để khởi tạo con.

    var child = new Child(parent1);
    parent2.Children.Add(child);
    
  2. Phụ huynh xác minh rằng người gọi thêm con vào cha mẹ đã được khởi tạo.

    class Child {
      public Child(Parent parent) {Parent = parent;}
      public Parent Parent {get; private set;}
    }
    class Parent {
      // singleton child
      private Child _child;
      public Child Child {
        get {return _child;}
        set {
          if (value.Parent != this) throw new Exception();
          _child=value;
        }
      }
      //children
      private List<Child> _children = new List<Child>();
      public ReadOnlyCollection<Child> Children { get {return _children;} }
      public void AddChild(Child child) {
        if (child.Parent != this) throw new Exception();
        _children.Add(child);
      }
    }
    

    Nhược điểm: Người gọi vẫn có quy trình hai bước để thiết lập cha mẹ.

    Nhược điểm: kiểm tra thời gian chạy - làm giảm hiệu suất và thêm mã vào mỗi add / setter.

  3. Cha mẹ đặt tham chiếu cha mẹ của con (cho chính nó) khi con được thêm / gán cho cha mẹ. Cha mẹ setter là nội bộ.

    class Child {
      public Parent Parent {get; internal set;}
    }
    class Parent {
      // singleton child
      private Child _child;
      public Child Child {
        get {return _child;}
        set {
          value.Parent = this;
          _child = value;
        }
      }
      //children
      private List<Child> _children = new List<Child>();
      public ReadOnlyCollection<Child> Children { get {return _children;} }
      public void AddChild(Child child) {
        child.Parent = this;
        _children.Add(child);
      }
    }
    

    Nhược điểm: Đứa trẻ được tạo ra mà không có tài liệu tham khảo cha mẹ. Đôi khi khởi tạo / xác nhận yêu cầu cha mẹ có nghĩa là một số khởi tạo / xác nhận phải được thực hiện trong trình thiết lập cha mẹ của con. Các mã có thể nhận được phức tạp. Sẽ dễ dàng hơn nhiều để thực hiện đứa trẻ nếu nó luôn có tài liệu tham khảo cha mẹ của nó.

  4. Cha mẹ trưng bày các phương thức thêm vào nhà máy để một đứa trẻ luôn có một tài liệu tham khảo cho cha mẹ. Ctor trẻ em là nội bộ. Phụ huynh setter là riêng tư.

    class Child {
      internal Child(Parent parent, init-params) {Parent = parent;}
      public Parent Parent {get; private set;}
    }
    class Parent {
      // singleton child
      public Child Child {get; private set;}
      public void CreateChild(init-params) {
          var child = new Child(this, init-params);
          Child = value;
      }
      //children
      private List<Child> _children = new List<Child>();
      public ReadOnlyCollection<Child> Children { get {return _children;} }
      public Child AddChild(init-params) {
        var child = new Child(this, init-params);
        _children.Add(child);
        return child;
      }
    }
    

    Nhược điểm: Không thể sử dụng cú pháp khởi tạo như new Child(){prop = value}. Thay vào đó phải làm:

    var c = parent.AddChild(); 
    c.prop = value;

    Nhược điểm: Phải nhân đôi các tham số của hàm tạo con trong các phương thức add-Factory.

    Nhược điểm: Không thể sử dụng trình thiết lập thuộc tính cho một đứa trẻ độc thân. Có vẻ khập khiễng rằng tôi cần một phương pháp để đặt giá trị nhưng cung cấp quyền truy cập đọc thông qua một getter thuộc tính. Nó bị lệch.

  5. Con tự thêm vào cha mẹ được tham chiếu trong hàm tạo của nó. Ctor trẻ em là công khai. Không có quyền truy cập công khai từ cha mẹ.

    //singleton
    class Child{
      public Child(ParentWithChild parent) {
        Parent = parent;
        Parent.Child = this;
      }
      public ParentWithChild Parent {get; private set;}
    }
    class ParentWithChild {
      public Child Child {get; internal set;}
    }
    
    //children
    class Child {
      public Child(ParentWithChildren parent) {
        Parent = parent;
        Parent._children.Add(this);
      }
      public ParentWithChildren Parent {get; private set;}
    }
    class ParentWithChildren {
      internal List<Child> _children = new List<Child>();
      public ReadOnlyCollection<Child> Children { get {return _children;} }
    }

    Nhược điểm: cú pháp gọi là không tốt. Thông thường, người ta gọi một addphương thức trên cha mẹ thay vì chỉ tạo một đối tượng như thế này:

    var parent = new ParentWithChildren();
    new Child(parent); //adds child to parent
    new Child(parent);
    new Child(parent);

    Và thiết lập một thuộc tính thay vì chỉ tạo một đối tượng như thế này:

    var parent = new ParentWithChild();
    new Child(parent); // sets parent.Child

...

Tôi mới biết rằng SE không cho phép một số câu hỏi chủ quan và rõ ràng đây là một câu hỏi chủ quan. Nhưng, có lẽ đó là một câu hỏi chủ quan tốt.


14
Thực hành tốt nhất là trẻ em không nên biết về cha mẹ của chúng.
Telastyn

4
xin vui lòng không đăng chéo : stackoverflow.com/questions/26643528/ từ
gnat

2
@Telastyn Tôi không thể không đọc nó như một cái lưỡi, và nó rất vui nhộn. Cũng hoàn toàn chết đẫm máu chính xác. Steven, thuật ngữ để xem xét là "acyclic" vì có rất nhiều tài liệu về lý do tại sao bạn nên tạo đồ thị theo chu kỳ nếu có thể.
Jimmy Hoffa

10
@Telastyn bạn nên thử sử dụng nhận xét đó về cách nuôi dạy con.stackexchange
Fabio Marcolini

2
Hừm. Không biết cách di chuyển bài đăng (không thấy điều khiển cờ). Tôi đăng lại cho các lập trình viên kể từ khi ai đó nói với tôi rằng nó thuộc về nơi đó.
Steven Broshar

Câu trả lời:


19

Tôi sẽ tránh xa mọi tình huống cần thiết cho trẻ biết về cha mẹ.

Có nhiều cách để truyền thông điệp từ trẻ đến cha mẹ thông qua các sự kiện. Bằng cách này, phụ huynh, khi thêm, chỉ cần đăng ký vào một sự kiện mà đứa trẻ kích hoạt, mà không cần đứa trẻ phải biết trực tiếp về cha mẹ. Sau tất cả, đây có thể là mục đích sử dụng của một đứa trẻ biết về cha mẹ của nó, để có thể sử dụng cha mẹ để có hiệu quả. Ngoại trừ bạn sẽ không muốn đứa trẻ thực hiện công việc của cha mẹ, vì vậy thực sự những gì bạn muốn làm chỉ đơn giản là nói với cha mẹ rằng điều gì đó đã xảy ra. Do đó, những gì bạn cần xử lý là một sự kiện trên trẻ, trong đó cha mẹ có thể tận dụng.

Mẫu này cũng có tỷ lệ rất tốt nếu sự kiện này trở nên hữu ích cho các lớp khác. Có lẽ đó là một chút quá mức, nhưng nó cũng ngăn bạn tự bắn vào chân mình sau này, vì nó trở nên hấp dẫn khi muốn sử dụng cha mẹ trong lớp con của bạn, điều mà chỉ cặp vợ chồng hai lớp thậm chí còn nhiều hơn. Tái cấu trúc các lớp như vậy sau đó rất tốn thời gian và có thể dễ dàng tạo ra các lỗi trong chương trình của bạn.

Mong rằng sẽ giúp!


6
Hãy tránh xa mọi tình huống cần thiết cho trẻ biết về cha mẹ - tại sao? Câu trả lời của bạn dựa trên giả định rằng đồ thị đối tượng hình tròn là một ý tưởng tồi. Mặc dù đôi khi đây là trường hợp (ví dụ: khi quản lý bộ nhớ thông qua tính năng đếm lại ngây thơ - không phải là trường hợp trong C #), nói chung nó không phải là một điều xấu. Đáng chú ý, Mẫu Người quan sát (thường được sử dụng để gửi các sự kiện) liên quan đến việc có thể quan sát ( Child) duy trì một tập hợp các quan sát viên ( Parent), giới thiệu lại tính tuần hoàn theo cách ngược (và đưa ra một số vấn đề của chính nó).
amon

1
Bởi vì các phụ thuộc theo chu kỳ có nghĩa là cấu trúc mã của bạn theo cách mà bạn không thể có cái này mà không có cái kia. Theo bản chất của mối quan hệ cha-con, chúng phải là các thực thể riêng biệt hoặc nếu không bạn có nguy cơ có hai lớp kết hợp chặt chẽ cũng có thể là một lớp khổng lồ duy nhất với một danh sách cho tất cả sự chăm sóc cẩn thận được đưa vào thiết kế của nó. Tôi không thấy mô hình Observer giống như mẫu cha-con khác với thực tế là một lớp có tham chiếu đến một số lớp khác. Đối với tôi cha mẹ-con là cha mẹ có sự phụ thuộc mạnh mẽ vào đứa trẻ, nhưng không phải là nghịch đảo.
Neil

Tôi đồng ý. Sự kiện là cách tốt nhất để xử lý mối quan hệ cha-con trong trường hợp này. Đó là mẫu tôi sử dụng rất thường xuyên và nó làm cho mã rất dễ bảo trì, thay vì phải lo lắng về việc lớp con đang làm gì với cha mẹ thông qua tham chiếu.
Eternal21

@Neil: phụ thuộc lẫn nhau của đối tượng hợp tạo thành một phần tự nhiên của nhiều mô hình dữ liệu. Trong một mô phỏng cơ học của một chiếc xe hơi, các bộ phận khác nhau của chiếc xe sẽ cần truyền lực cho nhau; điều này thường được xử lý tốt hơn bằng cách có tất cả các bộ phận của xe coi chính động cơ mô phỏng là đối tượng "cha mẹ", hơn là có sự phụ thuộc theo chu kỳ giữa tất cả các thành phần, nhưng nếu các thành phần có thể phản ứng với bất kỳ kích thích nào từ bên ngoài cha mẹ họ sẽ cần một cách để thông báo cho cha mẹ nếu những kích thích đó có bất kỳ ảnh hưởng nào mà cha mẹ cần biết.
supercat

2
@Neil: Nếu tên miền bao gồm một đối tượng rừng có phụ thuộc dữ liệu tuần hoàn không thể sửa chữa, bất kỳ mô hình nào của miền cũng sẽ làm như vậy. Trong nhiều trường hợp, điều này sẽ ngụ ý thêm rằng khu rừng sẽ hành xử như một vật thể khổng lồ duy nhất cho dù người ta có muốn hay không . Mẫu tổng hợp phục vụ để tập trung sự phức tạp của khu rừng vào một đối tượng lớp duy nhất được gọi là Root tổng hợp. Tùy thuộc vào độ phức tạp của miền được mô hình hóa, gốc tổng hợp đó có thể hơi lớn và khó sử dụng, nhưng nếu độ phức tạp là không thể tránh khỏi (như trường hợp với một số miền) thì tốt hơn ...
supercat

10

Tôi nghĩ rằng tùy chọn 3 của bạn có thể là sạch nhất. Bạn đã viết.

Nhược điểm: Đứa trẻ được tạo ra mà không có tài liệu tham khảo cha mẹ.

Tôi không thấy đó là nhược điểm. Trong thực tế, thiết kế chương trình của bạn có thể được hưởng lợi từ các đối tượng con có thể được tạo mà không cần cha mẹ trước. Ví dụ, nó có thể làm cho việc kiểm tra trẻ cách ly dễ dàng hơn rất nhiều. Nếu một người dùng mô hình của bạn quên thêm một đứa trẻ vào cha mẹ của nó và gọi một phương thức dự kiến ​​thuộc tính cha mẹ được khởi tạo trong lớp con của bạn, thì anh ta sẽ có một ngoại lệ ref null - đó chính xác là những gì bạn muốn: một sự cố sớm xảy ra do sai sử dụng.

Và nếu bạn nghĩ rằng một đứa trẻ cần thuộc tính cha mẹ của nó được khởi tạo trong hàm tạo trong mọi trường hợp vì lý do kỹ thuật, hãy sử dụng một cái gì đó như "đối tượng cha mẹ null" làm giá trị mặc định (mặc dù điều này có nguy cơ che dấu lỗi).


Nếu một đối tượng con không thể làm bất cứ điều gì hữu ích mà không có cha mẹ, thì đối tượng con bắt đầu mà không có cha mẹ sẽ yêu cầu nó phải có một SetParentphương pháp cần hỗ trợ thay đổi cha mẹ của một đứa trẻ hiện tại (điều này có thể khó thực hiện và / hoặc vô nghĩa) hoặc sẽ chỉ có thể gọi được một lần. Mô hình hóa các tình huống như tập hợp (như Mike Brown gợi ý) có thể tốt hơn nhiều so với việc có con bắt đầu mà không có cha mẹ.
supercat

Nếu trường hợp sử dụng yêu cầu một cái gì đó dù chỉ một lần, thiết kế phải luôn cho phép. Sau đó, hạn chế khả năng đó vào hoàn cảnh đặc biệt là dễ dàng. Thêm một khả năng như vậy sau đó thường là không thể. Giải pháp tốt nhất là Lựa chọn 3. Có thể với đối tượng thứ ba, "Mối quan hệ" giữa cha mẹ và con cái sao cho [Cha mẹ] ---> [Mối quan hệ (Cha mẹ sở hữu con)] <--- [Con]. Điều này cũng cho phép nhiều trường hợp [Mối quan hệ] như [Con] ---> [Mối quan hệ (Con thuộc sở hữu của cha mẹ)] <--- [Cha mẹ].
DocSalvager

3

Không có gì để ngăn chặn sự gắn kết cao giữa hai lớp được sử dụng chung với nhau (ví dụ: Order và LineItem sẽ thường tham chiếu lẫn nhau). Tuy nhiên, trong những trường hợp này, tôi có xu hướng tuân thủ các quy tắc Thiết kế hướng miền và mô hình hóa chúng dưới dạng Tổng hợp với Cha mẹ là gốc tổng hợp. Điều này cho chúng ta biết rằng AR chịu trách nhiệm về thời gian tồn tại của tất cả các đối tượng trong tập hợp của nó.

Vì vậy, nó sẽ giống như kịch bản thứ tư của bạn trong đó cha mẹ đưa ra một phương thức để tạo ra các con của nó chấp nhận bất kỳ tham số cần thiết nào để khởi tạo đúng cách các con và thêm nó vào bộ sưu tập.


1
Tôi sẽ sử dụng định nghĩa tổng hợp hơi lỏng lẻo, cho phép tồn tại các tham chiếu bên ngoài vào các phần của tổng hợp khác với gốc, với điều kiện - theo quan điểm của người quan sát bên ngoài - hành vi sẽ nhất quán với mỗi phần của tổng hợp chỉ giữ một tham chiếu đến thư mục gốc chứ không phải bất kỳ phần nào khác. Theo tôi, nguyên tắc chính là mỗi đối tượng có thể thay đổi nên có một chủ sở hữu ; một tập hợp là một tập hợp các đối tượng được sở hữu bởi một đối tượng duy nhất ("gốc tổng hợp"), cần biết tất cả các tham chiếu tồn tại đến các phần của nó.
supercat

3

Tôi sẽ đề nghị có các đối tượng "nhà máy trẻ em" được truyền cho phương thức cha mẹ để tạo ra một đứa trẻ (sử dụng đối tượng "nhà máy con"), đính kèm nó và trả về một khung nhìn. Bản thân đối tượng con sẽ không bao giờ được phơi bày bên ngoài cha mẹ. Cách tiếp cận này có thể hoạt động độc đáo cho những thứ như mô phỏng. Trong một mô phỏng điện tử, một đối tượng "nhà máy trẻ em" cụ thể có thể đại diện cho các thông số kỹ thuật cho một số loại bóng bán dẫn; cái khác có thể đại diện cho các thông số kỹ thuật cho một điện trở; một mạch cần hai bóng bán dẫn và bốn điện trở có thể được tạo ra với mã như:

var q2N3904 = new TransistorSpec(TransistorType.NPN, 0.691, 40);
var idealResistor4K7 = new IdealResistorSpec(4700.0);
var idealResistor47K = new IdealResistorSpec(47000.0);

var Q1 = Circuit.AddComponent(q2N3904);
var Q2 = Circuit.AddComponent(q2N3904);
var R1 = Circuit.AddComponent(idealResistor4K7);
var R2 = Circuit.AddComponent(idealResistor4K7);
var R3 = Circuit.AddComponent(idealResistor47K);
var R4 = Circuit.AddComponent(idealResistor47K);

Lưu ý rằng trình giả lập không cần giữ lại bất kỳ tham chiếu nào đến đối tượng người tạo trẻ em và AddComponentsẽ không trả về tham chiếu đến đối tượng được tạo và giữ bởi trình giả lập, mà là một đối tượng thể hiện chế độ xem. Nếu AddComponentphương thức là chung, đối tượng khung nhìn có thể bao gồm các hàm dành riêng cho thành phần nhưng sẽ không hiển thị các thành viên mà cha mẹ sử dụng để quản lý tệp đính kèm.


2

Danh sách tuyệt vời. Tôi không biết phương pháp nào là "tốt nhất" nhưng đây là một phương pháp để tìm ra phương pháp biểu cảm nhất.

Bắt đầu với lớp cha mẹ và con đơn giản nhất có thể. Viết mã của bạn với những người. Một khi bạn nhận thấy sự sao chép mã có thể được đặt tên, bạn đặt nó vào một phương thức.

Có lẽ bạn nhận được addChild(). Có lẽ bạn sẽ có được một cái gì đó giống như addChildren(List<Child>)hoặc addChildrenNamed(List<String>)hoặc loadChildrenFrom(String)hoặc newTwins(String, String)hoặc Child.replicate(int).

Nếu vấn đề của bạn thực sự là buộc phải có mối quan hệ một-nhiều thì có lẽ bạn nên

  • buộc nó trong các setters có thể dẫn đến nhầm lẫn hoặc mệnh đề ném
  • bạn loại bỏ các setters và tạo các phương thức sao chép hoặc di chuyển đặc biệt - đó là biểu cảm và dễ hiểu

Đây không phải là một câu trả lời nhưng tôi hy vọng bạn tìm thấy một trong khi đọc nó.


0

Tôi đánh giá cao việc có các liên kết từ trẻ em đến cha mẹ có những nhược điểm như đã lưu ý ở trên.

Tuy nhiên, đối với nhiều kịch bản, cách giải quyết với các sự kiện và các cơ chế "ngắt kết nối" khác cũng mang lại sự phức tạp của riêng chúng và các dòng mã bổ sung.

Ví dụ: nâng cao một sự kiện từ Trẻ em được nhận bởi Cha mẹ sẽ liên kết cả hai mặc dù theo kiểu khớp nối lỏng lẻo.

Có lẽ đối với nhiều kịch bản, rõ ràng với tất cả các nhà phát triển ý nghĩa của một thuộc tính Child.Parent. Đối với phần lớn các hệ thống tôi làm việc trên này đã hoạt động tốt. Quá kỹ thuật có thể tốn thời gian và giáo dục. Gây nhầm lẫn!

Có một phương thức Parent.AttachChild () thực hiện tất cả các công việc cần thiết để ràng buộc Trẻ với cha mẹ của nó. Mọi người đều rõ điều này có nghĩa là gì

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.