"Pekerjaan nyata" yang sah dalam konstruktor?

23

Saya sedang mengerjakan desain, tetapi tetap saja memukul penghalang jalan. Saya memiliki kelas tertentu (ModelDef) yang pada dasarnya adalah pemilik pohon simpul kompleks yang dibangun dengan mem-parsing skema XML (pikirkan DOM). Saya ingin mengikuti prinsip-prinsip desain yang baik (SOLID), dan memastikan bahwa sistem yang dihasilkan mudah diuji. Saya memiliki setiap niat menggunakan DI untuk meneruskan dependensi ke dalam konstruktor ModelDef (sehingga ini dapat dengan mudah ditukar, jika perlu, selama pengujian).

Apa yang saya perjuangkan adalah penciptaan pohon simpul. Pohon ini akan terdiri dari objek "nilai" sederhana yang tidak perlu diuji secara independen. (Namun, saya masih bisa meneruskan Pabrik Abstrak ke ModelDef untuk membantu pembuatan benda-benda ini.)

Tetapi saya terus membaca bahwa seorang konstruktor tidak boleh melakukan pekerjaan nyata (mis. Cacat: Pembuat pekerjaan nyata ). Ini masuk akal bagi saya jika "pekerjaan nyata" berarti membangun objek yang sangat bergantung berat yang nantinya ingin dihilangkan untuk pengujian. (Itu harus diteruskan melalui DI.)

Tapi bagaimana dengan benda bernilai ringan seperti pohon simpul ini? Pohon itu harus dibuat di suatu tempat, kan? Mengapa tidak melalui konstruktor ModelDef (menggunakan, katakanlah, metode buildNodeTree ())?

Saya tidak benar-benar ingin membuat pohon simpul di luar ModelDef dan kemudian meneruskannya (melalui konstruktor DI), karena membuat pohon simpul dengan menguraikan skema memerlukan sejumlah besar kode kompleks - kode yang perlu diuji secara menyeluruh . Saya tidak ingin menurunkannya ke "lem" kode (yang seharusnya relatif sepele, dan kemungkinan tidak akan langsung diuji).

Saya telah berpikir untuk meletakkan kode untuk membuat simpul pohon di objek "builder" yang terpisah, tetapi ragu untuk menyebutnya "builder", karena itu tidak benar-benar cocok dengan Pola Builder (yang tampaknya lebih peduli dengan menghilangkan telescoping konstruktor). Tetapi bahkan jika saya menyebutnya sesuatu yang berbeda (misalnya NodeTreeConstructor), masih terasa seperti sedikit peretasan hanya untuk menghindari konstruktor ModelDef membangun pohon simpul. Itu harus dibangun di suatu tempat; mengapa tidak di objek yang akan memilikinya?

Gurtz
sumber
7
Anda harus selalu waspada terhadap pernyataan selimut seperti itu. Aturan umum adalah kode dengan cara yang jelas, fungsional, mudah untuk diuji, digunakan kembali, dan dipelihara, dengan cara apa pun yang mungkin, yang bervariasi tergantung pada situasi Anda. Jika Anda mendapatkan apa-apa selain kompleksitas kode dan kebingungan mencoba mengikuti "aturan" seperti itu maka itu bukan aturan yang sesuai untuk situasi Anda. Semua "pola" dan fitur bahasa ini adalah alat; gunakan yang terbaik untuk pekerjaan spesifik Anda.
Jason C

Jawaban:

26

Dan, selain apa yang disarankan Ross Patterson, pertimbangkan posisi ini yang merupakan kebalikannya:

  1. Ambil pepatah seperti "Jangan Melakukan Pekerjaan Nyata di Konstruktor-Mu" dengan sebutir garam.

  2. Konstruktor sebenarnya hanyalah metode statis. Jadi, secara struktural, sebenarnya tidak ada banyak perbedaan antara:

    a) konstruktor sederhana dan banyak metode pabrik statis yang kompleks, dan

    b) konstruktor sederhana dan banyak konstruktor yang lebih kompleks.

