Odseparowanie modelu danych od modelu domenowego

1

Pracuję sobie nad swoim pet projektem gdzie od początku rozdzieliłem sobie domene od innych rzeczy tj. infrastruktura, persystencja itd.
Przykładowo mam obiekt (rich model) Offer a encja hibernatowa reprezentująca go to OfferEntity. Mam repozytorium jpa

@Repository
public interface OfferEntityRepository extends JpaRepository<OfferEntity, Long> {
}

Mam też repozytorium na wyższym poziomie, które jest portem dla domeny

public interface OfferRepository {
    Offer save(Offer offer);
    Optional<Offer> find(long offerId);
    void delete(long offerId);
}

Poniżej implementacja tego portu czyli adapter, który mapuje obiekt domenowy na ten persystentny i używa tego niskopoziomowego repozytorium.

@Service
@Transactional
@RequiredArgsConstructor
public class JpaOfferRepository implements OfferRepository {

    private final OfferConverter offerConverter;
    private final OfferEntityRepository offerEntityRepository;

    @Override
    public Offer save(Offer offer) {
        Objects.requireNonNull(offer);
        if (offer.getId() != null) {
            return offerEntityRepository.findById(offer.getId())
                    .map($ -> {
                        OfferEntity offerToUpdate = offerConverter.convert(offer);
                        return offerConverter.convert(offerEntityRepository.save(offerToUpdate));
                    })
                    .orElseThrow(IllegalArgumentException::new);
        }
        OfferEntity savedOffer = offerEntityRepository.save(offerConverter.convert(offer));
        return offerConverter.convert(savedOffer);
    }

    @Override
    public Optional<Offer> find(long offerId) {
        return offerEntityRepository.findById(offerId).map(offerConverter::convert);
    }

    @Override
    public void delete(long offerId) {
        if (offerEntityRepository.existsById(offerId)) {
            offerEntityRepository.deleteById(offerId);
        }
    }
}

Generalnie ładnie to wygląda jak obiekty domenowe nie są złożone. Ale jak w klasie Offer dochodzą kolejne złożone obiekty domenowe, od
których on zależy np. użytkownik, który stworzył ofertę, kolekcja zamówień i inne kolekcje. Potem odtworzenie tego obiektu domenowego przez warstwę
persystencji staje się złożone i kosztowne, a ta warstwa nawet nie wie po co ma to wyciągać, może tylko po to żeby w domenie ktoś zmienił tylko tytuł ogłoszenia?
To już prościej by było zawołać prosty update w sql z serwisu niż przechodzić przez tą warstwę, która w tym przypadku odtwarza cały złożony obiekt z bazy danych składając w całość jego poszczególne części.
Kolejna rzecz to mam w klasie User metodę buy:

public class User {
      //...
      public Purchase buy(Offer offer, Integer amountPurchased) {
        Purchase purchase = new Purchase.Builder(this, offer, amountPurchased).build();
        offer.addPurchase(purchase);
        purchases.add(purchase);
        return purchase;
    }
}

i PurchaseService

@Service
@Transactional
@RequiredArgsConstructor
public class PurchaseService {

    private final OfferService offerService;
    private final UserService userService;
    private final PurchaseRepository purchaseRepository;

    public Either<Error, Long> purchase(PurchaseCommand purchaseCommand) {
        return offerService.getOffer(purchaseCommand.offerId())
                .flatMap(offer -> {
                    User buyer = userService.getUser(purchaseCommand.buyer());
                    return purchase(offer, buyer, purchaseCommand.amount());
                });
    }

    private Either<Error, Long> purchase(Offer offer, User buyer, Integer amount) {
        try {
            Purchase purchase = buyer.buy(offer, amount);
            return Either.right(purchaseRepository.save(purchase).getId());
        } catch (IllegalArgumentException | NullPointerException e) {
            return Either.left(Error.badRequest(e.getMessage()));
        }
    }
}

Na pierwszy rzut oka może wyglądać logicznie. Ale z drugiej strony zastanawiam się czy to, że obiekty
domenowe są ze sobą tak powiązane daje jakąś korzyść. Może tylko komplikuje bo później żeby takie obiekty
odtworzyć w warstwie persystencji robi się złożone i kosztowne a i tak się okazuje później, że w danym use case
potrzebujemy tylko jakiś wycinek tych danych a nie całość. I zapis obiektu też robi się skomplikowany bo trzeba decydować,
która strona relacji jest rodzicem i która powinna zapisać lub nie.

Jakie macie przemyślenia na ten temat? Czy to nie jest próba nadmiernego odwzorowania rzeczywistości?

2

Cześć @lukascode! Fajnie że zadałeś pytanie na forum 👋

lukascode napisał(a):

To już prościej by było zawołać prosty update w sql z serwisu niż przechodzić przez tą warstwę, która w tym przypadku odtwarza cały złożony obiekt z bazy danych składając w całość jego poszczególne części.

Jeśli uważasz że tworzenie obiektów domenowych jest zbyt kosztowne, to możesz dodać do wyżej-poziomowego repozytorium funkcję która po prostu zmienia tytuł:

OfferRepository highRepo = ; //weź skąds
highRepo.renameOrder("old title", "new title"); // nie potrzebujesz instancji żeby zrobić rename

