Menerapkan prinsip-prinsip SOLID

13

Saya cukup baru dengan prinsip-prinsip desain SOLID . Saya memahami sebab dan manfaatnya, tetapi saya gagal menerapkannya pada proyek yang lebih kecil yang ingin saya refactor sebagai latihan praktis untuk menggunakan prinsip-prinsip SOLID. Saya tahu tidak perlu mengubah aplikasi yang berfungsi dengan sempurna, tetapi saya tetap ingin mengubahnya jadi saya mendapatkan pengalaman desain untuk proyek masa depan.

Aplikasi memiliki tugas berikut (sebenarnya jauh lebih dari itu tetapi mari kita tetap sederhana): Aplikasi ini harus membaca file XML yang berisi definisi Tabel Tabel / Kolom / Tampilan dll dan membuat file SQL yang dapat digunakan untuk membuat skema database ORACLE.

(Catatan: Tolong jangan membahas mengapa saya membutuhkannya atau mengapa saya tidak menggunakan XSLT dan sebagainya, ada alasannya, tetapi semuanya di luar topik.)

Sebagai permulaan, saya memilih untuk hanya melihat Tabel dan Kendala. Jika Anda mengabaikan kolom, Anda dapat menyatakannya dengan cara berikut:

Kendala adalah bagian dari tabel (atau lebih tepatnya, bagian dari pernyataan CREATE TABLE), dan kendala juga dapat merujuk tabel lain.

Pertama, saya akan menjelaskan seperti apa aplikasi saat ini (tidak menerapkan SOLID):

Saat ini, aplikasi memiliki kelas "Tabel" yang berisi daftar pointer ke Kendala yang dimiliki oleh tabel, dan daftar pointer ke Kendala yang merujuk pada tabel ini. Setiap kali koneksi terbentuk, koneksi mundur juga akan dibuat. Tabel ini memiliki metode createStatement () yang pada gilirannya memanggil fungsi createStatement () dari setiap kendala. Metode Said sendiri akan menggunakan koneksi ke tabel pemilik dan tabel referensi untuk mengambil nama mereka.

Jelas, ini tidak berlaku untuk SOLID sama sekali. Misalnya, ada dependensi melingkar, yang membengkak kode dalam hal metode "tambah" / "hapus" yang diperlukan dan beberapa destruktor objek besar.

Jadi ada beberapa pertanyaan:

  1. Haruskah saya menyelesaikan dependensi melingkar menggunakan Injeksi Ketergantungan? Jika demikian, saya kira Constraint harus menerima tabel pemilik (dan opsional yang direferensikan) dalam konstruktornya. Tapi bagaimana saya bisa melindas daftar kendala untuk satu tabel?
  2. Jika kelas Tabel keduanya menyimpan keadaan itu sendiri (misalnya nama tabel, komentar tabel dll) dan tautan ke Kendala, apakah ini satu atau dua "tanggung jawab", memikirkan Prinsip Tanggung Jawab Tunggal?
  3. Dalam kasus 2. benar, haruskah saya membuat kelas baru di lapisan bisnis logis yang mengelola tautan? Jika demikian, 1. jelas tidak lagi relevan.
  4. Haruskah metode "createStatement" menjadi bagian dari kelas Tabel / Kendala atau haruskah saya memindahkannya juga? Jika ya, kemana? Satu kelas Manajer per setiap kelas penyimpanan data (mis. Tabel, Kendala, ...)? Atau lebih tepatnya membuat kelas manajer per tautan (mirip dengan 3.)?

Setiap kali saya mencoba menjawab salah satu pertanyaan ini saya menemukan diri saya berputar-putar di suatu tempat.

Masalahnya jelas menjadi jauh lebih kompleks jika Anda memasukkan kolom, indeks dan sebagainya, tetapi jika kalian membantu saya dengan masalah Tabel / Kendala yang sederhana, saya mungkin dapat mengerjakan sisanya sendiri.

Tim Meyer
sumber
3
Bahasa apa yang Anda gunakan? Bisakah Anda memposting setidaknya beberapa kode kerangka? Sangat sulit untuk membahas kualitas kode dan kemungkinan refactoring tanpa melihat kode yang sebenarnya.
Péter Török
Saya menggunakan C ++ tetapi saya mencoba untuk tidak membahasnya karena Anda dapat memiliki masalah ini dalam bahasa apa pun
Tim Meyer
Ya, tetapi penerapan pola dan refactoring bergantung pada bahasa. Misalnya @ back2dos menyarankan AOP dalam jawabannya di bawah ini, yang jelas tidak berlaku untuk C ++.
Péter Török
Silakan merujuk ke programmers.stackexchange.com/questions/155852/… untuk lebih lanjut tentang prinsip
LCJ

