Apakah objek konfigurasi tunggal adalah ide yang buruk?


43

Pada sebagian besar aplikasi saya, saya memiliki objek "config" tunggal atau statis, yang bertugas membaca berbagai pengaturan dari disk. Hampir semua kelas menggunakannya, untuk berbagai keperluan. Pada dasarnya itu hanya tabel hash dari pasangan nama / nilai. Ini hanya baca, jadi saya belum terlalu peduli dengan kenyataan bahwa saya memiliki begitu banyak negara global. Tapi sekarang saya mulai dengan pengujian unit, itu mulai menjadi masalah.

Satu masalah adalah bahwa Anda biasanya tidak ingin menguji dengan konfigurasi yang sama dengan yang Anda jalankan. Ada beberapa solusi untuk ini:

  • Berikan objek konfigurasi setter yang HANYA digunakan untuk pengujian, sehingga Anda dapat lulus dalam pengaturan yang berbeda.
  • Lanjutkan menggunakan objek konfigurasi tunggal, tetapi ubahlah dari satu singleton ke instance yang Anda berikan di mana-mana yang diperlukan. Kemudian Anda dapat membangunnya sekali dalam aplikasi Anda, dan sekali dalam pengujian Anda, dengan pengaturan yang berbeda.

Namun demikian, Anda masih memiliki masalah kedua: hampir semua kelas dapat menggunakan objek konfigurasi. Jadi dalam sebuah tes, Anda perlu mengatur konfigurasi untuk kelas yang diuji, tetapi juga SEMUA dependensinya. Ini dapat membuat kode pengujian Anda jelek.

Saya mulai sampai pada kesimpulan bahwa objek konfigurasi semacam ini adalah ide yang buruk. Apa yang kamu pikirkan? Apa saja alternatifnya? Dan bagaimana Anda memulai refactoring aplikasi yang menggunakan konfigurasi di mana-mana?


Konfigurasi mendapatkan pengaturannya dengan membaca file pada disk, bukan? Jadi mengapa tidak hanya memiliki file "test.config" yang dapat dibaca?
Anon.

Itu memecahkan masalah pertama, tetapi bukan yang kedua.
JW01

@ JW01: Kalau begitu, apakah semua pengujian Anda memerlukan konfigurasi yang sangat berbeda? Anda harus mengatur konfigurasi itu di suatu tempat selama pengujian, bukan?
Anon.

Benar. Tetapi jika saya terus menggunakan kumpulan pengaturan tunggal (dengan kumpulan pengujian yang berbeda), maka semua pengujian saya akhirnya menggunakan pengaturan yang sama. Dan karena saya mungkin ingin menguji perilaku dengan pengaturan yang berbeda, ini tidak ideal.
JW01

"Jadi dalam sebuah tes, kamu perlu mengatur konfigurasi untuk kelas yang diuji, tetapi juga SEMUA dependensinya. Ini dapat membuat kode pengujian kamu jelek." Punya contoh? Saya punya perasaan bahwa itu bukan struktur seluruh aplikasi Anda yang terhubung ke kelas konfigurasi tetapi struktur kelas konfigurasi itu sendiri yang membuat hal-hal "jelek". Tidakkah seharusnya mengatur konfigurasi kelas secara otomatis mengkonfigurasi dependensinya?
AndrewKS

Jawaban:


35

Saya tidak punya masalah dengan objek konfigurasi tunggal, dan dapat melihat keuntungan dalam menjaga semua pengaturan di satu tempat.

Namun, menggunakan objek tunggal itu di mana-mana akan menghasilkan kopling tingkat tinggi antara objek konfigurasi dan kelas yang menggunakannya. Jika Anda perlu mengubah kelas config, Anda mungkin harus mengunjungi setiap instance yang digunakan dan memeriksa apakah Anda tidak merusak apa pun.

