Prinsip Segregasi Antarmuka: Apa yang harus dilakukan jika antarmuka memiliki tumpang tindih yang signifikan?

9

Dari Pengembangan Perangkat Lunak, Prinsip, Pola, dan Praktek Agile: Pearson New International Edition :

Terkadang, metode yang digunakan oleh berbagai kelompok klien akan tumpang tindih. Jika tumpang tindih kecil, maka antarmuka untuk grup harus tetap terpisah. Fungsi umum harus dideklarasikan di semua antarmuka yang tumpang tindih. Kelas server akan mewarisi fungsi-fungsi umum dari masing-masing antarmuka tersebut, tetapi akan mengimplementasikannya hanya sekali.

Paman Bob, berbicara tentang kasus ketika ada tumpang tindih kecil.

Apa yang harus kita lakukan jika ada tumpang tindih yang signifikan?

Katakan kita punya

Class UiInterface1;
Class UiInterface2;
Class UiInterface3;

Class UiIterface : public UiInterface1, public UiInterface2, public UiInterface3{};

Apa yang harus kita lakukan jika ada tumpang tindih yang signifikan antara UiInterface1dan UiInterface2?

q126y
sumber
Ketika saya menemukan antarmuka yang sangat tumpang tindih, saya membuat antarmuka induk, yang mengelompokkan metode umum dan kemudian mewarisi dari yang umum ini untuk membuat spesialisasi. TAPI! Jika Anda tidak pernah ingin orang lain menggunakan antarmuka umum tanpa spesialisasi, maka Anda sebenarnya perlu melakukan duplikasi kode, karena jika Anda memperkenalkan antarmuka umum orang tua, orang dapat menggunakannya.
Andy
Pertanyaannya agak kabur bagi saya, orang bisa menjawab dengan banyak solusi berbeda tergantung dari kasusnya. Mengapa tumpang tindih itu tumbuh?
Arthur Havlicek

Jawaban:

1

Pengecoran

Ini hampir pasti akan menjadi garis singgung lengkap dengan pendekatan buku yang dikutip, tetapi satu cara untuk menyesuaikan diri dengan ISP adalah dengan merangkul pola pikir casting di satu area pusat basis kode Anda menggunakan QueryInterfacependekatan gaya-COM.

Banyak godaan untuk mendesain antarmuka yang tumpang tindih dalam konteks antarmuka murni seringkali berasal dari keinginan untuk membuat antarmuka "mandiri" lebih dari melakukan satu tanggung jawab yang tepat, seperti penembak jitu.

Misalnya, mungkin tampak aneh untuk mendesain fungsi klien seperti ini:

// Returns the absolute position of an entity as the sum
// of its own position and the position of its ancestors.
// `position` and `parenting` parameters should point to the 
// same object.
Vec2i abs_position(IPosition* position, IParenting* parenting)
{
     const Vec2i xy = position->xy();
     auto parent = parenting->parent();
     if (parent)
     {
         // If the entity has a parent, return the sum of the
         // parent position and the entity's local position.
         return xy + abs_position(dynamic_cast<IPosition*>(parent),
                                  dynamic_cast<IParenting*>(parent));
     }
     return xy;
}

... juga sangat jelek / berbahaya, mengingat bahwa kita membocorkan tanggung jawab untuk melakukan pengecoran rawan kesalahan ke kode klien menggunakan antarmuka ini dan / atau melewatkan objek yang sama sebagai argumen beberapa kali ke beberapa parameter yang sama fungsi. Jadi kita akhirnya sering ingin merancang antarmuka yang lebih terdilusi yang mengkonsolidasikan keprihatinan IParentingdan IPositiondi satu tempat, seperti IGuiElementatau sesuatu seperti itu yang kemudian menjadi rentan terhadap tumpang tindih dengan masalah antarmuka ortogonal yang juga akan tergoda untuk memiliki lebih banyak fungsi anggota untuk alasan "swasembada" yang sama.

Mencampur Tanggung Jawab vs. Casting

Ketika merancang antarmuka dengan tanggung jawab ultra-singular yang benar-benar disuling, godaan sering kali adalah menerima beberapa antarmuka yang downcasting atau menggabungkan untuk memenuhi beberapa tanggung jawab (dan karenanya menginjak baik ISP maupun SRP).

Dengan menggunakan pendekatan gaya-COM (hanya QueryInterfacebagian), kami bermain dengan pendekatan downcasting tetapi mengkonsolidasikan casting ke satu tempat sentral dalam basis kode, dan dapat melakukan sesuatu yang lebih seperti ini:

