Зачем наследовать класс, а не добавлять свойства?

39

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

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

public class OrderDateInfo : NamedEntity { }

Из того, что я мог бы собрать, это прежде всего используется, чтобы связать вещи на переднем конце.

Для меня это имеет смысл, так как дает конкретное имя классу, а не полагается на общее NamedEntity. С другой стороны, существует ряд таких классов, которые просто не имеют дополнительных свойств.

Есть ли недостатки этого подхода?

Robotron
источник
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);

С другой стороны, существует ряд таких классов, которые просто не имеют дополнительных свойств.

Это вариант шаблона интерфейса маркера , где вы реализуете пустой интерфейс исключительно для того, чтобы иметь возможность использовать тип интерфейса для проверки, помечен ли данный класс этим интерфейсом.
Вы используете унаследованные классы вместо реализованных интерфейсов, но цель та же, поэтому я буду называть это «помеченным классом».

На первый взгляд, нет ничего плохого в маркерных интерфейсах / классах. Они синтаксически и технически действительны, и при их использовании нет никаких недостатков при условии, что маркер универсально верен (во время компиляции) и не является условным .

Именно так вы должны различать разные исключения, даже если эти исключения на самом деле не имеют каких-либо дополнительных свойств / методов по сравнению с базовым методом.

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

Flater
источник
4
«Вы можете наследовать только от одного класса, но вы можете реализовать много интерфейсов». Это не относится ко всем языкам, хотя. Некоторые языки допускают наследование нескольких классов.
Кеннет К.
как сказал Кеннет, два довольно популярных языка имеют возможность множественного наследования, C ++ и Python. class 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, но это только усложнит вашу работу, не так ли?

Becuzz
источник
4
Прошу прощения за мою новизну, но мне любопытно, почему не было бы лучше сделать так, чтобы OrderDateInfo ИМЕЛО объект NamedEntity как свойство?
Адам Б
9
@AdamB Это правильный выбор дизайна. Лучше это или нет, зависит от того, для чего это будет использоваться среди других вещей. В случае исключения я не могу создать новый класс со свойством исключения, потому что тогда язык (для меня C #) не позволит мне его выбросить. Могут быть и другие конструктивные соображения, которые исключают использование композиции, а не наследования. Много раз композиция лучше. Это зависит только от вашей системы.
Becuzz
17
@AdamB По той же причине, по которой вы наследуете от базового класса и ДО добавляете методы или свойства, в которых отсутствует базовый класс, вы не создаете новый класс и не помещаете базовый класс в качестве свойства. Есть разница между человеком, являющимся млекопитающим, и имеющим млекопитающее.
Logarr
8
@Logarr правда, есть разница между мною и млекопитающим. Но если я останусь верным своему внутреннему млекопитающему, ты никогда не узнаешь разницу. Вы можете удивиться, почему я могу давать так много разных видов молока по прихоти.
Candied_Orange
5
@AdamB Вы можете сделать это, и это называется композицией по наследованию. Но вы бы переименовали NamedEntityэто, например EntityName, в так, чтобы композиция имела смысл при описании.
Себастьян Редл
28

Это мое любимое использование наследства. Я использую его в основном для исключений, которые могли бы использовать более точные имена

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

candied_orange
источник
1
Хм, хорошая точка повторных исключений. Я собирался сказать, что это было ужасно. Но вы убедили меня в другом с отличным вариантом использования.
Дэвид Арно
Йо-Йо Хо и бутылка рома. Это редкий орлоп. Это часто протекает из-за отсутствия надлежащего дуба между досками. К шкафчику Дэйви Джонса с землевладельцами, которые его построили. Перевод: редко цепочка наследования настолько хороша, что базовый класс можно абстрактно / эффективно игнорировать.
Радар Боб
Я думаю, что стоит отметить, что новый класс, наследуемый от другого , добавляет новое свойство - имя нового класса. Это именно то, что вы используете при создании исключений с лучшими именами.
Логан Пикап
1

ИМХО дизайн класса неправильный. Должен быть.

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

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

OrderDateInfoИмеет ли A Nameболее естественное отношение и создает два простых для понимания класса, которые не вызвали бы первоначальный вопрос.

Любой метод , который принят в NamedEntityкачестве параметра должен быть только заинтересован в Idи Nameсвойствах, так что любые такие методы должны быть изменены , чтобы принять EntityNameвместо этого.

Единственная техническая причина, по которой я согласился бы с оригинальным дизайном, - это привязка свойств, о которой упоминал ОП. Каркас дерьма не сможет справиться с дополнительным свойством и привязаться к нему object.Name.Id. Но если ваша обязательная структура не справляется с этим, у вас есть еще один технический долг, который можно добавить в список.

Я согласился бы с ответом @ Flater, но с большим количеством интерфейсов, содержащих свойства, вы в итоге написали много стандартного кода, даже с прекрасными автоматическими свойствами C #. Представьте, что вы делаете это на Java!

batwad
источник
1

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

Я вижу ключевые слова класса, но не вижу никакого поведения. Это означает, что я бы начал рассматривать этот класс как структуру данных. В этом духе я перефразирую ваш вопрос как

Почему общая структура данных верхнего уровня?

Таким образом, вы можете использовать тип данных верхнего уровня. Это позволяет использовать систему типов для обеспечения политики для больших наборов различных структур данных, обеспечивая наличие всех свойств.

Зачем иметь структуру данных, которая включает в себя верхний уровень, но ничего не добавляет?

Таким образом, вы можете использовать тип данных более низкого уровня. Это позволяет вводить подсказки в систему ввода, чтобы выразить назначение переменной

Top level data structure - Named
   property: name;

Bottom level data structure - Person

В приведенной выше иерархии нам удобно указывать имя человека, чтобы люди могли получать и изменять свое имя. Хотя было бы разумно добавить дополнительные свойства в Personструктуру данных, решаемая нами проблема не требует идеального моделирования Person, поэтому мы не добавили общие свойства, такие как ageи т. Д.

Таким образом, использование системы ввода в действие позволяет выразить намерение этого Именованного элемента так, чтобы оно не нарушало обновления (например, документацию) и могло быть легко расширено ageпозднее (если вы обнаружите, что поле действительно нужно позже ).

Эдвин Бак
источник