Шаблоны для распространения изменений в объектной модели ..?

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 сделал беспорядок, тогда необходимо установить флаг 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)
    {
        ...
    }
}

Это кажется очень неестественным и вызывает дублирование логики. Например, если бы у меня был объект House, который не имеет общего родительского объекта наследования с Zoo ..

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

Я еще не нашел хорошую стратегию для решения этих ситуаций. Что еще доступно? Как это можно сделать проще?

ConditionRacer
источник
Вы правы в том, что № 1 плохой, и я тоже не заинтересован в № 2; как правило, вы хотите избежать побочных эффектов, а вместо этого вы увеличиваете их. Что касается варианта №3, почему вы не можете выделить AnimalMakeMess в статический метод, который могут вызывать все классы?
Доваль
4
# 1 не обязательно плох, если вы общаетесь через интерфейс (IAnimalObserver) вместо этого конкретного класса Parent.
coredump

Ответы:

11

Я должен был иметь дело с этим пару раз. В первый раз я использовал вариант 2 (события), и, как вы сказали, он стал действительно сложным. Если вы пойдете по этому пути, я настоятельно рекомендую вам провести очень тщательные юнит-тесты, чтобы убедиться, что события выполнены правильно, и вы не оставляете висячие ссылки, в противном случае это действительно большая проблема для устранения.

Во второй раз я просто реализовал родительское свойство как функцию детей, поэтому оставляю Dirtyсвойство для каждого животного и позволяю Animal.IsDirtyreturn this.Animals.Any(x => x.IsDirty). Это было в модели. Над моделью был Контроллер, и работа контроллера состояла в том, чтобы знать, что после того, как я изменил модель (все действия в модели были пропущены через контроллер, поэтому он знал, что что- то изменилось), то он знал, что должен вызывать определенную ре -функции оценки, такие как запуск ZooMaintenanceотдела, чтобы проверить, Zooбыл ли грязный снова. В качестве альтернативы я мог просто ZooMaintenanceотменить проверки до некоторого запланированного более позднего времени (каждые 100 мс, 1 секунда, 2 минуты, 24 часа, все, что было необходимо).

Я обнаружил, что последнее было намного проще поддерживать, и мои опасения по поводу проблем с производительностью так и не оправдались.

редактировать

Другой способ справиться с этим - шаблон шины сообщений . Вместо того, чтобы использовать Controllerкак в моем примере, вы вводите каждый объект с IMessageBusсервисом. Затем Animalкласс может опубликовать сообщение, например "Mess Made", и ваш Zooкласс может подписаться на сообщение "Mess Made". Служба шины сообщений позаботится о том, чтобы уведомить, Zooкогда какое-либо животное публикует одно из этих сообщений, и может повторно оценить его IsDirtyсвойство.

Это имеет то преимущество, что Animalsбольше не нужна Zooссылка, и Zooне нужно беспокоиться о подписке и отмене подписки на события от каждого из них Animal. Наказанием является то, что всем Zooклассам, подписавшимся на это сообщение, придется пересмотреть свои свойства, даже если это не было одно из его животных. Это может иметь или не иметь большого значения. Если есть только один или два Zooэкземпляра, это, вероятно, хорошо.

Edit 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.

svidgen
источник
3
  • Вариант 1 на самом деле довольно прост. Это просто обратная ссылка. Но обобщите это с вызванным интерфейсом Dwellingи предоставьте MakeMessметод для этого. Это нарушает круговую зависимость. Затем, когда животное создает беспорядок, оно dwelling.MakeMess()тоже зовет .

В духе lex parsimoniae , я собираюсь пойти с этим, хотя я, вероятно, буду использовать цепное решение ниже, зная меня. (Это та же самая модель, которую предлагает Бенджамин Альберт.)

Обратите внимание, что если бы вы моделировали таблицы реляционной базы данных, связь шла бы по-другому: у Animal была бы ссылка на Zoo, а коллекция Animals для Zoo была бы результатом запроса.

  • Взяв эту идею дальше, вы можете использовать цепочечную архитектуру. То есть создайте интерфейс 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()

Когда животное движется, просто установите нового делегата.

  • В крайнем случае, вы можете использовать Аспектно-ориентированное программирование (AOP) с советом «после» на MakeMess.
обкрадывать
источник
2

Я бы пошел с 1, но я бы сделал отношения родитель-потомок вместе с логикой уведомления в отдельную оболочку. Это устраняет зависимость Animal от Zoo и позволяет автоматически управлять отношениями родитель-ребенок. Но для этого необходимо сначала преобразовать объекты в иерархии в интерфейсы / абстрактные классы и написать специальную оболочку для каждого интерфейса. Но это может быть удалено с помощью генерации кода.

Что-то типа :

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 отслеживают изменения в сущностях. Они создают обертки вокруг сущностей и заставляют вас работать с ними. Эти обертки обычно создаются с использованием рефлексии и динамической генерации кода.

Euphoric
источник
1

Два варианта я часто использую. Вы можете использовать второй подход и поместить логику для связывания события в самой коллекции на родительский элемент.

Альтернативный подход (который может фактически использоваться с любым из трех вариантов) состоит в том, чтобы использовать сдерживание. Сделайте AnimalContainer (или даже сделайте его коллекцией), который может жить в доме, зоопарке или чем-то еще. Он предоставляет функции отслеживания, связанные с животными, но позволяет избежать проблем с наследованием, поскольку он может быть включен в любой объект, который нуждается в нем.

Эй Джей Хендерсон
источник
0

Вы начинаете с основной ошибки: дочерние объекты не должны знать о своих родителях.

Знают ли строки, что они в списке? Нет. Знают ли даты, что они существуют в календаре? Нет.

Лучший вариант - изменить дизайн, чтобы такого сценария не было.

После этого рассмотрим инверсию управления. Вместо того , чтобы MakeMessна Animalс побочным эффектом или события, передать Zooв метод. Вариант 1 хорош, если вам нужно защитить инвариант, который Animalвсегда должен где-то жить. Это не родитель, а ассоциация сверстников.

Иногда со 2 и 3 все будет в порядке, но основной архитектурный принцип, который нужно соблюдать, заключается в том, что дети не знают о своих родителях.

Telastyn
источник
Я подозреваю, что это больше похоже на кнопку отправки в форме, чем на строку в списке.
svidgen
1
@svidgen - тогда передайте обратный вызов. Более надежный, чем событие, легче рассуждать, и никаких порочных ссылок на вещи, которые он не должен знать.
Теластин