Salah satu cara untuk mengatasinya adalah dengan membuat banyak antarmuka yang mengekspos bagian-bagian dari objek konfigurasi yang diperlukan oleh berbagai bagian aplikasi. Daripada mengizinkan kelas-kelas lain untuk mengakses objek konfigurasi, Anda memberikan contoh antarmuka ke kelas yang membutuhkannya. Dengan begitu, bagian-bagian aplikasi yang menggunakan konfigurasi bergantung pada kumpulan bidang yang lebih kecil di antarmuka daripada seluruh kelas konfigurasi. Terakhir, untuk pengujian unit Anda dapat membuat kelas khusus yang mengimplementasikan antarmuka.

Jika Anda ingin menjelajahi ide-ide ini lebih lanjut, saya sarankan membaca tentang prinsip-prinsip SOLID , khususnya Prinsip Segregasi Antarmuka dan Prinsip Pembalikan Ketergantungan .


7

Saya memisahkan kelompok pengaturan terkait dengan antarmuka.

Sesuatu seperti:

public interface INotificationEmailSettings {
   public string To { get; set; }
}

public interface IMediaFileSettings {
    public string BasePath { get; set; }
}

dll.

Sekarang, untuk lingkungan nyata, satu kelas akan mengimplementasikan banyak antarmuka ini. Kelas itu mungkin berasal dari DB atau konfigurasi aplikasi atau apa pun yang Anda miliki, tetapi sering kali ia tahu cara mendapatkan sebagian besar pengaturan.

Tetapi pemisahan dengan antarmuka membuat dependensi aktual jauh lebih eksplisit dan berbutir halus, dan ini merupakan peningkatan yang jelas untuk testabilitas.

Tapi .. Saya tidak selalu ingin dipaksa untuk menyuntikkan atau memberikan pengaturan sebagai ketergantungan. Ada argumen yang dapat dibuat bahwa saya harus, untuk konsistensi atau kesaksian. Namun dalam kehidupan nyata sepertinya pembatasan yang tidak perlu.

Jadi, saya akan menggunakan kelas statis sebagai fasad, menyediakan entri yang mudah dari mana saja ke pengaturan, dan dalam kelas statis itu saya akan mencari layanan implementasi antarmuka dan mendapatkan pengaturan.

Saya tahu lokasi layanan mendapatkan jempol cepat dari sebagian besar, tapi mari kita hadapi itu, memberikan ketergantungan melalui konstruktor adalah berat, dan kadang-kadang berat itu lebih dari yang saya mau tanggung. Layanan Lokasi adalah solusi untuk mempertahankan testabilitas melalui pemrograman ke antarmuka dan memungkinkan beberapa implementasi sambil tetap memberikan kemudahan (dalam beberapa situasi yang diukur dengan tepat dan tepat) dari titik masuk tunggal statis.

public static class AllSettings {
    public INotificationEmailSettings NotificationEmailSettings {
        get {
            return ServiceLocator.Get<INotificationEmailSettings>();
        }
    }
}

Saya menemukan campuran ini menjadi yang terbaik dari semua dunia.


3

Ya, seperti yang Anda sadari, objek konfigurasi global membuat pengujian unit menjadi sulit. Memiliki setter "rahasia" untuk pengujian unit adalah hack cepat, yang meskipun tidak bagus, bisa sangat berguna: memungkinkan Anda untuk mulai menulis tes unit, sehingga Anda dapat memperbaiki kode Anda ke arah desain yang lebih baik dari waktu ke waktu.

(Referensi wajib: Bekerja Efektif dengan Kode Legacy . Ini berisi ini dan banyak lagi trik yang tak ternilai untuk pengujian kode legacy unit.)

Dalam pengalaman saya, yang terbaik adalah memiliki ketergantungan sesedikit mungkin pada konfigurasi global. Yang belum tentu nol - itu semua tergantung pada keadaan. Memiliki beberapa kelas "penyelenggara" tingkat tinggi yang mengakses konfigurasi global dan meneruskan properti konfigurasi aktual ke objek yang mereka buat dan gunakan dapat bekerja dengan baik, selama penyelenggara hanya melakukan itu, misalnya tidak mengandung banyak kode yang dapat diuji sendiri. Hal ini memungkinkan Anda untuk menguji sebagian besar atau semua fungsi penting unit yang dapat diuji dari aplikasi, sementara masih tidak sepenuhnya mengganggu skema konfigurasi fisik yang ada.

