Kapan obsesi primitif bukan bau kode?

22

Saya telah membaca banyak artikel baru-baru ini yang menggambarkan obsesi primitif sebagai bau kode.

Ada dua manfaat dari menghindari obsesi primitif:

  1. Itu membuat model domain lebih eksplisit. Misalnya, saya dapat berbicara dengan analis bisnis tentang Kode Pos alih-alih string yang berisi kode pos.

  2. Semua validasi ada di satu tempat, bukan di seluruh aplikasi.

Ada banyak artikel di luar sana yang menggambarkan ketika itu adalah kode bau. Sebagai contoh, saya dapat melihat manfaat dari menghilangkan obsesi primitif untuk kode pos seperti ini:

public class Address
{
    public ZipCode ZipCode { get; set; }
}

Berikut adalah konstruktor dari Kode Pos:

public ZipCode(string value)
    {
        // Perform regex matching to verify XXXXX or XXXXX-XXXX format
        _value = value;
    }

Anda akan melanggar prinsip KERING menempatkan logika validasi di mana-mana kode pos digunakan.

Namun, bagaimana dengan objek berikut:

  1. Tanggal Lahir: Periksa apakah tanggal yang lebih besar dari yang ditentukan dan kurang dari hari ini.

  2. Gaji: Periksa lebih besar atau sama dengan nol.

Apakah Anda akan membuat objek DateOfBirth dan objek Gaji? Manfaatnya adalah Anda dapat membicarakannya saat mendeskripsikan model domain. Namun, apakah ini kasus overengineering karena tidak ada banyak validasi. Apakah ada aturan yang menjelaskan kapan dan kapan tidak menghilangkan obsesi primitif atau haruskah Anda selalu melakukannya jika memungkinkan?

Saya kira saya bisa membuat tipe alias bukan kelas, yang akan membantu dengan poin satu di atas.

w0051977
sumber
8
"Anda akan melanggar prinsip KERING menempatkan logika validasi di mana-mana kode pos digunakan." Itu tidak benar. Validasi harus dilakukan segera setelah data dimasukkan ke dalam modul Anda . Jika ada lebih dari satu "titik masuk" validasi harus dalam unit yang dapat digunakan kembali , dan itu tidak perlu menjadi (atau seharusnya) DTO ...
Timothy Truckle
1
Bagaimana Anda memberi "mindate" dan "tanggal hari ini" kepada DateOfBirthkonstruktor untuk diperiksa?
Caleth
11
Manfaat lain dari membuat tipe kustom adalah keamanan tipe. Jika Anda memiliki Salarydan Distancekeberatan, Anda tidak dapat menggunakannya secara tidak sengaja secara bergantian. Anda bisa jika keduanya tipe double.
Scroog1
3
@ w0051977 Pernyataan Anda (seperti yang saya mengerti) menyiratkan bahwa hal lain selain memiliki validasi dalam konstruktor DTO akan melanggar KERING. Faktanya validasi harus di luar DTO ...
Timothy Truckle
2
Bagi saya itu semua masalah ruang lingkup. Jika Anda memberikan primitif cakupan yang luas, maka ada banyak cara di mana mereka dapat disalahgunakan dan salah penanganan. Jadi Anda biasanya ingin memberi mereka ruang lingkup yang lebih sempit, dan salah satu cara untuk melakukannya adalah merancang kelas yang mewakili konsep menggunakan primitif, disimpan secara pribadi sebagai internal, untuk mengimplementasikannya. Sekarang ruang lingkup primitif sempit dan tidak mungkin disalahgunakan / salah penanganan, dan Anda dapat mempertahankan invarian secara efektif. Tetapi jika ruang lingkup primitif sempit untuk memulai, ini mungkin berlebihan dan memperkenalkan banyak kopling dan kode tambahan untuk dipelihara.

Jawaban:

17

Obsesi Primitif menggunakan tipe data primitif untuk mewakili gagasan domain.

Yang sebaliknya adalah "pemodelan domain", atau mungkin "over engineering".

Apakah Anda akan membuat objek DateOfBirth dan objek Gaji?

