Di mana Anda harus memvalidasi keadaan agregat “lainnya”?

8

Skenario:

Pelanggan memesan, kemudian, setelah menerima produk, memberikan umpan balik pada proses pemesanan.

Asumsikan akar agregat berikut:

  • Pelanggan
  • Memesan
  • Umpan balik

Berikut adalah aturan bisnisnya:

  1. Seorang pelanggan hanya dapat memberikan umpan balik atas pesanan mereka sendiri, bukan milik orang lain.
  2. Pelanggan hanya dapat memberikan umpan balik jika pesanan telah dibayar.

    class Feedback {
        public function __construct($feedbackId,
                                    Customer $customer,
                                    Order $order,
                                    $content) {
            if ($customer->customerId() != $order->customerId()) {
                // Error
            }
            if (!$order->isPaid()) {
                // Error
            }
            $this->feedbackId = $feedbackId;
            $this->customerId = $customerId;
            $this->orderId = $orderId;
            $this->content = $content;
        }
    }
    

Sekarang, anggap bisnis menginginkan aturan baru:

  1. Pelanggan hanya dapat memberikan umpan balik jika Supplierbarang pesanan masih beroperasi.

    class Feedback {
        public function __construct($feedbackId,
                                    Customer $customer,
                                    Order $order,
                                    Supplier $supplier,
                                    $content) {
            if ($customer->customerId() != $order->customerId()) {
                // Error
            }
            if (!$order->isPaid()) {
                // Error
            }
            // NEW RULE HERE
            if (!$supplier->isOperating()) {
                // Error
            }
            $this->feedbackId = $feedbackId;
            $this->customerId = $customerId;
            $this->orderId = $orderId;
            $this->content = $content;
        }
    }
    

Saya telah menempatkan implementasi dari dua aturan pertama dalam Feedback agregat itu sendiri. Saya merasa nyaman melakukan ini, terutama mengingat bahwa Feedbackagregat merujuk semua agregat lainnya berdasarkan identitas. Misalnya, sifat-sifat Feedbackkomponen menunjukkan bahwa ia mengetahui keberadaan agregat lain, jadi saya merasa nyaman mengetahui tentang status read only dari agregat ini juga.

Namun, berdasarkan sifat itu, yang Feedbackagregat tidak memiliki pengetahuan tentang keberadaan dari Supplieragregat, sehingga seharusnya ia memiliki pengetahuan tentang membaca satunya negara dari agregat ini?

Solusi alternatif untuk menerapkan aturan 3 adalah dengan memindahkan logika ini ke yang sesuai CommandHandler. Namun, ini terasa seperti memindahkan logika domain dari "pusat" arsitektur berbasis bawang saya.

Tinjauan arsitektur bawang saya

Magnus
sumber
Antarmuka repositori adalah bagian dari domain. Jadi logika konstruksi (yang dengan sendirinya dianggap sebagai layanan dalam buku DDD) dapat memanggil repositori Order untuk menanyakan apakah pemasok Order masih beroperasi.
Euforia
Pertama, Supplierstatus operasi agregat tidak akan ditanyai melalui Orderrepositori; Supplierdan Orderdua agregat terpisah. Kedua, ada pertanyaan di milis DDD / CQRS tentang meneruskan akar agregat dan repositori ke metode akar agregat lainnya (termasuk konstruktor). Ada berbagai pendapat, tetapi Greg Young menyebutkan bahwa melewati akar agregat sebagai parameter adalah umum, sementara orang lain mengatakan bahwa repositori lebih erat terkait dengan infrastruktur daripada domain. Misalnya, repositori "abstrak dalam koleksi memori" dan tidak memiliki logika.
Magnus
Bukankah Pemasok terkait dengan Pemesanan? Apa yang terjadi ketika Pemasok yang tidak terkait dengan Pesanan dilewatkan? Ya, "apakah pemasok beroperasi" bukan logika. Ini permintaan sederhana. Juga, ada alasan mengapa hal itu umum: Tanpanya, kode Anda menjadi jauh lebih kompleks dan membutuhkan informasi di mana kesalahan dapat terjadi. Juga, "antarmuka repositori" bukan infrastruktur. Implementasi repositori adalah.
Euforia
Kamu benar. Sama seperti Customerkaleng hanya memberikan umpan balik pada salah satu pesanan mereka sendiri ( $order->customerId() == $customer->customerId()), kami juga harus membandingkan ID pemasok ( $order->supplierId() == $supplier->supplierId()). Aturan pertama melindungi pengguna yang memberikan nilai yang salah. Aturan kedua menjaga terhadap programmer yang memasok nilai yang salah. Namun demikian, pemeriksaan apakah pemasok beroperasi harus dalam Feedbackentitas, atau dalam pengendali perintah. Dimana pertanyaannya?
Magnus
2
Dua komentar, tidak terkait langsung dengan pertanyaan. Pertama, melewati akar Agregat sebagai argumen ke agregat lain terlihat salah - itu harus Id - tidak ada yang berguna yang bisa dilakukan oleh agregat dengan agregat lain. Kedua, Pelanggan dan Pemasok adalah ... sulit, buku catatan dalam kedua kasus adalah dunia nyata: Anda tidak dapat menghentikan pemasok di dunia nyata dengan mengirimkan perintah CeaseOperations ke model domain Anda.
VoiceOfUnreason

