Pola untuk kelas yang hanya melakukan satu hal

24

Katakanlah saya memiliki prosedur yang berfungsi :

void doStuff(initalParams) {
    ...
}

Sekarang saya menemukan bahwa "melakukan hal-hal" adalah operasi compex. Prosedurnya menjadi besar, saya membaginya menjadi beberapa prosedur yang lebih kecil dan segera saya menyadari bahwa memiliki semacam keadaan akan berguna saat melakukan hal-hal, sehingga saya perlu melewati lebih sedikit parameter antara prosedur kecil. Jadi, saya memfaktorkannya ke dalam kelasnya sendiri:

class StuffDoer {
    private someInternalState;

    public Start(initalParams) {
        ...
    }

    // some private helper procedures here
    ...
}

Dan saya menyebutnya seperti ini:

new StuffDoer().Start(initialParams);

atau seperti ini:

new StuffDoer(initialParams).Start();

Dan inilah yang terasa salah. Saat menggunakan .NET atau Java API, saya selalu tidak pernah menelepon new SomeApiClass().Start(...);, yang membuat saya curiga saya salah melakukannya. Tentu, saya dapat menjadikan konstruktor StuffDoer pribadi dan menambahkan metode pembantu statis:

public static DoStuff(initalParams) {
    new StuffDoer().Start(initialParams);
}

Tapi kemudian saya memiliki kelas yang antarmuka eksternal hanya terdiri dari satu metode statis, yang juga terasa aneh.

Maka pertanyaan saya: Apakah ada pola yang mapan untuk jenis kelas ini itu

  • hanya memiliki satu titik masuk dan
  • tidak memiliki status "dikenali secara eksternal", yaitu, keadaan instance hanya diperlukan selama pelaksanaan satu titik masuk itu?
Heinzi
sumber
Saya telah dikenal melakukan hal-hal seperti bool arrayContainsSomestring = new List<string>(stringArray).Contains("somestring");ketika semua yang saya pedulikan adalah bahwa informasi tertentu dan metode ekstensi LINQ tidak tersedia. Berfungsi dengan baik, dan pas di dalam if()kondisi tanpa harus melompat melalui lingkaran. Tentu saja, Anda ingin bahasa yang dikumpulkan sampah jika Anda menulis kode seperti itu.
CVn
Jika itu bahasa-agnostik , mengapa menambahkan java dan c # juga? :)
Steven Jeuris
Saya ingin tahu, apa fungsi dari 'satu metode' ini? Ini akan sangat membantu dalam menentukan apa pendekatan terbaik dalam skenario spesifik, tetapi pertanyaan yang menarik tetap!
Steven Jeuris
@StevenJeuris: Saya menemukan masalah ini dalam berbagai situasi, tetapi dalam kasus saat ini merupakan metode yang mengimpor data ke dalam basis data lokal (memuatnya dari server web, melakukan beberapa manipulasi dan menyimpannya dalam basis data).
Heinzi
1
Jika Anda selalu memanggil metode ketika Anda membuat instance, mengapa tidak membuatnya sebagai logika inisialisasi di dalam konstruktor?
NoChance

Jawaban:

29

Ada pola yang disebut Metode Objek di mana Anda memfaktorkan metode tunggal yang besar dengan banyak variabel / argumen sementara ke dalam kelas yang terpisah. Anda melakukan ini daripada hanya mengekstraksi bagian-bagian metode ke dalam metode terpisah karena mereka memerlukan akses ke keadaan lokal (parameter dan variabel sementara) dan Anda tidak dapat berbagi keadaan lokal menggunakan variabel contoh karena mereka akan bersifat lokal untuk metode itu (dan metode diekstraksi dari itu) hanya dan tidak akan digunakan oleh sisa objek.

Jadi alih-alih, metode menjadi kelasnya sendiri, parameter dan variabel sementara menjadi variabel instan dari kelas baru ini, dan kemudian metode tersebut dipecah menjadi metode yang lebih kecil dari kelas baru. Kelas yang dihasilkan biasanya hanya memiliki metode instance publik tunggal yang melakukan tugas yang dienkapsulasi oleh kelas.

