Bagaimana cara memilih antara Tell don't Ask dan Command Query Separation?

25

Prinsip Tell Don't Don't mengatakan:

Anda harus berusaha memberi tahu objek apa yang Anda ingin mereka lakukan; jangan bertanya kepada mereka tentang keadaan mereka, membuat keputusan, dan kemudian memberi tahu mereka apa yang harus dilakukan.

Masalahnya adalah bahwa, sebagai penelepon, Anda tidak boleh membuat keputusan berdasarkan keadaan objek yang dipanggil yang mengakibatkan Anda kemudian mengubah keadaan objek. Logika yang Anda laksanakan mungkin tanggung jawab objek yang disebut, bukan milik Anda. Bagi Anda untuk membuat keputusan di luar objek melanggar enkapsulasi.

Contoh sederhana "Katakan, jangan Tanya" adalah

Widget w = ...;
if (w.getParent() != null) {
  Panel parent = w.getParent();
  parent.remove(w);
}

dan versi kirim adalah ...

Widget w = ...;
w.removeFromParent();

Tetapi bagaimana jika saya perlu tahu hasil dari metode removeFromParent? Reaksi pertama saya adalah hanya mengubah removeFromParent untuk mengembalikan boolean yang menunjukkan apakah induk telah dihapus atau tidak.

Tapi kemudian saya menemukan Pola Pemisahan Permintaan Perintah yang mengatakan TIDAK untuk melakukan ini.

Ini menyatakan bahwa setiap metode harus berupa perintah yang melakukan tindakan, atau permintaan yang mengembalikan data ke pemanggil, tetapi tidak keduanya. Dengan kata lain, mengajukan pertanyaan tidak boleh mengubah jawabannya. Lebih formal, metode harus mengembalikan nilai hanya jika mereka secara transparan transparan dan karenanya tidak memiliki efek samping.

Apakah keduanya benar-benar berselisih satu sama lain dan bagaimana saya memilih di antara keduanya? Apakah saya setuju dengan Programmer Pragmatis atau Bertrand Meyer dalam hal ini?

Dakotah Utara
sumber
1
apa yang akan kamu lakukan dengan boolean?
David
1
Sepertinya Anda menyelam terlalu jauh ke pola pengkodean tanpa menyeimbangkan kegunaan
Izkata
Kembali boolean ... ini adalah contoh yang mudah untuk dikalahkan tetapi mirip dengan operasi tulis di bawah, tujuannya adalah untuk itu menjadi status operasi.
Dakotah North
2
Maksud saya adalah Anda tidak harus fokus pada "mengembalikan sesuatu" tetapi lebih pada apa yang ingin Anda lakukan dengannya. Seperti yang saya katakan di komentar lain, jika Anda fokus pada kegagalan, maka gunakan pengecualian, jika Anda ingin melakukan sesuatu ketika operasi selesai, maka gunakan panggilan balik atau acara, jika Anda ingin mencatat apa yang terjadi itu adalah tanggung jawab Widget ... Itu mengapa saya meminta kebutuhan Anda. Jika Anda tidak dapat menemukan contoh konkret di mana Anda harus mengembalikan sesuatu, mungkin itu berarti Anda tidak harus memilih di antara keduanya.
David
1
Command Query Separation adalah prinsip, bukan pola. Pola adalah sesuatu yang dapat Anda gunakan untuk menyelesaikan masalah. Prinsip adalah apa yang Anda ikuti untuk tidak menciptakan masalah.
candied_orange

Jawaban:

25

Sebenarnya masalah contoh Anda sudah menggambarkan kurangnya dekomposisi.

Mari kita ubah sedikit:

Book b = ...;
if (b.getShelf() != null) 
    b.getShelf().remove(b);

Ini tidak terlalu berbeda, tetapi membuat cacatnya lebih jelas: Mengapa buku ini tahu tentang raknya? Sederhananya, seharusnya tidak. Ini memperkenalkan ketergantungan buku di rak (yang tidak masuk akal) dan membuat referensi melingkar. Itu semua buruk.

Demikian juga, tidak perlu bagi widget untuk mengetahui orang tuanya. Anda akan mengatakan: "Ok, tapi widget membutuhkan orangtuanya untuk tata letak yang benar, dll." dan di bawah tenda widget mengetahui orang tuanya dan meminta metrik untuk menghitung metrik sendiri berdasarkan mereka dll. Menurut mengatakan, jangan bertanya ini salah. Orang tua harus memberi tahu semua anaknya untuk memberikan, menyampaikan semua informasi yang diperlukan sebagai argumen. Dengan cara ini seseorang dapat dengan mudah memiliki widget yang sama di dua orang tua (apakah ini benar-benar masuk akal atau tidak).

Untuk kembali ke contoh buku - masukkan pustakawan:

Librarian l = ...;
Book b = ...;
l.findShelf(b).remove(b);

Tolong dimengerti, bahwa kami tidak lagi meminta dalam arti memberi tahu, jangan bertanya .