Jeśli tytuły nie są unikalne, to możesz użyć jakiegoś pola które jest unikalne. Jeśli nie ma takiego pola, to możesz wyeksponować ID z bazy jako domenowe pole, możesz go nazwać key czy jak tam wolisz.

OfferRepository highRepo = ; //weź skąds
highRepo.renameOrder(14L, "new title"); // nie potrzebujesz instancji żeby zrobić rename
lukascode napisał(a):

Na pierwszy rzut oka może wyglądać logicznie. Ale z drugiej strony zastanawiam się czy to, że obiekty
domenowe są ze sobą tak powiązane daje jakąś korzyść. Może tylko komplikuje bo później żeby takie obiekty
odtworzyć w warstwie persystencji robi się złożone i kosztowne a i tak się okazuje później, że w danym use case
potrzebujemy tylko jakiś wycinek tych danych a nie całość. I zapis obiektu też robi się skomplikowany bo trzeba decydować,
która strona relacji jest rodzicem i która powinna zapisać lub nie.

Relacje obiektów domenowych nie koniecznie muszą być tożsame albo nawet podobne z tymi w bazie danych. Niektórzy ludzie robią taki skrót myślowy "w bazie X ma has many na Y, to w domenie też to dodam". Tylko jak widzisz, takie coś czasem prowadzi do problemów. Obiekty domenowe powinny być właśnie blisko Twoich use-caseów, a nie persystencji.

Moim skromnym zdaniem robienie wyżej-poziomowego repozytorium które mapuje baze na domenę, ale w taki sposób że domena i tak jest 1:1 z bazą mija się z celem.

Zalecałbym Ci, że jak tworzysz obiekty domenowe to w ogóle nie myśl o tym jakbyś je trzymał w bazie. Mogą mieć inne nazwy, typy, relacje, etc. Potem to jest właśnie robota repozytorium żeby zapisać domenowe modele w bazie w spójny sposób.

5

Mi to wygląda na problem z używaniem jednego modelu do wszystkiego. Nie znam Twojej domeny ale to co opisujesz zazwyczaj jest skutkiem przerośniętych agregatów i złego podziału domeny na konteksty.

Generalnie nie ma nic złego w posiadaniu kilku modeli takich jak twoja oferta.

Weź sobie za przykład obiekt Product. W module magazynowym będzie miał pola istotne z punktu widzenia logistyki czyli dostępność, wymiar, id magazynu, położenie wewnątrz magazynu, data przyjęcia, itd. Z punktu widzenia modułu zamówień atrybuty jak wymiar, masa, półka, regał są nieistotne. Liczy się cena i dostępność.

I teraz jeżeli będziesz miał jeden model produktu w całej aplikacji to skończy się to dokładnie tym co opisujesz. Jak każdy moduł ma swój model produktu to wtedy jest łatwiej bo modele są mniejsze i łatwiej utrzymywalne bo masz mniej logiki w modelu plus jak pobierasz produkt do zamówienia to nie musisz targać ze sobą całego syfu związanego z konfiguracją Hibernate’a czy innego ORMa, czyli pól i tabel z którymi powiązany jest twój obiekt

0

Jeśli uważasz że tworzenie obiektów domenowych jest zbyt kosztowne, to możesz dodać do wyżej-poziomowego repozytorium funkcję która po prostu zmienia tytuł

Tylko jak miałbym się tak "rozdrabniać" w tym repozytorium i aktualizować poszczególne pola to wgl rich model mi nie jest potrzebny.
Ponieważ zamiast wołać offer.setTitle(title) i zapisać offerRepository.save(offer) zrobiłbym to i tak bokiem przez repo.

I teraz jeżeli będziesz miał jeden model produktu w całej aplikacji to skończy się to dokładnie tym co opisujesz.

Tylko ten przykład z zamówieniem produktem magazynem itp. to wszędzie pokazują jako "wzorcowy" przykład DDD ale jak przyjdzie to w praktyce wykorzystać
to się komplikuje.

Jak każdy moduł ma swój model produktu to wtedy jest łatwiej bo modele są mniejsze i łatwiej utrzymywalne bo masz mniej logiki w modelu

Tylko właśnie o to chodzi, że na razie mam jeden moduł. Dla mnie na razie obiekt oferty jest tym samym wszędzie. Tak to wygląda teraz

public class Offer {
    private Long id;
    private String title;
    private String description;
    private String origin;
    private House house;
    private String customBrand;
    private ProductCondition condition;
    private OfferStatus status;
    private Integer size;
    private Integer amountUsed;
    private Integer amountAvailable;
    private Money price;
    private boolean singleTransaction;
    private Set<MediaFile> photos;
    private Set<Purchase> purchases = new HashSet<>();
    private User user;
    private Long version;

    private Offer(Builder builder) {
        this.id = builder.id;
        this.user = builder.user;
        this.title = builder.title;
        this.description = builder.description;
        this.origin = builder.origin;
        this.house = builder.house;
        this.customBrand = builder.customBrand;
        this.condition = builder.condition;
        this.status = builder.status;
        this.size = builder.size;
        this.amountUsed = builder.amountUsed;
        this.amountAvailable = builder.amountAvailable;
        this.price = builder.price;
        this.singleTransaction = builder.singleTransaction;
        this.photos = builder.photos;
        this.version = builder.version;
        this.user.addOffer(this);
    }

