Bagaimana cara menerapkan prinsip Segregasi Antarmuka dalam C?

15

Saya memiliki modul, katakan 'M', yang memiliki beberapa klien, katakan 'C1', 'C2', 'C3'. Saya ingin membagi namespace modul M, yaitu deklarasi API dan data yang dipaparkannya, ke dalam file header sedemikian rupa sehingga -

  1. untuk klien mana pun hanya data dan API yang diperlukannya yang terlihat; sisa namespace modul disembunyikan dari klien yaitu mematuhi prinsip Segregasi Antarmuka .
  2. deklarasi tidak diulang dalam beberapa file header yaitu tidak melanggar KERING .
  3. modul M tidak memiliki ketergantungan pada kliennya.
  4. seorang klien tidak terpengaruh oleh perubahan yang dibuat di bagian-bagian modul M yang tidak digunakan olehnya.
  5. klien yang ada tidak terpengaruh oleh penambahan (atau penghapusan) lebih banyak klien.

Saat ini saya berurusan dengan ini dengan membagi namespace modul tergantung pada kebutuhan kliennya. Sebagai contoh, pada gambar di bawah ini bagian-bagian berbeda dari namespace modul yang diperlukan oleh 3 kliennya ditampilkan. Persyaratan klien tumpang tindih. Namespace modul dibagi menjadi 4 file header terpisah - 'a', '1', '2' dan '3' .

Partisi namespace modul

Namun, ini melanggar beberapa persyaratan yang disebutkan di atas yaitu R3 dan R5. Persyaratan 3 dilanggar karena partisi ini tergantung pada sifat klien; juga pada penambahan klien baru, partisi ini berubah dan melanggar persyaratan 5. Seperti yang dapat dilihat di sisi kanan gambar di atas, dengan penambahan klien baru ruang modul sekarang dibagi menjadi 7 file header - 'a ',' b ',' c ',' 1 ',' 2 * ',' 3 * 'dan' 4 ' . File header yang dimaksudkan untuk 2 perubahan klien yang lebih lama, sehingga memicu rekondisi mereka.

Apakah ada cara untuk mencapai Segregasi Antarmuka dalam C dengan cara yang tidak dibuat-buat?
Jika ya, bagaimana Anda menangani contoh di atas?

Solusi hipotetis yang tidak realistis yang saya bayangkan adalah -
Modul ini memiliki 1 file header besar yang mencakup seluruh namespace-nya. File header ini dibagi menjadi beberapa bagian yang dapat dialamatkan dan subbagian seperti halaman Wikipedia. Setiap klien kemudian memiliki file header khusus yang dirancang untuk itu. File header khusus klien hanyalah daftar hyperlink ke bagian / subbagian file header lemak. Dan sistem build harus mengenali file header khusus klien sebagai 'dimodifikasi' jika ada bagian yang ditunjukkan di header Modul dimodifikasi.

work.bin
sumber
1
Mengapa masalah ini khusus untuk C? Apakah karena C tidak memiliki warisan?
Robert Harvey
Juga, apakah melanggar ISP membuat desain Anda bekerja lebih baik?
Robert Harvey
2
C tidak benar-benar mendukung konsep OOP (seperti antarmuka atau warisan). Kami puas dengan peretasan kasar (tapi kreatif). Mencari retas untuk mensimulasikan Antarmuka. Biasanya, seluruh file header adalah antarmuka ke modul.
work.bin
1
structadalah apa yang Anda gunakan di C saat Anda menginginkan antarmuka. Memang, metode agak sulit. Anda mungkin menemukan ini menarik: cs.rit.edu/~ats/books/ooc.pdf
Robert Harvey
Saya tidak bisa membuat antarmuka yang setara dengan menggunakan structdan function pointers.
work.bin

Jawaban:

5

Segregasi Antarmuka, secara umum, tidak boleh didasarkan pada persyaratan klien. Anda harus mengubah seluruh pendekatan untuk mencapainya. Saya akan mengatakan, memodulasi antarmuka dengan mengelompokkan fitur menjadi koheren kelompok yang . Itu adalah pengelompokan berdasarkan pada koherensi fitur itu sendiri, bukan persyaratan klien. Dalam hal ini, Anda akan memiliki satu set antarmuka, I1, I2, ... dll. Klien C1 dapat menggunakan I2 saja. Klien C2 dapat menggunakan I1 dan I5 dll. Perhatikan bahwa, jika klien menggunakan lebih dari satu Ii, bukan masalah. Jika Anda telah menguraikan antarmuka menjadi modul yang koheren, di situlah inti masalahnya.