Ini sudah mendekati solusi injeksi ketergantungan asli, seperti Spring for Java. Jika Anda dapat bermigrasi ke kerangka kerja seperti itu, baik. Tetapi dalam kehidupan nyata, dan terutama ketika berhadapan dengan aplikasi warisan, seringkali refactoring yang lambat dan teliti terhadap DI adalah kompromi terbaik yang dapat dicapai.


Saya benar-benar menyukai pendekatan Anda dan saya tidak akan menyarankan bahwa ide setter harus dijaga "rahasia" atau dianggap sebagai "peretasan cepat" dalam hal ini. Jika Anda menerima pendapat bahwa tes unit adalah pengguna / konsumen / bagian penting dari aplikasi Anda, maka memiliki test_atribut dan metode bukanlah hal yang buruk lagi, bukan? Saya setuju bahwa solusi asli untuk masalah ini adalah dengan menggunakan kerangka kerja DI, tetapi dalam contoh seperti milik Anda, ketika bekerja dengan kode warisan, atau kasus sederhana lainnya, tidak mengasingkan kode tes berlaku bersih.
Filip Dupanović

1
@ KRON, ini adalah trik yang sangat berguna dan praktis untuk membuat unit kode warisan dapat diuji, dan saya juga menggunakannya secara ekstensif. Namun, idealnya kode produksi tidak boleh mengandung fitur apa pun yang diperkenalkan hanya untuk tujuan pengujian. Dalam jangka panjang, di sinilah saya refactoring menuju.
Péter Török

2

Dalam pengalaman saya, di dunia Jawa, inilah yang dimaksudkan dengan Spring. Itu membuat dan mengelola objek (kacang-kacangan) berdasarkan pada properti tertentu yang ditetapkan pada saat runtime, dan sebaliknya transparan untuk aplikasi Anda. Anda dapat pergi dengan file test.config yang Anon. menyebutkan atau Anda dapat memasukkan beberapa logika dalam file konfigurasi yang akan ditangani Spring untuk menyetel properti berdasarkan beberapa kunci lain (mis. nama host atau lingkungan).

Mengenai masalah kedua Anda, Anda dapat menyiasatinya dengan beberapa arsitektur ulang tetapi tidak separah kelihatannya. Apa artinya ini dalam kasus Anda adalah bahwa Anda tidak akan, misalnya, memiliki objek global Config yang digunakan berbagai kelas lainnya. Anda baru saja mengkonfigurasi kelas-kelas lain melalui Spring dan kemudian tidak memiliki objek config karena semua konfigurasi yang diperlukan mendapatkannya melalui Spring, sehingga Anda dapat menggunakan kelas-kelas tersebut secara langsung dalam kode Anda.


2

Pertanyaan ini sebenarnya tentang masalah yang lebih umum daripada konfigurasi. Segera setelah saya membaca kata "singleton", saya langsung memikirkan semua masalah yang terkait dengan pola itu, yang paling tidak adalah kemampuan pengujian yang buruk.

Pola Singleton "dianggap berbahaya". Artinya, tidak selalu hal yang salah, tetapi biasanya memang demikian. Jika Anda pernah mempertimbangkan untuk menggunakan pola tunggal untuk apa pun , berhentilah untuk mempertimbangkan:

  • Apakah Anda perlu mensubklasifikasikannya?
  • Apakah Anda perlu memprogram ke antarmuka?
  • Apakah Anda perlu menjalankan unit test melawannya?
  • Apakah Anda perlu sering memodifikasinya?
  • Apakah aplikasi Anda dimaksudkan untuk mendukung berbagai platform / lingkungan produksi?
  • Apakah Anda sedikit khawatir tentang penggunaan memori?

Jika jawaban Anda adalah "ya" untuk semua ini (dan mungkin beberapa hal lain yang saya tidak pikirkan), maka Anda mungkin tidak ingin menggunakan singleton. Konfigurasi sering membutuhkan fleksibilitas yang jauh lebih besar daripada yang dapat diberikan oleh singleton (atau dalam hal ini, kelas instanceless).

