Багато малих класів проти логічного (але) складного успадкування


24

Мені цікаво, що краще в плані хорошого дизайну OOP, чистого коду, гнучкості та уникнення запахів коду в майбутньому. Образна ситуація, коли у вас є дуже багато подібних об'єктів, які вам потрібно представити як класи. Ці класи не мають жодної конкретної функціональності, просто класи даних і відрізняються лише назвою (і контекстом) Приклад:

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, not 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, добре використовувати практику успадкування. Однак якщо об'єкти просто мають поля з однаковою назвою і не мають доменних відносин, не використовуйте успадкування.

Досить часто окупається, щоб задати питання про "причину зміни" в процесі проектування: Якщо клас А отримає додаткове поле, чи очікуєте ви, що клас Б отримає його також? Якщо це правда, об’єкти поділяють відносини, і успадкування є хорошою ідеєю. Якщо ні, то потерпіть невелике повторення, щоб зберегти чіткі поняття в чіткому коді.


Звичайно, причина зміни - це хороший момент, але що в ситуації, коли ці заняття є і завжди будуть постійними?
user969153

20
@ user969153: Коли заняття справді будуть постійними, це не має значення. Вся хороша інженерія програмного забезпечення - це для майбутнього технічного обслуговування, якому потрібно внести зміни. Якщо майбутніх змін не буде, все піде, але повірте, зміни завжди будуть, якщо ви не запрограмуєте кошик для сміття.
титон

@ user969153: Як сказано, "причина змін" є евристичною. Це допомагає вам вирішити, нічого більше.
титон

2
Гарна відповідь. Причини зміни - S у SOLID-акронімі дядька Боба, який допоможе в розробці хорошого програмного забезпечення.
Клі

4
@ user969153, На мій досвід, "де ці заняття є і завжди будуть постійними?" це неправдивий випадок використання :) Мені ще потрібно працювати над значущим розміром додатком, який не зазнав абсолютно несподіваних змін.
cdkMoose

18

Використовуючи успадкування, ви закінчите щось подібне, але назви класів втратять його контекстуальне значення (через is-a, not has-a):

Саме так. Подивіться на це поза контекстом:

Class D : C
{
    String age;
}

Якими властивостями володіє D? Ви можете пригадати? Що робити, якщо ускладнити ієрархію далі:

Class E : B
{
    int numberOfWheels;
}

Class F : E
{
    String favouriteColour;
}

І що робити, якщо, жах на жахи, ви хочете додати поле imageUrl до F, але не E? Ви не можете отримати це з C і E.

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

Із подібних причин продукти також мали ідентифікатор, ім'я та опис. Але вони не були компонентами, а не компонентами. Ви ніколи не побачили двох речей разом.

Але, якийсь розробник прочитав про DRY і вирішив видалити всі підказки про дублювання з коду. Тож він назвав усе продуктом і усунув необхідність визначити ці поля. Вони були визначені у Продукті, і все, що потрібно для цих полів, може отримати від продукту.

Я не можу сказати це досить сильно: Це не дуже гарна ідея.

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

Зрештою, у нас були компоненти без описів. Таким чином, у нас почалися нульові описи в цих об'єктах. Не зводиться до нуля. Нуль. Завжди. Для будь-якого виробу типу 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.