Memperkenalkan objek Gaji dapat menjadi ide bagus karena alasan berikut: angka jarang berdiri sendiri dalam model domain, mereka hampir selalu memiliki dimensi dan unit. Kami biasanya tidak memodelkan sesuatu yang berguna jika kami menambah panjang waktu atau massa, dan kami jarang mendapatkan hasil yang baik ketika kami mencampur meter dan kaki.

Adapun DateOfBirth, mungkin - ada dua masalah untuk dipertimbangkan. Pertama, membuat Date non-primitif memberi Anda tempat untuk memusatkan semua kekhawatiran aneh seputar matematika tanggal. Banyak bahasa menyediakan satu di luar kotak; DateTime , java.util.Date . Ini adalah implementasi agnostik domain dari tanggal, tetapi mereka bukan primitif.

Kedua, DateOfBirthsebenarnya bukan waktu kencan; di sini di AS, "tanggal lahir" adalah konstruksi budaya / fiksi hukum. Kita cenderung mengukur tanggal lahir dari tanggal lokal kelahiran seseorang; Bob, lahir di California, mungkin memiliki tanggal kelahiran "lebih awal" daripada Alice, lahir di New York, meskipun ia adalah yang lebih muda dari keduanya.

Apakah ada aturan yang menjelaskan kapan dan kapan tidak menghilangkan obsesi primitif atau sebaiknya Anda selalu melakukannya jika memungkinkan.

Tentu tidak selalu; pada batas-batasnya, aplikasi tidak berorientasi objek . Cukup umum untuk melihat primitif digunakan untuk menggambarkan perilaku dalam tes .

VoiceOfUnasonason
sumber
1
Komentar pertama setelah kutipan di atas tampaknya tidak berurutan. Selain itu hanya menyatakan kembali subjek pertanyaan. Jika tidak, ini jawaban yang bagus, tetapi menurut saya ini sangat mengganggu.
JimmyJames
bukan C # DateTime atau java.util.Date yang merupakan tipe dasar yang tepat untuk DateOfBirth.
kevin cline
Mungkin ganti java.util.Datedenganjava.time.LocalDate
Koray Tugay
7

Sejujurnya: itu tergantung.

Selalu ada risiko overengineering kode Anda. Seberapa luas DateOfBirth dan Gaji akan digunakan? Apakah Anda hanya akan menggunakannya dalam tiga kelas yang sangat erat, atau apakah akan digunakan di seluruh aplikasi? Apakah Anda "hanya" merangkum mereka dalam Tipe / Kelas mereka sendiri untuk menegakkan satu kendala itu, atau dapatkah Anda memikirkan lebih banyak kendala / fungsi yang sebenarnya ada di sana?

Mari kita ambil Gaji misalnya: Apakah Anda memiliki operasi dengan "Gaji" (misalnya menangani mata uang yang berbeda, atau mungkin fungsi toString ())? Pertimbangkan apa itu Gaji / lakukan ketika Anda tidak melihatnya sebagai primitif sederhana, dan ada peluang bagus untuk Gaji menjadi kelasnya sendiri.

CharonX
sumber
Apakah jenis alias alternatif yang baik?
w0051977
@ w0051977 saya setuju dengan charonx dan ketik alias bisa menjadi alternatif
techagrammer
@ w0051977 alias tipe dapat menjadi alternatif jika tujuan utamanya adalah untuk menerapkan pengetikan yang ketat, untuk secara eksplisit menyatakan berapa nilai tertentu (Gaji) untuk menghindari penugasan "float dolar" (per Jam? Minggu? Bulan?) ke "gaji mengambang" (per Bulan? Tahun?). Itu benar-benar tergantung pada apa kebutuhan Anda.
CharonX
@CharonX, saya percaya desimal harus digunakan untuk gaji dan bukan float. Apa kamu setuju?
w0051977
@ w0051977 Jika Anda memiliki tipe desimal yang baik, maka yang itu lebih disukai, ya. (Saya sedang mengerjakan proyek C ++ saat ini, jadi booleans, integer dan float berada di garis depan pikiran saya)
CharonX
5