Sebagian besar dari sentimen negatif terhadap melakukan pekerjaan nyata dalam konstruktor berasal dari periode tertentu dari sejarah C ++ ketika ada perdebatan tentang apa tepatnya objek akan dibiarkan jika pengecualian dilemparkan dalam konstruktor, dan apakah destructor harus dipanggil dalam peristiwa semacam itu. Bagian dari sejarah C ++ telah berakhir, dan masalah telah diselesaikan, sementara dalam bahasa seperti Jawa tidak pernah ada masalah seperti ini sejak awal.

Pendapat saya adalah bahwa jika Anda hanya menghindari menggunakan newkonstruktor, (seperti niat Anda untuk menggunakan Injeksi Ketergantungan,) Anda harus baik-baik saja. Saya menertawakan pernyataan seperti "logika kondisional atau looping dalam konstruktor adalah tanda peringatan cacat".

Selain semua itu, secara pribadi, saya akan mengambil logika parsing XML dari konstruktor, bukan karena jahat memiliki logika kompleks dalam konstruktor, tetapi karena itu baik untuk mengikuti prinsip "pemisahan keprihatinan". Jadi, saya akan memindahkan logika parsing XML ke beberapa kelas yang terpisah sama sekali, bukan ke beberapa metode statis milik ModelDefkelas Anda .

Amandemen

Saya kira bahwa jika Anda memiliki metode di luar ModelDefyang membuat ModelDefdari XML, Anda akan perlu membuat instantiate beberapa struktur data pohon sementara yang dinamis, mengisi itu dengan mem-parsing XML Anda, dan kemudian membuat ModelDefstruktur baru Anda lewat itu sebagai parameter konstruktor. Jadi, itu mungkin bisa dianggap sebagai aplikasi dari pola "Builder". Ada analogi yang sangat dekat antara apa yang ingin Anda lakukan dan String& StringBuilderpasangan. Namun, saya telah menemukan T&J ini yang tampaknya tidak setuju, karena alasan yang tidak jelas bagi saya: Stackoverflow - StringBuilder dan Builder Pattern . Jadi, untuk menghindari perdebatan panjang di sini tentang apakah StringBuildermenerapkan atau tidak "membangun" pola, saya akan mengatakan merasa bebas untuk terinspirasi oleh bagaimanaStrungBuilder bekerja dalam menghasilkan solusi yang sesuai dengan kebutuhan Anda, dan menundanya menyebutnya sebagai aplikasi dari pola "Builder" sampai detail kecil itu diselesaikan.

Lihat pertanyaan baru ini: Programmer SE: Apakah "StringBuilder" aplikasi Pola Desain Builder?

Mike Nakis
sumber
3
@RichardLevasseur Saya hanya mengingatnya sebagai topik yang menjadi perhatian dan perdebatan di antara para programmer C ++ di awal hingga pertengahan tahun sembilan puluhan. Jika Anda melihat posting ini: gotw.ca/gotw/066.htm Anda akan melihat bahwa itu cukup rumit, dan hal-hal yang cukup rumit cenderung kontroversial. Saya tidak tahu pasti, tetapi saya berpikir bahwa pada awal tahun sembilan puluhan bagian dari barang-barang itu bahkan belum distandarisasi. Tapi maaf, saya tidak bisa memberikan referensi yang bagus.
Mike Nakis
1
@ Gurtz Saya akan menganggap kelas seperti itu sebagai "utilitas xml" khusus aplikasi, karena format file xml (atau struktur dokumen) mungkin terikat dengan aplikasi tertentu yang sedang Anda kembangkan, terlepas dari segala kemungkinan untuk gunakan kembali "ModelDef" Anda.
Mike Nakis
1
@ Gurtz jadi, saya mungkin akan membuat mereka contoh metode dari kelas "Aplikasi" utama, atau jika itu terlalu merepotkan, maka metode statis beberapa kelas pembantu, dengan cara yang sangat mirip dengan apa yang disarankan Ross Patterson.
Mike Nakis
1
@Gurtz meminta maaf karena tidak secara khusus menangani pendekatan "pembangun" sebelumnya. Saya mengubah jawaban saya.
Mike Nakis
3
@ Gurtz Mungkin tapi di luar keingintahuan akademis, itu tidak masalah. Jangan terjebak dalam "pola anti-pola". Pola benar-benar hanya nama untuk menggambarkan teknik pengkodean yang umum / berguna bagi orang lain dengan mudah. Lakukan apa yang perlu Anda lakukan, tampar label di kemudian hari jika Anda perlu menjelaskannya. Tidak apa-apa untuk mengimplementasikan sesuatu yang "seperti pola pembangun, dengan cara, mungkin", selama kode Anda masuk akal. Masuk akal untuk fokus pada pola ketika mempelajari teknik baru, hanya saja jangan terjebak dalam pemikiran bahwa semua yang Anda lakukan pasti merupakan suatu pola bernama.
Jason C
9

