Много маленьких классов против логического (но) сложного наследования


24

Мне интересно, что лучше с точки зрения хорошего ООП-дизайна, чистого кода, гибкости и предотвращения запаха кода в будущем. Ситуация с изображением, когда у вас есть много очень похожих объектов, которые вы должны представлять как классы. Эти классы не имеют никакой конкретной функциональности, только классы данных и отличаются только по имени (и контексту). Пример:

Class A
{
  String name;
  string description;
}

Class B
{
  String name;
  String count;
  String description;
}

Class C
{
  String name;
  String count;
  String description;
  String imageUrl;
}

Class D
{
  String name;
  String count;
}

Class E
{
  String name;
  String count;
  String imageUrl;
  String age;
}

Было бы лучше держать их в отдельных классах, чтобы получить «лучшую» читаемость, но с большим количеством повторений кода, или лучше использовать наследование, чтобы быть более СУХИМЫМ?

Используя наследование, вы получите что-то подобное, но имена классов утратят контекстное значение (из-за is-a, а не has-a):

Class A
{
  String name;
  String description;
}

Class B : A
{
  String count;
}

Class C : B
{
  String imageUrl;
}

Class D : C
{
  String age;
}

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

Ответы:


58

Общее правило гласит: «Предпочитать делегирование, а не наследование», а не «избегать всего наследования». Если объекты имеют логические отношения и B может использоваться везде, где ожидается A, рекомендуется использовать наследование. Однако, если у объектов просто есть поля с одинаковыми именами и они не связаны с доменом, не используйте наследование.

Довольно часто стоит задавать вопрос о «причине изменения» в процессе проектирования: если у класса A появляется дополнительное поле, ожидаете ли вы, что у класса B оно тоже будет? Если это правда, объекты имеют общие отношения, и наследование - хорошая идея. Если нет, перенесите небольшое повторение, чтобы сохранить разные понятия в отдельном коде.


Конечно, причина изменений - это хорошая точка зрения, но что в ситуации, где эти классы есть и всегда будут постоянными?
user969153

20
@ user969153: Когда классы действительно будут постоянными, это не имеет значения. Вся хорошая разработка программного обеспечения предназначена для будущего сопровождающего, которому необходимо внести изменения. Если в будущем изменений не будет, все пойдет, но поверьте мне, у вас всегда будут изменения, если вы не запрограммируете мусорную корзину.
13

@ user969153: Как говорится, "причина изменений" - это эвристика. Это поможет вам определиться, не более того.
августа

2
Хороший ответ. Причины для изменения - S в акрониме SOLID дяди Боба, который поможет в разработке хорошего программного обеспечения.
Кли

4
@ user969153, по моему опыту, "где эти классы и всегда будут постоянными?" недопустимый вариант использования :) Мне еще предстоит поработать над значимым по размеру приложением, которое не претерпело совершенно неожиданных изменений.
cdkMoose

18

Используя наследование, вы получите что-то подобное, но имена классов утратят контекстное значение (из-за is-a, а не has-a):

В точку. Посмотрите на это вне контекста:

Class D : C
{
    String age;
}

Какими свойствами обладает D? Ты можешь запомнить? Что если вы еще больше усложните иерархию:

Class E : B
{
    int numberOfWheels;
}

Class F : E
{
    String favouriteColour;
}

И что тогда, если, в ужасе от ужасов, вы хотите добавить поле imageUrl к F, но не к E? Вы не можете получить это из C и E.

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

Продукты также имели идентификатор, имя и описание по тем же причинам. Но они не были компонентами, и не были компонентами продуктов. Вы никогда не увидите две вещи вместе.

Но какой-то разработчик прочитал о DRY и решил, что он удалит все намеки на дублирование из кода. Поэтому он назвал все продуктом и покончил с необходимостью определить эти поля. Они были определены в Product, и все, что требовалось для этих полей, могло быть получено из Product.

Я не могу сказать это достаточно сильно: это не очень хорошая идея.

DRY не об устранении необходимости в общих свойствах. Если объект не является продуктом, не называйте его продуктом. Если Продукт НЕ ТРЕБУЕТ Описания, по самой своей природе он не определяет Описание как Свойство Продукта.

В итоге получилось, что у нас были Компоненты без описаний. Таким образом, мы начали иметь нулевые описания в этих объектах. Не обнуляется. Значение NULL. Всегда. Для любого продукта типа X ... или позже Y ... и N.

Это вопиющее нарушение принципа подстановки Лискова.

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

Если контекст классов делает очевидным, что вы должны, что будет очень редко, тогда и только тогда вы должны рассмотреть общий родительский класс. Но, если это свойства, подумайте, почему вы не используете интерфейс.


4

Наследование - это способ достижения полиморфного поведения. Если у ваших классов нет поведения, то наследование бесполезно. В этом случае я бы даже не рассматривал их как объекты / классы, а вместо этого структуры.

Если вы хотите иметь возможность размещать разные структуры в разных частях вашего поведения, рассмотрите интерфейсы с методами или свойствами get / set, такими как IHasName, IHasAge и т. Д. Это сделает проект намного чище и позволит лучше их составить. вид hiearchies.


3

Разве не для этого нужны интерфейсы? Несвязанные классы с разными функциями, но с похожим свойством.

IName = Interface(IInterface)
  function Getname : String;
  property Name : String read Getname
end;

Class Dog(InterfacedObject, IName)
private
  FName : String;
  Function GetName : String;
Public
  Name : Read Getname;
end;

Class Movie(InterfacedObject, IName)
private
  FCount;
  FName : String;
  Function GetName : String;
Public
  Name : String Read Getname;
  Count : read FCount; 
end;

1

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

Вот пример того, как это можно решить с помощью множественного наследования.

trait Nameable { String name; }
trait Describable { String description; }
trait Countable { Integer count; }
trait HasImageUrl { String imageUrl; }
trait Living { String age; }

class A : Nameable, Describable;
class B : Nameable, Describable, Countable;
class C : Nameable, Describable, Countable, HasImageUrl;
class D : Nameable, Countable;
class E : Nameable, Countable, HasImageUrl, Living;
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.