segera
sumber
1
Ini terdengar persis seperti apa yang saya lakukan. Terima kasih, setidaknya saya punya nama untuk itu sekarang. :-)
Heinzi
2
Tautan yang sangat relevan! Baunya seperti anti-pola bagi saya. Jika metode Anda adalah bahwa lama, mungkin bagian itu bisa diambil di reuseable kelas atau umumnya refactored dengan cara yang lebih baik? Saya juga belum pernah mendengar tentang pola ini sebelumnya, saya juga tidak dapat menemukan banyak sumber daya lain yang membuat saya percaya itu umumnya tidak banyak digunakan.
Steven Jeuris
1
@ SevenJeuris Saya tidak berpikir itu anti-pola. Suatu anti-pola akan mengacaukan metode yang dire-refraktasi dengan parameter lot alih-alih menyimpan keadaan ini yang umum di antara metode-metode ini dalam variabel instan.
Alfredo Osorio
@ AlfredoOsorio: Ya, itu mengacaukan metode refactored dengan banyak parameter. Mereka tinggal di objek, jadi lebih baik daripada melewatkan semuanya, tetapi lebih buruk daripada refactoring ke komponen yang dapat digunakan kembali yang hanya memerlukan subset parameter yang terbatas masing-masing.
Jan Hudec
13

Saya akan mengatakan bahwa kelas yang dimaksud (menggunakan titik masuk tunggal) dirancang dengan baik. Sangat mudah digunakan dan diperluas. Cukup PADAT.

Hal yang sama untuk no "externally recognizable" state. Enkapsulasi yang baik.

Saya tidak melihat masalah dengan kode seperti:

var doer = new StuffDoer(initialParams);
var result = doer.Calculate(extraParams);
jgauffin
sumber
1
Dan tentu saja, jika Anda tidak perlu menggunakan yang sama doerlagi, itu berkurang menjadi var result = new StuffDoer(initialParams).Calculate(extraParams);.
CVn
Hitung harus diubah namanya menjadi doStuff!
shabunc
1
Saya akan menambahkan bahwa jika Anda tidak ingin menginisialisasi (baru) kelas Anda di mana Anda menggunakannya (mungkin Anda ingin menggunakan Pabrik), Anda memiliki pilihan untuk membuat objek di tempat lain dalam logika bisnis Anda dan daripada meneruskan parameter secara langsung ke metode publik tunggal yang Anda miliki. Pilihan lainnya adalah menggunakan Factory dan memilikinya membuat metode yang mendapatkan parameter dan membuat Anda objek dengan semua params melalui konstruktor itu. Daripada Anda memanggil metode publik Anda tanpa params dari mana pun Anda inginkan.
Patkos Csaba
2
Jawaban ini terlalu sederhana. Apakah StringComparerkelas yang Anda gunakan new StringComparer().Compare( "bleh", "blah" )juga dirancang? Bagaimana dengan menciptakan keadaan ketika sama sekali tidak perlu? Oke perilakunya dienkapsulasi dengan baik (well, setidaknya untuk pengguna kelas, lebih banyak tentang itu dalam jawaban saya ), tapi itu tidak menjadikannya 'desain yang bagus' secara default. Sebagai contoh 'hasil pelaku' Anda. Ini hanya masuk akal jika Anda akan menggunakan doerterpisah dari result.
Steven Jeuris 8-12
1
Itu bukan salah satu alasan untuk ada, itu one reason to change. Perbedaannya mungkin tampak halus. Anda harus mengerti apa artinya itu. Itu tidak akan pernah membuat satu kelas metode
jgauffin
8

Dari sudut pandang prinsip SOLID , jawaban jgauffin masuk akal. Namun, Anda jangan lupa tentang prinsip-prinsip desain umum, misalnya menyembunyikan informasi .