    public void addPhoto(MediaFile photo) {
        photos.add(Objects.requireNonNull(photo));
    }

    public boolean hasPhoto(String key) {
        Objects.requireNonNull(key);
        return photos.stream().anyMatch(p -> p.getKey().equals(key));
    }

    public Optional<House> getHouse() {
        return Optional.ofNullable(house);
    }

    public Money getTotalPrice() {
        return price.multipliedBy(amountAvailable);
    }

    public void addPurchase(Purchase purchase) {
        Objects.requireNonNull(purchase);
        Assert.isTrue(this.equals(purchase.getOffer()), "Bad offer context");
        if (purchase.getAmountPurchased() > getAmountAvailable()) {
            throw new IllegalArgumentException("Invalid amount. Amount can not be greater than the amount available in offer");
        }
        if (isSingleTransaction() && purchase.getAmountPurchased() < getAmountAvailable()) {
            throw new IllegalArgumentException("Invalid amount. Seller is going to sell the whole product by single transaction");
        }
        Assert.isTrue(purchase.getPrice().equals(price), "Invalid price");
        Assert.isTrue(purchase.getTotalPrice().equals(price.multipliedBy(purchase.getAmountPurchased())), "Invalid price");
        this.amountUsed += purchase.getAmountPurchased();
        this.amountAvailable -= purchase.getAmountPurchased();
        this.condition = ProductCondition.USED;
        purchases.add(purchase);
    }

    public static class Builder {
        private final User user;
        private final String title;
        private final String description;
        private final String origin;
        private final Integer size;
        private final Integer amountUsed;
        private final Integer amountAvailable;
        private final Money price;
        private final ProductCondition condition;

        private Long id;
        private Long version;
        private House house;
        private String customBrand;
        private boolean singleTransaction;
        private Set<MediaFile> photos = new HashSet<>();
        private OfferStatus status = OfferStatus.NEW;

        public Builder(User user, String title, String description, String origin,
                       Integer size, Integer amountUsed, Integer amountAvailable, Money price, ProductCondition condition) {
            Objects.requireNonNull(user);
            Objects.requireNonNull(user.getId());
            Assert.hasLength(title, "Title is mandatory");
            Assert.hasLength(description, "Description is mandatory");
            Assert.hasLength(origin, "Origin is mandatory");
            this.user = user;
            this.title = WordUtils.capitalize(StringUtils.normalizeSpace(title.trim().toLowerCase()));
            this.description = StringUtils.normalizeSpace(description.trim());
            this.origin = WordUtils.capitalize(StringUtils.normalizeSpace(origin.trim().toLowerCase()));
            this.size = Objects.requireNonNull(size);
            this.amountUsed = Objects.requireNonNull(amountUsed);
            this.amountAvailable = Objects.requireNonNull(amountAvailable);
            this.price = Objects.requireNonNull(price);
            this.condition = Objects.requireNonNull(condition);
        }

        public Builder id(Long id) {
            this.id = Objects.requireNonNull(id);
            return this;
        }

        public Builder version(Long version) {
            this.version = Objects.requireNonNull(version);
            return this;
        }

        public Builder status(OfferStatus status) {
            this.status = Objects.requireNonNull(status);
            return this;
        }

        public Builder house(House house) {
            this.house = Objects.requireNonNull(house);
            return this;
        }

        public Builder customBrand(String customBrand) {
            Assert.hasLength(customBrand, "CustomBrand can not be empty");
            this.customBrand = customBrand;
            return this;
        }

        public Builder singleTransaction(boolean singleTransaction) {
            this.singleTransaction = singleTransaction;
            return this;
        }

        public Builder photos(Set<MediaFile> photos) {
            this.photos = Objects.requireNonNull(photos);
            return this;
        }

        public Offer build() {
            validate();
            return new Offer(this);
        }

        private void validate() {
            Assert.isTrue(price.isPositiveOrZero(), "Price should not be negative");
            Assert.isTrue(size > 0, "Size should be greater than 0");
            Assert.isTrue(amountUsed >= 0, "AmountUsed should be greater than or equal 0");
            Assert.isTrue(amountAvailable >= 0, "AmountAvailable should be greater or equal 0");
            if (Objects.isNull(customBrand) && Objects.isNull(house)) {
                throw new IllegalArgumentException("Either of customerBrand or house is mandatory");
            }
            if (amountAvailable > size) {
                throw new IllegalArgumentException("AmountAvailable can not be greater than size");
            }
            if (amountUsed > size) {
                throw new IllegalArgumentException("AmountUsed can not be greater than size");
            }
            if (amountAvailable + amountUsed > size) {
                throw new IllegalArgumentException("Bad volume");
            }
            if (ProductCondition.NEW == condition) {
                Assert.isTrue(amountUsed == 0, "AmountUsed should be equal to 0 in case product is new");
            } else {
                Assert.isTrue(amountUsed > 0, "AmountUsed should be greater than 0 in case product is used");
            }
        }
    }
}
1
lukascode napisał(a):

