전파 패턴은 개체 모델을 변경합니다 ..?


22

여기에 내가 다루기가 항상 좌절하는 일반적인 시나리오가 있습니다.

부모 개체가있는 개체 모델이 있습니다. 부모는 자식 개체를 포함합니다. 이 같은.

public class Zoo
{
    public List<Animal> Animals { get; set; }
    public bool IsDirty { get; set; }
}

각 자식 개체에는 다양한 데이터와 메서드가 있습니다

public class Animal
{
    public string Name { get; set; }
    public int Age { get; set; }

    public void MakeMess()
    {
        ...
    }
}

자식이 변경되면 (이 경우 MakeMess 메서드가 호출 될 때) 부모의 일부 값을 업데이트해야합니다. Animal의 특정 임계 값이 엉망이되면 Zoo의 IsDirty 플래그를 설정해야합니다.

이 시나리오를 처리하는 몇 가지 방법이 있습니다 (내가 아는 것).

1) 각 동물은 변경 사항을 알리기 위해 부모 동물원 참조를 가질 수 있습니다.

public class Animal
{
    public Zoo Parent { get; set; }
    ...

    public void MakeMess()
    {
        Parent.OnAnimalMadeMess();
    }
}

Animal을 부모 객체에 연결하기 때문에 최악의 옵션처럼 느껴집니다. 집에 사는 동물을 원한다면 어떻게해야합니까?

2) 다른 옵션은 C #과 같은 이벤트를 지원하는 언어를 사용하는 경우 부모가 이벤트 변경을 구독하도록하는 것입니다.

public class Animal
{
    public event OnMakeMessDelegate OnMakeMess;

    public void MakeMess()
    {
        OnMakeMess();
    }
}

public class Zoo
{
    ...

    public void SubscribeToChanges()
    {
        foreach (var animal in Animals)
        {
            animal.OnMakeMess += new OnMakeMessDelegate(OnMakeMessHandler);
        }
    }

    public void OnMakeMessHandler(object sender, EventArgs e)
    {
        ...
    }
}

이것은 작동하는 것처럼 보이지만 경험상 유지하기가 어렵습니다. 동물이 동물원을 변경하면 이전 동물원에서 이벤트를 구독 취소하고 새 동물원에서 다시 구독을 취소해야합니다. 컴포지션 트리가 깊어 질 때만 악화됩니다.

3) 다른 옵션은 로직을 상위로 이동하는 것입니다.

public class Zoo
{
    public void AnimalMakesMess(Animal animal)
    {
        ...
    }
}

이것은 매우 부자연스럽고 논리가 중복됩니다. 예를 들어 Zoo와 공통 상속 부모를 공유하지 않는 House 객체가있는 경우

public class House
{
    // Now I have to duplicate this logic
    public void AnimalMakesMess(Animal animal)
    {
        ...
    }
}

아직 이러한 상황을 처리하기위한 좋은 전략을 찾지 못했습니다. 다른 무엇을 사용할 수 있습니까? 어떻게 이것을 더 간단하게 만들 수 있습니까?


당신은 # 1이 나쁘다는 것에 맞습니다. 그리고 저는 # 2에도 관심이 없습니다. 일반적으로 부작용을 피하고 대신 증가시키고 있습니다. 옵션 # 3과 관련하여 AnimalMakeMess를 모든 클래스가 호출 할 수있는 정적 메소드로 분해 할 수없는 이유는 무엇입니까?
Doval

4
특정 부모 클래스 대신 인터페이스 (IAnimalObserver)를 통해 통신하는 경우 # 1은 반드시 나쁘지 않습니다.
coredump

답변:


11

나는 이것을 두 번 처리해야했다. 처음으로 옵션 2 (이벤트)를 사용했는데 말했듯이 정말 복잡해졌습니다. 이 경로를 사용하는 경우 이벤트가 올바르게 수행되고 매달려있는 참조를 남기지 않도록 매우 철저한 단위 테스트가 필요하다고 제안합니다. 그렇지 않으면 디버그하기가 매우 어렵습니다.

