부모에 대한 자녀의 참조를 초기화하는 가장 좋은 방법은 무엇입니까?


35

많은 다른 부모 / 자식 클래스가있는 개체 모델을 개발 중입니다. 각 자식 개체에는 부모 개체에 대한 참조가 있습니다. 부모 참조를 초기화하는 몇 가지 방법을 생각할 수는 있지만 각 방법마다 상당한 단점이 있습니다. 아래에 설명 된 접근 방식이 가장 좋거나 더 나은 것을 고려하십시오.

아래 코드가 컴파일되는지 확인하지 않으므로 구문이 정확하지 않은 경우 내 의도를 확인하십시오.

내 자식 클래스 생성자 중 일부는 항상 표시하지는 않지만 부모 이외의 매개 변수를 사용합니다.

  1. 발신자는 부모를 설정하고 같은 부모에 추가해야합니다.

    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;} }
    }

    단점 : 상위 설정은 소비자를위한 2 단계 프로세스입니다.

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

    단점 : 오류가 발생하기 쉽습니다. 호출자는 자식을 초기화하는 데 사용 된 것과 다른 부모에 자식을 추가 할 수 있습니다.

    var child = new Child(parent1);
    parent2.Children.Add(child);
  2. 부모는 호출자가 초기화 된 부모에 자식을 추가하는지 확인합니다.

    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);
      }
    }

    단점 : 발신자에게는 여전히 부모를 설정하는 2 단계 프로세스가 있습니다.

    단점 : 런타임 검사 – 성능을 줄이고 모든 추가 / 세터에 코드를 추가합니다.

  3. 부모는 자식을 부모에 추가 / 할당 할 때 자식의 부모 참조 (자체로)를 설정합니다. 부모 세터는 내부에 있습니다.

    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);
      }
    }

    단점 : 자식은 부모 참조없이 생성됩니다. 때로는 초기화 / 확인에 부모가 필요합니다. 이는 자식의 부모 세터에서 초기화 / 검증을 수행해야 함을 의미합니다. 코드가 복잡해질 수 있습니다. 항상 부모 참조가 있으면 자식을 구현하는 것이 훨씬 쉽습니다.

  4. 부모는 팩토리 추가 메소드를 노출하므로 자식은 항상 부모 참조를 갖습니다. 아동 ctor는 내부입니다. 부모 세터는 비공개입니다.

    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;
      }
    }

    단점 : 과 같은 초기화 구문을 사용할 수 없습니다 new Child(){prop = value}. 대신해야합니다 :

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

    단점 : 추가 팩토리 메소드에서 하위 생성자의 매개 변수를 복제해야합니다.

    단점 : 싱글 톤 자식에는 속성 설정자를 사용할 수 없습니다. 값을 설정하지만 속성 getter를 통해 읽기 액세스를 제공하는 방법이 필요하다는 것은 절망적 인 것 같습니다. 일방적입니다.

  5. 자식은 생성자에서 참조 된 부모에 자신을 추가합니다. 아동 ctor는 공개입니다. 부모의 공개 추가 액세스 권한이 없습니다.

    //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;} }
    }

    단점 : 호출 구문이 좋지 않습니다. 일반적으로 add다음과 같은 객체를 만드는 대신 부모 에서 메소드를 호출합니다 .

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

    그리고 다음과 같이 객체를 만드는 것이 아니라 속성을 설정합니다.

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

...

나는 방금 SE가 주관적인 질문을 허용하지 않으며 이것이 주관적인 질문이라는 것을 배웠습니다. 그러나 아마도 좋은 주관적인 질문 일 것입니다.


14
모범 사례는 아이들이 부모에 대해 알면 안된다는 것입니다.
Telastyn


2
@Telastyn 나는 그것을 뺨에 혀로 읽을 수는 없지만 재미 있습니다. 또한 완전히 죽은 피의 정확성. Steven, 조사해야 할 용어는 "비순환 적"입니다. 왜 가능한 경우 그래프를 비순환 적으로 만들어야하는지에 대한 많은 문헌이 있습니다.
Jimmy Hoffa