Jeśli uważasz że tworzenie obiektów domenowych jest zbyt kosztowne, to możesz dodać do wyżej-poziomowego repozytorium funkcję która po prostu zmienia tytuł

Tylko jak miałbym się tak "rozdrabniać" w tym repozytorium i aktualizować poszczególne pola to wgl rich model mi nie jest potrzebny.
Ponieważ zamiast wołać offer.setTitle(title) i zapisać offerRepository.save(offer) zrobiłbym to i tak bokiem przez repo.

Może tak się okazać - nie byłoby to wcale takie złe. Jeśli Twój use-case nie wymaga obiektów biznesowych i możesz załatwić wszystko samym repo, to czemu by z tego nie skorzystać? Byłoby prostsze i łatwiejsze. To nie jest tak że musisz mieć obiekt biznesowy który jest encją. Niektórzy z tego korzystają bo ich use-case'y pasują to takiego modelu, ale to nie jest jakiś twardy wymóg. Jeśli Tobie byłoby prościej bez nich - go for it!.

Natomiast jeśli koniecznie chcesz korzystać z obiektów, to jeśli czasem potrzebujesz wczytać sam record, a czasami record z zależnosciami to możesz zrobić osobne klasy biznesowe, jedna która trzyma relacje, jedna która nie, np:

interface OrderRepository {
  OrderShallow fetchOrder(int orderId);
  OrderWithUsers fetchOrderWithUsers(int orderId);
  OrderDeep fetchOrderWithAllRelations(int orderId);
  void updateOrder(OrderShallow order);
}
lukascode napisał(a):

Tylko ten przykład z zamówieniem produktem magazynem itp. to wszędzie pokazują jako "wzorcowy" przykład DDD ale jak przyjdzie to w praktyce wykorzystać
to się komplikuje.

Podeślesz linka z tym kto to pokazuje? Bo moim zdaniem na 99% to wcale nie jest wzorcowe. Strzelam że ktoś usłyszał że warto mieć wartstwy, więc dodaje je proforma.

0

Podeślesz linka z tym kto to pokazuje? Bo moim zdaniem na 99% to wcale nie jest wzorcowe. Strzelam że ktoś usłyszał że warto mieć wartstwy, więc dodaje je proforma.

Pierwsze linki z google
https://github.com/ttulka/ddd-example-ecommerce
https://www.baeldung.com/java-modules-ddd-bounded-contexts
https://www.w3computing.com/articles/implementing-domain-driven-design-java/

W większości przykładów lecą na domenie order, orderItem, shipping itd.

Może tak się okazać - nie byłoby to wcale takie złe. Jeśli Twój use-case nie wymaga obiektów biznesowych i możesz załatwić wszystko samym repo, to czemu by z tego nie skorzystać? Byłoby prostsze i łatwiejsze. To nie jest tak że musisz mieć obiekt biznesowy który jest encją

Tylko właśnie wydaje mi się, że warto aby to było spójne tj. jak już się poszło w rich domain model to żeby z tego rich modelu korzystać
i żeby to tak zrobić żeby nie utrudniać sobie życia.

1
Riddle napisał(a):

Zalecałbym Ci, że jak tworzysz obiekty domenowe to w ogóle nie myśl o tym jakbyś je trzymał w bazie. Mogą mieć inne nazwy, typy, relacje, etc. Potem to jest właśnie robota repozytorium żeby zapisać domenowe modele w bazie w spójny sposób.

Jasne. Hulaj dusza, piekla nie ma.
...
az do momentu koniecznosci wprowadzenia kogokolwiek w kontekst kodu. Utrzymanie takich aplikacji staje sie koszmarem.

0
hyper-stack napisał(a):

Jasne. Hulaj dusza, piekla nie ma.
...
az do momentu koniecznosci wprowadzenia kogokolwiek w kontekst kodu. Utrzymanie takich aplikacji staje sie koszmarem.

Jeśli to się staje problemem, to można poprawić bazę (zmienić nazwy tabel, pól, dodać relacje), tak żeby baza dopasowała się do domeny, a nie odwrotnie.

1
Riddle napisał(a):
hyper-stack napisał(a):

Jasne. Hulaj dusza, piekla nie ma.
...
az do momentu koniecznosci wprowadzenia kogokolwiek w kontekst kodu. Utrzymanie takich aplikacji staje sie koszmarem.

Jeśli to się staje problemem, to można poprawić bazę (zmienić nazwy tabel, pól, dodać relacje), tak żeby baza dopasowała się do domeny, a nie odwrotnie.

To sie samo komentuje.

4

w 95% wystarczy jeden model - domenowy, mapowany na encje w bazie za pomocą konfiguracji ORMa.

Sytuacje gdy trzeba zmienić bazę relacyjną na nierelacyjną w monolicie to naprawdę rzadki use case. Jak już jest taka potrzeba to zwykle i tak trzeba rozpruć monolit i wyodrębnić moduł jako osobną usługę z osobną bazą danych.

Ja wiem że fajnie jest być future proof ale zwykle taki overengineering się nie zwraca. To tak jak z robieniem interfejsu dla każdej klasy bo mogę zmienić implementację . No niby mogę ale i tak zwykle kończy się to głębokim refaktorem bo nie da się wszystkiego przewidzieć z góry.