Jawaban:

8

Anda dapat mulai dari sudut pandang yang berbeda untuk menerapkan "Prinsip Tanggung Jawab Tunggal" di sini. Apa yang Anda tunjukkan kepada kami adalah (kurang lebih) hanya model data aplikasi Anda. SRP di sini berarti: pastikan model data Anda hanya bertanggung jawab untuk menyimpan data - tidak kurang, tidak lebih.

Jadi ketika Anda akan membaca file XML Anda, membuat model data dari itu dan menulis SQL, apa yang tidak boleh Anda lakukan adalah mengimplementasikan apa pun ke dalam Tablekelas Anda yang XML atau SQL spesifik. Anda ingin aliran data Anda terlihat seperti ini:

[XML] -> ("Read XML") -> [Data model of DB definition] -> ("Write SQL") -> [SQL]

Jadi satu-satunya tempat di mana kode spesifik XML harus ditempatkan adalah kelas bernama, misalnya Read_XML,. Satu-satunya tempat untuk kode khusus SQL adalah kelas Write_SQL. Tentu saja, mungkin Anda akan membagi 2 tugas menjadi lebih banyak sub-tugas (dan membagi kelas Anda menjadi beberapa kelas manajer), tetapi "model data" Anda seharusnya tidak bertanggung jawab dari lapisan itu. Jadi jangan tambahkan createStatementke salah satu kelas model data Anda, karena ini memberikan tanggung jawab model data Anda untuk SQL.

Saya tidak melihat masalah ketika Anda menjelaskan bahwa sebuah Table bertanggung jawab untuk memegang semua bagiannya, (nama, kolom, komentar, kendala ...), itulah ide di balik model data. Tetapi Anda menggambarkan "Tabel" juga bertanggung jawab untuk manajemen memori beberapa bagiannya. Itu masalah khusus C ++, yang tidak akan Anda hadapi dengan mudah dalam bahasa seperti Java atau C #. Cara C ++ untuk menghilangkan tanggung jawab tersebut adalah menggunakan smart pointer, mendelegasikan kepemilikan ke lapisan yang berbeda (misalnya, perpustakaan pendorong atau ke lapisan pointer "pintar" Anda sendiri). Namun berhati-hatilah, dependensi siklik Anda mungkin "mengganggu" beberapa implementasi smart pointer.

Sesuatu yang lebih tentang SOLID: di sini ada artikel yang bagus

http://cre8ivethought.com/blog/2011/08/23/software-development-is-not-a-jenga-game

menjelaskan SOLID dengan contoh kecil. Mari kita coba menerapkannya pada kasus Anda:

  • Anda tidak hanya membutuhkan kelas Read_XMLdan Write_SQL, tetapi juga kelas ketiga yang mengelola interaksi kedua kelas tersebut. Mari kita sebut a ConversionManager.

  • Menerapkan prinsip DI bisa berarti di sini: ConversionManager tidak harus membuat contoh Read_XMLdan Write_SQLdengan sendirinya. Sebaliknya, benda-benda itu dapat disuntikkan melalui konstruktor. Dan konstruktor harus memiliki tanda tangan seperti ini

    ConversionManager(IDataModelReader reader, IDataModelWriter writer)

di mana IDataModelReaderantarmuka dari mana Read_XMLmewarisi, dan IDataModelWritersama untuk Write_SQL. Ini membuat ConversionManagerekstensi terbuka (Anda dengan mudah menyediakan pembaca atau penulis yang berbeda) tanpa harus mengubahnya - jadi kami memiliki contoh untuk prinsip Terbuka / Tertutup. Pikirkan tentang itu apa yang harus Anda ubah ketika Anda ingin mendukung vendor database lain -yaitu, Anda tidak perlu mengubah apa pun dalam model data Anda, cukup sediakan SQL-Writer yang lain.

Doc Brown
sumber
Meskipun ini adalah latihan SOLID yang sangat masuk akal, (upvoted) perhatikan bahwa itu melanggar "old school Kay / Holub OOP" dengan mengharuskan pengambil dan penentu untuk Model Data yang cukup anemia. Ini juga mengingatkan saya pada kata-kata kasar Steve Yegge yang terkenal .
user949300
2

Nah, Anda harus menerapkan S SOLID dalam hal ini.

Tabel memuat semua batasan yang didefinisikan di atasnya. Batasan memegang semua tabel yang dirujuk. Model polos dan sederhana.

Apa yang Anda yakini, adalah kemampuan untuk melakukan pencarian terbalik, yaitu untuk mencari tahu dengan mana batasan beberapa tabel direferensikan.
Jadi yang Anda inginkan sebenarnya adalah layanan pengindeksan. Itu adalah tugas yang sama sekali berbeda dan karenanya harus dilakukan oleh objek yang berbeda.

