Layanan Injeksi DDD pada Panggilan Metode Entitas


11

Format pertanyaan pendek

Apakah dalam praktik terbaik DDD dan OOP untuk menyuntikkan layanan pada panggilan metode entitas?

Contoh format panjang

Katakanlah kita memiliki kasus Order-LineItems klasik di DDD, di mana kita memiliki Entitas Domain yang disebut Order, yang juga bertindak sebagai Root Agregat, dan Entitas itu terdiri tidak hanya dari Object Value-nya, tetapi juga koleksi Item Baris Entitas.

Misalkan kita ingin sintaks yang lancar dalam aplikasi kita, sehingga kita dapat melakukan sesuatu seperti ini (mencatat sintaks pada baris 2, di mana kita memanggil getLineItemsmetode):

$order = $orderService->getOrderByID($orderID);
foreach($order->getLineItems($orderService) as $lineItem) {
  ...
}

Kami tidak ingin menyuntikkan segala macam LineItemRepository ke dalam OrderEntity, karena itu merupakan pelanggaran terhadap beberapa prinsip yang dapat saya pikirkan. Tetapi, kelancaran sintaksis adalah sesuatu yang benar-benar kita inginkan, karena mudah dibaca dan dipelihara, serta diuji.

Pertimbangkan kode berikut, perhatikan metode getLineItemsdi OrderEntity:

interface IOrderService {
    public function getOrderByID($orderID) : OrderEntity;
    public function getLineItems(OrderEntity $orderEntity) : LineItemCollection;
}

class OrderService implements IOrderService {
    private $orderRepository;
    private $lineItemRepository;

    public function __construct(IOrderRepository $orderRepository, ILineItemRepository $lineItemRepository) {
        $this->orderRepository = $orderRepository;
        $this->lineItemRepository = $lineItemRepository;
    }

    public function getOrderByID($orderID) : OrderEntity {
        return $this->orderRepository->getByID($orderID);
    }

    public function getLineItems(OrderEntity $orderEntity) : LineItemCollection {
        return $this->lineItemRepository->getLineItemsByOrderID($orderEntity->ID());
    }
}

class OrderEntity {
    private $ID;
    private $lineItems;

    public function getLineItems(IOrderServiceInternal $orderService) {
        if(!is_null($this->lineItems)) {
            $this->lineItems = $orderService->getLineItems($this);
        }
        return $this->lineItems;
    }
}

Apakah itu cara yang diterima untuk menerapkan sintaks yang lancar dalam Entitas tanpa melanggar prinsip-prinsip inti DDD dan OOP? Bagi saya kelihatannya baik-baik saja, karena kita hanya mengekspos lapisan layanan, bukan lapisan infrastruktur (yang bersarang di dalam layanan)

Jawaban:


9

Tidak apa- apa untuk melewatkan layanan Domain dalam panggilan entitas. Katakanlah, kita perlu menghitung jumlah faktur dengan beberapa algoritma rumit yang dapat bergantung pada, katakanlah, tipe pelanggan. Berikut ini tampilannya:

class Invoice
{
    private $currency;
    private $customerId;

    public function __construct()
    {
    }

    public function sum(InvoiceCalculator $calculator)
    {
        $sum =
            new SumRecord(
                $calculator->calculate($this)
            )
        ;

        if ($sum->isZero()) {
            $this->events->add(new ZeroSumCalculated());
        }

        return $sum;
    }
}

Namun pendekatan lain adalah untuk memisahkan logika bisnis yang terletak di layanan domain melalui peristiwa domain . Perlu diingat bahwa pendekatan ini hanya menyiratkan layanan aplikasi yang berbeda, tetapi cakupan transaksi database yang sama.

Pendekatan ketiga adalah yang saya sukai: jika saya menemukan diri saya menggunakan layanan domain, itu mungkin berarti saya melewatkan beberapa konsep domain, karena saya memodelkan konsep saya terutama dengan kata benda , bukan kata kerja. Jadi, idealnya, saya tidak memerlukan layanan domain sama sekali dan sebagian besar dari semua logika bisnis saya berada di dekorator .


6

Saya kaget membaca beberapa jawaban di sini.

