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 Product
model anemik, setidaknya dengan getter.
Di sisi lain, OOP mengatakan sebuah Product
objek 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 Product
tahu 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?
Jawaban:
Kau menulis
dan dalam komentar.
Ini adalah kesalahpahaman umum.
Product
adalah 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,Product
mungkin 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
Product
kelas. Misalnya, mungkin ada operasiCalcTotalPrice(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.
sumber
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.Account.transfer
untuk 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,Account
kegigihan 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.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.
sumber
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.
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
Product
objek 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.Itu pasti bekerja - penyimpanan persisten Anda secara efektif menjadi panggilan balik. Saya mungkin akan membuat antarmuka lebih sederhana:
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".
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.
sumber
Storage
antarmuka 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.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.
Storage
Contoh antarmuka Anda adalah yang sangat bagus, dengan asumsiStorage
kemudian 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" suatuAccount
setelah saya menelepontransfer(amount)
. Saya harus benar berharap bahwa fungsi bisnistransfer()
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.
sumber
AccountNumber
harus tahu bahwa itu dapat direpresentasikan sebagaiTextField
. Jika orang lain (seperti "Tampilan") akan tahu ini, itu adalah kopling yang seharusnya tidak ada, karena komponen itu perlu tahu apa yangAccountNumber
terdiri dari, yaitu internal.Hindari menyebarkan pengetahuan tentang bidang yang tidak perlu. Semakin banyak hal yang diketahui tentang bidang individu semakin sulit untuk menambah atau menghapus bidang:
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.
LinkedHashMap
adalah 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.
sumber
Map
, Anda mengusulkanString
atauList
. 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.Storage
adalah 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.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
sumber