2
lukascode napisał(a):

Może tylko komplikuje bo później żeby takie obiekty
odtworzyć w warstwie persystencji robi się złożone i kosztowne a i tak się okazuje później, że w danym use case
potrzebujemy tylko jakiś wycinek tych danych a nie całość. I zapis obiektu też robi się skomplikowany bo trzeba decydować,
która strona relacji jest rodzicem i która powinna zapisać lub nie.

Wbrew temu, co sugerują tutoriale, modelowanie nie polega na robieniu mapowania 1 do 1 tabeli z bazy na jedną klasę w kodzie. Możesz mieć wiele różnych modeli, nawet każdy przypadek użycia może mieć swój oddzielny model.
A dwa, to poszczególne klasy mogą pełnić różne role w różnych kontekstach.

lukascode napisał(a):

Tylko jak miałbym się tak "rozdrabniać" w tym repozytorium i aktualizować poszczególne pola to wgl rich model mi nie jest potrzebny.
Ponieważ zamiast wołać offer.setTitle(title) i zapisać offerRepository.save(offer) zrobiłbym to i tak bokiem przez repo.

Jeśli masz takie metody w jakiejś klasie, to jedno jest pewne - to nie jest repozytorium. Repozytorium to kolekcja obiektów biznesowych, nie wrapper na bazę służący do ustawiania wartości poszczególnych kolumn.

5

Jak wyżej - nie ma potrzeby żeby encje domenowe odpowiadały encjom bazodanowym i w praktyce tak nie ma, według mnie to spojrzenie bierze się ze sposobu nauczania na uczelniach baz danych.
Załózmy że mamy Użytkownika w kontekście ofert jak i powiadomień(notyfikacji). Jeśli ładujesz użytkownika w kontekście ofert, nie potrzebujesz takich danych jak email, numer telefonu i kolekcji urządzeń mobilnych na które masz wysłac pusha z notyfikacjami oraz całego zestawu reguł powiadomień (tj z jakiego powodu ma byc powiadomiony i jak). Z kolei jak ładujesz użytkownika w kontekscie zmian powiadomień (np user.registerDevice(Device device)) to nie potrzebujesz załadować wszystkich ofert zwiazanych z danym użytkownikiem.
Dodatkowo pomocne moga być eventy domenowe w celu propogancji zdarzeń w kontekście innych domen. Przykładowo, jeśli klient dokonał zamówienia online, event OrderPlaced może posłuzyć do tego żeby wywołac zmianę liczby dostepnych produktów w kontekście magazynowym

0

Wbrew temu, co sugerują tutoriale, modelowanie nie polega na robieniu mapowania 1 do 1 tabeli z bazy na jedną klasę w kodzie. Możesz mieć wiele różnych modeli, nawet każdy przypadek użycia może mieć swój oddzielny model.
A dwa, to poszczególne klasy mogą pełnić różne role w różnych kontekstach.

@somekind Możesz pokazać jakiś krótki przykład w kodzie?

1
somekind napisał(a):

Repozytorium to kolekcja obiektów biznesowych, nie wrapper na bazę służący do ustawiania wartości poszczególnych kolumn.

???

Repozytorium nie jest literalną kolekcją obiektów, zapewnia jedynie warstwę abstrakcji, która udostępnia dostęp do obiektów domenowych w sposób zrozumiały dla logiki aplikacji.
Bedac de facto bezstanowym serwisem, umozliwia wylacznie operowanie na obiektach domenowych umozliwiajac ich storage i manipulacje nimi.

Zatem ostatecznie, z grubsza rzecz biorac, w przeciwienstwie do tego co mowisz, repozytorium jest "wrapperem na baze" i trywializujac jego role "ustawia wartosci poszczegolnych kolumn".

1
hyper-stack napisał(a):

Repozytorium nie jest literalną kolekcją obiektów, zapewnia jedynie warstwę abstrakcji, która udostępnia dostęp do obiektów domenowych w sposób zrozumiały dla logiki aplikacji.

No właśnie! A skoro jest warstwą abstrakcji, to nie może wystawiać operacji na szczegółach implementacji.

Bedac de facto bezstanowym serwisem, umozliwia wylacznie operowanie na obiektach domenowych umozliwiajac ich storage i manipulacje nimi.

No, na obiektach. Nie elementach obiektów, tylko obiektach.

Zatem ostatecznie, z grubsza rzecz biorac, w przeciwienstwie do tego co mowisz, repozytorium jest "wrapperem na baze" i trywializujac jego role "ustawia wartosci poszczegolnych kolumn".

Nie, nie jest wrapperem na bazę - zupełnie prawidłowe jest posiadanie repozytorium bez bazy. Repozytorium to abstrakcja, ma udostępniać interfejs kolekcji obiektów, a szczegółami implementacji zajmuje się już konkretna implementacja - z uwzględnieniem tego, czy pod spodem jest baza, pliki czy API webowe.

http://commitandrun.pl/2016/05/11/Repozytorium_najbardziej_niepotrzebny_wzorzec_projektowy/

1
somekind napisał(a):
hyper-stack napisał(a):

