Чому успадковувати клас та не додавати властивості?


39

Я знайшов дерево спадкування у нашій (досить великій) кодовій базі, яка має щось подібне:

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

public class OrderDateInfo : NamedEntity { }

З того, що я міг зібрати, це в першу чергу використовується для прив’язки речей на передній частині.

Для мене це має сенс, оскільки воно дає конкретну назву класу, а не покладаючись на загальне NamedEntity. З іншого боку, існує ряд таких класів, які просто не мають додаткових властивостей.

Чи є якісь недоліки у цього підходу?


25
Нагорі не згадується: Ви можете відрізнити методи, що стосуються лише OrderDateInfos, від тих, які стосуються інших NamedEntitys
Caleth

8
З тієї ж причини, що якби у вас, скажімо, був Identifierклас, який не має нічого спільного, NamedEntityале вимагає Idі Nameвласності, і власності, ви не використовували б NamedEntityнатомість. Контекст та правильне використання класу - це не просто властивості та методи, якими він володіє.
Ніл

Відповіді:


15

Для мене це має сенс, оскільки воно дає конкретну назву класу, а не покладатися на загальну NamedEntity. З іншого боку, існує ряд таких класів, які просто не мають додаткових властивостей.

Чи є якісь недоліки у цього підходу?

Підхід непоганий, але є кращі рішення. Коротше кажучи, інтерфейс був би набагато кращим рішенням для цього. Основна причина, чому інтерфейси та успадкування тут різні, полягає в тому, що ви можете успадковувати лише один клас, але ви можете реалізувати багато інтерфейсів .

Наприклад, врахуйте, що ви назвали суб’єкти та суб'єкти, що перевіряються. У вас є кілька об'єктів:

Oneне є суб'єктом аудиту, ані іменованним суб'єктом. Все просто:

public class One 
{ }

Twoє названим суб'єктом господарювання, але не є аудитором. Це по суті те, що у вас зараз:

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

public class Two : NamedEntity 
{ }

Threeє іменованим, і ревізованим записом. Тут ви стикаєтеся з проблемою. Ви можете створити AuditedEntityбазовий клас, але ви не можете зробити Threeспадкове і те, AuditedEntity і NamedEntity :

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

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

Однак ви можете подумати про вирішення проблеми, отримавши AuditedEntityспадок від NamedEntity. Це розумний хак для того, щоб забезпечити, що кожен клас повинен успадковувати (безпосередньо) один клас.

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

public class Three : AuditedEntity
{ }

Це все ще працює. Але те, що ви тут зробили, - це те, що кожен суб'єкт, що перевіряється, по суті є також названою сутністю . Що підводить мене до мого останнього прикладу. Fourє аудиторською організацією, але не іменованою організацією. Але ви не можете дозволити спадщині Fourз того, AuditedEntityяк ви тоді також зробили б це NamedEntityзавдяки спадщині між AuditedEntity andNamedEntity ".

Використовуючи спадщину, немає способу зробити так Threeі Fourпрацювати, якщо ви не почнете дублювати класи (що відкриває цілий новий набір проблем).

Використовуючи інтерфейси, цього легко досягти:

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; }
}

Єдиним незначним недоліком тут є те, що вам все одно доведеться реалізувати інтерфейс. Але ви отримуєте всі переваги від загального багаторазового використання без будь-яких недоліків, які виникають, коли вам потрібні варіанти для декількох загальних типів для даної сутності.

Але ваш поліморфізм залишається недоторканим:

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);

З іншого боку, існує ряд таких класів, які просто не мають додаткових властивостей.

Це зміна схеми інтерфейсу маркера , де ви реалізуєте порожній інтерфейс виключно, щоб мати можливість використовувати тип інтерфейсу, щоб перевірити, чи заданий клас "позначений" цим інтерфейсом.
Ви використовуєте успадковані класи замість реалізованих інтерфейсів, але мета та сама, тому я буду називати це "позначеним класом".