Sekali lagi, ISP bukan berbasis klien. Ini adalah tentang penguraian antarmuka menjadi modul yang lebih kecil. Jika ini dilakukan dengan benar, itu juga akan memastikan bahwa klien terkena fitur sesedikit yang mereka butuhkan.

Dengan pendekatan ini, klien Anda dapat meningkat ke angka apa pun tetapi Anda tidak terpengaruh. Setiap klien akan menggunakan satu atau beberapa kombinasi antarmuka berdasarkan kebutuhan mereka. Apakah akan ada kasus bahwa klien, C, perlu menyertakan say I1 dan I3, tetapi tidak menggunakan semua fitur dari antarmuka ini? Ya, itu bukan masalah. Itu hanya menggunakan jumlah antarmuka paling sedikit.

Nazar Merza
sumber
Anda tentu berarti kelompok yang terpisah atau tidak tumpang tindih , saya kira?
Doc Brown
Ya, lepas dan tidak tumpang tindih.
Nazar Merza
3

The Antarmuka Pemisahan Prinsip mengatakan:

Tidak ada klien yang harus dipaksa untuk bergantung pada metode yang tidak digunakannya. ISP membagi antarmuka yang sangat besar menjadi yang lebih kecil dan lebih spesifik sehingga klien hanya perlu tahu tentang metode yang menarik bagi mereka.

Ada beberapa pertanyaan yang belum terjawab di sini. Satu adalah:

Seberapa kecil?

Kamu bilang:

Saat ini saya berurusan dengan ini dengan membagi namespace modul tergantung pada kebutuhan kliennya.

Saya menyebutnya pengetikan manual bebek . Anda membangun antarmuka yang mengekspos hanya apa yang dibutuhkan klien. Prinsip pemisahan antarmuka bukan hanya mengetik bebek manual.

Tetapi ISP juga bukan sekadar panggilan untuk antarmuka peran "koheren" yang dapat digunakan kembali. Tidak ada desain antarmuka peran yang "koheren" yang dapat dengan sempurna melindungi terhadap penambahan klien baru dengan kebutuhan perannya sendiri.

ISP adalah cara untuk mengisolasi klien dari dampak perubahan pada layanan. Itu dimaksudkan untuk membuat build berjalan lebih cepat saat Anda membuat perubahan. Tentu itu memiliki manfaat lain, seperti tidak melanggar klien, tapi itulah poin utamanya. Jika saya mengubah count()tanda tangan fungsi layanan , bagus jika klien yang tidak menggunakan count()tidak perlu diedit dan dikompilasi ulang.

Inilah MENGAPA saya peduli tentang Prinsip Segregasi Antarmuka. Itu bukan sesuatu yang saya anggap penting. Ini memecahkan masalah nyata.

Jadi cara itu harus diterapkan harus menyelesaikan masalah untuk Anda. Tidak ada cara menghafal otak mati untuk menerapkan ISP yang tidak dapat dikalahkan hanya dengan contoh yang tepat dari perubahan yang diperlukan. Anda seharusnya melihat bagaimana sistem berubah dan membuat pilihan yang akan membuat segalanya menjadi tenang. Mari menjelajahi opsi.

