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ć?
źródło
Odpowiedzi:
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
Dirty
właściwość dla każdego zwierzęcia i pozwólAnimal.IsDirty
wró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 uruchamianieZooMaintenance
departamentu, aby sprawdzić, czyZoo
ponownie jest brudny. Alternatywnie mogłem po prostu odłożyćZooMaintenance
kontrole 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ć
Controller
podobnego w moim przykładzie, wstrzykujesz każdy obiektIMessageBus
usługą.Animal
Klasa może opublikować wiadomość, jak „bałagan made” aZoo
klasa może zapisać się komunikatem „Bałagan na miarę”. Usługa magistrali komunikatów zajmie się powiadomieniem,Zoo
kiedy dowolne zwierzę opublikuje jeden z tych komunikatów, i może ponownie ocenić swojąIsDirty
właściwość.Ma to tę zaletę, że
Animals
nie potrzebuje jużZoo
referencji iZoo
nie musi się martwić o subskrybowanie i anulowanie subskrypcji wydarzeń z każdego z nichAnimal
. Kara polega na tym, że wszystkieZoo
klasy 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 dwaZoo
wystą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ę
Animal
klasą, będzie oczywiste, że poMakeMess
wywołaniu propaguje wiadomość do klasyZoo
i będzie oczywiste dlaZoo
klasy, 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ż tylkoZoo
trzeba powiadomić, jeśliAnimal
zrobi 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.źródło
Stworzyłem prosty schemat klas, który opisuje twoją domenę:
Każdy
Animal
maHabitat
bałagan.Habitat
Nie 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
Animal
to obchodzi, ponieważ będzie zachowywać się inaczej w każdymHabitat
.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:
Przykład betonu
Habitat
. oczywiście każdaHabitat
podklasa może implementować te metody inaczej.Oczywiście możesz mieć wiele podklas zwierząt, z których każda miesza to inaczej:
To jest klasa klienta, to w zasadzie wyjaśnia, w jaki sposób można użyć tego projektu.
Oczywiście w swojej prawdziwej aplikacji możesz poinformować
Habitat
i zarządzać,Animal
jeśli potrzebujesz.źródło
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.
Opcja interfejsu ma tę zaletę, że jest prawie tak prosta, jak opcja 1, ale umożliwia także dość łatwe trzymanie zwierząt w
House
lubFairlyLand
.źródło
Dwelling
i podajMakeMess
metodę. To przerywa zależność cykliczną. Potem, gdy zwierzę robi bałagan, również dzwonidwelling.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.
Messable
, a w każdym komunikacie zawieraj odniesienie donext
. Po stworzeniu bałaganu zadzwońMakeMess
do następnego elementu.Zoo tutaj jest zamieszane w robienie bałaganu, ponieważ robi się bałagan. Mieć:
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)
lubHouse.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.
Kiedy zwierzę się poruszy, po prostu ustaw nowego delegata.
źródło
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 :
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.
źródło
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.
źródło
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
MakeMess
naAnimal
z efektem ubocznym lub zdarzenia, należy przekazaćZoo
do metody. Opcja 1 jest w porządku, jeśli chcesz chronić niezmiennika, któryAnimal
zawsze 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.
źródło