Mengapa mewarisi kelas dan tidak menambahkan properti?

39

Saya menemukan pohon warisan di basis kode kami (agak besar) yang kira-kira seperti ini:

public class NamedEntity
{
    public int Id { get; set; }
    public string Name { get; set; }
}

public class OrderDateInfo : NamedEntity { }

Dari apa yang bisa saya kumpulkan, ini terutama digunakan untuk mengikat barang di front-end.

Bagi saya, ini masuk akal karena memberi nama konkret ke kelas, daripada mengandalkan generik NamedEntity. Di sisi lain, ada sejumlah kelas yang tidak memiliki properti tambahan.

Apakah ada kelemahan dari pendekatan ini?

robotron
sumber
25
Upside tidak disebutkan: Anda dapat membedakan metode yang hanya relevan dengan OrderDateInfos dari orang-orang yang relevan dengan lainnya NamedEntitys
Caleth
8
Untuk alasan yang sama bahwa jika Anda kebetulan memiliki, katakanlah, Identifierkelas yang tidak ada hubungannya dengan NamedEntitytetapi membutuhkan properti Iddan Nameproperti, Anda tidak akan menggunakannya NamedEntitysebagai gantinya. Konteks dan penggunaan kelas yang tepat lebih dari sekedar properti dan metode yang dimilikinya.
Neil

Jawaban:

15

Bagi saya, ini masuk akal karena memberikan nama konkret ke kelas, daripada mengandalkan NamedEntity generik. Di sisi lain, ada sejumlah kelas yang tidak memiliki properti tambahan.

Apakah ada kelemahan dari pendekatan ini?

Pendekatannya tidak buruk, tetapi ada solusi yang lebih baik tersedia. Singkatnya, sebuah antarmuka akan menjadi solusi yang jauh lebih baik untuk ini. Alasan utama mengapa antarmuka dan pewarisan berbeda di sini adalah karena Anda hanya dapat mewarisi dari satu kelas, tetapi Anda dapat mengimplementasikan banyak antarmuka .

Misalnya, pertimbangkan bahwa Anda telah menamai entitas, dan entitas yang diaudit. Anda memiliki beberapa entitas:

Onebukan entitas yang diaudit atau entitas yang dinamai. Itu sederhana:

public class One 
{ }

Twoadalah entitas bernama tetapi bukan entitas yang diaudit. Pada dasarnya itulah yang Anda miliki sekarang:

public class NamedEntity
{
    public int Id { get; set; }
    public string Name { get; set; }
}

public class Two : NamedEntity 
{ }

Threeadalah entri yang dinamai dan diaudit. Di sinilah Anda mengalami masalah. Anda dapat membuat AuditedEntitykelas dasar, tetapi Anda tidak dapat membuat Threemewarisi baik AuditedEntity dan NamedEntity :

public class AuditedEntity
{
    public DateTime CreatedOn { get; set; }
    public DateTime UpdatedOn { get; set; }
}

public class Three : NamedEntity, AuditedEntity  // <-- Compiler error!
{ }

Namun, Anda mungkin memikirkan solusinya dengan AuditedEntitymewarisi dari NamedEntity. Ini adalah retasan cerdas untuk memastikan bahwa setiap kelas hanya perlu mewarisi (langsung) dari satu kelas lain.

public class AuditedEntity : NamedEntity
{
    public DateTime CreatedOn { get; set; }
    public DateTime UpdatedOn { get; set; }
}

public class Three : AuditedEntity
{ }

