Wzory do propagowania zmian w górę modelu obiektowego ..?

22

Oto typowy scenariusz, który zawsze frustruje mnie.

Mam model obiektu z obiektem nadrzędnym. Element nadrzędny zawiera niektóre obiekty podrzędne. Coś takiego.

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

Każdy obiekt podrzędny ma różne dane i metody

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

    public void MakeMess()
    {
        ...
    }
}

Kiedy dziecko zmienia się, w tym przypadku, gdy wywoływana jest metoda MakeMess, pewna wartość w obiekcie nadrzędnym musi zostać zaktualizowana. Powiedzmy, że kiedy pewien próg Animalów popełnił bałagan, należy ustawić flagę Zoo IsDirty.

Istnieje kilka sposobów radzenia sobie z tym scenariuszem (o których wiem).

1) Każde zwierzę może mieć nadrzędny numer referencyjny Zoo w celu komunikowania zmian.

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

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

To wydaje się najgorszą opcją, ponieważ łączy Animal z obiektem nadrzędnym. Co jeśli chcę zwierzęcia mieszkającego w domu?

2) Inną opcją, jeśli używasz języka obsługującego zdarzenia (takiego jak C #), jest to, aby rodzic subskrybował zmiany zdarzeń.

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

To wydaje się działać, ale z doświadczenia staje się trudne do utrzymania. Jeśli zwierzęta kiedykolwiek zmienią zoo, musisz anulować subskrypcję wydarzeń w starym zoo i ponownie zapisać się w nowym zoo. To tylko pogarsza się, gdy drzewo kompozycji pogłębia się.

3) Inną opcją jest przeniesienie logiki do rodzica.

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

Wydaje się to bardzo nienaturalne i powoduje duplikację logiki. Na przykład, gdybym miał obiekt House, który nie ma wspólnego wspólnego obiektu nadrzędnego z Zoo ..

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

Nie znalazłem jeszcze dobrej strategii radzenia sobie z tymi sytuacjami. Co jeszcze jest dostępne? Jak można to uprościć?

ConditionRacer
źródło
Masz rację, że nr 1 jest zły, a ja też nie lubię nr 2; generalnie chcesz uniknąć efektów ubocznych, a zamiast tego je zwiększasz. Jeśli chodzi o opcję nr 3, dlaczego nie można rozdzielić AnimalMakeMess na metodę statyczną, którą wszystkie klasy mogą wywoływać?
Doval
4
# 1 nie jest koniecznie zły, jeśli komunikujesz się przez interfejs (IAnimalObserver) zamiast tej konkretnej klasy nadrzędnej.
coredump

Odpowiedzi:

11

Musiałem sobie z tym poradzić kilka razy. Za pierwszym razem użyłem opcji 2 (zdarzenia) i jak powiedziałeś, stało się to naprawdę skomplikowane. Jeśli pójdziesz tą drogą, wysoce sugeruję, że potrzebujesz bardzo dokładnych testów jednostkowych, aby upewnić się, że zdarzenia zostały wykonane poprawnie i nie pozostawiasz zwisających referencji, w przeciwnym razie debugowanie jest bardzo trudne.

Za drugim razem właśnie wdrożyłem właściwość nadrzędną jako funkcję dzieci, więc zachowaj Dirtywłaściwość dla każdego zwierzęcia i pozwól Animal.IsDirtywrócić this.Animals.Any(x => x.IsDirty). To było w modelu. Nad modelem był kontroler, a zadaniem kontrolera było wiedzieć, że po zmianie modelu (wszystkie działania na modelu zostały przekazane przez kontroler, więc wiedział, że coś się zmieniło), a następnie wiedział, że musi zadzwonić -funkcje oceny, takie jak uruchamianie ZooMaintenancedepartamentu, aby sprawdzić, czy Zooponownie jest brudny. Alternatywnie mogłem po prostu odłożyć ZooMaintenancekontrole do pewnego zaplanowanego późniejszego czasu (co 100 ms, 1 sekunda, 2 minuty, 24 godziny, cokolwiek było konieczne).

Odkryłem, że ten drugi jest o wiele prostszy w utrzymaniu, a moje obawy przed problemami z wydajnością nigdy się nie ziściły.

Edytować

Innym sposobem radzenia sobie z tym jest wzorzec szyny komunikatów . Zamiast używać Controllerpodobnego w moim przykładzie, wstrzykujesz każdy obiekt IMessageBususługą. AnimalKlasa może opublikować wiadomość, jak „bałagan made” a Zooklasa może zapisać się komunikatem „Bałagan na miarę”. Usługa magistrali komunikatów zajmie się powiadomieniem, Zookiedy dowolne zwierzę opublikuje jeden z tych komunikatów, i może ponownie ocenić swoją IsDirtywłaściwość.