Repozytorium nie jest literalną kolekcją obiektów, zapewnia jedynie warstwę abstrakcji, która udostępnia dostęp do obiektów domenowych w sposób zrozumiały dla logiki aplikacji.

No właśnie! A skoro jest warstwą abstrakcji, to nie może wystawiać operacji na szczegółach implementacji.

Pelna zgoda, aczkolwiek nigdzie nie twierdzilem ze jest inaczej

somekind napisał(a):

Bedac de facto bezstanowym serwisem, umozliwia wylacznie operowanie na obiektach domenowych umozliwiajac ich storage i manipulacje nimi.

No, na obiektach. Nie elementach obiektów, tylko obiektach.

Pelna zgoda, aczkolwiek nigdzie nie twierdzilem ze jest inaczej

somekind napisał(a):

Zatem ostatecznie, z grubsza rzecz biorac, w przeciwienstwie do tego co mowisz, repozytorium jest "wrapperem na baze" i trywializujac jego role "ustawia wartosci poszczegolnych kolumn".

Nie, nie jest wrapperem na bazę - zupełnie prawidłowe jest posiadanie repozytorium bez bazy. Repozytorium to abstrakcja, ma udostępniać interfejs kolekcji obiektów, a szczegółami implementacji zajmuje się już konkretna implementacja - z uwzględnieniem tego, czy pod spodem jest baza, pliki czy API webowe.

Pelna zgoda, baza jest tutaj jedynie przykladem storage'u, uogulniajac chodzi o jakikolwiek data source, wiec uscislalac: Repozytorium jest wrapperem na data source.

Ostatecznie roznica miedzy tym:

somekind napisał(a):

... Repozytorium to kolekcja obiektów biznesowych, ...

i tym:

somekind napisał(a):

... Repozytorium to abstrakcja, ma udostępniać interfejs kolekcji obiektów, ...

stanowi dla mnie problem, bo kolekcja obiektow i interfejs kolekcji obiektow jako jedno i to samo, zupelnie mi sie nie spina.

0

Owszem, użyłem skrótu myślowego. Przepraszam.

Mam nadzieję, że już wszystko jasne.

0
somekind napisał(a):
hyper-stack napisał(a):

Repozytorium nie jest literalną kolekcją obiektów, zapewnia jedynie warstwę abstrakcji, która udostępnia dostęp do obiektów domenowych w sposób zrozumiały dla logiki aplikacji.

No właśnie! A skoro jest warstwą abstrakcji, to nie może wystawiać operacji na szczegółach implementacji.

Bedac de facto bezstanowym serwisem, umozliwia wylacznie operowanie na obiektach domenowych umozliwiajac ich storage i manipulacje nimi.

No, na obiektach. Nie elementach obiektów, tylko obiektach.

Element obiektów raczej nie jest szczegółem implementacji. Repozytorium może normalnie edytować jedno pole obiektu domenowego.

0

@Aleksander-32 Wracając do Twojego przykładu w jaki sposób najlepiej rozdzielić w kodzie te różne konteksty, że np mamy 2 różne modele użytkownika w różnych kontekstach
tak aby w tym wszystkim się łatwo połapać i jakie nazewnictwo stosować?
coś takiego?

offer
  User
  UserRepository
  Order
  OrderRepository
notification
  User
  UserRepository
  Device
  DeviceRepository

Nie powstanie nam potem eksplozja klas? Chyba, że lepiej mieć jedno UserRepository, które zwraca inny obiekt użytkownika w zależności od kontekstu?
Może dziedziczenie by tu się sprawdziło, czyli mamy jakiegoś bazowego użytkownika z podstawowymi danymi i dziedziczymy jak chcemy doszczegółowić?
Kolejna rzecz to którym repozytorium powinniśmy zapisywać zmiany na modelu? Np. user.registerDevice(Device device)) to robimy save na
repozytorium użytkownika czy na repozytorium DeviceRepository?

1
Riddle napisał(a):

Element obiektów raczej nie jest szczegółem implementacji. Repozytorium może normalnie edytować jedno pole obiektu domenowego.

Nie. To wtedy nie jest już repozytorium.
Do tego mamy wówczas do czynienia z cieknącą abstrakcją, na dodatek ze złamaniem enkapsulacji.

Chcecie sobie programować proceduralnie, to programujcie, ale nie mieszajcie do tego konceptów z DDD.

1
somekind napisał(a):
Riddle napisał(a):

Element obiektów raczej nie jest szczegółem implementacji. Repozytorium może normalnie edytować jedno pole obiektu domenowego.

Nie. To wtedy nie jest już repozytorium.
Do tego mamy wówczas do czynienia z cieknącą abstrakcją, na dodatek ze złamaniem enkapsulacji.

Chcecie sobie programować proceduralnie, to programujcie, ale nie mieszajcie do tego konceptów z DDD.

Hmm tylko może właśnie to, że zmieniamy jedną kolumnę to jest szczegół implementacyjny repozytorium a domena nie musi tego wiedzieć?
Np. jak mamy use case, że zmieniamy hasło to nie możemy na repozytorium sobie wystawić metody changePassword(userId, password)?

0
somekind napisał(a):
Riddle napisał(a):

