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?