Saya melihat beberapa masalah dengan pendekatan yang diberikan:

  • Seperti yang Anda tunjukkan pada diri Anda sendiri, orang tidak berharap untuk menggunakan kata kunci 'baru', ketika objek dibuat tidak mengelola keadaan apa pun . Desain Anda mencerminkan niatnya. Orang-orang yang menggunakan kelas Anda mungkin menjadi bingung tentang apa yang diaturnya, dan apakah panggilan selanjutnya ke metode ini dapat menghasilkan perilaku yang berbeda.
  • Dari perspektif orang yang menggunakan kelas keadaan batinnya tersembunyi dengan baik, tetapi ketika ingin membuat modifikasi ke kelas atau hanya memahaminya, Anda membuat hal-hal menjadi lebih kompleks. Saya sudah menulis banyak tentang masalah yang saya lihat dengan metode pemisahan hanya untuk membuatnya lebih kecil , terutama ketika memindahkan status ke ruang kelas. Anda memodifikasi cara API Anda harus digunakan hanya untuk memiliki fungsi yang lebih kecil!Bahwa menurut saya sudah pasti terlalu jauh.

Beberapa referensi terkait

Mungkin argumen utama terletak pada sejauh mana merentangkan Prinsip Tanggung Jawab Tunggal . "Jika Anda membawanya ke ekstrem itu dan membangun kelas yang memiliki satu alasan untuk ada, Anda mungkin berakhir dengan hanya satu metode per kelas. Ini akan menyebabkan bentangan besar kelas bahkan untuk proses yang paling sederhana, menyebabkan sistem menjadi sulit dimengerti dan sulit diubah. "

Referensi lain yang relevan terkait dengan topik ini: "Bagi program Anda menjadi metode yang melakukan satu tugas yang dapat diidentifikasi. Simpan semua operasi dalam metode pada tingkat abstraksi yang sama ." - Kent Beck Key di sini adalah "tingkat abstraksi yang sama". Itu tidak berarti "satu hal", seperti yang sering ditafsirkan. Level abstraksi ini sepenuhnya tergantung pada konteksnya yang Anda rancang.

Jadi apa pendekatan yang tepat?

Tanpa mengetahui case konkret Anda, sulit untuk mengatakannya. Ada skenario di mana saya kadang-kadang (tidak sering) menggunakan pendekatan serupa. Ketika saya ingin memproses dataset, tanpa ingin membuat fungsionalitas ini tersedia untuk seluruh ruang kelas. Saya menulis posting blog tentang itu, bagaimana lambdas dapat lebih meningkatkan enkapsulasi . Saya juga memulai pertanyaan tentang topik di sini di Programmer . Berikut ini adalah contoh terbaru di mana saya menggunakan teknik ini.

new TupleList<Key, int>
{
    { Key.NumPad1, 1 },
            ...
    { Key.NumPad3, 16 },
    { Key.NumPad4, 17 },
}
    .ForEach( t =>
    {
        var trigger = new IC.Trigger.EventTrigger(
                        new KeyInputCondition( t.Item1, KeyInputCondition.KeyState.Down ) );
        trigger.ConditionsMet += () => AddMarker( t.Item2 );
        _inputController.AddTrigger( trigger );
    } );

Karena kode sangat 'lokal' dalam ForEach tidak digunakan kembali di tempat lain, saya dapat menyimpannya di lokasi yang tepat di mana ia relevan. Menguraikan kode sedemikian rupa sehingga kode yang bergantung satu sama lain sangat dikelompokkan bersama membuatnya lebih mudah dibaca menurut pendapat saya.

Alternatif yang mungkin

  • Dalam C # Anda bisa menggunakan metode ekstensi sebagai gantinya. Jadi operasikan argumen secara langsung yang Anda berikan ke metode 'satu hal' ini.
  • Lihat apakah fungsi ini sebenarnya bukan milik kelas lain.
  • Jadikan fungsi statis di kelas statis . Ini kemungkinan besar adalah pendekatan yang paling tepat, seperti juga tercermin dalam API umum yang Anda tuju.