Untuk memecahnya ke versi yang sangat sederhana:

class Table {
      void addConstraint(Constraint constraint) { ... }
      bool removeConstraint(Constraint constraint) { ... }
      Iterator<Constraint> getConstraints() { ... }
}
class Constraint {
      //actually I am not so sure these two should be exposed directly at all
      void addReference(Table to) { ... }
      bool removeReference(Table to) { ... }
      Iterator<Table> getReferencedTables() { ... }
}
class Database {
      void addTable(Table table) { ... }
      bool removeTable(Table table) { ... }
      Iterator<Table> getTables() { ... }
}
class Index {
      Iterator<Constraint> getConstraintsReferencing(Table target) { ... }
}

Adapun implementasi indeks, ada 3 cara untuk pergi:

  • yang getContraintsReferencingmetode bisa benar-benar hanya merangkak seluruh Databaseuntuk Tablecontoh dan merangkak mereka Constraints untuk mendapatkan hasilnya. Bergantung pada seberapa mahal ini dan seberapa sering Anda membutuhkannya, ini bisa menjadi pilihan.
  • bisa juga menggunakan cache. Jika model database Anda dapat berubah setelah ditentukan, Anda dapat mempertahankan cache dengan memancarkan sinyal dari masing-masing Tabledan Constraintinstance, ketika mereka berubah. Solusi yang sedikit lebih sederhana adalah Indexmembangun "indeks snapshot" dari keseluruhan Databaseuntuk dikerjakan, yang kemudian akan Anda buang. Itu tentu saja hanya mungkin, jika aplikasi Anda membuat perbedaan besar antara "waktu pemodelan" dan "waktu permintaan". Jika agak mungkin melakukan keduanya pada saat yang sama, maka ini tidak layak.
  • Pilihan lain adalah menggunakan AOP untuk mencegat seluruh panggilan pembuatan dan mempertahankan indeks sesuai.
back2dos
sumber
Jawaban yang sangat terperinci, saya menyukai solusi Anda sejauh ini! Apa yang akan Anda pikirkan jika saya melakukan DI untuk kelas Table, memberikannya daftar kendala selama konstruksi? Saya memiliki kelas TableParser, yang dapat bertindak sebagai pabrik atau bekerja sama dengan pabrik untuk kasus itu.
Tim Meyer
@ Tim Meyer: DI belum tentu injeksi konstruktor. DI juga dapat dilakukan oleh fungsi anggota. Jika Tabel harus mendapatkan semua bagiannya melalui konstruktor tergantung pada apakah Anda ingin bagian-bagian itu hanya ditambahkan pada waktu konstruksi dan tidak pernah berubah nanti, atau jika Anda ingin membuat tabel langkah demi langkah. Itu harus menjadi dasar keputusan desain Anda.
Doc Brown
1

Obat untuk dependensi melingkar adalah dengan bersumpah bahwa Anda tidak akan pernah membuatnya. Saya menemukan bahwa tes coding pertama adalah pencegah yang kuat.

Lagi pula, dependensi melingkar selalu dapat dipatahkan dengan memperkenalkan kelas dasar abstrak. Ini tipikal untuk representasi grafik. Di sini tabel adalah node dan batasan kunci asing adalah edge. Jadi buat kelas Tabel abstrak dan kelas Kendala abstrak dan mungkin kelas Kolom abstrak. Maka semua implementasi dapat bergantung pada kelas abstrak. Ini mungkin bukan representasi terbaik yang mungkin, tetapi ini merupakan peningkatan dari kelas yang saling digabungkan.

Tetapi, seperti yang Anda duga, solusi terbaik untuk masalah ini mungkin tidak memerlukan pelacakan hubungan objek. Jika Anda hanya ingin menerjemahkan XML ke SQL, maka Anda tidak perlu representasi in-memory dari grafik kendala. Grafik kendala akan bagus jika Anda ingin menjalankan algoritma grafik, tetapi Anda tidak menyebutkannya jadi saya akan menganggap itu bukan keharusan. Anda hanya perlu daftar tabel dan daftar kendala dan pengunjung untuk setiap dialek SQL yang ingin Anda dukung. Hasilkan tabel, kemudian hasilkan kendala eksternal ke tabel. Sampai persyaratan berubah, saya tidak akan punya masalah dengan memasangkan generator SQL ke XML DOM. Simpan besok untuk besok.

kevin cline
sumber
Di sinilah "(sebenarnya jauh lebih banyak dari itu tetapi mari kita tetap sederhana)" ikut bermain. Misalnya, ada beberapa kasus di mana saya perlu menghapus tabel, jadi saya perlu memeriksa apakah ada kendala yang mereferensikan tabel ini.
Tim Meyer