Saya telah melihat sejarah beberapa proyek pustaka kelas С dan Java pada GitHub dan CodePlex, dan saya melihat tren beralih ke kelas pabrik sebagai lawan instantiasi objek langsung.
Mengapa saya harus menggunakan kelas pabrik secara ekstensif? Saya memiliki perpustakaan yang cukup bagus, di mana objek dibuat dengan cara kuno - dengan memanggil konstruktor publik kelas. Dalam komit terakhir, penulis dengan cepat mengubah semua konstruktor publik dari ribuan kelas menjadi internal, dan juga menciptakan satu kelas pabrik besar dengan ribuan CreateXXX
metode statis yang baru saja mengembalikan objek baru dengan memanggil konstruktor internal kelas. API proyek eksternal rusak, dilakukan dengan baik.
Mengapa perubahan seperti itu bermanfaat? Apa gunanya refactoring dengan cara ini? Apa manfaat mengganti panggilan ke konstruktor kelas publik dengan panggilan metode pabrik statis?
Kapan saya harus menggunakan konstruktor publik, dan kapan saya harus menggunakan pabrik?
Jawaban:
Kelas pabrik sering diterapkan karena memungkinkan proyek untuk mengikuti prinsip-prinsip SOLID lebih dekat. Secara khusus, prinsip pemisahan segregasi dan dependensi.
Pabrik dan antarmuka memungkinkan fleksibilitas jangka panjang yang jauh lebih banyak. Ini memungkinkan desain yang lebih terpisah - dan karenanya lebih dapat diuji -. Berikut adalah daftar yang tidak lengkap tentang mengapa Anda mungkin menempuh jalan ini:
Pertimbangkan situasi ini.
Majelis A (-> berarti tergantung pada):
Saya ingin memindahkan Kelas B ke Majelis B, yang bergantung pada Majelis A. Dengan dependensi konkret ini saya harus memindahkan sebagian besar seluruh hierarki kelas saya. Jika saya menggunakan antarmuka, saya bisa menghindari banyak rasa sakit.
Majelis A:
Sekarang saya bisa memindahkan kelas B ke perakitan B tanpa rasa sakit sama sekali. Itu masih tergantung pada antarmuka dalam perakitan A.
Menggunakan wadah IoC untuk menyelesaikan ketergantungan Anda memungkinkan Anda lebih fleksibel. Tidak perlu memperbarui setiap panggilan ke konstruktor setiap kali Anda mengubah dependensi kelas.
Mengikuti prinsip pemisahan antarmuka dan prinsip inversi dependensi memungkinkan kita untuk membangun aplikasi yang sangat fleksibel dan dipisahkan. Setelah Anda mengerjakan salah satu dari jenis aplikasi ini, Anda tidak akan pernah lagi ingin kembali menggunakan
new
kata kunci.sumber
Seperti kata whatsisname , saya percaya ini adalah kasus desain perangkat lunak pemujaan kargo . Pabrik, terutama jenis abstrak, hanya dapat digunakan ketika modul Anda membuat beberapa instance kelas dan Anda ingin memberi pengguna kemampuan modul ini untuk menentukan jenis yang akan dibuat. Persyaratan ini sebenarnya sangat jarang, karena sebagian besar waktu Anda hanya memerlukan satu instance dan Anda dapat langsung melewati instance itu alih-alih membuat pabrik yang eksplisit.
Masalahnya adalah, pabrik (dan lajang) sangat mudah diimplementasikan dan oleh karena itu banyak orang menggunakannya, bahkan di tempat-tempat di mana mereka tidak perlu. Jadi ketika programmer berpikir "Pola desain apa yang harus saya gunakan dalam kode ini?" Pabrik adalah yang pertama muncul di benaknya.
Banyak pabrik diciptakan karena "Mungkin, suatu hari, saya perlu membuat kelas-kelas itu secara berbeda" dalam pikiran. Yang jelas merupakan pelanggaran terhadap YAGNI .
Dan pabrik menjadi usang ketika Anda memperkenalkan kerangka kerja IoC, karena IoC hanyalah semacam pabrik. Dan banyak kerangka kerja IoC mampu menciptakan implementasi pabrik tertentu.
Juga, tidak ada pola desain yang mengatakan untuk membuat kelas statis besar dengan
CreateXXX
metode yang hanya memanggil konstruktor. Dan itu terutama tidak disebut Pabrik (atau Pabrik Abstrak).sumber
Mode pola pabrik berasal dari keyakinan yang hampir dogmatis di antara coders dalam bahasa "C-style" (C / C ++, C #, Java) yang menggunakan kata kunci "baru" itu buruk, dan harus dihindari dengan segala cara (atau paling tidak tersentralisasi). Ini, pada gilirannya, berasal dari interpretasi yang sangat ketat dari Prinsip Tanggung Jawab Tunggal ("S" SOLID), dan juga dari Prinsip Inversi Ketergantungan ("D"). Secara sederhana, SRP mengatakan bahwa idealnya objek kode harus memiliki satu "alasan untuk berubah", dan hanya satu; bahwa "alasan untuk berubah" adalah tujuan utama dari objek itu, "tanggung jawabnya" dalam basis kode, dan hal lain apa pun yang memerlukan perubahan kode tidak boleh mengharuskan membuka file kelas itu. DIP bahkan lebih sederhana; objek kode tidak boleh bergantung pada objek konkret lain,
Contoh kasus, dengan menggunakan "baru" dan konstruktor publik, Anda menggabungkan kode panggilan ke metode konstruksi spesifik dari kelas beton tertentu. Kode Anda sekarang harus tahu bahwa kelas MyFooObject ada, dan memiliki konstruktor yang mengambil string dan int. Jika konstruktor itu membutuhkan lebih banyak informasi, semua penggunaan konstruktor harus diperbarui untuk memasukkan informasi itu termasuk yang sedang Anda tulis sekarang, dan oleh karena itu mereka diharuskan memiliki sesuatu yang valid untuk diteruskan, sehingga mereka harus memiliki atau diubah untuk mendapatkannya (menambahkan lebih banyak tanggung jawab ke objek yang mengkonsumsi). Selain itu, jika MyFooObject pernah diganti dalam basis kode oleh BetterFooObject, semua penggunaan kelas lama harus berubah untuk membangun objek baru, bukan yang lama.
Jadi, sebagai gantinya, semua konsumen MyFooObject harus langsung bergantung pada "IFooObject", yang mendefinisikan perilaku menerapkan kelas termasuk MyFooObject. Sekarang, konsumen IFooObjects tidak bisa hanya membangun IFooObject (tanpa memiliki pengetahuan bahwa kelas beton tertentu adalah IFooObject, yang tidak mereka butuhkan), jadi mereka harus diberi contoh kelas atau metode yang mengimplementasikan IFooObject dari luar, oleh objek lain yang memiliki tanggung jawab untuk mengetahui cara membuat objek IFooOb yang benar untuk keadaan tersebut, yang dalam bahasa kita biasanya dikenal sebagai Pabrik.
Sekarang, di sinilah teori bertemu kenyataan; suatu objek tidak pernah bisa ditutup untuk semua jenis perubahan sepanjang waktu. Contoh kasus, IFooObject sekarang menjadi objek kode tambahan dalam basis kode, yang harus berubah setiap kali antarmuka yang dibutuhkan oleh konsumen atau implementasi perubahan IFooObjects. Itu memperkenalkan tingkat kompleksitas baru yang terlibat dalam mengubah cara objek berinteraksi satu sama lain dalam abstraksi ini. Selain itu, konsumen masih harus berubah, dan lebih dalam lagi, jika antarmuka itu sendiri digantikan oleh yang baru.
Seorang programmer yang baik tahu bagaimana menyeimbangkan YAGNI ("Kamu Tidak Akan Membutuhkannya") dengan SOLID, dengan menganalisis desain dan menemukan tempat-tempat yang sangat mungkin harus berubah dengan cara tertentu, dan refactoring mereka agar lebih toleran terhadap bahwa jenis perubahan, karena dalam kasus "Anda adalah akan membutuhkannya".
sumber
Foo
untuk menentukan konstruktor publik harus dapat digunakan untuk pembuatanFoo
instance, atau pembuatan tipe lain dalam paket / perakitan yang sama , tetapi tidak boleh digunakan untuk pembuatan jenis yang diturunkan di tempat lain. Saya tidak tahu alasan khusus yang memaksa suatu bahasa / kerangka kerja tidak dapat mendefinisikan konstruktor terpisah untuk digunakan dalamnew
ekspresi, dibandingkan permintaan dari konstruktor subtipe, tapi saya tidak tahu bahasa apa pun yang membuat perbedaan itu.Konstruktor baik-baik saja ketika mengandung pendek, kode sederhana.
Ketika inisialisasi menjadi lebih dari menetapkan beberapa variabel ke bidang, pabrik masuk akal. Berikut ini beberapa manfaatnya:
Panjang, kode rumit lebih masuk akal di kelas khusus (pabrik). Jika kode yang sama dimasukkan ke dalam konstruktor yang memanggil banyak metode statis, ini akan mencemari kelas utama.
Dalam beberapa bahasa dan beberapa kasus, melempar pengecualian pada konstruktor adalah ide yang sangat buruk , karena dapat menimbulkan bug.
Saat Anda memanggil konstruktor, Anda, pemanggil, perlu mengetahui jenis persis dari instance yang ingin Anda buat. Ini tidak selalu terjadi (sebagai
Feeder
, saya hanya perlu membuatAnimal
untuk memberi makan; Saya tidak peduli apakah itu aDog
atau aCat
).sumber
Feeder
mungkin tidak menggunakan keduanya, dan sebaliknya memanggil metodeKennel
objek itugetHungryAnimal
.Builder Pattern
juga. Bukan?Jika Anda bekerja dengan antarmuka maka Anda dapat tetap independen dari implementasi yang sebenarnya. Pabrik dapat dikonfigurasikan (melalui properti, parameter atau metode lain) untuk membuat instantiate salah satu dari sejumlah implementasi yang berbeda.
Satu contoh sederhana: Anda ingin berkomunikasi dengan perangkat tetapi Anda tidak tahu apakah itu akan melalui Ethernet, COM atau USB. Anda mendefinisikan satu antarmuka dan 3 implementasi. Saat runtime Anda kemudian dapat memilih metode mana yang Anda inginkan dan pabrik akan memberi Anda implementasi yang sesuai.
Sering menggunakannya ...
sumber
Ini adalah gejala keterbatasan dalam sistem modul Java / C #.
Pada prinsipnya, tidak ada alasan Anda tidak dapat menukar satu implementasi dari kelas dengan yang lain dengan konstruktor dan tanda tangan metode yang sama. Ada yang bahasa yang memungkinkan ini. Namun, Java dan C # bersikeras bahwa setiap kelas memiliki pengidentifikasi unik (nama yang sepenuhnya memenuhi syarat) dan kode klien berakhir dengan ketergantungan kode keras di atasnya.
Anda dapat semacam mendapatkan sekitar ini dengan mengutak-atik sistem file dan opsi compiler sehingga
com.example.Foo
peta ke file yang berbeda, tapi ini mengejutkan dan unintuitive. Bahkan jika Anda melakukannya, kode Anda masih terikat hanya pada satu implementasi kelas. Yaitu Jika Anda menulis kelasFoo
yang tergantung pada kelasMySet
, Anda dapat memilih implementasiMySet
pada waktu kompilasi tetapi Anda masih tidak dapat instantiateFoo
menggunakan dua implementasi yang berbeda dariMySet
.Keputusan desain yang tidak menguntungkan ini memaksa orang untuk menggunakan
interface
kode yang tidak perlu untuk membuktikan kode mereka di masa depan terhadap kemungkinan bahwa mereka nantinya akan membutuhkan implementasi sesuatu yang berbeda, atau untuk memfasilitasi pengujian unit. Ini tidak selalu layak; jika Anda memiliki metode apa pun yang melihat bidang pribadi dari dua instance kelas, tidak akan dapat mengimplementasikannya dalam sebuah antarmuka. Karena itulah, misalnya, Anda tidak melihat antarmukaunion
JavaSet
. Namun, di luar tipe dan koleksi numerik, metode biner tidak umum, jadi Anda biasanya bisa lolos begitu saja.Tentu saja, jika Anda menelepon
new Foo(...)
Anda masih memiliki ketergantungan pada kelas , maka Anda memerlukan pabrik jika Anda ingin kelas dapat membuat instantiate antarmuka secara langsung. Namun, biasanya ide yang lebih baik untuk menerima contoh di konstruktor dan membiarkan orang lain memutuskan implementasi yang akan digunakan.Terserah Anda untuk memutuskan apakah perlu membengkak basis kode Anda dengan antarmuka dan pabrik. Di satu sisi, jika kelas yang dimaksud adalah internal ke basis kode Anda, refactoring kode sehingga menggunakan kelas yang berbeda atau antarmuka di masa depan adalah sepele; Anda bisa memanggil YAGNI dan refactor nanti jika situasinya muncul. Tetapi jika kelas adalah bagian dari API publik dari perpustakaan yang telah Anda terbitkan, Anda tidak memiliki opsi untuk memperbaiki kode klien. Jika Anda tidak menggunakan
interface
dan kemudian membutuhkan beberapa implementasi, Anda akan terjebak di antara batu dan tempat yang sulit.sumber
new
hanya menjadi sintaksis gula untuk memanggil metode statis bernama khusus (yang akan dihasilkan secara otomatis jika suatu tipe memiliki "konstruktor publik" "tetapi tidak secara eksplisit memasukkan metode). IMHO, jika kode hanya menginginkan hal standar yang membosankan yang diterapkanList
, harus memungkinkan antarmuka untuk memberikannya tanpa klien harus mengetahui implementasi tertentu (misalnyaArrayList
).Menurut pendapat saya, mereka hanya menggunakan Pabrik Sederhana, yang bukan pola desain yang tepat dan tidak boleh bingung dengan Pabrik Abstrak atau Metode Pabrik.
Dan karena mereka telah menciptakan "kelas kain besar dengan ribuan metode statis CreateXXX", yang terdengar anti-pola (kelas Dewa mungkin?).
Saya pikir Pabrik Sederhana dan metode pembuat statis (yang tidak memerlukan kelas eksternal), dapat berguna dalam beberapa kasus. Misalnya, ketika konstruksi suatu objek memerlukan berbagai langkah seperti instancing objek lain (misalnya komposisi yang disukai).
Saya tidak akan menyebut bahkan menyebutnya Pabrik, tetapi hanya sekelompok metode yang dikemas dalam beberapa kelas acak dengan akhiran "Pabrik".
sumber
int x
danIFooService fooService
. Anda tidak ingin lewat difooService
mana - mana, jadi Anda membuat pabrik dengan metodeCreate(int x)
dan menyuntikkan layanan di dalam pabrik.IFactory
kemana sajaIFooService
.Sebagai pengguna perpustakaan, jika perpustakaan memiliki metode pabrik, maka Anda harus menggunakannya. Anda akan berasumsi bahwa metode pabrik memberi penulis perpustakaan fleksibilitas untuk membuat perubahan tertentu tanpa mempengaruhi kode Anda. Misalnya mereka mungkin mengembalikan turunan dari beberapa subclass dalam metode pabrik, yang tidak akan bekerja dengan konstruktor sederhana.
Sebagai pencipta perpustakaan, Anda akan menggunakan metode pabrik jika Anda ingin menggunakan fleksibilitas itu sendiri.
Dalam kasus yang Anda jelaskan, Anda tampaknya memiliki kesan bahwa mengganti konstruktor dengan metode pabrik tidak ada gunanya. Itu tentu menyebalkan bagi semua orang yang terlibat; perpustakaan tidak boleh menghapus apa pun dari APInya tanpa alasan yang kuat. Jadi jika saya telah menambahkan metode pabrik, saya akan membiarkan konstruktor yang ada tersedia, mungkin sudah usang, sampai metode pabrik tidak hanya memanggil konstruktor itu lagi dan kode menggunakan konstruktor polos berfungsi kurang baik dari yang seharusnya. Kesan Anda mungkin benar.
sumber
Ini tampaknya sudah usang di zaman Scala dan pemrograman fungsional. Fondasi fungsi yang solid menggantikan trilyun kelas.
Juga perlu dicatat bahwa Java double
{{
tidak berfungsi lagi ketika menggunakan pabrik yaituYang memungkinkan Anda membuat kelas anonim dan menyesuaikannya.
Dalam dilema ruang waktu faktor waktu sekarang sangat menyusut berkat CPU cepat sehingga pemrograman fungsional memungkinkan penyusutan ruang yaitu ukuran kode sekarang praktis.
sumber