Sangat valid untuk meneruskan layanan domain ke metode entitas dalam DDD untuk mendelegasikan beberapa perhitungan bisnis. Sebagai contoh, bayangkan root agregat Anda (entitas) perlu mengakses sumber daya eksternal melalui http untuk melakukan beberapa logika bisnis dan meningkatkan suatu peristiwa. Jika Anda tidak menyuntikkan layanan melalui metode bisnis entitas, bagaimana lagi Anda akan melakukannya? Apakah Anda akan instantiate klien http di dalam entitas Anda? Itu terdengar seperti ide yang buruk.

Apa yang salah adalah menyuntikkan layanan secara agregat melalui konstruktornya. Tetapi melalui metode bisnis itu ok dan sangat normal.


1
Mengapa kasing yang Anda berikan tidak menjadi tanggung jawab Layanan Domain?
e_i_pi

1
itu adalah Layanan Domain, tetapi disuntikkan dalam metode bisnis. Lapisan aplikasi hanyalah orkestra,
diegosasw

Saya tidak berpengalaman dalam DDD tetapi tidak boleh Layanan Domain dipanggil dari Layanan Aplikasi dan setelah validasi Layanan Domain terus memanggil metode Entitas melalui Layanan Aplikasi itu? Saya menghadapi masalah yang sama dalam proyek saya, karena Layanan Domain menjalankan panggilan database melalui repositori ... Saya tidak tahu apakah ini baik-baik saja.
Muflix

Layanan domain harus mengatur, jika Anda memanggilnya dari aplikasi nanti itu berarti Anda entah bagaimana memproses responsnya dan kemudian Anda melakukan sesuatu dengannya. Mungkin itu terdengar seperti logika bisnis. Jika demikian, itu termasuk dalam lapisan Domain dan aplikasi nanti hanya menyelesaikan ketergantungan dan menyuntikkannya dalam agregat. Layanan domain bisa saja menyuntikkan repositori yang implementasinya mengenai basis data seharusnya berada di lapisan infrastruktur (hanya implementasi, bukan antarmuka / kontrak). Jika itu menggambarkan bahasa Anda di mana-mana, itu termasuk dalam domain.
diegosasw

5

Apakah dalam praktik terbaik DDD dan OOP untuk menyuntikkan layanan pada panggilan metode entitas?

Tidak, Anda tidak boleh menyuntikkan apa pun di dalam lapisan domain Anda (ini termasuk entitas, objek bernilai, pabrik, dan layanan domain). Lapisan ini harus agnostik dari kerangka apa pun, perpustakaan pihak ketiga atau teknologi dan tidak boleh membuat panggilan IO.

$order->getLineItems($orderService)

Ini salah karena Agregat seharusnya tidak membutuhkan yang lain kecuali dirinya sendiri untuk mengembalikan barang pesanan. The Seluruh Agregat harus sudah dimuat sebelum panggilan metode nya. Jika Anda merasa bahwa ini harus dimuat malas maka ada dua kemungkinan:

  1. Batas Agregat Anda salah, terlalu besar.

  2. Dalam usecase ini Anda menggunakan Agregat hanya untuk membaca. Solusi terbaik adalah dengan membagi model tulis dari model baca (yaitu menggunakan CQRS ). Dalam arsitektur yang lebih bersih ini Anda tidak diizinkan untuk meminta Agregat tetapi model yang sudah dibaca.


Jika saya memerlukan panggilan database untuk validasi, saya harus menyebutnya dalam layanan aplikasi dan meneruskan hasilnya ke layanan domain atau langsung ke agregat root daripada menyuntikkan repositori ke layanan domain?
Muflix

1
@ Muflix ya, itu benar
Constantin Galbenu

3

Gagasan kunci dalam pola taktis DDD: aplikasi mengakses semua data dalam aplikasi dengan bertindak pada akar agregat. Ini menyiratkan bahwa satu-satunya entitas yang dapat diakses di luar model domain adalah akar agregat.

Akar agregat pesanan tidak akan pernah menghasilkan referensi ke koleksi item barisnya yang akan memungkinkan Anda untuk mengubah koleksi, juga tidak akan menghasilkan koleksi referensi untuk setiap item baris yang akan memungkinkan Anda untuk mengubahnya. Jika Anda ingin mengubah agregat Pesanan, prinsip hollywood berlaku: "Katakan, jangan tanya".