10
당신은 parenting.stackexchange에 그 의견 사용하려고한다 @Telastyn
파비오 마르 콜리

2
흠. 게시물 이동 방법을 모릅니다 (플래그 컨트롤이 표시되지 않음). 누군가가 저에게 속해 있다고 말했기 때문에 프로그래머에게 다시 게시했습니다.
Steven Broshar

답변:


18

자녀가 반드시 부모에 대해 알아야하는 모든 시나리오에서 멀리 떨어져 있습니다.

이벤트를 통해 자식에서 부모로 메시지를 전달하는 방법이 있습니다. 이런 식으로 부모는 추가시 자식이 부모에 대해 직접 알 필요없이 자식이 트리거하는 이벤트에 등록하기 만하면됩니다. 결국, 이것은 부모를 어떤 효과로 사용할 수 있기 위해 부모에 대해 아는 어린이의 의도 된 사용법 일 것입니다. 자녀가 부모의 업무를 수행하는 것을 원하지 않기 때문에 실제로 원하는 것은 부모에게 무언가가 일어났다는 것을 알리는 것입니다. 따라서 처리해야 할 것은 자녀에 대한 이벤트이며 부모가 활용할 수 있습니다.

이 패턴은이 이벤트가 다른 클래스에 유용 할 경우 매우 잘 확장됩니다. 어쩌면 그것은 약간의 과잉 일 수도 있지만 나중에 두 발을 더 ​​묶는 자녀 수업에서 부모를 사용하고 싶어하기 때문에 나중에 발로 스스로를 쏘지 못하게합니다. 그런 클래스를 리팩토링하면 시간이 많이 걸리고 프로그램에서 쉽게 버그를 만들 수 있습니다.

희망이 도움이됩니다!


6
아동이 부모에 대해 알아야하는 시나리오를 피하십시오 ”– 왜? 당신의 대답은 원형 객체 그래프가 나쁜 생각이라는 가정에 달려 있습니다. C #의 경우가 아닌 순진한 참조 횟수를 통한 메모리 관리와 같은 경우가 종종 있지만, 일반적으로 나쁜 것은 아닙니다. 특히 Observer 패턴 (이벤트를 디스패치하는 데 자주 사용됨)에는 Child관찰자 세트 ( )를 유지하는 관찰자 ( Parent)가 포함됩니다.
amon

1
순환 종속성은 다른 코드없이 가질 수없는 방식으로 코드를 구성하는 것을 의미합니다. 부모 - 자식 관계의 특성상, 그들은 해야합니다 별도의 개체 수 그렇지 않으면 당신은뿐만 아니라 설계에주의 치료 넣어 모든 목록이 하나의 거대한 클래스 수 있습니다이 개 밀접하게 결합 클래스를 가진 위험이 있습니다. 한 클래스가 여러 클래스에 대한 참조를 가지고 있다는 사실 외에는 관찰자 패턴이 부모 자식과 어떻게 같은지 알지 못합니다. 나를 위해 부모 자식은 자식에 강한 의존성을 가지고 있지만 그 반대는 아닙니다.
Neil

동의한다. 이 경우 이벤트는 부모-자식 관계를 처리하는 가장 좋은 방법입니다. 그것은 내가 자주 사용하는 패턴이며 참조를 통해 자식 클래스가 부모에게 무엇을하는지 걱정할 필요없이 코드를 유지 관리하기가 쉽습니다.
Eternal21