З номіналом немає нічого поганого в маркерних інтерфейсах / класах. Вони синтаксично і технічно справедливі, і немає властивих недоліків їх використання за умови, що маркер є універсальним істинним (під час компіляції) і не є умовним .

Саме так слід розмежовувати різні винятки, навіть коли ці винятки насправді не мають додаткових властивостей / методів порівняно з базовим методом.

Так що в цьому немає нічого поганого, але я б радив використовувати це обережно, переконуючись, що ви не просто намагаєтесь приховати існуючу архітектурну помилку погано розробленим поліморфізмом.


4
"Ви можете успадковувати лише один клас, але ви можете реалізувати багато інтерфейсів." Але це правда не для кожної мови. Деякі мови дозволяють успадковувати кілька класів.
Кеннет К.

як сказав Кеннет, дві досить популярні мови мають можливість множинного успадкування, C ++ та Python. клас threeу вашому прикладі підводних каменів невикористання інтерфейсів дійсно дійсний, наприклад, на C ++, адже це працює нормально для всіх чотирьох прикладів. Насправді це все, мабуть, є обмеженням C # як мови, а не застережливою історією про не успадкування, коли вам слід використовувати інтерфейси.
WHN

63

Це те, що я використовую, щоб запобігти використанню поліморфізму.

Скажімо, у вас є 15 різних класів, які є NamedEntityбазовим класом десь у ланцюжку успадкування, і ви пишете новий метод, застосовний лише до OrderDateInfo.

Ви "могли" просто написати підпис як

void MyMethodThatShouldOnlyTakeOrderDateInfos(NamedEntity foo)

І сподівайтесь і моліться, щоб ніхто не зловживав системою типів, щоб загнати FooBazNamedEntityтуди.

Або ви "могли" просто написати void MyMethod(OrderDateInfo foo). Тепер це застосовується компілятором. Простий, елегантний і не покладається на те, щоб люди не помилялися.

Також, як зазначав @candied_orange , винятки є чудовим випадком цього. Дуже рідко (і я маю на увазі дуже, дуже, дуже рідко) ви коли-небудь хочете наздогнати все catch (Exception e). Швидше за все, ви хочете знайти SqlExceptionабо FileNotFoundExceptionдодаток або спеціальний виняток для вашої програми. Ці класи часто не дають більше даних або функціональних можливостей, ніж базовий Exceptionклас, але вони дозволяють розмежувати те, що вони представляють, не потребуючи їх огляду та перевірки поля типу або пошуку конкретного тексту.

Загалом, це хитрість отримати систему типів, щоб дозволити використовувати більш вузький набір типів, ніж ви могли б, якщо ви використовували базовий клас. Я маю на увазі, ви можете визначити всі свої змінні та аргументи як такі, що мають тип Object, але це просто ускладнить вашу роботу, чи не так?


4
Вибачте мою новизну, але мені цікаво, чому це не буде кращим підходом, щоб OrderDateInfo мав об’єкт NamedEntity як властивість?
Адам Б