Ma to tę zaletę, że Animalsnie potrzebuje już Zooreferencji i Zoonie musi się martwić o subskrybowanie i anulowanie subskrypcji wydarzeń z każdego z nich Animal. Kara polega na tym, że wszystkie Zooklasy subskrybujące tę wiadomość będą musiały ponownie ocenić swoje właściwości, nawet jeśli nie było to jedno z jej zwierząt. To może, ale nie musi być wielką rzeczą. Jeśli jest tylko jeden lub dwa Zoowystąpienia, prawdopodobnie jest w porządku.

Edytuj 2

Nie lekceważ prostoty opcji 1. Każdy, kto odwiedzi kod, nie będzie miał problemu z jego zrozumieniem. Dla kogoś, kto nazywa się Animalklasą, będzie oczywiste, że po MakeMesswywołaniu propaguje wiadomość do klasy Zooi będzie oczywiste dla Zooklasy, z której otrzymuje wiadomości. Pamiętaj, że w programowaniu obiektowym wywołanie metody nazywane było „komunikatem”. W rzeczywistości jedynym sensownym wyjściem z opcji 1 jest to, że więcej niż tylko Zootrzeba powiadomić, jeśli Animalzrobi bałagan. Gdyby było więcej obiektów, które wymagały powiadomienia, prawdopodobnie przeniósłbym się do magistrali komunikatów lub kontrolera.

Scott Whitlock
źródło
5

Stworzyłem prosty schemat klas, który opisuje twoją domenę: wprowadź opis zdjęcia tutaj

Każdy Animal ma Habitat bałagan.

HabitatNie obchodzi mnie, co i jak wiele zwierząt ma (chyba że jest zasadniczo częścią projektu, który w tym przypadku nie jest to opisane).

Ale Animalto obchodzi, ponieważ będzie zachowywać się inaczej w każdym Habitat.

Ten schemat jest podobny do diagramu UML wzorca projektowania strategii , ale użyjemy go inaczej.

Oto kilka przykładów kodu w Javie (nie chcę popełniać żadnych błędów specyficznych dla C #).

Oczywiście możesz wprowadzić własne poprawki do tego projektu, języka i wymagań.

Oto interfejs strategii:

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

    public float getCleanliness();
}

Przykład betonu Habitat. oczywiście każda Habitatpodklasa może implementować te metody inaczej.

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

    public float getCleanliness() {
        return cleanliness;
    }

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

Oczywiście możesz mieć wiele podklas zwierząt, z których każda miesza to inaczej:

public class Animel {
    private Habitat habitat;

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

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

To jest klasa klienta, to w zasadzie wyjaśnia, w jaki sposób można użyć tego projektu.

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

Oczywiście w swojej prawdziwej aplikacji możesz poinformować Habitati zarządzać, Animaljeśli potrzebujesz.

Benjamin Albert
źródło
3

W przeszłości miałem spory sukces z architekturami takimi jak Twoja opcja 2. Jest to najbardziej ogólna opcja i zapewni największą elastyczność. Ale jeśli masz kontrolę nad słuchaczami i nie zarządzasz wieloma typami subskrypcji, możesz łatwiej subskrybować wydarzenia, tworząc interfejs.

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

class MessEvent
{
  String DetailsOrWhatever;
}

Opcja interfejsu ma tę zaletę, że jest prawie tak prosta, jak opcja 1, ale umożliwia także dość łatwe trzymanie zwierząt w Houselub FairlyLand.

svidgen
źródło
3
  • Opcja 1 jest właściwie dość prosta. To tylko odnośnik. Ale uogólnij go z wywołanym interfejsem Dwellingi podaj MakeMessmetodę. To przerywa zależność cykliczną. Potem, gdy zwierzę robi bałagan, również dzwoni dwelling.MakeMess().

W duchu lex parsimoniae zamierzam wybrać ten, chociaż prawdopodobnie użyłbym poniższego rozwiązania łańcuchowego, znając mnie. (Jest to dokładnie ten sam model, który sugeruje @Benjamin Albert.)

Zauważ, że jeśli modelujesz tabele relacyjnych baz danych, relacja przebiegałaby w drugą stronę: Animal miałoby odniesienie do Zoo, a kolekcja Animals dla Zoo byłaby wynikiem zapytania.