Aturan praktis yang mungkin tergantung pada lapisan program. Untuk Domain (DDD) alias Entitas Layer (Martin, 2018), ini mungkin juga "untuk menghindari primitif untuk apa pun yang mewakili konsep domain / bisnis". Pembenarannya seperti yang dinyatakan oleh OP: model domain yang lebih ekspresif, validasi aturan bisnis, membuat konsep implisit eksplisit (Evans, 2004).

Tipe alias dapat menjadi alternatif yang ringan (Ghosh, 2017), dan dire-refoured ke kelas entitas saat diperlukan. Sebagai contoh, kita mungkin pertama mengharuskan Salarybe >=0, dan kemudian memutuskan untuk melarang $100.33333dan apa pun di atas $10,000,000(yang akan bangkrut klien). Penggunaan Nonnegativeprimitif untuk mewakili Salarydan konsep lain akan mempersulit refactoring ini.

Menghindari primitif juga dapat membantu menghindari rekayasa berlebihan. Misalkan kita perlu menggabungkan Gaji dan Tanggal Lahir ke dalam struktur data: misalnya, untuk memiliki lebih sedikit parameter metode atau untuk meneruskan data antar modul. Kemudian kita bisa menggunakan tuple dengan tipe (Salary, DateOfBirth). Memang, tuple dengan primitif,, (Nonnegative, Nonnegative)tidak informatif, sedangkan beberapa kembung class EmployeeDataakan menyembunyikan bidang yang diperlukan antara lain. Tanda tangan di katakan calcPension(d: (Salary, DateOfBirth))lebih fokus daripada di calcPension(d: EmployeeData), yang melanggar Prinsip Segregasi Antarmuka. Demikian juga, seorang spesialis class SalaryAndDateOfBirthtampaknya canggung dan mungkin merupakan pembunuhan berlebihan. Kemudian, kita dapat memilih untuk mendefinisikan kelas data; tupel dan tipe domain elemental mari kita menunda keputusan seperti itu.

Dalam lapisan luar (misalnya GUI) mungkin masuk akal untuk "menghapus" entitas ke primitif konstituen mereka (misalnya untuk dimasukkan ke dalam DAO). Ini mencegah abstraksi domain bocor ke lapisan luar, seperti yang disarankan dalam Martin (2018).

Referensi
E. Evans, "Desain Berbasis Domain", 2004
D. Ghosh, "Pemodelan Domain Fungsional dan Reaktif", 2017
RC Martin, "Arsitektur bersih", 2018

Tupolev._
sumber
+1 untuk semua referensi.
w0051977
4

Lebih baik menderita Obsesi Primitif atau menjadi Astronot Arsitektur ?

Kedua kasus ini bersifat patologis, dalam satu kasus Anda memiliki terlalu sedikit abstraksi, yang mengarah ke pengulangan dan dengan mudah mengira sebuah apel sebagai jeruk, dan yang lain Anda lupa untuk berhenti menggunakannya dan mulai menyelesaikan sesuatu, sehingga sulit untuk menyelesaikan apa pun. .

Seperti hampir selalu, Anda menginginkan moderasi, jalan tengah yang diharapkan baik-baik saja.

Ingatlah bahwa properti memang memiliki nama, selain tipe. Juga, menguraikan alamat menjadi bagian-bagian penyusunnya mungkin terlalu menyempit jika selalu dilakukan dengan cara yang sama. Tidak semua dunia berada di pusat kota NY.

Deduplicator
sumber
3

Jika Anda memang memiliki kelas gaji, itu bisa memiliki metode seperti ApplyRaise.

Di sisi lain kelas ZipCode Anda tidak harus memiliki validasi internal untuk menghindari duplikasi validasi di mana pun Anda bisa memiliki kelas ZipCodeValidator yang dapat disuntikkan, jadi jika sistem Anda berjalan baik pada alamat US dan UK, Anda bisa menyuntikkan saja validator yang benar dan ketika Anda harus menangani alamat AUS juga Anda bisa menambahkan validator baru.

Kekhawatiran lain adalah jika Anda harus menulis data ke database melalui EntityFramework maka perlu tahu cara menangani Gaji atau Kode Pos.