Anda sudah memberikan alasan terbaik untuk tidak melakukan pekerjaan ini di ModelDefkonstruktor:

  1. Tidak ada yang "ringan" tentang penguraian dokumen XML ke pohon simpul.
  2. Tidak ada yang jelas tentang ModelDefyang mengatakan itu hanya dapat dibuat dari dokumen XML.

Kedengarannya seperti kelas Anda harus memiliki berbagai metode statis seperti ModelDef.FromXmlString(string xmlDocument), ModelDef.FromXmlDoc(XmlDoc parsedNodeTree), dll

Ross Patterson
sumber
Terima kasih balasannya! Mengenai saran metode statis. Apakah ini pabrik statis yang membuat turunan ModelDef (dari berbagai sumber xml)? Atau apakah mereka akan bertanggung jawab untuk memuat objek ModelDef yang sudah dibuat? Jika yang terakhir, saya akan khawatir tentang memiliki objek hanya sebagian diinisialisasi (karena ModelDef membutuhkan pohon simpul untuk sepenuhnya diinisialisasi). Pikiran?
Gurtz
3
Maafkan saya karena ikut campur, tapi ya, apa yang dimaksud Ross adalah metode pabrik statis yang mengembalikan contoh yang dibangun sepenuhnya. Prototipe lengkapnya akan seperti itu public static ModelDef createFromXmlString( string xmlDocument ). Ini adalah praktik yang cukup umum. Terkadang saya juga melakukannya. Saran saya agar Anda juga dapat melakukan konstruktor saja adalah tipe standar respons saya dalam situasi di mana saya menduga bahwa pendekatan alternatif dianggap "tidak halal" tanpa alasan yang kuat.
Mike Nakis
1
@ Mike-Nakis, terima kasih telah menjelaskan. Jadi dalam hal ini metode pabrik statis akan membangun pohon simpul, dan kemudian meneruskannya ke konstruktor (mungkin pribadi) dari ModelDef. Masuk akal. Terima kasih.
Gurtz
@Gurtz Persis.
Ross Patterson
5

Saya pernah mendengar "aturan" itu sebelumnya. Dalam pengalaman saya itu benar dan salah.

Dalam orientasi objek yang lebih "klasik" kita berbicara tentang objek yang merangkum keadaan dan perilaku. Dengan demikian konstruktor objek harus memastikan bahwa objek diinisialisasi ke keadaan yang valid (dan memberi sinyal kesalahan jika argumen yang diberikan tidak membuat objek valid). Memastikan bahwa suatu objek diinisialisasi ke keadaan yang valid tentu terdengar seperti pekerjaan nyata bagi saya. Dan ide ini memiliki kelebihan, jika Anda memiliki objek yang hanya memungkinkan inisialisasi ke status yang valid melalui konstruktor dan objek dengan benar merangkumnya sehingga setiap metode yang mengubah status juga memeriksa bahwa itu tidak mengubah keadaan menjadi sesuatu yang buruk. ... maka objek itu pada dasarnya menjamin bahwa itu "selalu valid". Itu adalah properti yang sangat bagus!