Mengembalikan nilai dari dalam agregat baik-baik saja, karena nilai secara inheren tidak dapat diubah; Anda tidak dapat mengubah data saya dengan mengubah salinannya.

Menggunakan layanan domain sebagai argumen, untuk membantu agregat dalam memberikan nilai yang benar, adalah hal yang sangat masuk akal untuk dilakukan.

Anda biasanya tidak akan menggunakan layanan domain untuk memberikan akses ke data yang ada di dalam agregat, karena agregat seharusnya sudah memiliki akses ke sana.

$order = $orderService->getOrderByID($orderID);
foreach($order->getLineItems($orderService) as $lineItem) {
  ...
}

Jadi ejaan itu aneh, jika kita mencoba mengakses koleksi nilai item baris pesanan ini. Ejaan yang lebih alami

$order = $orderService->getOrderByID($orderID);
foreach($order->getLineItems() as $lineItem) {
  ...
}

Tentu saja, ini pra-mengandaikan bahwa item baris sudah dimuat.

Pola yang biasa adalah bahwa muatan agregat akan mencakup semua keadaan yang diperlukan untuk use case tertentu. Dengan kata lain, Anda mungkin memiliki beberapa cara berbeda untuk memuat agregat yang sama; metode repositori Anda sesuai untuk tujuan .

Pendekatan ini bukan sesuatu yang akan Anda temukan di Evans asli, di mana ia mengasumsikan bahwa agregat akan memiliki model data tunggal yang terkait dengannya. Itu jatuh lebih alami dari CQRS.


Terima kasih untuk ini. Saya sekarang telah membaca sekitar setengah dari "buku merah", dan memiliki rasa pertama saya menerapkan Prinsip Hollywood di lapisan infrastruktur. Membaca ulang semua jawaban ini, semuanya menghasilkan poin yang bagus, tetapi saya pikir jawaban Anda memiliki beberapa poin yang sangat penting terkait ruang lingkup lineItems()dan preloading setelah pengambilan pertama dari Root Agregat.
e_i_pi

3

Secara umum, objek nilai milik agregat tidak memiliki repositori sendiri. Agregat tanggung jawab root untuk mengisi mereka. Dalam kasus Anda, ini adalah tanggung jawab OrderRepository Anda untuk mengisi objek entitas dan nilai OrderLine.

Mengenai, implementasi infrastruktur dari OrderRepository, jika ORM, itu adalah hubungan satu-ke-banyak, dan Anda dapat memilih dengan bersemangat atau malas memuat OrderLine.

Saya tidak yakin apa arti layanan Anda sebenarnya. Cukup dekat dengan "Layanan Aplikasi". Jika ini masalahnya, umumnya bukan ide yang baik untuk menyuntikkan layanan ke Agregat root / Entity / Value Object. Layanan Aplikasi harus menjadi klien dari Agregat root / Entity / Value Object dan Layanan Domain. Hal lain tentang layanan Anda adalah, mengekspos objek nilai di Layanan Aplikasi juga bukan ide yang baik. Mereka harus diakses oleh root agregat.


2

Jawabannya adalah: pasti TIDAK, hindari layanan lewat metode entitas.

Solusinya sederhana: biarkan repositori Order mengembalikan Orde dengan semua LineItems-nya. Dalam kasus Anda, agregat adalah Order + LineItems, jadi jika repositori tidak mengembalikan agregat lengkap, maka agregat tidak melakukan tugasnya.

Prinsip yang lebih luas adalah: menjaga bit fungsional (mis. Logika domain) terpisah dari bit non-fungsional (mis. Ketekunan).

Satu hal lagi: jika Anda bisa, cobalah untuk tidak melakukan ini:

$order = $orderService->getOrderByID($orderID);
foreach($order->getLineItems() as $lineItem) {
  ...
}

Lakukan ini sebagai gantinya

$order = $orderService->getOrderByID($orderID);
$order->doSomethingSignificant();

Dalam desain berorientasi objek, kami mencoba untuk menghindari memancing di dalam data objek. Kami lebih suka meminta objek untuk melakukan apa yang kita inginkan.

Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.