두 번째로, 나는 방금 부모 속성을 어린이의 함수로 구현 했으므로 Dirty각 동물에 속성을 유지하고 Animal.IsDirty반환하십시오 this.Animals.Any(x => x.IsDirty). 그것은 모델에 있었다. 모델 위에 컨트롤러가 있었고 컨트롤러의 작업은 모델을 변경 한 후 (모델의 모든 작업이 컨트롤러를 통과하여 무언가 변경 되었음을 알았 음 ) 특정 re를 호출해야한다는 것을 알았습니다. - 더러워 ZooMaintenance졌는지 확인하기 위해 부서를 트리거하는 것과 같은 평가 기능 Zoo. 또는 ZooMaintenance예약 된 시간이 될 때까지 (필요한 100 ms, 1 초, 2 분, 24 시간마다) 점검을 푸시 할 수 있습니다 .

나는 후자가 유지하기가 훨씬 간단하고 성능 문제에 대한 두려움이 실현되지 않았다는 것을 알았습니다.

편집하다

이를 처리하는 또 다른 방법은 메시지 버스 패턴입니다. Controller내 예제에서 like를 사용하는 대신 모든 객체에 IMessageBus서비스 를 주입 합니다. Animal클래스는 "식당 만든"와 같은 메시지를 게시 할 수 있습니다 당신의 Zoo클래스는 "식당 만든"메시지를 구독 할 수 있습니다. 메시지 버스 서비스는 Zoo동물이 메시지 중 하나를 게시 할 때 이를 알리고 해당 IsDirty속성을 다시 평가할 수 있도록 관리 합니다.

이는 Animals더 이상 Zoo참조 가 필요하지 않다는 이점이 있으며 Zoo모든 단일 이벤트의 구독 및 구독 취소에 대해 걱정할 필요가 없습니다 Animal. 패널티는 해당 Zoo메시지를 구독 하는 모든 클래스가 동물이 아니더라도 속성을 다시 평가해야한다는 것입니다. 그것은 큰일 수도 아닐 수도 있습니다. 하나 또는 두 개의 Zoo인스턴스 만 있다면 괜찮을 것입니다.

편집 2

옵션 1의 단순성을 할인하지 마십시오. 코드를 다시 방문하는 사람은 코드를 이해하는 데 큰 문제가 없습니다. Animal클래스를 보고있는 누군가 MakeMess에게 메시지가 전달 될 때 메시지가 전달 Zoo되고 Zoo클래스가 메시지를받는 클래스 가 분명하다는 것을 알 수 있습니다 . 객체 지향 프로그래밍에서 메소드 호출은 "메시지"라고 불렀습니다. 실제로, 옵션 1에서 벗어나는 것이 의미가있는 유일한 시점 은 혼란을 초래할 Zoo경우 통지 이상 을 통지해야하는 경우 Animal입니다. 통보해야 할 객체가 더 있다면 메시지 버스 나 컨트롤러로 옮길 것입니다.


5

도메인을 설명하는 간단한 클래스 다이어그램을 만들었습니다. 여기에 이미지 설명을 입력하십시오

각각 Animal Habitat 엉망입니다.

Habitat(는 근본적으로이 경우는 그렇지 않은 기술 설계의 일부가 아닌)가 무슨 또는 얼마나 많은 동물 상관하지 않는다.

그러나 Animal매번 다르게 동작하기 때문에 걱정하지 마십시오 Habitat.

이 다이어그램은 전략 디자인 패턴 의 UML 다이어그램과 유사 하지만 다르게 사용합니다.