Tidak ada jawaban yang jelas tentang di mana harus menarik garis batas antara bagaimana kelas cerdas seharusnya, tetapi saya akan mengatakan bahwa saya cenderung untuk memindahkan logika bisnis, seperti memvalidasi, ke kelas logika bisnis yang menjadikan kelas data sebagai data murni karena hal ini tampaknya untuk bekerja lebih baik dengan EntityFramework.

Sedangkan untuk menggunakan alias tipe, nama anggota / properti harus memberikan semua informasi yang diperlukan tentang konten, jadi saya tidak akan menggunakan alias tipe.

Bengkok
sumber
Apakah jenis alias alternatif yang baik?
w0051977
2

(Apa pertanyaannya sebenarnya)

Kapan penggunaan tipe primitif bukan bau kode?

(Menjawab)

Ketika parameter tidak memiliki aturan di dalamnya - gunakan tipe primitif.

Gunakan tipe primitif untuk suka:

htmlEntityEncode(string value)

Gunakan objek untuk suka:

numberOfDaysSinceUnixEpoch(SimpleDate value)

Contoh terakhir ini memiliki aturan di dalamnya, yaitu, objek SimpleDateterdiri dari Year, Month, dan Day. Melalui penggunaan Object dalam hal ini, konsep SimpleDatevalid dapat diringkas dalam objek.

Insinyur PHP Stoked
sumber
1