// Returns the absolute position of an entity as the sum
// of its own position and the position of its ancestors.
// `obj` should implement `IPosition` and optionally `IParenting`.
Vec2i abs_position(Object* obj)
{
     // `Object::query_interface` returns nullptr if the interface is
     // not provided by the entity. `Object` is an abstract base class
     // inherited by all entities using this interface query system.
     IPosition* position = obj->query_interface<IPosition>();
     assert(position && "obj does not implement IPosition!");
     const Vec2i xy = position->xy();

     IParenting* parenting = obj->query_interface<IParenting>();
     if (parenting && parenting->parent()->query_interface<IPosition>())
     {
         // If the entity implements IParenting and has a parent, 
         // return the sum of the parent position and the entity's 
         // local position.
         return xy + abs_position(parenting->parent());
     }
     return xy;
}

... tentu saja mudah-mudahan dengan pembungkus tipe-aman dan semua yang Anda dapat membangun secara terpusat untuk mendapatkan sesuatu yang lebih aman daripada pointer mentah.

Dengan ini, godaan untuk mendesain antarmuka yang tumpang tindih sering dikurangi seminimal mungkin. Hal ini memungkinkan Anda untuk mendesain antarmuka dengan tanggung jawab yang sangat tunggal (kadang-kadang hanya satu fungsi anggota di dalamnya) yang Anda dapat mencampur dan mencocokkan semua yang Anda suka tanpa khawatir tentang ISP, dan mendapatkan fleksibilitas mengetik pseudo-bebek saat runtime di C ++ (meskipun tentu saja dengan trade-off dari penalti runtime ke objek permintaan untuk melihat apakah mereka mendukung antarmuka tertentu). Bagian runtime dapat menjadi penting dalam, katakanlah, pengaturan dengan kit pengembangan perangkat lunak di mana fungsi tidak akan memiliki informasi waktu kompilasi dari plugin sebelumnya yang mengimplementasikan antarmuka ini.

Templat

Jika templat adalah suatu kemungkinan (kami memiliki info waktu kompilasi yang diperlukan di muka yang tidak hilang pada saat kami mendapatkan suatu objek, yaitu), maka kami dapat melakukannya:

// Returns the absolute position of an entity as the sum
// of its own position and the position of its ancestors.
// `obj` should have `position` and `parent` methods.
template <class Entity>
Vec2i abs_position(Entity& obj)
{
     const Vec2i xy = obj.xy();
     if (obj.parent())
     {
         // If the entity has a parent, return the sum of the parent 
         // position and the entity's local position.
         return xy + abs_position(obj.parent());
     }
     return xy;
}

... tentu saja dalam kasus seperti itu, parentmetode harus mengembalikan Entitytipe yang sama , dalam hal ini kita mungkin ingin menghindari antarmuka langsung (karena mereka akan sering ingin kehilangan informasi jenis yang mendukung bekerja dengan basis pointer).

Sistem Entitas-Komponen

Jika Anda mulai mengejar pendekatan gaya COM lebih jauh dari sudut pandang fleksibilitas atau kinerja, Anda akan sering berakhir dengan sistem komponen entitas yang mirip dengan apa yang diterapkan mesin game di industri. Pada titik itu Anda akan benar-benar tegak lurus terhadap banyak pendekatan berorientasi objek, tetapi ECS mungkin berlaku untuk desain GUI (satu tempat yang saya renungkan menggunakan ECS di luar fokus berorientasi adegan, tetapi menganggapnya terlambat setelah menetapkan pendekatan gaya COM untuk mencoba di sana).

Perhatikan bahwa solusi gaya-COM ini benar-benar ada di luar sana sejauh desain alat GUI berjalan, dan ECS akan lebih, jadi itu bukan sesuatu yang akan didukung oleh banyak sumber daya. Namun itu pasti akan memungkinkan Anda untuk mengurangi godaan untuk merancang antarmuka yang memiliki tanggung jawab yang tumpang tindih ke minimum absolut, sering menjadikannya tidak menjadi perhatian.

Pendekatan Pragmatis

