DDD memenuhi OOP: Bagaimana menerapkan repositori berorientasi objek?

12

Implementasi khas dari repositori DDD tidak terlihat sangat OO, misalnya save()metode:

package com.example.domain;

public class Product {  /* public attributes for brevity */
    public String name;
    public Double price;
}

public interface ProductRepo {
    void save(Product product);
} 

Bagian infrastruktur:

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);
    }
} 

Antarmuka seperti itu mengharapkan Productmodel anemik, setidaknya dengan getter.

Di sisi lain, OOP mengatakan sebuah Productobjek harus tahu bagaimana cara menyelamatkan diri.

package com.example.domain;

public class Product {
    private String name;
    private Double price;

    void save() {
        // save the product
        // ???
    }
}

Masalahnya adalah, ketika yang Producttahu bagaimana cara menyimpan sendiri, itu berarti kode infrastruktur tidak lepas dari kode domain.

Mungkin kita bisa mendelegasikan penyimpanan ke objek lain:

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();
}

Bagian infrastruktur:

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"));
    }
}

Apa pendekatan terbaik untuk mencapai ini? Apakah mungkin untuk menerapkan repositori berorientasi objek?

ttulka
sumber
6
OOP mengatakan objek Produk harus tahu cara menyelamatkan dirinya sendiri - Saya tidak yakin itu benar-benar benar ... OOP itu sendiri tidak benar-benar menentukan itu, itu lebih merupakan masalah desain / pola (yang merupakan tempat DDD / apa pun yang Anda lakukan) -menggunakan datang)
jleach
1
Ingatlah bahwa dalam konteks OOP, ini berbicara tentang objek. Hanya objek, bukan kegigihan data. Pernyataan Anda menunjukkan bahwa keadaan objek tidak boleh dikelola di luar itu sendiri, yang saya setujui. Repositori bertanggung jawab untuk memuat / menyimpan dari beberapa lapisan persistensi (yang berada di luar ranah OOP). Properti dan metode kelas harus mempertahankan integritas mereka sendiri, ya, tetapi ini tidak berarti objek lain tidak dapat bertanggung jawab untuk mempertahankan keadaan. Dan, getter dan setter adalah untuk memastikan integritas data yang masuk / keluar dari objek.
jleach
1
"Ini tidak berarti objek lain tidak dapat bertanggung jawab untuk mempertahankan negara." - Saya tidak mengatakan itu. Pernyataan penting adalah, bahwa suatu objek harus aktif . Ini berarti objek (dan tidak ada orang lain) dapat mendelegasikan operasi ini ke objek lain, tetapi tidak sebaliknya: tidak ada objek yang hanya mengumpulkan informasi dari objek pasif untuk memproses operasi egoisnya sendiri (seperti yang dilakukan repo dengan getter) . Saya mencoba menerapkan pendekatan ini dalam cuplikan di atas.
ttulka
1
@ jleach Anda benar, pemahaman kami tentang OOP berbeda, bagi saya pengambil + setter sama sekali bukan OOP, kalau tidak pertanyaan saya tidak masuk akal. Bagaimanapun, terima kasih! :-)
ttulka
1
Berikut ini adalah artikel tentang poin saya: martinfowler.com/bliki/AnemicDomainModel.html Saya tidak menentang model anemia dalam semua kasus, misalnya itu adalah strategi yang baik untuk pemrograman fungsional. Hanya tidak OOP.
ttulka

Jawaban:

7

Kau menulis

Di sisi lain, OOP mengatakan objek Produk harus tahu cara menyelamatkan diri

dan dalam komentar.

... harus bertanggung jawab atas semua operasi yang dilakukan dengannya

Ini adalah kesalahpahaman umum. Productadalah objek domain, jadi harus bertanggung jawab atas operasi domain yang melibatkan objek produk tunggal , tidak kurang, tidak lebih - jadi jelas tidak untuk semua operasi. Biasanya kegigihan tidak dilihat sebagai operasi domain. Justru sebaliknya, dalam aplikasi perusahaan, tidak jarang mencoba untuk mencapai ketidaktahuan kegigihan dalam model domain (setidaknya sampai tingkat tertentu), dan menjaga mekanika persistensi dalam kelas repositori yang terpisah adalah solusi yang populer untuk ini. "DDD" adalah teknik yang bertujuan untuk aplikasi semacam ini.