Jadi masalah umumnya datang ketika kita mencoba memecah semuanya menjadi beberapa bagian kecil untuk menguji dan mengejek barang. Karena jika suatu objek dienkapsulasi dengan benar maka Anda tidak dapat benar-benar masuk ke sana dan mengganti FooBarService dengan FooBarService yang diolok-olok dan Anda (mungkin) tidak bisa hanya mengubah nilai mau tak mau agar sesuai dengan pengujian Anda (atau, mungkin diperlukan lebih banyak kode daripada tugas sederhana).

Dengan demikian kita mendapatkan "aliran pemikiran" lainnya, yaitu SOLID. Dan dalam aliran pemikiran ini, kemungkinan besar lebih benar bahwa kita seharusnya tidak melakukan pekerjaan nyata dalam konstruktor. Kode SOLID seringkali (tetapi tidak selalu) lebih mudah untuk diuji. Tetapi juga bisa lebih sulit untuk dipikirkan. Kami memecah kode kami menjadi objek kecil dengan satu tanggung jawab, dan dengan demikian sebagian besar objek kami tidak lagi merangkum keadaan mereka (dan umumnya berisi keadaan atau perilaku). Kode validasi umumnya diekstraksi ke dalam kelas validator dan disimpan terpisah dari negara. Tetapi sekarang kita telah kehilangan kohesi, kita tidak bisa lagi memastikan bahwa objek kita valid ketika kita mendapatkannya dan sepenuhnyaTentu saja kita harus selalu memvalidasi bahwa prasyarat yang kita pikir miliki tentang objek itu benar sebelum kita mencoba melakukan sesuatu dengan objek tersebut. (Tentu saja, secara umum Anda melakukan validasi dalam satu lapisan dan kemudian menganggap bahwa objek tersebut valid di lapisan bawah.) Tetapi lebih mudah untuk menguji!

Jadi siapa yang benar?

Tidak ada yang benar-benar. Kedua aliran pemikiran memiliki kelebihan. Saat ini SOLID adalah semua kemarahan dan semua orang berbicara tentang SRP dan Open / Closed dan semua jazz itu. Tetapi hanya karena sesuatu itu populer tidak berarti bahwa itu adalah pilihan desain yang benar untuk setiap aplikasi. Jadi itu tergantung. Jika Anda bekerja di basis kode yang sangat mengikuti prinsip-prinsip SOLID maka ya, kerja nyata di konstruktor mungkin adalah ide yang buruk. Tetapi sebaliknya, lihat situasinya dan cobalah untuk menggunakan penilaian Anda. Sifat-sifat apa objek Anda keuntungan dari melakukan pekerjaan di constructor, apa sifat apakah itu kehilangan ? Seberapa cocokkah dengan keseluruhan arsitektur aplikasi Anda?

Pekerjaan nyata di konstruktor bukan antiputern, itu bisa sangat berlawanan bila digunakan di tempat yang benar. Tetapi harus didokumentasikan dengan jelas (bersama dengan pengecualian yang dapat dilemparkan, jika ada) dan seperti halnya dengan keputusan desain - harus sesuai dengan gaya umum yang digunakan dalam basis kode saat ini.

wasatz
sumber
Ini jawaban yang fantastis.
jrahhali
0

Ada masalah mendasar dengan aturan ini dan ini, apa yang merupakan "pekerjaan nyata"?

Anda dapat melihat dari artikel asli yang diposting dalam pertanyaan bahwa penulis mencoba untuk mendefinisikan apa "pekerjaan nyata" itu, tetapi sangat cacat. Agar suatu praktik menjadi baik, ia perlu prinsip yang jelas. Maksud saya, sehubungan dengan rekayasa perangkat lunak, gagasan tersebut harus portabel (agnostik untuk bahasa apa pun), diuji, dan terbukti. Sebagian besar dari apa yang diperdebatkan dalam artikel itu tidak sesuai dengan kriteria pertama itu. Berikut adalah beberapa indikator yang penulis sebutkan dalam artikel tentang apa yang merupakan "pekerjaan nyata" dan mengapa mereka bukan definisi yang buruk.

