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


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.