Element obiektów raczej nie jest szczegółem implementacji. Repozytorium może normalnie edytować jedno pole obiektu domenowego.

Nie. To wtedy nie jest już repozytorium.
Do tego mamy wówczas do czynienia z cieknącą abstrakcją, na dodatek ze złamaniem enkapsulacji.

A czemu?

Przecież repozytorium może zapisać pole z obiektu domenowego jako dowolne inne pole (lub nawet nie pole) w persystencji. Czemu zapisanie wszystkich pól z obiektu domenowego na raz w persystencji miałoby być full DDD, ale pojedynczego wybranego pola już nie?

class Person {
  String pesel;
  String name;
  String surname;
  boolean status;
}

interface PersonRepository {
  void savePerson(Person person); // to jest DDD?
  void changeStatusByPesel(String pesel, boolean newStatus);  // a to jest leaky?
}

@somekind Nic co używa PersonRepository albo Person nie ma pojęcia o bazie danych lub persystencji, więc czemu to miałaby być leaky-abstrakcja? Wie tylko że istnieje pole pesel i status, ale każdy kto używa Person też wie o tych polach. A same pola mogą być zapisane w bazie tak jak implementacji repozytorium wymyśli, więc ja nie widzę czemu to miałaby być cieknąca abstrakcja? 🤔

lukascode napisał(a):

Hmm tylko może właśnie to, że zmieniamy jedną kolumnę to jest szczegół implementacyjny repozytorium a domena nie musi tego wiedzieć?
Np. jak mamy use case, że zmieniamy hasło to nie możemy na repozytorium sobie wystawić metody changePassword(userId, password)?

No właśnie na moje możemy, bo to co korzysta z repozytorium nie musi wiedzieć jaką kolumnę zmieniamy - wie tylko że zmieniamy hasło domenowe.

0
lukascode napisał(a):

Hmm tylko może właśnie to, że zmieniamy jedną kolumnę to jest szczegół implementacyjny repozytorium a domena nie musi tego wiedzieć?
Np. jak mamy use case, że zmieniamy hasło to nie możemy na repozytorium sobie wystawić metody changePassword(userId, password)?

Po co w ogóle mieszać jakieś repozytoria do procesu zmiany hasła? Repozytorium ma wyglądać jak kolekcja obiektów domenowych, hasło nie jest obiektem domenowym.
Do wykonywania operacji na danych służą DAO - DataAccessObjecty.

Riddle napisał(a):

Przecież repozytorium może zapisać pole z obiektu domenowego jako dowolne inne pole (lub nawet nie pole) w persystencji. Czemu zapisanie wszystkich pól z obiektu domenowego na raz w persystencji miałoby być full DDD, ale pojedynczego wybranego pola już nie?

Podaj przykład kolekcji, która ma metody do zarządzania polami obiektów w niej przechowywanych zamiast całymi obiektami.

Wie tylko że istnieje pole pesel i status, ale każdy kto używa Person też wie o tych polach. A same pola mogą być zapisane w bazie tak jak implementacji repozytorium wymyśli, więc ja nie widzę czemu to miałaby być cieknąca abstrakcja? 🤔

Bo abstrakcja jest kolekcją obiektów, a nie operacji możliwych do wykonania na polach tych obiektów.

1
somekind napisał(a):

Bo abstrakcja jest kolekcją obiektów, a nie operacji możliwych do wykonania na polach tych obiektów.

@lukascode Somekindowi chodziło o to, jak rozumiem, że zmieniając hasło używając repository.updatePassword(userId) możemy niechcący ominąć jakąś logikę która jest w obiekcie domenowym (np. jak obiekt domenowy sobie zmapuje jakoś hasło przed zapisaniem) - myślę że właśnie to miał na mysli mówiąc że "łamie enkapsulację". I nawet ma rację mówiąc że to nie do końca jest zgodne z DDD (bo faktycznie DDD mówi że repozytorium to kolekcja obiektów).

Ale oprócz tego, mówił też że to jest leaky-abstraction - i to moim zdaniem już przeginka, bo żaden szczegół persystencji nie wycieka przecież. To nie jest tak że wszystko co jest niezgodne z DDD automatycznie jest leaky abstrakcją.

0

To pytanie czy jak mamy obiekt User i mamy tam wiele innych pól i zmienimy tylko nazwisko to zapisując ten obiekt przez repository za każdym razem będziemy to hasło zapisywać i przeliczać hash hasła? Czy nie optymalniej właśnie jest zawołać ’updatePassword’ na repozytorium wtedy kiedy faktycznie tego potrzebujemy?
A może pójść o krok dalej i zrobić model per use case czyli obiekt UserChangePassword i UserChangePasswordRepository.save(userChangePassword)?

0
lukascode napisał(a):

To pytanie czy jak mamy obiekt User i mamy tam wiele innych pól i zmienimy tylko nazwisko to zapisując ten obiekt przez repository za każdym razem będziemy to hasło zapisywać i przeliczać hash hasła? Czy nie optymalniej właśnie jest zawołać ’updatePassword’ na repozytorium wtedy kiedy faktycznie tego potrzebujemy?
A może pójść o krok dalej i zrobić model per use case czyli obiekt UserChangePassword i UserChangePasswordRepository.save(userChangePassword)?

