Typowa implementacja repozytorium DDD nie wygląda zbyt dobrze, na przykład save()
metoda:
package com.example.domain;
public class Product { /* public attributes for brevity */
public String name;
public Double price;
}
public interface ProductRepo {
void save(Product product);
}
Część infrastruktury:
package com.example.infrastructure;
// imports...
public class JdbcProductRepo implements ProductRepo {
private JdbcTemplate = ...
public void save(Product product) {
JdbcTemplate.update("INSERT INTO product (name, price) VALUES (?, ?)",
product.name, product.price);
}
}
Taki interfejs oczekuje, że Product
będzie to model anemiczny, przynajmniej z getterami.
Z drugiej strony OOP mówi, że Product
obiekt powinien wiedzieć, jak się uratować.
package com.example.domain;
public class Product {
private String name;
private Double price;
void save() {
// save the product
// ???
}
}
Chodzi o to, że gdy Product
umie się zapisać, oznacza to, że kod infrastruktury nie jest oddzielony od kodu domeny.
Może możemy przekazać zapisywanie innemu obiektowi:
package com.example.domain;
public class Product {
private String name;
private Double price;
void save(Storage storage) {
storage
.with("name", this.name)
.with("price", this.price)
.save();
}
}
public interface Storage {
Storage with(String name, Object value);
void save();
}
Część infrastruktury:
package com.example.infrastructure;
// imports...
public class JdbcProductRepo implements ProductRepo {
public void save(Product product) {
product.save(new JdbcStorage());
}
}
class JdbcStorage implements Storage {
private final JdbcTemplate = ...
private final Map<String, Object> attrs = new HashMap<>();
private final String tableName;
public JdbcStorage(String tableName) {
this.tableName = tableName;
}
public Storage with(String name, Object value) {
attrs.put(name, value);
}
public void save() {
JdbcTemplate.update("INSERT INTO " + tableName + " (name, price) VALUES (?, ?)",
attrs.get("name"), attrs.get("price"));
}
}
Jakie jest najlepsze podejście do tego? Czy można zaimplementować repozytorium obiektowe?
Odpowiedzi:
Napisałeś
i w komentarzu.
Jest to powszechne nieporozumienie.
Product
jest obiektem domeny, więc powinien być odpowiedzialny za operacje domeny , które dotyczą jednego obiektu produktu, nie mniej, nie więcej - więc zdecydowanie nie za wszystkie operacje. Zwykle trwałość nie jest postrzegana jako operacja domeny. Wręcz przeciwnie, w aplikacjach korporacyjnych nierzadko próbuje się osiągnąć ignorancję w modelu domenowym (przynajmniej w pewnym stopniu), a utrzymywanie mechaniki trwałości w oddzielnej klasie repozytorium jest popularnym rozwiązaniem. „DDD” to technika, która ma na celu tego rodzaju aplikacje.Więc co może być rozsądną operacją domenową dla
Product
? Zależy to w rzeczywistości od kontekstu domeny systemu aplikacji. Jeśli system jest mały i obsługuje wyłącznie operacje CRUD, wówczas rzeczywiścieProduct
może pozostać dość „anemiczny”, jak w twoim przykładzie. W przypadku takich aplikacji może być dyskusyjne, czy umieszczenie operacji bazy danych w osobnej klasie repo lub użycie DDD w ogóle jest kłopotliwe.Jednak gdy tylko aplikacja obsługuje rzeczywiste operacje biznesowe, takie jak kupowanie lub sprzedawanie produktów, utrzymywanie ich w magazynie i zarządzanie nimi, lub obliczanie podatków dla nich, dość często zaczynasz odkrywać operacje, które można rozsądnie umieścić w
Product
klasie. Na przykład może istnieć operacja,CalcTotalPrice(int noOfItems)
która oblicza cenę za pozycje n określonego produktu, biorąc pod uwagę rabaty ilościowe.Krótko mówiąc, projektując klasy, musisz pomyśleć o swoim kontekście, w którym jesteś z pięciu światów Joela Spolsky'ego , a jeśli system zawiera wystarczającą logikę domeny, więc DDD będzie korzystne. Jeśli odpowiedź brzmi „tak”, jest mało prawdopodobne, że skończysz z modelem anemicznym tylko dlatego, że trzymasz mechanizmy trwałości poza klasami domen.
źródło
Account.transfer(amount)
powinien zachować transfer. Jak to robi, to odpowiedzialność obiektu, a nie jakiegoś zewnętrznego bytu. Z drugiej strony wyświetlanie obiektu jest zwykle operacją domeny! Wymagania zwykle opisują bardzo szczegółowo, jak powinny wyglądać rzeczy. Jest to część języka wśród członków projektu, biznesu lub nie.Account.transfer
dla zwykle obejmuje dwa obiekty konta i obiekt jednostki pracy. Operacja polegająca na utrwalaniu transakcji mogłaby wówczas stanowić część tego ostatniego (razem z wywołaniami powiązanych repozytoriów), więc nie wchodzi w grę metoda „transferu”. W ten sposóbAccount
może pozostać ignorantem. Nie twierdzę, że jest to z pewnością lepsze niż twoje domniemane rozwiązanie, ale twoje jest również tylko jednym z kilku możliwych podejść.Ćwicz teorię atutów.
Doświadczenie uczy nas, że Product.Save () prowadzi do wielu problemów. Aby obejść te problemy, opracowaliśmy wzorzec repozytorium.
Na pewno łamie zasadę OOP dotyczącą ukrywania danych produktu. Ale działa dobrze.
Znacznie trudniej jest stworzyć zestaw spójnych reguł obejmujących wszystko, niż stworzyć ogólne dobre reguły z wyjątkami.
źródło
Pomaga pamiętać, że między tymi dwiema ideami nie ma napięcia - obiekty wartości, agregaty, repozytoria to tablica używanych wzorców, co niektórzy uważają za wykonane poprawnie.
Skąd. Obiekty hermetyzują własne struktury danych. Twoja reprezentacja produktu w pamięci jest odpowiedzialna za prezentowanie zachowań produktu (czymkolwiek są); ale trwały magazyn jest tam (za repozytorium) i ma swoją własną pracę.
Musi istnieć sposób na skopiowanie danych między reprezentacją bazy danych w pamięci a jej utrwaloną pamiątką. Na granicy rzeczy stają się dość prymitywne.
Zasadniczo bazy danych tylko do zapisu nie są szczególnie przydatne, a ich odpowiedniki w pamięci nie są bardziej użyteczne niż sortowanie „utrwalone”. Nie ma sensu umieszczać informacji w
Product
obiekcie, jeśli nigdy nie zamierzasz ich wyciągać. Niekoniecznie będziesz używać „pobierających” - nie próbujesz udostępniać struktury danych produktu, a na pewno nie powinieneś udostępniać zmiennego dostępu do wewnętrznej reprezentacji Produktu.To z pewnością działa - twoje trwałe miejsce pamięci skutecznie staje się oddzwanianiem. Prawdopodobnie uprościłbym interfejs:
Będzie istniało sprzężenie między reprezentacją w pamięci a mechanizmem przechowywania, ponieważ informacje muszą być przekazywane z miejsca na miejsce (iz powrotem). Zmiana informacji, które zostaną udostępnione, wpłynie na oba końce rozmowy. Równie dobrze moglibyśmy wyrazić to wyraźnie, gdy tylko możemy.
Takie podejście - przekazywanie danych za pośrednictwem wywołań zwrotnych, odegrało ważną rolę w rozwoju próbnych prób w TDD .
Pamiętaj, że przekazywanie informacji do oddzwaniania ma te same ograniczenia, co zwracanie informacji z zapytania - nie powinieneś przekazywać zmiennych kopii swoich struktur danych.
To podejście jest trochę sprzeczne z tym, co Evans opisał w Niebieskiej Księdze, w której zwracanie danych za pomocą zapytania było normalnym sposobem postępowania, a obiekty domeny zostały zaprojektowane specjalnie w celu uniknięcia mieszania się w „obawach związanych z trwałością”.
Należy pamiętać o jednej rzeczy - Niebieska Księga została napisana piętnaście lat temu, gdy Java 1.4 wędrowała po ziemi. W szczególności książka poprzedza generyczne Java - mamy teraz o wiele więcej dostępnych technik niż wtedy, gdy Evans rozwijał swoje pomysły.
źródło
Storage
interfejs w taki sam sposób, jak Ty, a potem wziąłem pod uwagę wysokie sprzężenie i zmieniłem go. Ale masz rację, i tak istnieje nieuniknione sprzężenie, więc dlaczego nie uczynić tego bardziej wyraźnym.Bardzo dobre obserwacje, całkowicie się z nimi zgadzam. Oto moje omówienie (poprawka: tylko slajdy) na dokładnie ten temat: Object-Oriented Domain-Driven Design .
Krótka odpowiedź: nie. W twojej aplikacji nie powinno być obiektu czysto technicznego i niezwiązanego z domeną. To tak, jak zaimplementowanie struktury rejestrowania w aplikacji księgowej.
Twój
Storage
przykład interfejsu jest doskonały, zakładając, żeStorage
jest on wtedy uważany za jakiś szkielet zewnętrzny, nawet jeśli go napiszesz.Ponadto
save()
w obiekcie należy zezwolić tylko wtedy, gdy jest on częścią domeny („język”). Na przykład nie powinienem być zobowiązany do „zapisania”Account
po zadzwonieniutransfer(amount)
. Słusznie powinienem oczekiwać, że funkcja biznesowatransfer()
utrzyma mój transfer.Podsumowując, myślę, że pomysły DDD są dobre. Używanie wszechobecnego języka, ćwiczenie domeny rozmową, ograniczonymi kontekstami itp. Bloki konstrukcyjne wymagają jednak poważnego remontu, jeśli są zgodne z orientacją obiektową. Szczegółowe informacje można znaleźć w połączonej talii.
źródło
AccountNumber
powinien wiedzieć, że może być reprezentowany jakoTextField
. Jeśli inni (jak na „View”), to wiedziałby, że jest sprzężenie, które nie powinny istnieć, ponieważ składnik ten musiałby wiedzieć, coAccountNumber
składa, czyli wewnętrzne.Unikaj niepotrzebnego rozpowszechniania wiedzy o polach. Im więcej rzeczy wie o danym polu, tym trudniej jest go dodać lub usunąć:
W tym przypadku produkt nie ma pojęcia, czy zapisujesz plik dziennika, bazę danych czy jedno i drugie. Tutaj metoda zapisu nie ma pojęcia, czy masz 4 lub 40 pól. To luźno powiązane. To dobra rzecz.
Oczywiście jest to tylko jeden przykład tego, jak możesz osiągnąć ten cel. Jeśli nie lubisz budować i analizować łańcucha znaków jako DTO, możesz także użyć kolekcji.
LinkedHashMap
jest moim ulubionym, ponieważ zachowuje porządek, a toString () wygląda dobrze w pliku dziennika.Jakkolwiek to robisz, nie rozpowszechniaj wiedzy na temat różnych dziedzin. Jest to forma łączenia, którą ludzie często ignorują, dopóki nie jest za późno. Chcę, aby jak najmniej rzeczy statycznie wiedziało, ile pól ma mój obiekt, jak to możliwe. W ten sposób dodanie pola nie wymaga wielu edycji w wielu miejscach.
źródło
Map
, proponujesz aString
lub aList
. Ale, jak wspomniał @VoiceOfUnreason w swojej odpowiedzi, połączenie wciąż istnieje, po prostu nie jest jednoznaczne. Nadal niepotrzebna jest znajomość struktury danych produktu, aby zapisać go zarówno w bazie danych, jak i pliku dziennika, przynajmniej w przypadku odczytu jako obiektu.Storage
Jest częścią domeny (jak również repozytorium interfejs) i sprawia, że taka trwałości API. Po zmianie lepiej poinformować klientów w czasie kompilacji, ponieważ i tak muszą zareagować, aby nie ulec awarii w czasie wykonywania.Istnieje alternatywa dla wspomnianych już wzorów. Wzorzec Memento doskonale nadaje się do enkapsulacji stanu wewnętrznego obiektu domeny. Obiekt memento reprezentuje migawkę stanu publicznego obiektu domeny. Obiekt domeny wie, jak utworzyć ten stan publiczny ze stanu wewnętrznego i odwrotnie. Repozytorium działa wtedy tylko z publiczną reprezentacją państwa. Dzięki temu wewnętrzne wdrożenie jest oddzielone od jakichkolwiek szczegółów dotyczących trwałości i musi jedynie utrzymać zamówienie publiczne. Również twój obiekt domeny nie musi ujawniać żadnych programów pobierających, które rzeczywiście sprawiłyby, że byłby trochę anemiczny.
Aby uzyskać więcej informacji na ten temat, polecam świetną książkę: „Wzorce, zasady i praktyki projektowania opartego na domenie” Scott Millett i Nick Tune
źródło