다음은 Java의 코드 예제입니다 (C # 관련 실수는 원하지 않습니다).

물론이 디자인, 언어 및 요구 사항을 직접 조정할 수 있습니다.

이것은 전략 인터페이스입니다.

public interface Habitat {
    public void messUp(float magnitude);

    public float getCleanliness();
}

콘크리트의 예 Habitat. 물론 각 Habitat서브 클래스는 이러한 메소드를 다르게 구현할 수 있습니다.

public class Zoo implements Habitat {
    public float cleanliness = 1;

    public float getCleanliness() {
        return cleanliness;
    }

    public void messUp(float magnitude) {
        cleanliness -= magnitude;
    }
}

물론 여러 개의 동물 서브 클래스를 가질 수 있습니다.

public class Animel {
    private Habitat habitat;

    public void makeMess() {
        habitat.messUp(.05f);
    }

    public Animel addTo(Habitat habitat) {
        this.habitat = habitat;
        return this;
    }
}

이것이 클라이언트 클래스이며 기본적으로이 디자인을 사용하는 방법을 설명합니다.

public class ZooKeeper {
    public Habitat zoo = new Zoo();

    public ZooKeeper() {
        new Animal()
            .addTo( zoo )
            .makeMess();

        if (zoo.getCleanliness() < 0.5f) {
            System.out.println("The zoo is really messy");
        } else {
            System.out.println("The zoo looks clean");
        }
    }
}

물론 실제 응용 프로그램에서 필요한 경우 Habitat알려주고 관리 Animal할 수 있습니다.


3

과거의 옵션 2와 같은 아키텍처로 상당한 성공을 거두었습니다. 가장 일반적인 옵션이며 최대한의 유연성을 제공합니다. 그러나 리스너를 제어하고 많은 구독 유형을 관리하지 않는 경우 인터페이스를 작성하여 이벤트를 더 쉽게 구독 할 수 있습니다.

interface MessablePlace
{
  void OnMess(object sender, MessEvent e);
}

class MessEvent
{
  String DetailsOrWhatever;
}

인터페이스 옵션은 옵션 1만큼이나 간단하다는 장점이 있지만, House또는 에 동물을 아주 쉽게 넣을 수 있습니다 FairlyLand.


3
  • 옵션 1은 실제로 매우 간단합니다. 그것은 단지 역 참조입니다. 그러나 인터페이스라는 인터페이스로 일반화 Dwelling하고 MakeMess메소드를 제공 하십시오. 순환 의존성이 깨집니다. 그런 다음 동물이 엉망이되면 전화 dwelling.MakeMess()도합니다.

lex parsimoniae 의 정신으로 , 나는 이것을 알고있을 것입니다. 아마도 아래의 체인 솔루션을 사용하고있을 것입니다. (이것은 @Benjamin Albert가 제안한 것과 동일한 모델입니다.)

관계형 데이터베이스 테이블을 모델링하는 경우 관계는 다른 방식으로 진행됩니다. 동물은 동물원에 대한 참조를 가지며 동물원에 대한 동물 콜렉션은 조회 결과가됩니다.

  • 그 아이디어를 더 멀리 가져 가면 체인 아키텍처를 사용할 수 있습니다. 즉, 인터페이스를 만들고 Messable각 혼란스러운 항목에 대한 참조를 포함하십시오 next. 엉망을 만든 후 MakeMess다음 항목을 호출 하십시오.

그래서 여기 동물원은 엉망이되기 때문에 엉망이됩니다. 있다:

Zoo implements Messable
House implements Messable
Animal implements Messable
   Messable next

   MakeMess()
       messy = true
       next.MakeMess

이제 엉망이 생겼다는 메시지를 얻는 일련의 것들이 있습니다.

  • 옵션 2, 발행 / 구독 모델은 여기서 작동 할 수 있지만 실제로는 무겁습니다. 객체와 컨테이너는 알려진 관계를 가지고 있기 때문에 그보다 일반적인 것을 사용하는 것이 약간 무겁습니다.

  • 옵션 3 :이 특정한 경우, 전화는 Zoo.MakeMess(animal)또는 House.MakeMess(animal)실제로 나쁜 옵션이 아닙니다. 집과 동물원이 지저분 해지는 의미가 다를 수 있기 때문입니다.

체인 경로를 내려 가지 않더라도 여기에 두 가지 문제가있는 것처럼 들립니다 .1) 문제는 객체에서 컨테이너로 변경 사항을 전파하는 것과 관련이 있습니다 .2) 인터페이스를 분리하려는 것처럼 들립니다. 동물들이 살 수있는 곳을 추상화하는 용기.

...

일급 함수가 있다면, 엉망으로 만든 함수를 Animal에 전달하여 호출 할 수 있습니다. 인터페이스 대신 함수를 제외하고는 체인 아이디어와 조금 비슷합니다.

public Animal
    Function afterMess

    public MakeMess()
        messy = true
        afterMess()

동물이 움직일 때 새로운 대의원을 설정하십시오.

  • 극단적으로, MakeMess에 대한 "after"조언과 함께 Aspect Oriented Programming (AOP)을 사용할 수 있습니다.

2