9
@AdamB Це правильний вибір дизайну. Краще це чи ні, залежить від того, для чого він буде використовуватися, серед іншого. У випадку винятку я не можу створити новий клас із властивістю винятку, оскільки тоді мова (C # для мене) не дозволить мені його кинути. Можуть бути й інші міркування щодо дизайну, які б перешкоджали використанню композиції, а не успадкування. У багато разів склад краще. Це просто залежить від вашої системи.
Becuzz

17
@AdamB З тієї ж причини, що при успадкуванні від базового класу та додаванні методів чи властивостей базовому бракує, ви не створюєте новий клас і ставите базовий як властивість. Існує відмінність між БУДИМОЮ СУЧАСЬКОЮ людиною і МАЙСЬКОЮ.
Логарр

8
@Logarr правда, є різниця між тим, що я буду ссавцем і маю ссавцем. Але якщо я залишаюся вірним своєму внутрішньому ссавцю, ти ніколи не дізнаєшся різниці. Вам може бути цікаво, чому я можу давати стільки різних видів молока на примху.
candied_orange

5
@AdamB Ви можете це зробити, і це відомо як композиція над успадкуванням. Але ви б перейменували NamedEntityце, наприклад EntityName, на так, щоб композиція мала сенс при описі.
Себастьян Редл

28

Це моє улюблене використання спадщини. Я використовую його здебільшого для виключень, які можуть використовувати кращі, конкретніші імена

Звичайне питання спадкування, що веде до довгих ланцюгів і викликає проблему йо-йо , не стосується тут, оскільки немає нічого, що мотивує вас зав'язати ланцюг.


1
Хм, хороші точкові винятки. Я збирався сказати, що це було жахливо робити. Але ви переконали мене в іншому випадку з відмінною справою використання.
Девід Арно

Йо-Йо Хо і пляшка рому. Це рідкісний орлоп - яр. Він часто протікає, якщо не хоче належного дуба між дошками. До шафки Дейві Джонса з землею, що її збудувала. ПЕРЕКЛАД: Рідко ланцюг успадкування настільки хороший, що базовий клас може абстрактно / ефективно ігнорувати.
radarbob

Я думаю, що варто зазначити, що новий клас, успадкований від іншого , додає нову властивість - назву нового класу. Це саме те, що ви використовуєте, створюючи винятки з кращими іменами.
Логан Пікап

1

ІМХО дизайн класу неправильний. Вона повинна бути.

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

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

OrderDateInfoHAS A Nameє більш природним відношенням і створює два простих для розуміння класи, які б не спровокували оригінальне запитання.

Будь-який метод, прийнятий NamedEntityяк параметр, повинен був зацікавити лише властивості Idта Nameвластивості, тому будь-які подібні методи слід замінити, щоб прийняти EntityNameзамість них.

Єдина технічна причина, яку я прийняв би за оригінальний дизайн, - це прив'язка власності, про яку згадувала ОП. Crap Framework не зможе впоратись із зайвою властивістю та прив’язатись до нього object.Name.Id. Але якщо ваша зобов’язальна рамка не справляється з цим, у вас є ще кілька технічних боргів, які потрібно додати до списку.

Я б ішов разом з відповіддю @ Флатера, але з великою кількістю інтерфейсів, що містять властивості, ви в кінцевому підсумку пишете багато кодового шаблону, навіть із чудовими автоматичними властивостями C #. Уявіть, що ви робите це на Java!


1

Класи викривають поведінку, а структури даних викривають дані.

Я бачу ключові слова класу, але не бачу жодної поведінки. Це означає, що я б почав розглядати цей клас як структуру даних. У цьому сенсі я переформулюю ваше питання як

Чому є спільна структура даних верхнього рівня?

Таким чином, ви можете використовувати тип даних верхнього рівня. Це дозволяє використовувати систему типів для забезпечення політики у великих наборах різних структур даних, гарантуючи, що властивості існують.

Чому є структура даних, що включає верхній рівень, але нічого не додає?

Таким чином, ви можете використовувати тип даних нижчого рівня. Це дозволяє вводити підказки в систему набору тексту для вираження мети змінної

Top level data structure - Named
   property: name;

Bottom level data structure - Person

У вищевказаній ієрархії ми вважаємо зручним вказати, що ім’я називається Особою, щоб люди могли отримати та змінити своє ім’я. Хоча може бути доцільним додати додаткові властивості до Personструктури даних, проблема, яку ми вирішуємо, не потребує досконалого моделювання Особи, і тому ми нехтуємо додаванням загальних властивостей, наприклад ageтощо.

Таким чином, це використання системи набору тексту для вираження наміру цього найменування елемента таким чином, що не порушується з оновленнями (як-от документація) і може бути розширено на останній день легко (якщо ви знайдете, що вам справді потрібне ageполе пізніше ).

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.