@Neil : 객체 인스턴스 의 상호 의존성 은 많은 데이터 모델의 자연스러운 부분을 형성합니다. 자동차의 기계적 시뮬레이션에서 자동차의 다른 부분은 서로 힘을 전달해야합니다. 이것은 일반적으로 자동차의 모든 부품이 시뮬레이션 엔진 자체를 "부모"객체로 간주하여 모든 부품 사이에 주기적 종속성을 갖는 것보다 더 잘 처리되지만, 부품이 부모 외부의 자극에 반응 할 수있는 경우 그러한 자극이 부모에게 알아야 할 영향이있는 경우 부모에게 알리는 방법이 필요합니다.
supercat

2
@Neil : 도메인에 순환 할 수없는 순환 데이터 종속성이있는 포리스트 개체가 포함 된 경우 도메인의 모든 모델도 그렇게합니다. 많은 경우에 이것은 숲이 원하든 원하지 않든 하나의 거대한 물체로서 행동 할 것임을 암시 할 것이다 . 집계 패턴은 포리스트의 복잡성을 집계 루트라는 단일 클래스 개체로 집중시키는 역할을합니다. 모델링되는 도메인의 복잡성에 따라 집계 루트가 다소 커지고 다루기
어려울

10

나는 당신의 선택 3이 가장 깨끗하다고 ​​생각합니다. 당신은 썼습니다.

단점 : 자식은 부모 참조없이 생성됩니다.

나는 그것을 단점으로 보지 않습니다. 실제로 프로그램 디자인은 부모없이 먼저 만들 수있는 자식 개체의 이점을 얻을 수 있습니다. 예를 들어, 격리 된 자식 테스트를 훨씬 쉽게 할 수 있습니다. 모델의 사용자가 부모에 자식을 추가하는 것을 잊고 자식 클래스에서 부모 속성이 초기화 될 것으로 예상되는 메소드를 호출하면 null 참조 예외가 발생합니다. 정확히 원하는 것 : 잘못된 초기 충돌 용법.

또한 기술적 인 이유로 모든 상황에서 생성자에서 부모 속성을 초기화해야한다고 생각되는 경우 "null parent object"와 같은 것을 기본값으로 사용하십시오 (마스킹 오류의 위험이 있음).


자식 개체가 부모없이 유용한 작업을 수행 할 수없는 경우 부모없이 자식 개체를 시작 SetParent하면 기존 자식의 부모를 변경하는 것을 지원해야하는 방법이 필요합니다. (또는 말도 안되는) 또는 한 번만 호출 할 수 있습니다. Mike Brown이 제안한 것처럼 집계와 같은 상황을 모델링하는 것은 부모없이 자녀를 시작시키는 것보다 훨씬 좋습니다.
supercat December

유스 케이스가 한 번이라도 무언가를 요구하는 경우, 설계에서 항상 허용해야합니다. 그런 다음 해당 기능을 특수 상황으로 제한하는 것은 쉽습니다. 그러나 이러한 기능을 나중에 추가하는 것은 일반적으로 불가능합니다. 가장 좋은 해결책은 옵션 3입니다. 아마도 부모와 자식 사이에 [부모] ---> [관계 (부모가 자식을 소유 함)] <--- [자식]과 같은 세 번째 "관계"개체가있을 것입니다. 또한 [Child] ---> [Relationship (Child is own to Parent)] <--- [Parent]와 같은 여러 [Relationship] 인스턴스도 허용합니다.
DocSalvager

3

일반적으로 함께 사용되는 두 클래스 사이의 높은 응집력을 막을 수있는 것은 없습니다 (예 : Order와 LineItem은 서로를 참조합니다). 그러나 이러한 경우 도메인 기반 디자인 규칙을 따르고 부모가 집계 루트 인 집계 로 모델링하는 경향이 있습니다. 이것은 AR이 모든 객체의 수명에 대한 책임이 AR에 있음을 알려줍니다.

따라서 부모가 자식을 올바르게 초기화하고 컬렉션에 추가하는 데 필요한 매개 변수를 허용하는 자식을 만드는 방법을 노출하는 시나리오 4와 가장 비슷합니다.