1과 함께 가지만 알림 논리와 함께 부모-자식 관계를 별도의 래퍼로 만듭니다. 이렇게하면 Zoo에 대한 Animal의 종속성이 제거되고 부모-자식 관계를 자동으로 관리 할 수 ​​있습니다. 그러나이를 위해서는 먼저 계층 구조의 개체를 인터페이스 / 추상 클래스로 다시 만들고 각 인터페이스에 대한 특정 래퍼를 작성해야합니다. 그러나 코드 생성을 사용하여 제거 할 수 있습니다.

같은 것 :

public interface IAnimal
{
    string Name { get; set; }
    int Age { get; set; }

    void MakeMess();
}

public class Animal : IAnimal
{
    public string Name { get; set; }
    public int Age { get; set; }

    public void MakeMess()
    {
        // makes mess
    }
}

public class ZooAnimals
{
    class AnimalInZoo : IAnimal
    {
        public IAnimal _animal;
        public ZooAnimals _zoo;

        public AnimalInZoo(IAnimal animal, ZooAnimals zoo)
        {
            _animal = animal;
            _zoo = zoo;
        }

        public string Name { get { return _animal.Name; } set { _animal.Name = value; } }
        public int Age { get { return _animal.Age; } set { _animal.Age = value; } }

        public void MakeMess()
        {
            _animal.MakeMess();
            _zoo.IsDirty = true;
        }
    }

    private Collection<AnimalInZoo> animals = new Collection<AnimalInZoo>();

    public IAnimal Add(IAnimal animal)
    {
        if (animal is AnimalInZoo)
        {
            var inZoo = (AnimalInZoo)animal;
            if (inZoo._zoo != this)
            {
                // animal is in a different zoo, what to do ?
                // either move animal to this zoo
                // or throw an exception so caller is forced to remove the animal from previous zoo first
            }
        }

        var anim = new AnimalInZoo(animal, this);
        animals.Add(anim);
        return anim;
    }

    public IAnimal Remove(IAnimal animal)
    {
        if (!(animal is AnimalInZoo))
        {
            // animal is not in zoo, throw an exception?
        }
        var inZoo = (AnimalInZoo)animal;
        if (inZoo._zoo != this)
        {
            // animal is in a different zoo, throw an exception?
        }

        animals.Remove(inZoo);
        return inZoo._animal;
    }

    public bool IsDirty { get; set; }
}

이것은 실제로 일부 ORM이 엔티티에 대한 변경 추적을 수행하는 방법입니다. 그들은 엔티티 주위에 랩퍼를 작성하고 그와 함께 작업하게합니다. 이러한 래퍼는 일반적으로 리플렉션 및 동적 코드 생성을 사용하여 만들어집니다.


1

내가 자주 사용하는 두 가지 옵션. 두 번째 접근 방식을 사용하고 이벤트 자체를 부모에 대한 컬렉션 자체에 배선하는 논리를 넣을 수 있습니다.

대체 방법 (실제로 세 가지 옵션 중 하나와 함께 사용할 수 있음)은 격리를 사용하는 것입니다. 집이나 동물원 또는 다른 곳에 살 수있는 AnimalContainer를 만들거나 컬렉션으로 만드십시오. 동물과 관련된 추적 기능을 제공하지만 필요한 개체에 포함될 수 있으므로 상속 문제를 피합니다.


0

기본 오류로 시작합니다. 자식 개체는 부모에 대해 알아야합니다.

문자열이 목록에 있다는 것을 알고 있습니까? 아니요. 날짜가 캘린더에 존재한다는 것을 알고 있습니까? 아니.

가장 좋은 방법은 이런 종류의 시나리오가 존재하지 않도록 디자인을 변경하는 것입니다.

이후에는 제어의 반전을 고려하십시오. 대신 MakeMessAnimal부작용 또는 이벤트와, 전달 Zoo방법으로. Animal항상 어딘가에 살아야 하는 불변을 보호해야 할 경우 옵션 1이 좋습니다 . 그것은 부모가 아니라 동료 협회입니다.

때때로 2와 3은 괜찮을 것입니다. 그러나 따라야 할 주요 건축 원칙은 아이들이 부모에 대해 모른다는 것입니다.


나는 이것이 목록의 문자열보다 양식의 제출 버튼과 비슷하다고 생각합니다.
svidgen

1
@svidgen-그런 다음 콜백을 전달하십시오. 사건보다 더 무모하고, 추론하기 쉽고, 알 필요가없는 것에 대한 나쁜 언급은 없습니다.
Telastyn
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.