Jika Anda ingin hampir semua manfaat dari singleton tanpa rasa sakit, maka gunakan kerangka injeksi ketergantungan seperti Spring atau Castle atau apa pun yang tersedia untuk lingkungan Anda. Dengan begitu Anda hanya perlu mendeklarasikannya sekali dan wadah itu akan secara otomatis memberikan contoh untuk apa pun yang membutuhkannya.


0

Salah satu cara saya menangani ini dalam C # adalah memiliki objek tunggal yang mengunci selama pembuatan instance dan menginisialisasi semua data sekaligus untuk digunakan oleh semua objek klien. Jika singleton ini dapat menangani pasangan kunci / nilai, Anda bisa menyimpan segala macam data termasuk kunci kompleks untuk digunakan oleh banyak klien dan jenis klien yang berbeda. Jika Anda ingin tetap dinamis dan memuat data klien baru sesuai kebutuhan, Anda dapat memverifikasi keberadaan kunci utama klien dan, jika tidak ada, muat data untuk klien itu, tambahkan ke kamus utama. Mungkin juga konfigurasi singleton primer dapat berisi set data klien di mana banyak tipe klien yang sama menggunakan data yang sama yang diakses melalui konfigurasi primer singleton. Sebagian besar organisasi objek konfigurasi ini tergantung pada bagaimana klien konfigurasi Anda perlu mengakses informasi ini dan apakah informasi itu statis atau dinamis. Saya telah menggunakan konfigurasi kunci / nilai serta API objek tertentu bergantung pada yang diperlukan.

Salah satu lajang konfigurasi saya dapat menerima pesan untuk dimuat ulang dari basis data. Dalam hal ini, saya memuat koleksi objek kedua dan hanya mengunci koleksi utama untuk menukar koleksi. Ini mencegah pemblokiran bacaan pada utas lainnya.

Jika memuat dari file konfigurasi, Anda bisa memiliki hierarki file. Pengaturan dalam satu file dapat menentukan file mana yang akan dimuat. Saya telah menggunakan mekanisme ini dengan layanan C # Windows yang memiliki banyak komponen opsional, masing-masing dengan pengaturan konfigurasi mereka sendiri. Pola struktur file adalah sama di seluruh file, tetapi dimuat atau tidak berdasarkan pada pengaturan file utama.


0

Kelas yang Anda gambarkan terdengar seperti objek antipattern Dewa kecuali dengan data alih-alih fungsi. Itu masalah. Anda mungkin harus meminta data konfigurasi dibaca dan disimpan di objek yang sesuai sambil hanya membaca data lagi jika diperlukan karena alasan tertentu.

Selain itu Anda menggunakan Singleton karena alasan yang tidak sesuai. Lajang hanya boleh digunakan ketika keberadaan beberapa objek akan membuat keadaan buruk. Menggunakan Singleton dalam kasus ini tidak tepat karena memiliki lebih dari satu pembaca konfigurasi tidak boleh menyebabkan status kesalahan segera. Jika Anda memiliki lebih dari satu, Anda mungkin melakukan kesalahan, tetapi tidak mutlak bahwa Anda hanya memiliki satu dari pembaca konfigurasi.

Akhirnya, menciptakan keadaan global seperti itu merupakan pelanggaran enkapsulasi karena Anda mengizinkan lebih banyak kelas daripada yang perlu diketahui mengakses data secara langsung.


0

Saya pikir jika ada banyak pengaturan, dengan "rumpun" yang terkait, masuk akal untuk membagi mereka menjadi antarmuka yang terpisah dengan implementasi masing-masing - kemudian menyuntikkan antarmuka tersebut di mana diperlukan dengan DI. Anda mendapatkan testabilitas, kopling lebih rendah, dan SRP.

Saya terinspirasi oleh masalah ini oleh Joshua Flanagan dari Los Techies; dia menulis artikel tentang masalah itu beberapa waktu lalu.

Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.