Dalam versi pertama, rak adalah milik buku dan Anda memintanya. Dalam versi kedua, kami tidak membuat asumsi tentang buku itu. Yang kami tahu adalah bahwa pustakawan dapat memberi tahu kami rak yang berisi buku itu. Agaknya dia bergantung pada semacam tabel pencarian untuk melakukannya, tetapi kita tidak tahu pasti (dia juga bisa memeriksa semua buku di setiap rak) dan sebenarnya kita tidak ingin tahu .
Kami tidak bergantung pada apakah atau bagaimana respons pustakawan digabungkan dengan statusnya atau objek lain yang bergantung padanya. Kami memberi tahu pustakawan untuk menemukan rak.
Dalam skenario pertama, kami secara langsung menyandikan hubungan antara buku dan rak sebagai bagian dari kondisi buku dan mengambil status ini secara langsung. Ini sangat rapuh, karena kami juga membuat asumsi bahwa rak yang diberikan kembali oleh buku berisi buku (ini adalah kendala yang harus kami pastikan, kalau tidak, kami mungkin tidak dapat removemengambil buku dari rak yang sebenarnya ada di dalamnya).

Dengan diperkenalkannya pustakawan, kami memodelkan hubungan ini secara terpisah, sehingga mencapai pemisahan perhatian.

back2dos
sumber
Saya pikir ini adalah poin yang sangat valid, tetapi tidak menjawab pertanyaan sama sekali. Dalam versi Anda pertanyaannya adalah, haruskah metode hapus (Buku b) di kelas Shelf memiliki nilai balik?
scarfridge
1
@scarfridge: Pada intinya, katakan, jangan tanya adalah akibat wajar dari pemisahan kekhawatiran yang tepat. Jika Anda memikirkannya, saya menjawab pertanyaan itu. Setidaknya saya akan berpikir begitu;)
back2dos
1
@ scarfridge Alih-alih nilai balik, seseorang dapat melewati fungsi / delegasi yang dipanggil pada kegagalan. Jika berhasil, Anda selesai, bukan?
Diberkatilah Yahu
2
@BlessYahu Itu sepertinya terlalu direkayasa untuk saya. Saya pribadi merasa bahwa pemisahan Command-query lebih ideal di dunia nyata (multi-threaded). IMHO tidak apa-apa untuk metode dengan efek samping untuk mengembalikan nilai selama nama metode jelas menunjukkan, bahwa itu akan mengubah keadaan objek. Pertimbangkan metode pop () dari stack. Tetapi metode dengan nama permintaan tidak boleh memiliki efek samping.
scarfridge
13

Jika Anda perlu tahu hasilnya, maka Anda lakukan; itu kebutuhan anda .

Ungkapan "metode harus mengembalikan nilai hanya jika mereka secara transparan transparan dan karenanya tidak memiliki efek samping" adalah panduan yang baik untuk diikuti (terutama jika Anda menulis program Anda dengan gaya fungsional , untuk konkurensi atau alasan lain), tetapi itu tidak mutlak.

Misalnya, Anda mungkin perlu mengetahui hasil operasi penulisan file (benar atau salah). Itu adalah contoh metode yang mengembalikan nilai, tetapi selalu menghasilkan efek samping; tidak ada jalan lain untuk itu.

Untuk memenuhi Pemisahan Command / Query, Anda harus melakukan operasi file dengan satu metode, dan kemudian memeriksa hasilnya dengan metode lain, teknik yang tidak diinginkan karena memisahkan hasil dari metode yang menyebabkan hasil. Bagaimana jika sesuatu terjadi pada keadaan objek Anda antara panggilan file dan pemeriksaan status?

Intinya adalah ini: jika Anda menggunakan hasil pemanggilan metode untuk membuat keputusan di tempat lain dalam program ini, maka Anda tidak melanggar Tell Don't Ask. Jika, di sisi lain, Anda membuat keputusan untuk objek berdasarkan panggilan metode ke objek itu, maka Anda harus memindahkan keputusan itu ke objek itu sendiri untuk menjaga enkapsulasi.

Ini sama sekali tidak bertentangan dengan Command Query Separation; pada kenyataannya, itu lebih lanjut menegakkannya, karena tidak ada lagi kebutuhan untuk mengekspos metode eksternal untuk keperluan status.

Robert Harvey
sumber
1
Orang dapat berargumen bahwa dalam contoh Anda, jika operasi "tulis" gagal, pengecualian harus dilemparkan, sehingga tidak perlu untuk metode "dapatkan status".
David
@ David: Kemudian kembali ke contoh OP, yang akan mengembalikan true jika perubahan benar-benar terjadi, false jika tidak. Saya ragu Anda ingin melemparkan pengecualian di sana.
Robert Harvey 8/11
Bahkan contoh OP tidak terasa cocok untuk saya. Entah Anda ingin diperhatikan jika penghapusan tidak memungkinkan, dalam hal ini Anda akan menggunakan pengecualian atau Anda ingin diperhatikan ketika widget benar-benar dihapus dalam hal ini Anda bisa menggunakan mekanisme acara atau panggilan balik. Saya hanya tidak melihat contoh nyata di mana Anda tidak ingin menghormati "Pola Pemisahan Permintaan Perintah"
David
@ David: Saya telah mengedit jawaban saya untuk menjelaskan.
Robert Harvey 8-11
Bukan untuk menempatkan titik terlalu baik di atasnya, tetapi seluruh ide di balik pemisahan permintaan-perintah adalah bahwa siapa pun yang mengeluarkan perintah untuk menulis file tidak peduli tentang hasilnya - setidaknya, tidak dalam arti tertentu (pengguna mungkin nanti tarik daftar semua operasi file terbaru, tetapi itu tidak memiliki korelasi dengan perintah awal). Jika Anda perlu mengambil tindakan setelah perintah selesai, terlepas dari apakah Anda peduli atau tidak dengan hasilnya, cara yang benar untuk melakukannya adalah dengan model pemrograman asinkron menggunakan peristiwa yang (mungkin) berisi detail tentang perintah asli dan / atau perintahnya. hasil.
Aaronaught
2