Pertama-tama tanyakan pada diri Anda: apakah membuat perubahan pada antarmuka layanan sulit sekarang? Jika tidak, pergilah ke luar dan mainkan sampai Anda tenang. Ini bukan latihan intelektual. Pastikan obatnya tidak lebih buruk dari penyakitnya.

  1. Jika banyak klien menggunakan subset fungsi yang sama, itu berpendapat untuk antarmuka yang dapat digunakan kembali "koheren". Subset ini kemungkinan berfokus pada satu gagasan yang dapat kita pikirkan sebagai peran yang diberikan layanan kepada klien. Sangat menyenangkan ketika ini bekerja. Ini tidak selalu berhasil.

  2.  

    1. Jika banyak klien menggunakan himpunan fungsi yang berbeda, mungkin saja klien tersebut benar-benar menggunakan layanan melalui berbagai peran. Tidak apa-apa tapi itu membuat peran sulit dilihat. Temukan mereka dan cobalah untuk menggoda mereka. Itu dapat menempatkan kita kembali dalam kasus 1. Klien hanya menggunakan layanan melalui lebih dari satu antarmuka. Tolong jangan mulai casting layanan. Jika ada yang berarti melewati layanan ke klien lebih dari sekali. Itu bekerja tetapi itu membuat saya mempertanyakan apakah layanan ini bukan bola lumpur besar yang perlu dipecah.

    2. Jika banyak klien menggunakan himpunan bagian yang berbeda tetapi Anda tidak melihat peran bahkan memungkinkan bahwa klien mungkin menggunakan lebih dari satu maka Anda tidak memiliki yang lebih baik daripada mengetik bebek untuk merancang antarmuka Anda. Cara mendesain antarmuka ini memastikan bahwa klien tidak terpapar pada satu fungsi pun yang tidak digunakan tetapi hampir menjamin bahwa menambahkan klien baru akan selalu melibatkan penambahan antarmuka baru yang sementara implementasi layanan tidak perlu tahu tentang hal itu antarmuka yang akan menggabungkan antarmuka peran. Kami hanya menukar satu rasa sakit dengan yang lain.

  3. Jika banyak klien menggunakan himpunan bagian yang berbeda, tumpang tindih, klien baru diharapkan untuk menambahkan bahwa akan memerlukan himpunan himpunan bagian yang tidak dapat diprediksi, dan Anda tidak ingin memecah layanan kemudian mempertimbangkan solusi yang lebih fungsional. Karena dua opsi pertama tidak berfungsi dan Anda benar-benar berada di tempat yang buruk di mana tidak ada yang mengikuti pola dan lebih banyak perubahan datang maka pertimbangkan untuk menyediakan setiap fungsi antarmuka itu sendiri. Mengakhiri di sini bukan berarti ISP gagal. Jika ada yang gagal itu adalah paradigma berorientasi objek. Antarmuka metode tunggal mengikuti ISP secara ekstrim. Ini adalah pengetikan keyboard yang lumayan tetapi Anda mungkin menemukan ini tiba-tiba membuat antarmuka dapat digunakan kembali. Sekali lagi, pastikan tidak ada

Jadi ternyata mereka bisa menjadi sangat kecil.

Saya menganggap pertanyaan ini sebagai tantangan untuk menerapkan ISP dalam kasus yang paling ekstrem. Namun perlu diingat bahwa ekstrem sebaiknya dihindari. Dalam desain yang dipikirkan dengan baik yang menerapkan prinsip - prinsip PADAT lainnya masalah ini biasanya tidak terjadi atau masalah, hampir sebanyak.


Pertanyaan lain yang belum terjawab adalah:

Siapa yang memiliki antarmuka ini?

Berkali-kali saya melihat antarmuka yang dirancang dengan apa yang saya sebut mentalitas "perpustakaan". Kita semua bersalah atas pengkodean monyet-lihat-monyet-di mana Anda hanya melakukan sesuatu karena itulah yang Anda lakukan. Kami bersalah atas hal yang sama dengan antarmuka.

Ketika saya melihat sebuah antarmuka yang dirancang untuk kelas di perpustakaan saya dulu berpikir: oh, orang-orang ini adalah pro. Ini harus menjadi cara yang tepat untuk melakukan antarmuka. Apa yang saya gagal mengerti adalah bahwa batas perpustakaan memiliki kebutuhan dan masalah sendiri. Untuk satu hal, perpustakaan sama sekali tidak tahu tentang desain klien itu. Tidak semua batas sama. Dan kadang-kadang bahkan batas yang sama memiliki cara berbeda untuk melewatinya.

Berikut adalah dua cara sederhana untuk melihat desain antarmuka:

  • Antarmuka yang dimiliki layanan. Beberapa orang merancang setiap antarmuka untuk mengekspos semua layanan yang dapat dilakukan. Anda bahkan dapat menemukan opsi refactoring di IDE yang akan menulis antarmuka untuk Anda menggunakan kelas apa pun yang Anda berikan.

  • Antarmuka yang dimiliki klien. ISP sepertinya berpendapat bahwa ini benar dan layanan yang dimiliki salah. Anda harus memecah setiap antarmuka dengan kebutuhan klien dalam pikiran. Karena klien memiliki antarmuka, ia harus mendefinisikannya.