Penggunaan newkata kunci . Definisi itu secara mendasar cacat karena spesifik domain. Beberapa bahasa tidak menggunakan newkata kunci. Pada akhirnya apa yang dia isyaratkan adalah bahwa itu seharusnya tidak membangun objek lain. Namun dalam banyak bahasa bahkan nilai yang paling dasar adalah objek itu sendiri. Jadi nilai apa pun yang diberikan dalam konstruktor juga membangun objek baru. Itu membuat ide ini terbatas pada bahasa-bahasa tertentu, dan indikator yang buruk untuk apa yang merupakan "pekerjaan nyata".

Objek tidak sepenuhnya diinisialisasi setelah konstruktor selesai . Ini adalah aturan yang baik, tetapi juga bertentangan dengan beberapa aturan lain yang disebutkan dalam artikel itu. Sebuah contoh yang baik tentang bagaimana hal itu dapat bertentangan dengan yang lain disebutkan pertanyaan yang membawa saya ke sini. Dalam pertanyaan itu seseorang khawatir tentang menggunakan sortmetode dalam konstruktor dalam apa yang tampaknya JavaScript karena prinsip ini. Dalam contoh ini orang tersebut membuat objek yang mewakili daftar objek lain yang diurutkan. Untuk tujuan diskusi, bayangkan bahwa kami memiliki daftar objek yang tidak disortir dan kami membutuhkan objek baru untuk mewakili daftar yang diurutkan. Kami membutuhkan objek baru ini karena beberapa bagian dari perangkat lunak kami mengharapkan daftar yang diurutkan, dan mari kita sebut objek iniSortedList. Objek baru ini menerima daftar yang tidak disortir dan objek yang dihasilkan harus mewakili daftar objek yang sekarang diurutkan. Jika kita mengikuti aturan lain yang disebutkan dalam dokumen itu, yaitu tidak ada metode panggilan statis, tidak ada struktur aliran kontrol, tidak lebih dari penugasan, maka objek yang dihasilkan tidak akan dibangun dalam keadaan valid melanggar aturan lain yang sepenuhnya diinisialisasi setelah konstruktor selesai. Untuk memperbaikinya kita perlu melakukan beberapa pekerjaan dasar untuk membuat daftar yang tidak disortir diurutkan dalam konstruktor. Melakukan ini akan melanggar 3 aturan lainnya, membuat aturan lain tidak relevan.

Pada akhirnya, aturan untuk tidak melakukan "pekerjaan nyata" dalam sebuah konstruktor ini tidak jelas dan cacat. Mencoba mendefinisikan apa "pekerjaan nyata" adalah latihan yang sia-sia. Aturan terbaik dalam artikel itu adalah bahwa ketika konstruktor selesai harus sepenuhnya diinisialisasi. Ada sejumlah praktik terbaik lainnya yang akan membatasi seberapa banyak pekerjaan yang dilakukan dalam konstruktor. Sebagian besar dari ini dapat diringkas dalam prinsip-prinsip SOLID, dan prinsip-prinsip yang sama tidak akan mencegah Anda melakukan pekerjaan di konstruktor.

PS. Saya merasa wajib untuk mengatakan bahwa sementara saya menyatakan di sini bahwa tidak ada yang salah dengan melakukan beberapa pekerjaan di konstruktor, itu bukan tempat untuk melakukan banyak pekerjaan juga. SRP akan menyarankan bahwa seorang konstruktor harus melakukan pekerjaan yang cukup untuk membuatnya valid. Jika konstruktor Anda memiliki terlalu banyak baris kode (sangat subyektif yang saya tahu) maka mungkin melanggar prinsip ini, dan mungkin dapat dipecah menjadi metode dan objek yang lebih kecil dan lebih baik.

zquintana
sumber