Вот общий сценарий, с которым мне всегда приходится сталкиваться.
У меня есть объектная модель с родительским объектом. Родитель содержит несколько дочерних объектов. Что-то вроде этого.
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)
{
...
}
}
Я еще не нашел хорошую стратегию для решения этих ситуаций. Что еще доступно? Как это можно сделать проще?
источник
Ответы:
Я должен был иметь дело с этим пару раз. В первый раз я использовал вариант 2 (события), и, как вы сказали, он стал действительно сложным. Если вы пойдете по этому пути, я настоятельно рекомендую вам провести очень тщательные юнит-тесты, чтобы убедиться, что события выполнены правильно, и вы не оставляете висячие ссылки, в противном случае это действительно большая проблема для устранения.
Во второй раз я просто реализовал родительское свойство как функцию детей, поэтому оставляю
Dirty
свойство для каждого животного и позволяюAnimal.IsDirty
returnthis.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
беспорядок. Если бы было больше объектов, о которых нужно было уведомить, я бы, вероятно, перешел на шину сообщений или контроллер.источник
Я сделал простую диаграмму классов, которая описывает ваш домен:
У каждого
Animal
естьHabitat
проблема.Ему
Habitat
все равно, сколько или сколько животных у него есть (если только это не является принципиально частью вашего дизайна, который в данном случае вы описали, но это не так).Но это
Animal
все равно, потому что он будет вести себя по-разному в каждомHabitat
.Эта диаграмма аналогична UML-диаграмме шаблона разработки стратегии , но мы будем использовать ее по-другому.
Вот несколько примеров кода на Java (я не хочу допускать ошибок, специфичных для C #).
Конечно, вы можете внести свои собственные изменения в этот дизайн, язык и требования.
Это интерфейс Стратегии:
Пример из бетона
Habitat
. Конечно, каждыйHabitat
подкласс может реализовывать эти методы по-разному.Конечно, у вас может быть несколько подклассов животных, каждый из которых портит их по-своему:
Это класс клиента, это в основном объясняет, как вы можете использовать этот дизайн.
Конечно, в вашем реальном приложении вы можете дать
Habitat
знать и управлять,Animal
если вам нужно.источник
У меня был достаточный успех с такими архитектурами, как ваш вариант 2 в прошлом. Это наиболее общий вариант, который обеспечит максимальную гибкость. Но если вы контролируете своих слушателей и не управляете множеством типов подписок, вы можете проще подписаться на события, создав интерфейс.
Преимущество интерфейса состоит в том, что он почти такой же простой, как и вариант 1, но также позволяет вам довольно легко размещать животных в
House
илиFairlyLand
.источник
Dwelling
и предоставьтеMakeMess
метод для этого. Это нарушает круговую зависимость. Затем, когда животное создает беспорядок, оноdwelling.MakeMess()
тоже зовет .В духе lex parsimoniae , я собираюсь пойти с этим, хотя я, вероятно, буду использовать цепное решение ниже, зная меня. (Это та же самая модель, которую предлагает Бенджамин Альберт.)
Обратите внимание, что если бы вы моделировали таблицы реляционной базы данных, связь шла бы по-другому: у Animal была бы ссылка на Zoo, а коллекция Animals для Zoo была бы результатом запроса.
Messable
и включите в каждый беспорядочный элемент ссылку наnext
. После создания беспорядка, позвонитеMakeMess
на следующий пункт.Так что зоопарк здесь занимается созданием беспорядка, потому что он тоже становится грязным. Есть:
Так что теперь у вас есть цепочка вещей, которые получают сообщение о том, что был создан беспорядок.
Вариант 2, модель публикации / подписки могла бы работать здесь, но кажется действительно тяжелой. Объект и контейнер имеют известную взаимосвязь, поэтому кажется немного сложным использовать что-то более общее, чем это.
Вариант 3: В данном конкретном случае, звонить
Zoo.MakeMess(animal)
илиHouse.MakeMess(animal)
не очень плохой вариант, потому что дом может иметь другую семантику для беспорядка, чем в зоопарке.Даже если вы не идете по цепочечному маршруту, похоже, здесь есть две проблемы: 1) проблема заключается в распространении изменения от объекта к его контейнеру, 2) звучит так, как будто вы хотите выделить интерфейс для контейнер для абстракции, где животные могут жить.
...
Если у вас есть первоклассные функции, вы можете передать функцию (или делегировать) в Animal для вызова после того, как она создаст беспорядок. Это немного похоже на идею цепочки, за исключением функции вместо интерфейса.
Когда животное движется, просто установите нового делегата.
источник
Я бы пошел с 1, но я бы сделал отношения родитель-потомок вместе с логикой уведомления в отдельную оболочку. Это устраняет зависимость Animal от Zoo и позволяет автоматически управлять отношениями родитель-ребенок. Но для этого необходимо сначала преобразовать объекты в иерархии в интерфейсы / абстрактные классы и написать специальную оболочку для каждого интерфейса. Но это может быть удалено с помощью генерации кода.
Что-то типа :
Именно так некоторые ORM отслеживают изменения в сущностях. Они создают обертки вокруг сущностей и заставляют вас работать с ними. Эти обертки обычно создаются с использованием рефлексии и динамической генерации кода.
источник
Два варианта я часто использую. Вы можете использовать второй подход и поместить логику для связывания события в самой коллекции на родительский элемент.
Альтернативный подход (который может фактически использоваться с любым из трех вариантов) состоит в том, чтобы использовать сдерживание. Сделайте AnimalContainer (или даже сделайте его коллекцией), который может жить в доме, зоопарке или чем-то еще. Он предоставляет функции отслеживания, связанные с животными, но позволяет избежать проблем с наследованием, поскольку он может быть включен в любой объект, который нуждается в нем.
источник
Вы начинаете с основной ошибки: дочерние объекты не должны знать о своих родителях.
Знают ли строки, что они в списке? Нет. Знают ли даты, что они существуют в календаре? Нет.
Лучший вариант - изменить дизайн, чтобы такого сценария не было.
После этого рассмотрим инверсию управления. Вместо того , чтобы
MakeMess
наAnimal
с побочным эффектом или события, передатьZoo
в метод. Вариант 1 хорош, если вам нужно защитить инвариант, которыйAnimal
всегда должен где-то жить. Это не родитель, а ассоциация сверстников.Иногда со 2 и 3 все будет в порядке, но основной архитектурный принцип, который нужно соблюдать, заключается в том, что дети не знают о своих родителях.
источник