Jadi siapa yang benar?

Pertimbangkan plugin:

masukkan deskripsi gambar di sini

Siapa yang memiliki antarmuka di sini? Klien? Pelayanan?

Ternyata keduanya.

Warna di sini adalah lapisan. Lapisan merah (kanan) tidak seharusnya tahu apa-apa tentang lapisan hijau (kiri). Lapisan hijau dapat diubah atau diganti tanpa menyentuh lapisan merah. Dengan begitu setiap lapisan hijau bisa dicolokkan ke lapisan merah.

Saya suka mengetahui apa yang seharusnya diketahui tentang apa, dan apa yang tidak seharusnya diketahui. Bagi saya, "apa yang tahu tentang apa?", Adalah satu-satunya pertanyaan arsitektur yang paling penting.

Mari kita perjelas kosakata:

[Client] --> [Interface] <|-- [Service]

----- Flow ----- of ----- control ---->

Klien adalah sesuatu yang digunakan.

Suatu layanan adalah sesuatu yang digunakan.

Interactor kebetulan keduanya.

ISP mengatakan memecah antarmuka untuk klien. Baik, mari terapkan itu di sini:

  • Presenter(layanan) tidak boleh menentukan Output Port <I>antarmuka. Antarmuka harus dipersempit untuk apa Interactor(di sini bertindak sebagai klien) kebutuhan. Itu berarti antarmuka TAHU tentang Interactordan, untuk mengikuti ISP, harus berubah dengannya. Dan ini baik-baik saja.

  • Interactor(di sini bertindak sebagai layanan) tidak boleh menentukan Input Port <I>antarmuka. Antarmuka harus dipersempit dengan apa yang Controller(klien) butuhkan. Itu berarti antarmuka TAHU tentang Controllerdan, untuk mengikuti ISP, harus berubah dengannya. Dan ini tidak baik.

Yang kedua tidak baik karena lapisan merah tidak seharusnya tahu tentang lapisan hijau. Jadi apakah ISP salah? Yah agak. Tidak ada prinsip yang mutlak. Ini adalah kasus di mana orang-orang bodoh yang menyukai antarmuka untuk menunjukkan segala sesuatu yang dapat dilakukan layanan ternyata benar.

Setidaknya, mereka benar jika Interactortidak melakukan apa pun selain dari kebutuhan use case ini. Jika Interactorhal-hal untuk kasus penggunaan lain tidak ada alasan ini Input Port <I>harus tahu tentang mereka. Tidak yakin mengapa Interactortidak bisa hanya fokus pada satu Use Case jadi ini bukan masalah, tetapi banyak hal terjadi.

Tetapi input port <I>antarmuka tidak bisa menjadi budak bagi Controllerklien dan menjadikannya plugin yang benar. Ini adalah batas 'perpustakaan'. Toko pemrograman yang sama sekali berbeda bisa menulis lapisan hijau bertahun-tahun setelah lapisan merah diterbitkan.

Jika Anda melewati batas 'perpustakaan' dan Anda merasa perlu menerapkan ISP walaupun Anda tidak memiliki antarmuka di sisi lain, Anda harus menemukan cara untuk mempersempit antarmuka tanpa mengubahnya.

Salah satu cara untuk melakukannya adalah adaptor. Letakkan di antara suka klien Controlerdan Input Port <I>antarmuka. Adaptor menerima Interactorsebagai Input Port <I>dan mendelegasikan itu berfungsi untuk itu. Namun, itu hanya mengekspos apa yang Controllerdibutuhkan klien melalui antarmuka peran atau antarmuka yang dimiliki oleh lapisan hijau. Adaptor tidak mengikuti ISP sendiri tetapi memungkinkan kelas yang lebih kompleks ingin Controllermenikmati ISP. Ini berguna jika ada lebih sedikit adapter daripada klien seperti Controlleritu yang menggunakannya dan ketika Anda berada dalam situasi yang tidak biasa di mana Anda melewati batas perpustakaan dan, meskipun diterbitkan, perpustakaan tidak akan berhenti berubah. Melihat Anda, Firefox. Sekarang perubahan itu hanya merusak adaptor Anda.