Alternatifnya, tentu saja, sedikit rileks penjaga Anda, atau desain antarmuka di tingkat granular dan kemudian mulai mewarisi mereka untuk membuat antarmuka kasar yang Anda gunakan, seperti IPositionPlusParentingyang berasal dari keduanya IPositiondanIParenting(semoga dengan nama yang lebih baik dari itu). Dengan antarmuka murni, seharusnya tidak melanggar ISP sebanyak pendekatan monolitik mendalam-hierarki yang biasa diterapkan (Qt, MFC, dll. Di mana dokumentasi sering merasa perlu untuk menyembunyikan anggota yang tidak relevan mengingat tingkat pelanggaran ISP yang berlebihan dengan jenis-jenis itu). desain), sehingga pendekatan pragmatis mungkin hanya menerima beberapa tumpang tindih di sana-sini. Namun pendekatan gaya-COM semacam ini menghindari perlunya membuat antarmuka terkonsolidasi untuk setiap kombinasi yang pernah Anda gunakan. Perhatian "swasembada" sepenuhnya dihilangkan dalam kasus-kasus seperti itu, dan itu akan sering menghilangkan sumber utama godaan untuk merancang antarmuka yang memiliki tanggung jawab yang tumpang tindih yang ingin bertarung dengan SRP dan ISP.


sumber
11

Ini adalah panggilan penilaian yang harus Anda buat, berdasarkan kasus per kasus.

Pertama-tama, ingatlah bahwa prinsip-prinsip SOLID hanyalah ... prinsip. Itu bukan aturan. Itu bukan peluru perak. Itu hanya prinsip. Itu tidak mengambil dari pentingnya mereka, Anda harus selalu cenderung mengikuti mereka. Namun begitu mereka menimbulkan rasa sakit, Anda harus membuangnya sampai Anda membutuhkannya.

Dengan mengingat hal itu, pikirkan mengapa Anda memisahkan antarmuka Anda. Gagasan sebuah antarmuka adalah untuk mengatakan "Jika kode konsumsi ini memerlukan seperangkat metode yang harus diimplementasikan pada kelas yang dikonsumsi, saya perlu menetapkan kontrak pada implementasi: Jika Anda memberi saya objek dengan antarmuka ini, saya dapat bekerja dengan itu. "

Tujuan ISP adalah untuk mengatakan "Jika kontrak yang saya butuhkan hanya sebagian dari antarmuka yang ada, saya tidak boleh memaksakan antarmuka yang ada pada setiap kelas di masa depan yang dapat diteruskan ke metode saya."

Pertimbangkan kode berikut:

public interface A
{
    void X();
    void Y();
}

public class Foo
{
     public void ConsumeX(A a)
     {
         a.X();
     }
}

Sekarang kita memiliki situasi di mana, jika kita ingin meneruskan objek baru ke ConsumeX, ia harus mengimplementasikan X () dan Y () agar sesuai dengan kontrak.

Jadi haruskah kita mengubah kodenya, sekarang, agar terlihat seperti contoh berikut?

public interface A
{
    void X();
    void Y();
}

public interface B
{
    void X();
}

public class Foo
{
     public void ConsumeX(B b)
     {
         b.X();
     }
}

ISP menyarankan kita harus, jadi kita harus condong ke arah keputusan itu. Tetapi, tanpa konteks, sulit untuk memastikan. Apakah mungkin kami akan memperpanjang A dan B? Apakah mungkin mereka akan memperluas secara mandiri? Apakah mungkin B akan pernah menerapkan metode yang tidak diperlukan oleh A? (Jika tidak, kita dapat membuat A berasal dari B.)

Ini adalah panggilan penghakiman yang harus Anda lakukan. Dan, jika Anda benar-benar tidak memiliki cukup informasi untuk melakukan panggilan itu, Anda mungkin harus mengambil opsi paling sederhana, yang mungkin merupakan kode pertama.

Mengapa? Karena mudah untuk berubah pikiran nanti. Saat Anda membutuhkan kelas baru itu, cukup buat antarmuka baru dan terapkan keduanya di kelas lama Anda.

pdr
sumber
1
"Pertama-tama, ingatlah bahwa prinsip-prinsip SOLID hanyalah ... prinsip. Itu bukan aturan. Itu bukan peluru perak. Itu hanya prinsip. Itu bukan untuk mengambil dari pentingnya mereka, Anda harus selalu bersandar terhadap mengikuti mereka. Tapi begitu mereka memperkenalkan tingkat rasa sakit, Anda harus membuang mereka sampai Anda membutuhkannya. " Ini harus ada di halaman pertama setiap buku pola / prinsip desain. Itu harus muncul juga setiap 50 halaman sebagai pengingat.
Christian Rodriguez