Шаблони розповсюдження змінюють модель об'єкта ..?


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's заплутався, тоді потрібно встановити прапор IsDirty в зоопарку.

Є кілька способів впоратися з цим сценарієм (який я знаю).

1) Кожна тварина може мати посилання на батьківський зоопарк, щоб повідомляти про зміни.

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

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

Це здається найгіршим варіантом, оскільки він з'єднує Тварину з його батьківським об'єктом. Що робити, якщо я хочу тварину, яка живе в будинку?

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)
    {
        ...
    }
}

Я ще не знайшов гарної стратегії вирішення цих ситуацій. Що ще є? Як це можна спростити?


Ви маєте рацію про те, що №1 поганий, і я теж не захоплююся №2; як правило, ви хочете уникати побічних ефектів, а замість цього їх збільшуєте. Що стосується варіанта №3, чому ви не можете виділити AnimalMakeMess в статичний метод, який можуть викликати всі класи?
Доваль

4
№1 не є поганим, якщо ви спілкуєтесь через інтерфейс (IAnimalObserver) замість цього певного класу батьків.
coredump

Відповіді:


11

Мені довелося зіткнутися з цим пару разів. Я вперше застосував варіант 2 (події) і, як ви сказали, це стало справді складним. Якщо ви йдете цим маршрутом, я настійно пропоную вам провести дуже ретельні одиничні тести, щоб переконатися, що події виконані правильно, і ви не залишаєте звисаючих посилань, інакше налагодження дійсно велике.

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

Я виявив, що останнє було набагато простішим у підтримці, і мої побоювання щодо проблем із виконанням роботи ніколи не здійснилися.

Редагувати

Ще один спосіб вирішення цього питання - схема шини повідомлень . Замість того, щоб використовувати Controllerподібний у своєму прикладі, ви вводите кожен об'єкт 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 , я збираюся поїхати з цим, хоча, мабуть, застосував би ланцюговий розчин нижче, знаючи мене. (Це просто та сама модель, яку пропонує @ Бенджамін Альберт.)

Зауважте, що якби ви моделювали таблиці реляційних баз даних, співвідношення пішло б іншим шляхом: тварина матиме посилання на зоопарк, а колекція тварин для зоопарку - результат запиту.

  • Зробивши цю ідею далі, ви могли використовувати ланцюгову архітектуру. Тобто створіть інтерфейс 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, але я зробив би стосунки батько-дитина разом з логікою сповіщення в окрему обгортку. Це знімає залежність тварини від зоопарку і дозволяє автоматично керувати відносинами батько-дитина. Але для цього потрібно переробити об'єкти в ієрархії спочатку в інтерфейси / абстрактні класи та написати певну обгортку для кожного інтерфейсу. Але це можна було б видалити за допомогою генерації коду.

Щось на зразок :

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

Ви починаєте з основної невдачі: дочірні предмети не повинні знати про своїх батьків.

Чи знають рядки, що вони в списку? Ні. Чи знають дати, що вони існують у календарі? Ні.

Найкращий варіант - змінити свій дизайн, щоб такого сценарію не існувало.

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

Іноді 2 і 3 буде добре, але головний архітектурний принцип, який слід дотримуватися, - це те, що діти не знають про своїх батьків.


Я підозрюю, що це більше схоже на кнопку подання у формі, ніж на рядок у списку.
svidgen

1
@svidgen - потім передайте зворотний дзвінок. Більш нерозумний, ніж подія, простіший в міркуванні і жодних неслухняних посилань на речі, про які він не повинен знати.
Теластин
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.