Jawaban:

1

Jika kebenaran transaksional membutuhkan satu agregat mengetahui tentang kondisi saat ini dari agregat lain, maka model Anda salah.

Dalam kebanyakan kasus, kebenaran transaksional tidak diperlukan . Bisnis cenderung memiliki toleransi terhadap data latensi dan basi. Ini terutama berlaku untuk inkonsistensi yang mudah dideteksi dan mudah diperbaiki.

Jadi perintah akan dijalankan oleh agregat yang mengubah status. Untuk melakukan pemeriksaan yang belum tentu benar, diperlukan belum tentu salinan terbaru dari keadaan agregat lainnya.

Untuk perintah pada agregat yang ada, pola yang biasa adalah meneruskan Repositori ke agregat, dan agregat akan meneruskan statusnya ke repositori, yang menyediakan kueri yang mengembalikan keadaan / proyeksi kekal dari agregat lainnya

class Feedback {
    void downvote(Repository<Supplier.State> query) {
        Supplier.State supplier = query.getById(this->supplierId);
        boolean isOperating = state.isOperating();
        ....
    }
}

Tetapi pola konstruksi aneh - ketika Anda membuat objek, penelepon sudah mengetahui keadaan internal, karena menyediakannya. Pola yang sama berfungsi, hanya terlihat tidak berguna

class Feedback {
    __construct(SupplierId supplierId, SupplierOperatingQuery query ...) {
        Supplier.State supplier = query.getById(this->supplierId);
        boolean isOperating = state.isOperating();
        ....
    }
}

Kami mengikuti aturan dengan menjaga semua logika domain di objek domain, tetapi kami tidak benar-benar melindungi invarian bisnis dengan cara yang bermanfaat dengan melakukannya (karena semua informasi yang sama tersedia untuk komponen aplikasi). Untuk pola pembuatan, akan sama baiknya untuk menulis

class Feedback {
    __construct(Supplier.State supplier, ...) {
        boolean isOperating = state.isOperating();
        ....
    }
}
VoiceOfUnreason
sumber
1. Apakah SupplierOperatingQuerykueri model yang dibaca, atau "Kueri" pada nama menyesatkan? 2. Konsistensi transaksional tidak diperlukan. Tidak masalah jika pemasok menghentikan operasi satu detik sebelum pelanggan meninggalkan umpan balik, tetapi apakah itu berarti kita tidak seharusnya memeriksanya? 3. Dalam contoh Anda, apakah menyediakan "layanan permintaan" daripada objek itu sendiri menegakkan konsistensi transaksional? Jika ya, bagaimana caranya? 4. Bagaimana penggunaan layanan kueri tersebut memengaruhi pengujian unit?
Magnus
1. Kueri, dalam artian menyebut itu tidak mengubah keadaan apa pun. 3. Tidak ada konsistensi transaksional dengan layanan permintaan, tidak ada interaksi antara itu dan menjalankan perintah yang memodifikasi agregat lainnya secara bersamaan. 4. Dalam hal ini, ini akan menjadi bagian dari spi model domain, jadi cukup berikan implementasi pengujian. Hmm, itu agak aneh, meskipun - DomainService mungkin bukan istilah terbaik untuk digunakan.
VoiceOfUnreason
2. Ingat, karena data yang Anda gunakan di sini melewati batas agregat, cek Anda mungkin memberikan jawaban yang salah (mis: cek Anda mengatakan itu tidak OK, tetapi seharusnya karena agregat lainnya berubah). Jadi mungkin lebih baik untuk memindahkan cek ke model baca (selalu terima perintah, tetapi buat laporan pengecualian jika model tidak konsisten). Anda juga dapat mengatur bahwa klien hanya mengirim perintah yang seharusnya berhasil - yaitu, klien tidak boleh mengirim perintah yang diharapkan gagal, berdasarkan pemahamannya tentang keadaan saat ini.
VoiceOfUnreason
1. Secara umum disukai "sisi penulisan" untuk meminta "sisi baca" (misalnya, proyeksi yang bersumber dari peristiwa). "... dalam arti bahwa menyebutnya tidak mengubah keadaan apa pun" - juga tidak hanya menggunakan accessor abadi, yang menurut saya jauh lebih sederhana. 2. Akan baik-baik saja untuk menggandakan cek dalam model baca, tetapi jika Anda memindahkannya (baca: Pindahkan dari server), Anda membuat masalah untuk diri sendiri. Pertama, aturan bisnis Anda harus diduplikasi di setiap klien (browser web dan klien seluler). Kedua, mudah untuk melewati pemeriksaan ini:
magnus
3. "... tidak ada interaksi di antara itu dan perintah yang menjalankan secara bersamaan memodifikasi agregat lainnya" - tidak juga memuat agregat Pemasok itu sendiri, karena hanya agregat Umpan Balik yang sedang dimodifikasi. 4. Jadi SupplierOperatingQuery adalah antarmuka yang membutuhkan implementasi konkret, artinya Anda harus membuat implementasi tiruan dalam pengujian unit Anda hanya untuk menguji nilai benar / salah dari satu variabel yang sudah ada di objek lain? Baunya seperti berlebihan. Mengapa tidak membuat CustomerOwnsOrderQuery dan OrderIsPaidQuery juga ??
Magnus
-1