Jadi apa yang bisa menjadi operasi domain yang masuk akal untuk Product? Ini sebenarnya tergantung pada konteks domain dari sistem aplikasi. Jika sistem ini kecil dan hanya mendukung operasi CRUD secara eksklusif, maka memang, Productmungkin tetap "anemia" seperti pada contoh Anda. Untuk jenis aplikasi semacam itu, mungkin bisa diperdebatkan jika menempatkan operasi basis data ke dalam kelas repo yang terpisah, atau menggunakan DDD sama sekali, tidak masalah.

Namun, segera setelah aplikasi Anda mendukung operasi bisnis nyata, seperti membeli atau menjual produk, menyimpannya dalam stok dan mengelolanya, atau menghitung pajak untuknya, cukup umum Anda mulai menemukan operasi yang dapat ditempatkan secara masuk akal di Productkelas. Misalnya, mungkin ada operasi CalcTotalPrice(int noOfItems)yang menghitung harga untuk `n item produk tertentu ketika memperhitungkan diskon volume.

Jadi singkatnya, ketika Anda mendesain kelas, Anda perlu memikirkan konteks Anda, di mana dari lima dunia Joel Spolsky Anda, dan jika sistem tersebut mengandung cukup logika domain maka DDD akan menguntungkan. Jika jawabannya adalah ya, sangat tidak mungkin Anda berakhir dengan model anemia hanya karena Anda menjaga mekanisme ketekunan dari kelas domain.

Doc Brown
sumber
Poin Anda terdengar sangat masuk akal bagi saya. Jadi, produk menjadi struktur data anemia ketika melintasi perbatasan konteks struktur data anemia (database) dan repositori adalah gateway. Tapi ini masih berarti saya harus menyediakan akses ke struktur internal objek melalui pengambil dan setter, yang kemudian menjadi bagian dari API dan dapat dengan mudah disalahgunakan oleh kode lain, yang tidak ada hubungannya dengan kegigihan. Adakah praktik yang baik bagaimana menghindari ini? Terima kasih!
ttulka
"Tapi ini masih berarti aku harus menyediakan akses ke struktur internal objek melalui pengambil dan setter" - tidak mungkin. Keadaan internal dari objek domain persisten-bodoh biasanya diberikan secara eksklusif oleh seperangkat atribut terkait domain. Untuk atribut ini, getter dan setter (atau inisialisasi konstruktor) harus ada, jika tidak tidak ada operasi domain "menarik" yang mungkin dilakukan. Dalam beberapa kerangka kerja, ada juga fitur kegigihan yang tersedia yang memungkinkan untuk bertahan atribut pribadi dengan refleksi, sehingga enkapsulasi hanya rusak untuk mekanisme ini, bukan untuk "kode lain".
Doc Brown
1
Saya setuju bahwa kegigihan biasanya bukan bagian dari operasi domain, namun itu harus menjadi bagian dari operasi domain "nyata" di dalam objek yang membutuhkannya. Misalnya Account.transfer(amount)harus bertahan transfer. Bagaimana melakukannya, itu adalah tanggung jawab objek, bukan entitas eksternal. Menampilkan objek di sisi lain adalah biasanya operasi domain! Persyaratan biasanya menggambarkan dengan sangat rinci bagaimana barang akan terlihat. Ini adalah bagian dari bahasa di antara anggota proyek, bisnis atau lainnya.
Robert Bräutigam
@ RobertBräutigam: klasik Account.transferuntuk biasanya melibatkan dua objek akun, dan satu unit objek kerja. Operasi bertahan transaksional kemudian dapat menjadi bagian dari yang terakhir (bersama dengan panggilan ke repo terkait), sehingga tetap keluar dari metode "transfer". Dengan begitu, Accountkegigihan bisa tetap bertahan. Saya tidak mengatakan ini tentu lebih baik daripada solusi yang seharusnya, tetapi milik Anda juga hanya salah satu dari beberapa pendekatan yang mungkin.
Doc Brown
1
@ RobertBräutigam Cukup yakin Anda terlalu banyak berpikir tentang hubungan antara objek dan tabel. Pikirkan tentang objek yang memiliki keadaan untuk dirinya sendiri, semuanya dalam memori. Setelah melakukan transfer di objek akun Anda, Anda akan dibiarkan dengan objek dengan status baru. Itulah yang ingin Anda pertahankan, dan untungnya objek akun menyediakan cara untuk memberi tahu Anda tentang keadaan mereka. Itu tidak berarti negara mereka harus sama dengan tabel dalam database - yaitu jumlah yang ditransfer dapat berupa objek uang yang berisi jumlah mentah dan mata uang.
Steve Chamaillard
5

Berlatih teori truf.

Pengalaman mengajarkan kita bahwa Product.Save () mengarah ke banyak masalah. Untuk mengatasi masalah itu, kami menemukan pola repositori.

Tentu itu melanggar aturan OOP menyembunyikan data produk. Tapi itu bekerja dengan baik.

Jauh lebih sulit untuk membuat seperangkat aturan yang konsisten yang mencakup segalanya daripada membuat beberapa aturan umum yang baik yang memiliki pengecualian.

Ewan
sumber
3

DDD bertemu dengan OOP

Perlu diingat bahwa tidak ada ketegangan antara dua ide ini - objek bernilai, agregat, repositori adalah susunan pola yang digunakan yang oleh beberapa orang dianggap sebagai OOP dilakukan dengan benar.

Di sisi lain, OOP mengatakan objek Produk harus tahu cara menyelamatkan diri.

Tidak begitu. Objek merangkum struktur datanya sendiri. Representasi Anda dalam memori atas suatu Produk bertanggung jawab untuk menunjukkan perilaku produk (apa pun itu); tetapi penyimpanan persisten ada di sana (di belakang repositori) dan memiliki pekerjaan sendiri yang harus dilakukan.

Perlu ada beberapa cara untuk menyalin data antara representasi dalam memori dari basis data, dan kenang-kenangan yang masih ada. Pada batas , hal-hal cenderung menjadi sangat primitif.

Pada dasarnya, menulis hanya basis data tidak terlalu berguna, dan mereka dalam memori setara tidak lebih berguna daripada jenis "bertahan". Tidak ada gunanya memasukkan informasi ke Productobjek jika Anda tidak akan pernah mengeluarkan informasi itu. Anda tidak perlu menggunakan "getter" - Anda tidak mencoba berbagi struktur data produk, dan Anda tentu tidak boleh berbagi akses yang bisa berubah ke representasi internal Produk.

Mungkin kita bisa mendelegasikan penyimpanan ke objek lain:

Itu pasti bekerja - penyimpanan persisten Anda secara efektif menjadi panggilan balik. Saya mungkin akan membuat antarmuka lebih sederhana:

interface ProductStorage {
    onProduct(String name, double price);
}

Ada akan menjadi kopling antara dalam representasi memori dan mekanisme penyimpanan, karena informasi yang perlu mendapatkan dari sini ke sana (dan kembali lagi). Mengubah informasi yang akan dibagikan akan berdampak pada kedua ujung percakapan. Jadi, sebaiknya kita buat yang eksplisit di mana kita bisa.

Pendekatan ini - melewatkan data melalui callback, memainkan peran penting dalam pengembangan ejekan dalam TDD .

Perhatikan bahwa meneruskan informasi ke panggilan balik memiliki semua pembatasan yang sama seperti mengembalikan informasi dari kueri - Anda tidak boleh melewati salinan struktur data Anda yang bisa berubah.

Pendekatan ini sedikit bertentangan dengan apa yang dideskripsikan Evans dalam Blue Book, di mana mengembalikan data melalui kueri adalah cara normal untuk menyelesaikan berbagai hal, dan objek domain secara khusus dirancang untuk menghindari pencampuran dalam "masalah kegigihan".

Saya mengerti DDD sebagai teknik OOP dan jadi saya ingin sepenuhnya memahami kontradiksi yang tampaknya.

Satu hal yang perlu diingat - Buku Biru ditulis lima belas tahun yang lalu, ketika Jawa 1.4 menjelajahi bumi. Secara khusus, buku ini mendahului generik Java - kami memiliki lebih banyak teknik yang tersedia bagi kami saat ini ketika Evans mengembangkan gagasannya.

VoiceOfUnreason
sumber
2
Juga layak untuk disebutkan: "simpan sendiri" akan selalu memerlukan interaksi dengan objek lain (baik objek sistem file, atau database, atau layanan web jarak jauh, beberapa di antaranya mungkin memerlukan sesi yang akan dibuat untuk kontrol akses). Jadi objek seperti itu tidak akan berdiri sendiri dan mandiri. OOP karena itu tidak dapat mensyaratkan ini, karena tujuannya adalah untuk merangkum objek dan mengurangi kopling.
Christophe
Terima kasih atas jawaban yang bagus. Pertama, saya mendesain Storageantarmuka dengan cara yang sama seperti yang Anda lakukan, kemudian saya menganggap sambungan tinggi dan mengubahnya. Tapi Anda benar, toh ada kopling yang tidak dapat dihindari, jadi mengapa tidak membuatnya lebih eksplisit.
ttulka
1
"Pendekatan ini sedikit bertentangan dengan apa yang dijelaskan oleh Evans dalam Blue Book" - jadi ada beberapa ketegangan :-) Itu sebenarnya poin dari pertanyaan saya, saya mengerti DDD sebagai teknik OOP dan jadi saya ingin sepenuhnya memahami kontradiksi yang tampaknya.
ttulka
1
Dalam pengalaman saya, masing-masing dari hal-hal ini (OOP secara umum, DDD, TDD, pick-your-acronym) semua terdengar bagus dan baik dalam diri mereka sendiri, tetapi setiap kali datang ke implementasi "dunia nyata", selalu ada tradeoff atau kurang idealisme yang harus membuatnya bekerja.
jleach
Saya tidak setuju dengan gagasan bahwa kegigihan (dan presentasi) entah bagaimana "istimewa". Mereka tidak. Mereka harus menjadi bagian dari pemodelan untuk memperluas permintaan persyaratan. Tidak perlu ada batas artifisial (berbasis data) di dalam aplikasi, kecuali ada persyaratan aktual yang bertentangan.
Robert Bräutigam
1

Pengamatan yang sangat bagus, saya sepenuhnya setuju dengan Anda tentang mereka. Berikut adalah pembicaraan saya (koreksi: slide saja) persis hal ini: Object-Oriented Domain-Driven Design .

Jawaban singkat: tidak. Seharusnya tidak ada objek dalam aplikasi Anda yang murni teknis dan tidak memiliki relevansi domain. Itu seperti menerapkan kerangka kerja logging dalam aplikasi akuntansi.

StorageContoh antarmuka Anda adalah yang sangat bagus, dengan asumsi Storagekemudian dianggap beberapa kerangka kerja eksternal, bahkan jika Anda menulisnya.

Juga, save()dalam suatu objek hanya boleh diizinkan jika itu adalah bagian dari domain ("bahasa"). Sebagai contoh, saya seharusnya tidak diharuskan untuk secara eksplisit "menyimpan" suatu Accountsetelah saya menelepon transfer(amount). Saya harus benar berharap bahwa fungsi bisnis transfer()akan bertahan transfer saya.

Secara keseluruhan, saya pikir ide-ide DDD adalah yang bagus. Menggunakan bahasa di mana-mana, menggunakan domain dengan percakapan, konteks terbatas, dll. Namun, blok bangunan memerlukan perombakan serius jika kompatibel dengan orientasi objek. Lihat dek terkait untuk detail.

Robert Bräutigam
sumber
Apakah pembicaraan Anda di suatu tempat untuk ditonton? (Saya melihat hanya slide di bawah tautan). Terima kasih!
ttulka
Saya hanya punya rekaman ceramah berbahasa Jerman, di sini: javadevguy.wordpress.com/2018/11/26/…
Robert Bräutigam
Bicara hebat! (Untungnya saya berbicara bahasa Jerman). Saya pikir seluruh blog Anda layak dibaca ... Terima kasih atas pekerjaan Anda!
ttulka
Slider Robert yang sangat berwawasan. Saya menemukan itu sangat ilustratif tetapi saya merasa pada akhirnya, banyak solusi yang ditujukan untuk tidak melanggar enkapsulasi dan LoD didasarkan pada memberikan banyak tanggung jawab terhadap objek domain: pencetakan, serialisasi, format UI, dll. Tidak t bahwa peningkatan sambungan antara domain dan teknis (detail implementasi)? Misalnya, AccountNumber digabungkan dengan Apache Wicket API. Atau Akun dengan objek Json apa saja? Apakah Anda pikir itu layak dimiliki?
Laiv
@Laiv Tata bahasa pertanyaan Anda menunjukkan bahwa ada yang salah dengan menggunakan teknologi untuk mengimplementasikan fungsi bisnis? Mari kita begini: Bukan kopling antara domain dan teknologi yang menjadi masalah, melainkan kopling antara berbagai tingkat abstraksi. Misalnya AccountNumber harus tahu bahwa itu dapat direpresentasikan sebagai TextField. Jika orang lain (seperti "Tampilan") akan tahu ini, itu adalah kopling yang seharusnya tidak ada, karena komponen itu perlu tahu apa yang AccountNumberterdiri dari, yaitu internal.
Robert Bräutigam
1

Mungkin kita bisa mendelegasikan penyimpanan ke objek lain

Hindari menyebarkan pengetahuan tentang bidang yang tidak perlu. Semakin banyak hal yang diketahui tentang bidang individu semakin sulit untuk menambah atau menghapus bidang:

public class Product {
    private String name;
    private Double price;

    void save(Storage storage) {
        storage.save( toString() );
    }
}

Di sini produk tidak tahu apakah Anda menyimpan ke file log atau database atau keduanya. Di sini metode penyimpanan tidak tahu apakah Anda memiliki 4 atau 40 bidang. Itu digabungkan secara longgar. Itu hal yang baik.

Tentu saja ini hanya satu contoh bagaimana Anda dapat mencapai tujuan ini. Jika Anda tidak suka membuat dan mengurai string untuk digunakan sebagai DTO, Anda juga dapat menggunakan koleksi. LinkedHashMapadalah favorit lama saya karena mempertahankan pesanan dan toString () terlihat bagus di file log.

Bagaimanapun Anda melakukannya, tolong jangan menyebarkan pengetahuan tentang bidang di sekitar. Ini adalah bentuk penggandengan yang sering diabaikan orang sampai terlambat. Saya ingin beberapa hal untuk mengetahui secara statis berapa banyak bidang yang dimiliki objek saya. Dengan begitu menambahkan bidang tidak melibatkan banyak pengeditan di banyak tempat.

candied_orange
sumber
Ini sebenarnya kode yang saya posting di pertanyaan saya, bukan? Saya menggunakan Map, Anda mengusulkan Stringatau List. Tapi, seperti @VoiceOfUnreason disebutkan dalam jawabannya, kopling masih ada, hanya saja tidak eksplisit. Masih tidak perlu mengetahui struktur data produk untuk menyimpannya baik dalam database atau file log, setidaknya ketika dibaca kembali sebagai objek.
ttulka
Saya mengubah metode simpan tetapi sebaliknya ya sama saja. Perbedaannya adalah kopling tidak lagi statis memungkinkan bidang baru ditambahkan tanpa memaksakan perubahan kode ke sistem penyimpanan. Itu membuat sistem penyimpanan dapat digunakan kembali pada berbagai produk. Itu hanya memaksa Anda untuk melakukan hal-hal yang agak tidak wajar seperti mengubah dobel menjadi string dan kembali menjadi dobel. Tapi itu bisa diatasi juga jika itu benar-benar masalah.
candied_orange
Lihat koleksi hetrogenous
candied_orange
Tetapi seperti yang saya katakan, saya melihat kopling masih ada (dengan parsing), hanya karena tidak statis (eksplisit) membawa kerugian karena tidak dapat diperiksa oleh kompiler dan lebih rentan kesalahan. Ini Storageadalah bagian dari domain (dan juga antarmuka repositori) dan membuat API persistensi seperti itu. Ketika diubah, lebih baik memberi tahu klien dalam waktu kompilasi, karena mereka tetap harus bereaksi agar tidak rusak saat runtime.
ttulka
Itu kesalahpahaman. Kompiler tidak dapat memeriksa file log atau DB. Semua itu memeriksa apakah satu file kode konsisten dengan file kode lain yang juga tidak dijamin konsisten dengan file log atau DB.
candied_orange
0

Ada alternatif untuk pola yang telah disebutkan. Pola Memento sangat bagus untuk merangkum keadaan internal objek domain. Objek kenang-kenangan merepresentasikan potret dari objek publik domain. Objek domain tahu cara membuat status publik ini dari kondisi internal dan sebaliknya. Repositori kemudian hanya berfungsi dengan representasi publik dari negara. Dengan itu implementasi internal dipisahkan dari setiap kegigihan spesifik dan hanya harus mempertahankan kontrak publik. Juga objek domain Anda belum memaparkan getter yang memang membuatnya sedikit anemia.

Untuk lebih lanjut tentang topik ini, saya merekomendasikan buku hebat: "Pola, Prinsip dan dan Praktek Desain Berbasis Domain" oleh Scott Millett dan Nick Tune

Roman Weis
sumber