Ini masih berfungsi. Tetapi apa yang telah Anda lakukan di sini dinyatakan bahwa setiap entitas yang diaudit secara inheren juga merupakan entitas yang dinamai . Yang membawa saya ke contoh terakhir saya. Fouradalah entitas yang diaudit tetapi bukan entitas yang bernama. Tetapi Anda tidak dapat membiarkan Fourwarisan dari AuditedEntitykarena Anda juga akan membuatnya menjadi NamedEntitywarisan karena antara AuditedEntity andNamedEntity`.

Menggunakan warisan, tidak ada cara untuk membuat keduanya Threedan Fourberfungsi kecuali Anda mulai menduplikasi kelas (yang membuka set masalah yang sama sekali baru).

Menggunakan antarmuka, ini dapat dengan mudah dicapai:

public interface INamedEntity
{
    int Id { get; set; }
    string Name { get; set; }
}

public interface IAuditedEntity
{
    DateTime CreatedOn { get; set; }
    DateTime UpdatedOn { get; set; }
}

public class One 
{ }

public class Two : INamedEntity 
{
    public int Id { get; set; }
    public string Name { get; set; }
}

public class Three : INamedEntity, IAuditedEntity
{
    public int Id { get; set; }
    public string Name { get; set; }
    DateTime CreatedOn { get; set; }
    DateTime UpdatedOn { get; set; }
}

public class Four : IAuditedEntity
{
    DateTime CreatedOn { get; set; }
    DateTime UpdatedOn { get; set; }
}

Satu-satunya kelemahan kecil di sini adalah Anda masih harus mengimplementasikan antarmuka. Tetapi Anda mendapatkan semua manfaat dari memiliki jenis yang dapat digunakan kembali secara umum, tanpa ada kelemahan yang muncul saat Anda memerlukan variasi pada beberapa jenis umum untuk entitas tertentu.

Tapi polimorfisme Anda tetap utuh:

var one = new One();
var two = new Two();
var three = new Three();
var four = new Four();

public void HandleNamedEntity(INamedEntity namedEntity) {}
public void HandleAuditedEntity(IAuditedEntity auditedEntity) {}

HandleNamedEntity(one);    //Error - not a named entity
HandleNamedEntity(two);
HandleNamedEntity(three);  
HandleNamedEntity(four);   //Error - not a named entity

HandleAuditedEntity(one);    //Error - not an audited entity
HandleAuditedEntity(two);    //Error - not an audited entity
HandleAuditedEntity(three);  
HandleAuditedEntity(four);

Di sisi lain, ada sejumlah kelas yang tidak memiliki properti tambahan.

Ini adalah variasi pada pola antarmuka marker , di mana Anda mengimplementasikan antarmuka kosong murni untuk dapat menggunakan jenis antarmuka untuk memeriksa apakah kelas yang diberikan "ditandai" dengan antarmuka ini.
Anda menggunakan kelas bawaan alih-alih antarmuka yang diimplementasikan, tetapi tujuannya sama, jadi saya akan menyebutnya sebagai "kelas bertanda".

Pada nilai nominal, tidak ada yang salah dengan antarmuka / kelas marker. Mereka secara sintaksis dan teknis sah, dan tidak ada kekurangan inheren untuk menggunakannya asalkan penanda secara universal benar (pada waktu kompilasi) dan tidak bersyarat .

Ini adalah persis bagaimana Anda harus membedakan antara pengecualian yang berbeda, bahkan ketika pengecualian itu sebenarnya tidak memiliki sifat / metode tambahan dibandingkan dengan metode dasar.

Jadi pada dasarnya tidak ada yang salah dengan melakukannya, tetapi saya akan menyarankan menggunakan ini dengan hati-hati, memastikan bahwa Anda tidak hanya mencoba untuk menutupi kesalahan arsitektur yang ada dengan polimorfisme yang dirancang dengan buruk.

Flater
sumber
4
"Anda hanya dapat mewarisi dari satu kelas, tetapi Anda dapat mengimplementasikan banyak antarmuka." Itu tidak berlaku untuk setiap bahasa. Beberapa bahasa mengizinkan pewarisan banyak kelas.
Kenneth K.
seperti kata Kenneth, dua bahasa yang cukup populer memiliki kemampuan untuk pewarisan berganda, C ++ dan Python. kelas threedalam contoh Anda dari perangkap tidak menggunakan antarmuka sebenarnya berlaku di C ++ misalnya, sebenarnya ini berfungsi dengan baik untuk keempat contoh. Sebenarnya ini semua tampaknya menjadi pembatasan C # sebagai bahasa daripada kisah peringatan tidak menggunakan warisan ketika Anda harus menggunakan antarmuka.
whn
63

Ini adalah sesuatu yang saya gunakan untuk mencegah polimorfisme digunakan.

Katakanlah Anda memiliki 15 kelas berbeda yang memiliki NamedEntitysebagai kelas dasar di suatu tempat dalam rantai pewarisan mereka dan Anda sedang menulis metode baru yang hanya berlaku untuk OrderDateInfo.

Anda "bisa" hanya menulis tanda tangan sebagai

void MyMethodThatShouldOnlyTakeOrderDateInfos(NamedEntity foo)

Dan berharap dan berdoa tidak ada yang menyalahgunakan sistem tipe untuk mendorong FooBazNamedEntitydi sana.

Atau Anda "bisa" menulis saja void MyMethod(OrderDateInfo foo). Sekarang dipaksakan oleh kompiler. Sederhana, anggun dan tidak bergantung pada orang yang tidak membuat kesalahan.

Juga, seperti yang ditunjukkan @candied_orange , pengecualian adalah kasus yang bagus untuk ini. Sangat jarang (dan maksud saya sangat, sangat, sangat jarang) Anda ingin menangkap semuanya catch (Exception e). Kemungkinan besar Anda ingin menangkap SqlExceptionatau FileNotFoundExceptionatau pengecualian khusus untuk aplikasi Anda. Kelas-kelas itu sering kali tidak memberikan data atau fungsionalitas lebih banyak daripada Exceptionkelas dasar , tetapi mereka memungkinkan Anda untuk membedakan apa yang mereka wakili tanpa harus memeriksanya dan memeriksa bidang jenis atau mencari teks tertentu.

Secara keseluruhan, ini adalah trik untuk mendapatkan sistem tipe yang memungkinkan Anda untuk menggunakan set tipe yang lebih sempit daripada yang Anda bisa jika Anda menggunakan kelas dasar. Maksud saya, Anda bisa mendefinisikan semua variabel dan argumen Anda memiliki tipe Object, tetapi itu hanya akan membuat pekerjaan Anda lebih sulit, bukan?

Becuzz
sumber
4
Maafkan kebaruan saya, tetapi saya ingin tahu mengapa ini tidak akan menjadi pendekatan yang lebih baik untuk membuat OrderDateInfo MEMILIKI objek NamedEntity sebagai properti?
Adam B
9
@ AdamB Itu pilihan desain yang valid. Apakah lebih baik atau tidak tergantung pada apa yang akan digunakan untuk hal-hal lain. Dalam kasus pengecualian, saya tidak bisa membuat kelas baru dengan properti pengecualian karena dengan demikian bahasa (C # untuk saya) tidak akan membiarkan saya membuangnya. Mungkin ada pertimbangan desain lain yang akan menghalangi penggunaan komposisi daripada pewarisan. Komposisi berkali-kali lebih baik. Itu tergantung pada sistem Anda.
Becuzz
17
@ AdamB Untuk alasan yang sama bahwa ketika Anda mewarisi dari kelas dasar dan DO menambahkan metode atau properti yang tidak dimiliki basis, Anda tidak membuat kelas baru dan menjadikannya sebagai properti. Ada perbedaan antara Manusia menjadi mamalia, dan MEMILIKI mamalia.
Logar
8
@ Logog benar ada perbedaan antara saya menjadi mamalia dan mamalia. Tetapi jika saya tetap setia pada mamalia batin saya, Anda tidak akan pernah tahu bedanya. Anda mungkin bertanya-tanya mengapa saya bisa memberikan begitu banyak jenis susu sambil iseng.
candied_orange
5
@ AdamB Anda dapat melakukannya, dan ini dikenal sebagai komposisi-lebih-warisan. Tetapi Anda akan mengganti nama NamedEntityuntuk ini, misalnya untuk EntityName, sehingga komposisi masuk akal ketika dijelaskan.
Sebastian Redl
28

Ini adalah pewarisan favorit saya. Saya menggunakannya sebagian besar untuk pengecualian yang bisa menggunakan nama yang lebih baik, lebih spesifik

Masalah warisan yang biasanya mengarah ke rantai panjang dan menyebabkan masalah yo-yo tidak berlaku di sini karena tidak ada yang memotivasi Anda untuk rantai.

candied_orange
sumber
1
Hmm, poin bagusnya pengecualian. Saya akan mengatakan itu adalah hal yang mengerikan untuk dilakukan. Tapi Anda telah meyakinkan saya sebaliknya dengan kasus penggunaan yang sangat baik.
David Arno
Yo-Yo Ho dan sebotol rum. Jarang orlop itu yar. Itu sering bocor karena kebutuhan oakum yang tepat di antara papan. Untuk Davey Jones loker dengan lubbers tanah apa yang membangunnya. TERJEMAHAN: Jarang rantai pewarisan begitu baik sehingga kelas dasar dapat secara abstrak / efektif diabaikan.
radarbob
Saya pikir itu worth menunjukkan bahwa kelas baru mewarisi dari yang lain tidak menambahkan properti baru - nama kelas baru. Ini persis seperti yang Anda gunakan saat membuat pengecualian dengan nama yang lebih baik.
Logan Pickup
1

IMHO desain kelas salah. Harus.

public class EntityName
{
    public int Id { get; set; }
    public string Text { get; set; }
}

public class OrderDateInfo
{
    public EntityName Name { get; set; }
}

OrderDateInfoHAS A Nameadalah hubungan yang lebih alami dan menciptakan dua kelas yang mudah dipahami yang tidak akan memancing pertanyaan awal.

Metode apa pun yang diterima NamedEntitysebagai parameter seharusnya hanya tertarik padaId dan Nameproperti, sehingga metode tersebut harus diubah untuk menerima EntityName.

Satu-satunya alasan teknis yang saya terima untuk desain aslinya adalah untuk pengikatan properti, yang disebutkan OP. Kerangka omong kosong tidak akan mampu mengatasi properti tambahan dan mengikatnya object.Name.Id. Tetapi jika kerangka kerja mengikat Anda tidak dapat mengatasinya maka Anda memiliki lebih banyak utang teknologi untuk ditambahkan ke daftar.

Saya akan mengikuti jawaban @ Flater, tetapi dengan banyak antarmuka yang mengandung properti Anda akhirnya menulis banyak kode boilerplate, bahkan dengan sifat otomatis C # yang indah. Bayangkan melakukannya di Jawa!

batwad
sumber
1

Kelas memperlihatkan perilaku, dan Struktur Data mengekspos data.

Saya melihat kata kunci kelas, tetapi saya tidak melihat perilaku apa pun. Ini berarti bahwa saya akan mulai melihat kelas ini sebagai struktur data. Dalam nada ini, saya akan ulangi pertanyaan Anda sebagai

Mengapa memiliki struktur data tingkat atas yang umum?

Jadi, Anda dapat menggunakan tipe data tingkat atas. Ini memungkinkan memanfaatkan sistem tipe untuk memastikan kebijakan di set besar struktur data yang berbeda dengan memastikan semua properti ada di sana.

Mengapa memiliki struktur data yang termasuk yang tingkat atas, tetapi tidak menambahkan apa pun?

Jadi, Anda dapat menggunakan tipe data level yang lebih rendah. Ini memungkinkan memasukkan petunjuk ke dalam sistem pengetikan untuk mengekspresikan tujuan variabel

Top level data structure - Named
   property: name;

Bottom level data structure - Person

Dalam hierarki di atas, kami merasa nyaman untuk menentukan bahwa Seseorang dinamai, sehingga orang dapat memperoleh dan mengubah nama mereka. Meskipun mungkin masuk akal untuk menambahkan properti tambahan ke Personstruktur data, masalah yang kami selesaikan tidak memerlukan pemodelan Person yang sempurna, dan karenanya kami lalai untuk menambahkan properti umum seperti age, dll.

Jadi ini adalah pengungkit dari sistem pengetikan untuk mengekspresikan maksud dari item Bernama ini dengan cara yang tidak putus dengan pembaruan (seperti dokumentasi) dan dapat diperpanjang pada tanggal yang terakhir dengan mudah (jika Anda merasa Anda benar-benar membutuhkan agebidang nanti ).

Edwin Buck
sumber