Terlepas dari contoh kanonik alamat email atau kode pos yang diberikan di tempat lain dalam pertanyaan ini, Di mana saya menemukan refactoring jauh dari Primitive Obsession dapat sangat membantu adalah dengan ID entitas (lihat https://andrewlock.net/using-strongly-typed-entity -id-untuk-menghindari-primitif-obsesi-bagian-1 / untuk contoh bagaimana melakukannya di .NET).

Saya telah kehilangan hitungan berapa kali bug merangkak masuk karena metode memiliki tanda tangan seperti ini:

int leaveId = 12345;
int submitterId = 23456;
int approverId = 34567;

SubmitLeaveApplication(leaveId, approverId, submitterId);

public void SubmitLeaveApplication(int leaveId, int submitterId, int approverId) {
  // implementation here
}

Mengkompilasi dengan baik, dan jika Anda tidak teliti dengan pengujian unit Anda, mungkin lulus juga. Namun, ubah ID entitas tersebut ke dalam kelas khusus domain, dan hai presto, waktu kompilasi:

LeaveId leaveId = 12345;
SubmitterId submitterId = 23456;
ApproverId approverId = 34567;

SubmitLeaveApplication(leaveId, approverId, submitterId);

public void SubmitLeaveApplication(LeaveId leaveId, SubmitterId submitterId, ApproverId approverId) {
  // implementation here
}

Bayangkan metode itu diskalakan hingga 10 atau lebih parameter, semua inttipe data (apalagi bau kode Daftar Parameter Panjang ). Semakin buruk ketika Anda menggunakan sesuatu seperti AutoMapper untuk bertukar antara objek domain dan DTO, dan refactoring yang Anda lakukan tidak diambil oleh pemetaan automagic.

David Keaveny
sumber
0

Anda akan melanggar prinsip KERING menempatkan logika validasi di mana-mana kode pos digunakan.

Di sisi lain, ketika berhadapan dengan banyak negara dan sistem kode pos yang berbeda, itu berarti Anda tidak dapat memvalidasi kode pos kecuali Anda tahu negara yang dimaksud. Jadi ZipCodekelas Anda juga perlu menyimpan negara.

Tetapi apakah Anda kemudian secara terpisah menyimpan negara sebagai bagian dari Address(yang mana kode pos juga bagian dari), dan bagian dari kode pos (untuk validasi)?

  • Jika Anda melakukannya, Anda melanggar KERING juga. Bahkan jika Anda tidak menyebutnya sebagai pelanggaran KERING (karena setiap instance memiliki tujuan yang berbeda), itu masih tidak perlu mengambil memori tambahan, di atas membuka pintu untuk bug ketika kedua nilai negara berbeda (yang secara logis tidak pernah seharusnya menjadi).
    • Atau, sebagai alternatif, itu mengarah pada Anda perlu menyinkronkan dua titik data untuk memastikan bahwa mereka selalu sama, yang menunjukkan bahwa Anda harus benar-benar menyimpan data ini dalam satu titik, sehingga mengalahkan tujuannya.
  • Jika tidak, maka itu bukan ZipCodekelas tetapi Addresskelas, yang lagi-lagi akan mengandung string ZipCodeyang berarti kita telah menjadi lingkaran penuh.

Misalnya, saya dapat berbicara dengan analis bisnis tentang Kode Pos alih-alih string yang berisi kode pos.

Manfaatnya adalah Anda dapat membicarakannya saat mendeskripsikan model domain.

Saya tidak mengerti pernyataan mendasar Anda bahwa ketika sebuah informasi memiliki tipe variabel tertentu, Anda entah bagaimana wajib menyebutkan tipe itu setiap kali Anda berbicara dengan seorang analis bisnis.

Mengapa? Mengapa Anda tidak dapat hanya berbicara tentang "kode pos" dan sepenuhnya mengabaikan jenis tertentu? Diskusi seperti apa yang Anda lakukan dengan analis bisnis Anda (bukan teknis!) Di mana jenis properti itu esensial untuk percakapan?

Dari mana saya berasal, kode pos selalu berupa angka. Jadi kita punya pilihan, kita bisa menyimpannya sebagai intatau sebagai string. Kita cenderung menggunakan string karena tidak ada harapan operasi matematika pada data, tetapi analis bisnis tidak pernah mengatakan kepada saya bahwa itu perlu string. Keputusan itu diserahkan kepada pengembang (atau bisa dibilang analis teknis, meskipun dalam pengalaman saya mereka tidak langsung berurusan dengan seluk beluk).

Seorang analis bisnis tidak peduli dengan tipe data, asalkan aplikasi melakukan apa yang diharapkan untuk dilakukan.


Validasi adalah binatang yang sulit untuk ditangani, karena bergantung pada apa yang diharapkan manusia.

Pertama, saya tidak setuju dengan argumen validasi sebagai cara untuk menunjukkan mengapa obsesi primitif harus dihindari, karena saya tidak setuju bahwa (sebagai kebenaran universal) data selalu perlu divalidasi setiap saat.

Misalnya, bagaimana jika ini adalah pencarian yang lebih rumit? Alih-alih pemeriksaan format sederhana, bagaimana jika validasi Anda memerlukan menghubungi API eksternal dan menunggu tanggapan? Apakah Anda benar-benar ingin memaksa aplikasi Anda untuk memanggil API eksternal ini untuk setiap ZipCodeobjek yang Anda instantiate?
Mungkin itu persyaratan bisnis yang ketat, dan tentu saja itu bisa dibenarkan. Tetapi ini bukan kebenaran universal. Akan ada banyak kasus penggunaan di mana ini lebih merupakan beban daripada solusi.

Sebagai contoh kedua, ketika memasukkan alamat Anda dalam formulir, itu biasa untuk memasukkan kode pos Anda sebelum negara Anda. Meskipun menyenangkan untuk memiliki umpan balik validasi langsung di UI, itu sebenarnya akan menjadi penghalang bagi saya (sebagai pengguna) jika aplikasi mengingatkan saya pada format kode pos yang "salah", karena sumber sebenarnya dari masalah ini adalah (misalnya) bahwa negara saya bukan negara yang dipilih secara default, dan dengan demikian validasi terjadi untuk negara yang salah.
Ini adalah pesan kesalahan yang salah, yang mengalihkan perhatian pengguna dan menyebabkan kebingungan yang tidak perlu.

Sama seperti bagaimana validasi abadi bukanlah kebenaran universal, tidak ada contoh saya. Itu kontekstual . Beberapa domain aplikasi memerlukan validasi data di atas segalanya. Domain lain tidak menempatkan validasi yang tinggi dalam daftar prioritas karena kerumitan yang dibawanya bertentangan dengan prioritas aktualnya (misalnya pengalaman pengguna, atau kemampuan untuk awalnya menyimpan data yang salah sehingga dapat diperbaiki, bukannya tidak pernah membiarkannya menjadi disimpan)

Tanggal Lahir: Periksa bahwa tanggal yang lebih besar dari yang ditentukan dan kurang dari hari ini.
Gaji: Periksa lebih besar atau sama dengan nol.

Masalah dengan validasi ini adalah bahwa mereka tidak lengkap, berlebihan atau menunjukkan masalah yang jauh lebih besar .

Memeriksa bahwa kencan lebih besar daripada pikiran berlebihan. Mindate secara harfiah berarti bahwa itu adalah tanggal sekecil mungkin. Selain itu, di mana Anda menarik garis relevansi? Apa gunanya mencegah DateTime.MinDatetetapi membiarkan DateTime.MinDate.AddSeconds(1)? Anda menghargai nilai tertentu yang tidak terlalu salah dibandingkan dengan banyak nilai lainnya.

Ulang tahun saya adalah 2 Januari 1978 (tidak, tapi anggap saja). Tetapi katakanlah data dalam aplikasi Anda salah, dan sebaliknya dikatakan ulang tahun saya adalah:

  • 1 Januari 1978
  • 1 Januari 1722
  • 1 Januari 2355

Semua tanggal ini salah. Tak satu pun dari mereka yang "lebih benar" daripada yang lain. Tetapi aturan validasi Anda hanya akan menangkap salah satu dari tiga contoh ini.

Anda juga telah sepenuhnya menghilangkan konteks tentang bagaimana Anda menggunakan data ini. Jika ini digunakan dalam mis. Bot pengingat ulang tahun, saya akan mengatakan validasinya tidak ada gunanya karena tidak ada konsekuensi buruk khusus untuk mengisi tanggal yang salah.
Di sisi lain, jika ini adalah data pemerintah dan Anda memerlukan tanggal lahir untuk mengotentikasi identitas seseorang (dan kegagalan untuk melakukannya mengarah pada konsekuensi buruk, misalnya menolak jaminan sosial seseorang), maka kebenaran data sangat penting dan Anda perlu sepenuhnya memvalidasi data. Validasi yang diajukan yang Anda miliki sekarang tidak memadai.

Untuk gaji, ada beberapa akal sehat bahwa itu tidak boleh negatif. Tetapi jika Anda secara realistis berharap bahwa data yang tidak masuk akal dimasukkan, saya sarankan Anda menyelidiki sumber dari data yang tidak masuk akal ini. Karena jika mereka tidak dapat dipercaya untuk memasukkan data sensis, Anda juga tidak dapat mempercayai mereka untuk memasukkan data yang benar .

Jika bukan gaji yang dihitung oleh aplikasi Anda, dan entah bagaimana itu mungkin berakhir dengan angka negatif (dan benar), maka pendekatan yang lebih baik akan dilakukan Math.Max(myValue, 0)untuk mengubah angka negatif menjadi 0, daripada gagal validasi. Karena jika logika Anda memutuskan bahwa hasilnya adalah angka negatif, gagal validasi berarti harus mengulang perhitungan, dan tidak ada alasan untuk berpikir bahwa itu akan muncul dengan nomor yang berbeda untuk kedua kalinya.
Dan jika muncul dengan nomor yang berbeda, itu sekali lagi membuat Anda curiga bahwa perhitungannya tidak konsisten dan karenanya tidak dapat dipercaya.

Ini bukan untuk mengatakan bahwa validasi tidak berguna. Tetapi validasi yang tidak berarti itu buruk, baik karena butuh usaha tanpa benar-benar menyelesaikan masalah, dan memberi orang rasa aman yang salah.

Flater
sumber
Tanggal lahir seseorang sebenarnya bisa melewati tanggal saat ini, jika bayi lahir sekarang di zona waktu yang telah dilewati ke hari berikutnya. Dan rumah sakit dapat menyimpan "tanggal lahir yang diharapkan" dalam database yang bisa jadi berbulan-bulan di masa depan. Apakah Anda ingin jenis yang berbeda untuk itu?
gnasher729
@ gnasher729: Saya tidak begitu yakin saya mengikuti, sepertinya Anda setuju dengan saya (validasi kontekstual dan tidak secara universal benar), tetapi ungkapan komentar Anda menunjukkan Anda berpikir saya tidak setuju. Atau apakah saya salah membaca?
Flater