Saya tahu ini adalah pertanyaan lama, tetapi saya ingin menunjukkan bahwa masalah ini secara langsung berasal dari premis yang salah. Artinya, akar agregat yang seharusnya kita asumsikan ada sama sekali tidak benar.

Hanya ada satu agregat root dalam sistem yang telah Anda jelaskan: Pelanggan. Baik Pesanan dan Umpan Balik, meskipun mereka mungkin merupakan agregat dalam hak mereka sendiri, bergantung pada Pelanggan untuk keberadaannya sehingga bukan merupakan akar agregat itu sendiri. Logika yang Anda berikan dalam konstruktor umpan balik Anda tampaknya menunjukkan bahwa Pesanan HARUS memiliki pelanggan dan Umpan Balik HARUS juga terkait dengan Pelanggan. Ini masuk akal. Bagaimana Pesanan atau Umpan Balik tidak dapat dikaitkan dengan Pelanggan? Selain itu, Pemasok tampaknya secara logis terkait dengan Pesanan (demikian juga dengan agregat ini).

Dengan pemikiran di atas, semua informasi yang Anda inginkan sudah tersedia di akar agregat Pelanggan dan menjadi jelas Anda menegakkan aturan Anda di tempat yang salah. Konstruktor adalah tempat yang mengerikan untuk menegakkan aturan bisnis dan harus dihindari dengan cara apa pun. Seharusnya terlihat seperti ini (Catatan: Saya tidak akan menyertakan konstruktor untuk Pelanggan dan Pesanan karena Pabrik mungkin harus digunakan. Juga tidak menampilkan semua metode antarmuka).

/*******************************\
   Interfaces, explained below 
\*******************************/

interface ICustomer
{
    public function getId() : int;
}

interface IUser extends ICustomer
{
    public function getUsername() : string;

    public function getPassword() : string;

    public function changeUsername( string $new ) : void;

    public function resetPassword( string $new ) : void;

}

interface IReviewer extends ICustomer
{
    public function provideFeedback( IOrder $order, string $content ) : void;
}

interface IBuyer extends ICustomer
{
    public function placeOrder( IOrder $order ) : void;
}

interface IOrder
{
    public function getCustomerId() : int;

    public function addFeedback( string $content ) : void;
}


interface IFeedback
{
    public function addContent( string $content ) : void;

    public function isValidContent( string $content ) : void;
}



/*******************************\
   Implentation
\*******************************/



class Customer implements IReviewer, IBuyer
{
    protected $id;

    protected $orders = [];

    public function provideFeedback( IOrder $order, string $content ) : void
    {
        if( $order->getCustomerId() !== $this->getId() )
            throw new \InvalidArgumentException('Customers can only provide feedback on their own orders');

        $order->addFeedback( $content );
    }
}


class Order implements IOrder
{
    protected $supplier;

    protected $feedbacks = [];

    public function addFeedback( string $content ) : void
    {
        if( false === $this->supplier->isOperating() )
            throw new \Exception('Feedback can only be added to orders if the supplier is still operating.');

        // could be any IFeedback
        $feedback = new Feedback( $this );

        $feedback->addContent( $content );

        $this->feedbacks[] = $feedback;
    }
}


class Feedback implements IFeedback
{
    protected $order;

    protected $content;

    public function __construct( IOrder $order )
    {    
         // we don't carry our business rules in constructors
         $this->order = $order;
    }

