Страницы

Поиск по вопросам

среда, 27 ноября 2019 г.

Как спроектировать иерархию классов, чтобы сделать простым добавление свойств в базовый класс?

#c_sharp #ооп #шаблоны_проектирования


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

Имеем, например, такую иерархию классов

class A_Base
{
  int x;
  int y;

  public A_Base(int x, int y) 
  { 
    this.x = x;
    this.y = y;
  }
}

class A_First : A_Base
{
  string name;

  public A_First(int x, int y, string name):base(x,y) 
  { 
    this.name = name; 
  }
}

class A_Second : A_First
{
  string surname;

  public A_Second(int x, int y, string name, string surname):base(x,y,name)
  {
    this.surname = surname;
  }
}


Теперь нам понадобилось в A_Base добавить ещё одно свойство (и соответственно, инициализировать
его в конструкторе)

class A_Base
{
  int x;
  int y;
  int z;

  public A_Base(int x, int y, int z) 
  { 
    this.x = x;
    this.y = y;
    this.z = z;
  }
}


Значит, придется во всей иерархии классов менять определение конструктора, дописывая
туда новый параметр z, хотя сами эти конструкторы мы не меняем - меняем только вызов
родителя. И как-то это не очень хорошо.

class A_First : A_Base
{
  string name;

  public A_First(int x, int y, int z, string name):base(x,y,z) 
  { 
    this.name = name; 
  }
}


Как у продвинутых ООП-товарищей принято поступать в подобных случаях?
Заводить отдельный класс/структуру, которая будет параметром для конструктора?

class A_Init
{
  int x;
  int y;
  int z;
}

class A_Base
{
  int x;
  int y;
  int z;

  public A_Base(A_Init init) 
  { 
    this.x = init.x;
    this.y = init.y;
    this.z = init.z;
  }
}


или есть ещё какой-нибудь хитрый паттерн?
    


Ответы

Ответ 1



tl;dr Решений может быть несколько, и зависят они от конкретной ситуации. В данном конкретном случае я бы обратил внимание на причину изменения спецификации. Откуда появился новый параметр z, почему он был не предусмотрен сразу? Ответ на этот вопрос поможет спроектировать иерархию правильно. Например (вариант №1), эксперты предметной области не могут описать все возможные сценарии. Это означает, что по мере работы над программой свойства будут появляться и изменяться, и это совершенно нормально с точки зрения бизнеса. Тогда да, структура типа A_Init будет подходящим решением. С другой стороны, если эта структура хранит значения по умолчанию для параметров x, y, z, то не лучше ли сразу сделать в иерархии пустой конструктор или конструктор только с самыми необходимыми параметрами, список которых точно не будет изменяться ©? Второй способ корректнее в том числе и потому, что поможет решить ту же проблему на всех уровнях иерархии. A_Init не гарантирует, что не придётся заводить A_Init2 для какого-нибудь из наследников. Потому: используйте значения по умолчанию везде, где это возможно. Передавайте через конструктор только инициализаторы для readonly полей. Вариант №2: идёт предварительное проектирование системы, пишется прототип. Для прототипа не так страшно, что изменения в код вносятся часто. Собственно, для выявления узких мест он и разрабатывается. Тогда ответ: ничего страшного не происходит, так и должно быть. Третий вариант: наследники являются на самом деле не наследниками, а декораторами. Тогда и реализация выглядит по-другому: class A_First : A_Base { A_Base decorating; string name; public A_First(A_Base decirating, string name) { this.decorating = decorating; this.name = name; } } Как понять, что речь идёт об этой ситуации? Обычно так, что A_Base является абстрактным классом, который почти ничего не умеет, а вот его наследники и выполняют основную работу. Четвёртый вариант является расширением варианта первого. В случае, если значения по умолчанию существуют, но алгоритм их получения запутан, логику их получения/вычисления можно вынести в отдельный класс, который Эрик Эванс назвал бы регламентом. Тогда все классы иерархии получали бы в конструкторе один или несколько регламентов и инициализировались бы в соответствии с правилами бизнес-логики. Отличие A_Init от регламента не в синтаксисе, но в сути. Если A_Init не содержит бизнес-логики, а только возвращает константы, то это, вероятно, не регламент.

Ответ 2



Это один из недостатков наследования - если меняется родительский класс, то изменения скорее всего будут влиять на все дочерние. И ничего тут поделать нельзя. Особенно этот недостаток будет проявляться в долгосрочных и крупных программах. Чтобы избежать таких вещей, можно подумать о замене наследования композицией или ознакомиться с проектированием по принципам SOLID, или посмотреть на другие парадигмы, например "Компонентно-ориентированное программирование". В последнем каждый класс строится из компонентов, наследование не используется, соответственно проблема исчезает, и вероятность ошибок существенно меньше, но когда компонентов много, то будет сложно в них ориентироваться.

Ответ 3



В рамках языка C# — никак. Это частный случай проблемы хрупкого базового класса. Вы должны понимать, что изменение базового класса (от которого есть или могут быть где-то производные классы) представляет собой breaking change всегда, и требует внимательного пересмотра всего кода. Поэтому старайтесь планировать заранее базовые публичные открытые для наследования классы так, чтобы их публичный интерфейс не нужно было менять в будущем; старайтесь сопротивляться таким изменениям; если всё же изменений базового класса не избежать, оповещайте всех возможных клиентов вашего базового класса о ваших изменениях, чтобы они пересмотрели свой код; старайтесь делать изменения так, чтобы на место, в котором требуется изменение, указывал компилятор. плохой пример: ваша функция принимала угол в градусах как число типа double, теперь она принимает угол в радианах также как число типа double пример получше: ваша функция принимала угол в градусах, теперь у вас появилась функция с новым именем, которая принимает угол в радианах, а старая маркирована атрибутом [Obsolete] И да, модификация базового класса нарушает Open/closed principle из SOLID.

Комментариев нет:

Отправить комментарий