1
외부 관찰자의 관점에서 볼 때 동작이 일관된 경우, 루트가 아닌 집계의 일부에 대한 외부 참조가 존재할 수 있도록 집계의 약간 더 느슨한 정의를 사용합니다. 골재의 모든 부분은 다른 부분이 아닌 루트에 대한 참조 만 보유합니다. 내 생각에 핵심 원칙은 각각의 가변 객체가 하나의 소유자를 가져야한다는 것입니다 . 집합은 단일 개체 ( "집계 루트")가 모두 소유 한 개체의 모음으로, 해당 부분에 존재하는 모든 참조를 알아야합니다.
supercat

3

아이를 생성하고 ( "자식 팩토리"객체를 사용하여) 연결하고보기를 반환하는 부모 메서드에 전달되는 "자식 팩토리"개체를 갖는 것이 좋습니다. 자식 개체 자체는 부모 외부에 노출되지 않습니다. 이 접근 방식은 시뮬레이션과 같은 작업에 적합합니다. 전자 시뮬레이션에서 하나의 특정 "자식 팩토리"객체는 어떤 종류의 트랜지스터에 대한 사양을 나타낼 수 있습니다. 다른 하나는 저항기의 사양을 나타낼 수 있습니다. 두 개의 트랜지스터와 네 개의 저항이 필요한 회로는 다음과 같은 코드로 만들 수 있습니다.

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);

시뮬레이터는 자식 생성자 객체에 대한 참조를 유지할 필요가 없으며 시뮬레이터가 AddComponent만들고 보유한 객체에 대한 참조를 반환하지 않고 뷰를 나타내는 객체를 반환합니다. 는 IF AddComponent방법이 일반적이며, 뷰 객체는 구성 요소 별 기능을 포함 할 수 있지만 것 없는 부모가 사용하는 첨부 파일을 관리 할 수있는 멤버를 노출합니다.


2

훌륭한 목록. 어떤 방법이 "최고"인지는 모르겠지만 여기에 가장 표현적인 방법을 찾아보십시오.

가능한 가장 간단한 부모 및 자식 클래스로 시작하십시오. 그것들로 코드를 작성하십시오. 이름을 지정할 수있는 코드 중복을 발견하면이를 메소드에 넣습니다.

아마 당신은 얻을 addChild(). 어쩌면 당신은 addChildren(List<Child>)또는 addChildrenNamed(List<String>)또는 loadChildrenFrom(String)또는 newTwins(String, String)또는 같은 것을 얻을 수 Child.replicate(int)있습니다.

문제가 실제로 일대 다 관계를 강요하는 것에 관한 것이라면 아마도

  • 혼동이나 던지기로 이어질 수있는 세터에서 강제로
  • 세터를 제거하고 특수 복사 또는 이동 방법을 작성합니다. 표현적이고 이해하기 쉽습니다.

이것은 답변이 아니지만 이것을 읽는 동안 하나를 찾으시기 바랍니다.


0

자녀와 부모 사이의 링크가 위와 같이 단점이 있음을 이해합니다.

그러나 많은 시나리오에서 이벤트 및 기타 "연결이 끊어진"메커니즘을 사용하는 해결 방법에는 고유 한 복잡성과 추가 코드 줄이 있습니다.

예를 들어 Child에서 Parent로 수신 할 이벤트를 발생시키는 것은 느슨한 커플 링 방식이지만 둘 다 함께 바인딩됩니다.

아마도 많은 시나리오에서 모든 개발자에게는 Child.Parent 속성의 의미가 분명합니다. 내가 작업 한 대부분의 시스템 에서이 작업은 정상적으로 작동했습니다. 오버 엔지니어링은 시간이 많이 걸리고… 혼란스러운!

Child를 부모에 바인딩하는 데 필요한 모든 작업을 수행하는 Parent.AttachChild () 메서드가 있어야합니다. 이 "의미"에 대한 모든 사람들이 분명합니다

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.