Jeśli mówimy po prostu o dependency inversion, gdzie korzystamy z polimorfizmu żeby dodać abstrakcję na bazę danych to myślę że wystarczyłoby.

Ale jeśli mówimy o pełnoprawnym repozytorium z DDD, to chyba żeby się wpisać w ten wzorzec musiałbyś spełnić pewne kryteria.

1
lukascode napisał(a):

To pytanie czy jak mamy obiekt User i mamy tam wiele innych pól i zmienimy tylko nazwisko to zapisując ten obiekt przez repository za każdym razem będziemy to hasło zapisywać i przeliczać hash hasła? Czy nie optymalniej właśnie jest zawołać ’updatePassword’ na repozytorium wtedy kiedy faktycznie tego potrzebujemy?
A może pójść o krok dalej i zrobić model per use case czyli obiekt UserChangePassword i UserChangePasswordRepository.save(userChangePassword)?

Ja bym powiedział że nie wszystko musi być robione w ramach DDD. Gdybym to ja ogarniał architekturę aplikacji, to raczej kwestie security robiłbym nie w stylu DDD z jakimiś repozytoriami i encjami domenowymi ;)

lukascode napisał(a):

@Aleksander-32 Wracając do Twojego przykładu w jaki sposób najlepiej rozdzielić w kodzie te różne konteksty, że np mamy 2 różne modele użytkownika w różnych kontekstach
tak aby w tym wszystkim się łatwo połapać i jakie nazewnictwo stosować?
coś takiego?

offer
  User
  UserRepository
  Order
  OrderRepository
notification
  User
  UserRepository
  Device
  DeviceRepository

Nie powstanie nam potem eksplozja klas? Chyba, że lepiej mieć jedno UserRepository, które zwraca inny obiekt użytkownika w zależności od kontekstu?
Może dziedziczenie by tu się sprawdziło, czyli mamy jakiegoś bazowego użytkownika z podstawowymi danymi i dziedziczymy jak chcemy doszczegółowić?
Kolejna rzecz to którym repozytorium powinniśmy zapisywać zmiany na modelu? Np. user.registerDevice(Device device)) to robimy save na
repozytorium użytkownika czy na repozytorium DeviceRepository?

Repository powinno zwracac obiekt domenowy a nie encje bazodanową. Eksplozja modelu raczej nie jest czymś złym, a zwłaszcza że w praktyce mamy często architekturę mikroserwisów, i de dacto domena notyfikacji jest w jakimś innym mikroserwisie, i serwis od ofert nic nie wie na temat urządeń użytkownika

3

Czemu masz w obu kontekstach User? Nie wierzę że w twojej domenie biznesowej nie istnieje lepsza nazwa. User to dość techniczna nazwa i mocno generyczna. Integration User jako nie osoba tylko tzw machine user to też użytkownik.

Popatrz na kilka pojęć jakimi można zastąpić Użytkownika:

  • Leasingobiorca
  • Kupujący
  • Sprzedający
  • Ubezpieczony
  • Alimenciarz xd 🤣
  • itd

Wierzę że znajdziesz odpowiednią nazwę twojego użytkownika w swoich kontekstach

0
Riddle napisał(a):

Ale oprócz tego, mówił też że to jest leaky-abstraction - i to moim zdaniem już przeginka, bo żaden szczegół persystencji nie wycieka przecież.

Wycieka szczegół domeny do kodu, który wywołuje repozytorium.

To nie jest tak że wszystko co jest niezgodne z DDD automatycznie jest leaky abstrakcją.

Nie, pewnie, że nie.
Nie trzeba mieć DDD, żeby mieć cieknące abstrakcje. Nawet nie trzeba mieć do tego programowania obiektowego.

lukascode napisał(a):

To pytanie czy jak mamy obiekt User i mamy tam wiele innych pól i zmienimy tylko nazwisko to zapisując ten obiekt przez repository za każdym razem będziemy to hasło zapisywać i przeliczać hash hasła? Czy nie optymalniej właśnie jest zawołać ’updatePassword’ na repozytorium wtedy kiedy faktycznie tego potrzebujemy?
A może pójść o krok dalej i zrobić model per use case czyli obiekt UserChangePassword i UserChangePasswordRepository.save(userChangePassword)?

Ale po co udawać, że DAO jest repozytorium, i po co mieszać jakieś zarządzanie użytkownikami do logiki biznesowej?

Aleksander-32 napisał(a):

Ja bym powiedział że nie wszystko musi być robione w ramach DDD. Gdybym to ja ogarniał architekturę aplikacji, to raczej kwestie security robiłbym nie w stylu DDD z jakimiś repozytoriami i encjami domenowymi ;)

Otóż to!

0

Czy znacie może jakieś przykładowe repozytoria na github gdzie zastosowano architekturę, o której rozmawiamy tj. z odseparowanym modelem domenowym i persystencji?
Najlepiej żeby to nie było e-commerce.
Jak już chcę wzorować się na czymś co jest zgodne ze sztuką i całym ddd.

Zarejestruj się i dołącz do największej społeczności programistów w Polsce.

Otrzymaj wsparcie, dziel się wiedzą i rozwijaj swoje umiejętności z najlepszymi.