Jadi apa artinya ini? Itu berarti jujur ​​Anda belum memberikan informasi yang cukup bagi saya untuk memberi tahu Anda apa yang harus Anda lakukan. Saya tidak tahu apakah tidak mengikuti ISP menyebabkan Anda mengalami masalah. Saya tidak tahu apakah mengikutinya tidak akan menyebabkan lebih banyak masalah bagi Anda.

Saya tahu Anda sedang mencari prinsip panduan sederhana. ISP mencoba menjadi seperti itu. Tapi banyak yang tidak terucapkan. Saya percaya akan hal itu. Ya, tolong jangan memaksa klien untuk bergantung pada metode yang tidak mereka gunakan, tanpa alasan yang bagus!

Jika Anda memiliki alasan yang bagus, seperti mendesain sesuatu untuk menerima plugin, maka waspadai masalah tidak mengikuti penyebab ISP (sulit untuk berubah tanpa melanggar klien), dan cara untuk mengurangi mereka (tetap Interactoratau setidaknya Input Port <I>fokus pada satu stabil) gunakan kasing).

candied_orange
sumber
Terima kasih atas masukannya. Saya memiliki modul penyedia layanan yang memiliki banyak klien. Namespace-nya memiliki batasan logis yang koheren, tetapi kebutuhan klien memotong batasan logis ini. Dengan demikian membagi ruang nama berdasarkan batasan logis tidak membantu ISP. Karena itu saya telah membagi namespace berdasarkan kebutuhan klien seperti yang ditunjukkan pada diagram dalam pertanyaan. Tetapi ini membuatnya tergantung pada klien dan cara yang buruk untuk menghubungkan klien ke layanan, karena klien dapat ditambahkan / dihapus relatif sering, tetapi perubahan dalam layanan akan minimal.
work.bin
Saya sekarang condong ke arah layanan yang menyediakan antarmuka yang gemuk, seperti dalam namespace lengkapnya dan terserah klien untuk mengakses layanan ini melalui adaptor khusus klien. Dalam istilah C itu akan menjadi file pembungkus fungsi yang dimiliki oleh klien. Perubahan pada layanan akan memaksa kompilasi ulang adaptor tetapi tidak harus klien. .. <
contd
<contd> .. Ini pasti akan menjaga waktu build minimal dan menjaga sambungan antara klien dan layanan 'longgar' pada biaya runtime (memanggil fungsi wrapper perantara), meningkatkan ruang kode, meningkatkan ruang stack, meningkatkan penggunaan stack dan mungkin lebih banyak ruang pikir (programmer) dalam mempertahankan adaptor.
work.bin
Solusi saya saat ini memuaskan kebutuhan saya sekarang, pendekatan baru akan membutuhkan lebih banyak upaya dan mungkin melanggar YAGNI. Saya harus mempertimbangkan pro dan kontra dari masing-masing metode dan memutuskan ke mana harus pergi ke sini.
work.bin
1

Jadi poin ini:

existent clients are unaffected by the addition (or deletion) of more clients.

Menyerah bahwa Anda melanggar prinsip penting lainnya yaitu YAGNI. Saya akan peduli ketika saya memiliki ratusan klien. Memikirkan sesuatu di muka dan kemudian ternyata Anda tidak memiliki klien tambahan untuk kode ini melebihi tujuannya.

Kedua

 partitioning depends on the nature of clients

Mengapa kode Anda tidak menggunakan DI, inversi ketergantungan, tidak ada, tidak ada di perpustakaan Anda harus tergantung pada sifat klien Anda.

Akhirnya sepertinya Anda memerlukan lapisan tambahan di bawah kode Anda untuk melayani kebutuhan akan hal-hal yang tumpang tindih (DI sehingga kode menghadap depan Anda hanya bergantung pada lapisan tambahan ini, dan klien Anda hanya bergantung pada antarmuka menghadap depan) dengan cara ini Anda mengalahkan KERING.
Ini Anda akan od itu nyata. Jadi Anda membuat barang yang sama yang Anda gunakan di lapisan modul di bawah modul lain. Dengan cara ini memiliki lapisan di bawah yang Anda raih:

untuk klien mana pun hanya data dan API yang diperlukannya yang terlihat; sisa namespace modul disembunyikan dari klien yaitu mematuhi prinsip Segregasi Antarmuka.

Iya

deklarasi tidak diulang dalam beberapa file header yaitu tidak melanggar KERING. modul M tidak memiliki ketergantungan pada kliennya.

Iya

seorang klien tidak terpengaruh oleh perubahan yang dibuat di bagian-bagian modul M yang tidak digunakan olehnya.

Iya

klien yang ada tidak terpengaruh oleh penambahan (atau penghapusan) lebih banyak klien.

Iya

Mateusz
sumber
1

Informasi yang sama seperti yang diberikan dalam deklarasi selalu diulang dalam definisi. Hanya cara bahasa ini bekerja. Juga, mengulangi deklarasi dalam beberapa file header tidak melanggar KERING . Ini adalah teknik yang agak umum digunakan (setidaknya di perpustakaan standar).

Mengulangi dokumentasi atau implementasinya akan melanggar KERING .

Saya tidak akan repot dengan ini kecuali kode klien tidak ditulis oleh saya.

Maciej Chałapuk
sumber
0

Saya menafikan kebingungan saya. Namun contoh praktis Anda menarik solusi di kepala saya. Jika saya dapat mengutarakan kata-kata saya sendiri: semua partisi dalam modul Mmemiliki banyak hubungan eksklusif dengan banyak klien.

Struktur Sampel

M.h      // fat header
 - P1    // Partition 1
 - P2    // ... 2
   - P21 // ... 2 section 1
 - P3    // ... 3
C1.c     // Client 1 (Needs to include P1, P3)
C2.c     // ... 2 (Needs to include P2)
C3.c     // ... 3 (Needs to include P1, P21, P3)

Mh

#ifdef P1
#define _PREF_ P1_             // Define Prefix ("PREF") = P1_
 void _PREF_init();            // Some partition specific function
#endif /* P1 */

#ifdef P2
#define _PREF_ P2_
 void _PREF_init();
#endif /* P2 */

#if defined(P21) || defined (P2) // Part 2.1
#define _PREF_ P2_1_
 void _PREF_oddone();
#endif /* P21 */

#ifdef P3
#define _PREF_ P3_
 void _PREF_init();
#endif /* P3 */

Mc

Dalam file Mc, Anda sebenarnya tidak harus menggunakan #ifdefs karena apa yang Anda masukkan dalam file .c tidak mempengaruhi file klien selama fungsi yang ditentukan oleh file klien digunakan.

#include "M.h"
#define _PREF_ P1_        
void _PREF_init() { ... };

#define _PREF_ P2_
void _PREF_init() { ... }

#define _PREF_ P2_1_
void _PREF_oddone() { ... }

#define _PREF_ P3_
void _PREF_init() { ... }

C1.c

#define P1     // "invite" P1
#define P3     // "invite" P3
#include "M.h" // Open the door, but only the invited come in.

void main()
{
    P1_init();
    //P2_init();
    //P2_1_oddone();
    P3_init();
}

C2.c

#define P2
#include "M.h

void main()
{
    //P1_init();
    P2_init();
    P2_1_oddone();
    //P3_init();
}

C3.c

#define P1
#define P21
#define P3  
#include "M.h" 

void main()
{
    P1_init();
    //P2_init();
    P2_1_oddone();
    P3_init();
}

Sekali lagi, saya tidak yakin apakah ini yang Anda tanyakan. Jadi bawa dengan sebutir garam.

Sanchke Dellowar
sumber
Seperti apa rupa Mc? Apakah Anda mendefinisikan P1_init() dan P2_init() ?
work.bin
@ work.bin Saya kira Mc akan terlihat seperti file .c sederhana dengan pengecualian mendefinisikan namespace antara fungsi.
Sanchke Dellowar
Dengan asumsi kedua C1 dan C2 ada - apa P1_init()dan P2_init()tautan ke?
work.bin
Dalam file Mh / Mc, preprocessor akan diganti _PREF_dengan apa pun yang terakhir kali didefinisikan. Jadi _PREF_init()akan P1_init()karena pernyataan #define terakhir. Kemudian pernyataan define berikutnya akan menetapkan PREF sama dengan P2_, sehingga menghasilkan P2_init().
Sanchke Dellowar