    public function addContent( string $content ) : void
    {
        if( false === $this->isValidContent($content) )
            throw new \Exception("Content contains offensive language.");

        $this->content = $content;
    }
}

Baik. Mari kita uraikan ini sedikit. Hal pertama yang Anda perhatikan adalah seberapa jauh deklaratif model ini. Semuanya adalah tindakan, menjadi jelas DI MANA aturan bisnis harus berlaku. Desain di atas tidak hanya "melakukan" hal yang benar, tetapi "mengatakan" hal yang benar.

Apa yang akan membuat orang menganggap aturan sedang dijalankan di baris berikut?

// this is a BAD place for rules to execute
$feedback = new Feedback( $id, $customerId, $order, $supplier, $content);

Kedua, Anda dapat melihat bahwa semua logika yang berkaitan dengan memvalidasi aturan bisnis dilakukan sedekat mungkin dengan model yang terkait. Dalam contoh Anda, konstruktor (metode tunggal) sedang melakukan beberapa validasi terhadap model yang berbeda. Itu merusak desain SOLID. Di mana kami akan menambahkan cek untuk memastikan bahwa konten Umpan Balik tidak mengandung kata-kata buruk? Periksa lagi di konstruktor? Bagaimana jika berbagai jenis Umpan Balik memerlukan pemeriksaan konten yang berbeda? Jelek.

Ketiga, melihat antarmuka, Anda dapat melihat ada tempat alami untuk memperluas / memodifikasi aturan melalui komposisi. Misalnya, berbagai jenis pesanan dapat memiliki aturan yang berbeda mengenai kapan umpan balik dapat diberikan. Pesanan juga dapat memberikan berbagai jenis umpan balik, yang pada gilirannya dapat memiliki aturan yang berbeda untuk validasi.

Anda juga dapat melihat banyak antarmuka ICustomer *. Ini digunakan untuk menyusun agregat Pelanggan yang kami butuhkan di sini (mungkin tidak hanya disebut Pelanggan). Alasannya sederhana. Sangat mungkin bahwa Pelanggan adalah akar agregat BESAR yang tersebar di seluruh domain / DB Anda. Dengan menggunakan antarmuka, kita dapat menguraikan satu agregat (yang kemungkinan terlalu besar untuk dimuat) menjadi beberapa agregat root yang hanya menyediakan tindakan tertentu (seperti memesan atau memberikan umpan balik). Anda dapat melihat agregat dalam implementasi saya dapat KEDUA memesan dan memberikan umpan balik, tetapi tidak dapat digunakan untuk mengatur ulang kata sandi atau mengubah nama pengguna.

Jadi jawaban untuk pertanyaan Anda adalah bahwa agregat harus memvalidasi diri mereka sendiri. Jika mereka tidak dapat Anda kemungkinan memiliki model yang kurang.

slide sisi raja
sumber
1
Sementara batas agregat berbeda tergantung pada siapa yang merancang sistem, saya pikir "satu agregat" yang berasal dari pesanan benar-benar konyol. Contoh Anda tentang Pemasok yang menjadi bagian dari pesanan adalah contoh yang bagus - dapatkah Pemasok tidak ada sampai setelah Pesanan dibuat? Bagaimana dengan duplikat Pemasok:
magnus
@ user1420752 Saya pikir Anda mungkin memilikinya mundur. Model di atas menunjukkan kebalikannya. Bahwa suatu Pesanan tidak dapat ada tanpa Pemasok. Contoh saya hanya menggunakan informasi / aturan / hubungan yang saya dapat kumpulkan dari kode yang disediakan. Saya setuju bahwa, seperti halnya Pelanggan, Pesanan kemungkinan besar, agregat kompleks dengan haknya sendiri (meskipun bukan root). Satu yang juga mungkin memerlukan dekomposisi menjadi beberapa implementasi konkret a tergantung pada konteksnya. Poin yang saya ilustrasikan adalah bahwa entitas HARUS memvalidasi diri mereka sendiri. Seperti yang Anda lihat, lebih bersih seperti itu.
slide sisi raja
@ user1420752 Saya ingin menambahkan bahwa sering metode / konstruktor yang memerlukan banyak argumen adalah tanda model anemia di mana data dipisahkan dari perilaku (dan dengan demikian perlu disuntikkan dalam chuck besar ke potongan-potongan yang bekerja pada data ). Konstruktor Umpan Balik yang Anda berikan adalah contohnya. Model anemia cenderung mengurangi kohesi dan menambahkan semantik kopling tambahan (seperti memeriksa ID berkali-kali). Kohesi tinggi umumnya berarti bahwa setiap metode dalam suatu entitas menggunakan semua variabel instan itu. Ini secara alami mengarah pada penguraian agregat besar seperti Pelanggan atau Pesanan
slide sisi-raja