Steven Jeuris
sumber
Saya menemukan kemungkinan nama untuk pola-anti ini sebagaimana diuraikan dalam jawaban lain, Poltergeists .
Steven Jeuris
4

Saya akan mengatakan itu cukup bagus, tetapi API Anda masih harus menjadi metode statis pada kelas statis, karena itulah yang diharapkan pengguna. Fakta bahwa metode statis digunakan newuntuk membuat objek pembantu Anda dan melakukan pekerjaan adalah detail implementasi yang harus disembunyikan dari siapa pun yang memanggilnya.

Scott Whitlock
sumber
Wow! Anda berhasil mengatasinya jauh lebih singkat daripada saya. :) Terima kasih. (tentu saja saya melangkah lebih jauh di mana kita mungkin tidak setuju, tapi saya setuju dengan premis umum)
Steven Jeuris
2
new StuffDoer().Start(initialParams);

Ada masalah dengan pengembang yang tidak mengerti yang dapat menggunakan instance bersama dan menggunakannya

  1. beberapa kali (ok jika eksekusi sebelumnya (mungkin sebagian) tidak mengacaukan eksekusi selanjutnya)
  2. dari banyak utas (tidak OK, Anda secara eksplisit mengatakan itu memiliki keadaan internal).

Jadi ini membutuhkan dokumentasi eksplisit bahwa itu bukan thread aman dan jika ringan (membuatnya cepat, tidak mengikat sumber daya eksternal atau banyak memori) bahwa itu OK untuk instantiate dengan cepat.

Menyembunyikannya dalam metode statis membantu dengan ini, karena menggunakan kembali contoh tidak pernah terjadi.

Jika memiliki inisialisasi yang mahal, itu bisa (tidak selalu) bermanfaat untuk mempersiapkan inisialisasi sekali dan menggunakannya beberapa kali dengan membuat kelas lain yang hanya akan berisi status dan membuatnya dapat dikloning. Inisialisasi akan membuat status yang akan disimpan doer. start()akan mengkloningnya dan meneruskannya ke metode internal.

Ini juga akan memungkinkan untuk hal-hal lain seperti kondisi eksekusi parsial yang berkelanjutan. (Jika perlu waktu lama dan faktor-faktor eksternal, misalnya kegagalan pasokan listrik, dapat mengganggu pelaksanaan.) Tetapi hal-hal mewah tambahan ini biasanya tidak diperlukan, jadi tidak sepadan.

pengguna470365
sumber
Poin bagus. Semoga Anda tidak keberatan dengan pembersihan.
Steven Jeuris
2

Selain jawaban saya yang lebih terperinci dan dipersonalisasi , saya merasa pengamatan berikut ini layak mendapat jawaban terpisah.

Ada indikasi bahwa Anda mungkin mengikuti pola anti-Poltergeists .

Poltergeist adalah kelas dengan peran yang sangat terbatas dan siklus hidup yang efektif. Mereka sering memulai proses untuk objek lain. Solusi refactored mencakup realokasi tanggung jawab ke objek berumur panjang yang menghilangkan Poltergeists.

Gejala dan Konsekuensi

  • Jalur navigasi berlebihan.
  • Asosiasi sementara.
  • Kelas tanpa kewarganegaraan.
  • Obyek dan kelas sementara, berdurasi pendek.
  • Kelas operasi tunggal yang hanya ada untuk "benih" atau "meminta" kelas lain melalui asosiasi sementara.
  • Kelas dengan nama operasi "seperti kontrol" seperti start_process_alpha.

Solusi Refactored

Ghostbusters memecahkan Poltergeists dengan menghapusnya dari hirarki kelas sekaligus. Namun setelah dihapus, fungsi yang "disediakan" oleh poltergeist harus diganti. Ini mudah dengan penyesuaian sederhana untuk memperbaiki arsitektur.

Steven Jeuris
sumber