  • Idąc dalej tym pomysłem, możesz użyć architektury łańcuchowej. Oznacza to, że stwórz interfejs Messable, a w każdym komunikacie zawieraj odniesienie do next. Po stworzeniu bałaganu zadzwoń MakeMessdo następnego elementu.

Zoo tutaj jest zamieszane w robienie bałaganu, ponieważ robi się bałagan. Mieć:

Zoo implements Messable
House implements Messable
Animal implements Messable
   Messable next

   MakeMess()
       messy = true
       next.MakeMess

Teraz masz łańcuch rzeczy, które otrzymują wiadomość, że powstał bałagan.

  • Opcja 2, model publikowania / subskrybowania może tu działać, ale wydaje się bardzo ciężki. Obiekt i pojemnik mają znane powiązania, więc wydaje się, że ciężko jest użyć czegoś bardziej ogólnego.

  • Opcja 3: W tym konkretnym przypadku dzwonienie Zoo.MakeMess(animal)lub House.MakeMess(animal)wcale nie jest złą opcją, ponieważ dom może mieć inną semantykę bałaganu niż zoo.

Nawet jeśli nie pójdziesz ścieżką łańcucha, brzmi to tak, jakby były tutaj dwa problemy: 1) problem dotyczy propagacji zmiany z obiektu do kontenera, 2) brzmi, jakbyś chciał wyłączyć interfejs dla pojemnik do streszczenia, w którym zwierzęta mogą mieszkać.

...

Jeśli masz funkcje pierwszej klasy, możesz przekazać funkcję (lub delegować) do Animal, aby zadzwonić po tym, jak zrobi bałagan. To trochę przypomina ideę łańcucha, z wyjątkiem funkcji zamiast interfejsu.

public Animal
    Function afterMess

    public MakeMess()
        messy = true
        afterMess()

Kiedy zwierzę się poruszy, po prostu ustaw nowego delegata.

  • Jeśli chodzi o skrajność, możesz skorzystać z programowania zorientowanego na aspekty (AOP) z poradami „po” na temat MakeMess.
Obrabować
źródło
2

Wybrałbym 1, ale uczyniłbym relację rodzic-dziecko wraz z logiką powiadomień osobnym opakowaniem. To usuwa zależność Animal od Zoo i umożliwia automatyczne zarządzanie relacjami rodzic-dziecko. Wymaga to jednak ponownego przekształcenia obiektów w hierarchii w interfejsy / klasy abstrakcyjne i napisania konkretnego opakowania dla każdego interfejsu. Ale można to usunąć za pomocą generowania kodu.

Coś jak :

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

W ten sposób niektóre ORM śledzą zmiany jednostek. Tworzą owijki wokół jednostek i zmuszają do pracy z nimi. Te opakowania są zwykle tworzone przy użyciu refleksji i dynamicznego generowania kodu.

Euforyk
źródło
1

Dwie opcje, których często używam. Możesz użyć drugiego podejścia i umieścić logikę okablowania zdarzenia w samej kolekcji na obiekcie nadrzędnym.

Alternatywnym podejściem (które można faktycznie zastosować z dowolną z trzech opcji) jest użycie zabezpieczenia. Stwórz AnimalContainer (lub nawet stwórz kolekcję), która może mieszkać w domu, zoo lub czymkolwiek innym. Zapewnia funkcje śledzenia związane ze zwierzętami, ale pozwala uniknąć problemów z dziedziczeniem, ponieważ można je włączyć w dowolnym obiekcie, który tego potrzebuje.

AJ Henderson
źródło
0

Zaczynasz od podstawowej porażki: obiekty potomne nie powinny wiedzieć o swoich rodzicach.

Czy ciągi wiedzą, że są na liście? Nie. Czy daty wiedzą, że istnieją w kalendarzu? Nie.

Najlepszą opcją jest zmiana projektu, aby taki scenariusz nie istniał.

Następnie rozważ odwrócenie kontroli. Zamiast MakeMessna Animalz efektem ubocznym lub zdarzenia, należy przekazać Zoodo metody. Opcja 1 jest w porządku, jeśli chcesz chronić niezmiennika, który Animalzawsze musi gdzieś mieszkać. To nie jest rodzic, ale stowarzyszenie rówieśnicze.

Czasami 2 i 3 będą w porządku, ale kluczową zasadą architektury jest to, że dzieci nie wiedzą o swoich rodzicach.

Telastyn
źródło
Podejrzewam, że bardziej przypomina to przycisk przesyłania w formie aniżeli ciąg znaków na liście.
svidgen
1
@svidgen - Następnie przekaż połączenie zwrotne. Bardziej niezawodny niż wydarzenie, łatwiejsze do uzasadnienia i brak niegrzecznych odniesień do rzeczy, których nie musi wiedzieć.
Telastyn