Saya pikir Anda harus mengikuti insting awal Anda. Kadang-kadang desain harus sengaja berbahaya, untuk memperkenalkan kompleksitas segera ketika Anda berkomunikasi dengan objek sehingga Anda langsung menanganinya dengan benar, alih-alih mencoba untuk menanganinya pada objek itu sendiri dan menyembunyikan semua detail berdarah dan membuatnya sulit untuk dibersihkan. ubah asumsi yang mendasarinya.

Anda harus mendapatkan perasaan tenggelam jika suatu objek menawarkan Anda menerapkan setara opendan closeperilaku dan Anda segera mulai memahami apa yang Anda hadapi ketika Anda melihat nilai pengembalian boolean untuk sesuatu yang Anda pikir akan menjadi tugas atom sederhana.

Bagaimana Anda menangani nanti adalah urusan Anda. Anda dapat membuat abstraksi di atasnya, jenis widget removeFromParent(), tetapi Anda harus selalu memiliki fallback tingkat rendah, jika tidak, Anda mungkin membuat asumsi prematur.

Jangan mencoba menyederhanakan semuanya. Tidak ada yang mengecewakan ketika Anda mengandalkan sesuatu yang tampak elegan dan sangat polos, hanya untuk menyadari bahwa itu adalah horor sejati di saat-saat terburuk.

Filip Dupanović
sumber
2

Apa yang Anda gambarkan adalah "pengecualian" yang dikenal dengan prinsip Pemisahan Command-Query.

Dalam artikel ini Martin Fowler menjelaskan bagaimana ia akan mengancam pengecualian semacam itu.

Meyer suka menggunakan pemisahan perintah-kueri sepenuhnya, tetapi ada pengecualian. Memunculkan tumpukan adalah contoh yang bagus dari kueri yang mengubah keadaan. Meyer dengan benar mengatakan bahwa Anda dapat menghindari metode ini, tetapi ini adalah ungkapan yang berguna. Jadi saya lebih suka mengikuti prinsip ini ketika saya bisa, tetapi saya siap untuk melanggarnya untuk mendapatkan pop saya.

Dalam contoh Anda, saya akan mempertimbangkan pengecualian yang sama.

elviejo79
sumber
0

Pemisahan Command-Query luar biasa mudah disalahpahami.

Jika saya memberi tahu Anda di sistem saya ada perintah yang juga mengembalikan nilai dari permintaan dan Anda berkata "Ha! Anda melanggar!" Anda sedang melompat pistol.

Tidak, bukan itu yang dilarang.

Apa yang dilarang adalah ketika perintah itu adalah cara HANYA untuk membuat permintaan itu. Tidak. Saya tidak perlu mengubah status untuk bertanya. Itu tidak berarti saya harus menutup mata setiap kali saya mengubah keadaan.

Tidak masalah jika saya lakukan karena saya hanya akan membukanya lagi. Satu-satunya keuntungan dari reaksi berlebihan ini adalah memastikan desainer tidak malas dan lupa untuk memasukkan kueri yang tidak berubah.

Arti sebenarnya sangat halus sehingga hanya lebih mudah bagi orang-orang malas untuk mengatakan bahwa penghuni harus kembali batal. Ini membuatnya lebih mudah untuk memastikan bahwa Anda tidak melanggar aturan yang sebenarnya tetapi itu adalah reaksi berlebihan yang bisa menjadi rasa sakit yang nyata.

Antarmuka yang lancar dan iDSL "melanggar" reaksi berlebihan ini sepanjang waktu. Ada banyak kekuatan yang diabaikan jika Anda bereaksi berlebihan.

Anda dapat berdebat bahwa suatu perintah hanya boleh melakukan satu hal dan permintaan seharusnya hanya melakukan satu hal. Dan dalam banyak kasus itu adalah poin yang bagus. Siapa bilang hanya ada satu permintaan yang harus mengikuti perintah? Baik tapi itu bukan pemisahan perintah-permintaan. Itulah prinsip tanggung jawab tunggal.

Tampak seperti ini dan tumpukan pop bukan pengecualian aneh. Ini satu tanggung jawab. Pop hanya melanggar pemisahan kueri perintah jika Anda lupa untuk mengintip.

candied_orange
sumber