Подтвердить что ты не робот

Объявление типа детей в базовом классе; Это плохо или нет?

Недавно я наткнулся на некоторый код, который объявил типы children как перечисление в базовом классе. Вот простой пример:

public enum EmployeeType
{
    Manager,
    Secretary
}

public class Employee
{
    public string Code { get; set; }

    public EmployeeType Type { get; set; }
}

public class Manager : Employee
{
    public void Manage()
    {
        // Managing
    }
}

public class Secretary : Employee
{
    public void SetMeeting()
    {
        // Setting meeting
    }
}

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

  • Расширяемость: эта конструкция не будет расширяемой, потому что если вы хотите определить другой производный класс, скажем, Developer, например, вы также должны обновить перечисление EmployeeType, к которому у вас может не быть доступа.
  • Парадоксальное определение: теперь вы можете написать этот код:

    Secretary secretary = new Secretary();
    secretary.EmployeeType = EmployeeType.Manager;
    /*
    This is absurd semantically. But syntactically, it possible.
    */
    

Однако, когда я прочитал статью Википедии о наследовании, я не смог найти ответа на мой вопрос.

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

4b9b3361

Ответ 1

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

Когда вы устанавливаете отношения наследования между двумя классами, вы получаете возможность использовать динамическое связывание и полиморфизм.
Динамическое связывание означает во время выполнения, реализация метода которого на основе класса объекта Полиморфизм означает, что вы можете использовать переменную типа суперкласса для хранения ссылки на объект, класс которого является суперклассом или любым из его подклассов.

Одним из основных преимуществ динамического связывания и полиморфизма является то, что они могут помочь сделать код более легким для изменения. Если у вас есть фрагмент кода, который использует переменную типа суперкласса, например Employee, позднее вы можете создать совершенно новый подкласс, такой как менеджер, а старый фрагмент кода будет работать без изменений с экземплярами нового подкласса. Если Менеджер переопределяет любой из методов Employee, который вызывается фрагментом кода, динамическое связывание гарантирует выполнение Managers этих методов. Это будет верно, хотя класс Manager не существует, когда фрагмент кода был записан и скомпилирован.

Теперь вернемся к вопросу, почему нам нужен EmployeeType в Employee?
Могут быть функции, которые мы хотели бы реализовать в Employee, AKA CalculateSalary()

CalculateSalary(){
if (EmployeeType == EmployeeType.Manager) // Add management percent }

Искушение причины, но что, если Employee является одновременно менеджером и супервизором?
Это будет сложно! Просто мы можем реализовать CalculateSalary() как абстрактный метод, но мы потеряем цель!
Вот правило Coad:
Подкласс выражает особый тип, а не играет роль.
Замена Лискова Принцип - это тест на "Должен ли я наследовать от этого типа?"

Роль большого пальца для выше:

  • Does TypeB хочет разоблачить полный интерфейс (все общедоступные методы не менее) типа A, так что TypeB можно использовать там, где TypeA ожидается? Указывает наследование. Биплан Cessna откроет полный интерфейс самолета, если не больше. Так что это делает пригодный для самолёта.
  • Может ли TypeB получать только часть/часть поведения, отображаемого TypeA? Указывает на необходимость композиции. Птице может понадобиться только муха поведение самолета. В этом случае имеет смысл извлечь его как интерфейс/класс/оба и сделать его членом как классы.

Как нота из javaworld:
Убедитесь, что наследование моделирует отношения is-a.
Не используйте наследование только для повторного использования кода.
Не используйте наследование только для получения полиморфизма.
Оплатить композицию над наследством.

Я думаю, что в сценариях, подобных этому, роль менеджера - это роль, которую играет сотрудник, эта роль может меняться со временем, роль будет реализована как композиция. В качестве другого аспекта представления в модели домена и сопоставления его с реляционной базой данных. Очень сложно сопоставить наследование модели SQL (в итоге вы создаете столбцы, которые не всегда используются, используя представления и т.д.). Некоторые ORM пытаются справиться с этим, но это всегда быстро усложняется.
Например, Hibernate Один подход к иерархии таблиц в классе, который нарушает 2NF и может привести к непоследовательности, очень вероятно, к вашему образцу

Secretary secretary = new Secretary();
secretary.EmployeeType = EmployeeType.Manager;


Ответ 2

Звучит неплохо "Запах кода", я бы сказал, плохой дизайн. но я обычно делаю что-то подобное, и я чувствую себя приемлемым, хотя.

public enum EmployeeType
{
    UnKnown,//this can be used for extensiblity
    Manager,
    Secretary,    
}

public abstract class Employee//<--1
{
    public string Code { get; set; }

    public abstract EmployeeType Type { get; } //<--2
}

public class Manager : Employee
{
    public override EmployeeType Type { get { EmployeeType.Manager; } }//<--3
    public void Manage()
    {
        // Managing
    }
}

public class Secretary : Employee
{
    public override EmployeeType Type { get { EmployeeType.Secretary; } }//<--4
    public void SetMeeting()
    {
        // Setting meeting
    }
}

Тогда a factory этого.

EmployeeFactory.Create(employeeType);

Я сделал "4" изменения в вашем исходном коде, и теперь он выглядит как "Factory pattern". Может быть, автор кода понимает "шаблон Factory" неправильно. но кто знает.?

Ответ 3

Это определенно плохая практика. Оставляя в стороне сомнительное использование наследования (которое само по себе широко рассматривается как плохая практика, по сравнению с использованием композиции), оно нарушает принцип инверсии зависимостей путем введения неприятной циклической зависимости между родительским и дочерним классами.

При перепроектировании кода я ожидаю, что рассмотрю три проблемы:

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

Ответ 4

Я согласен с тем, что это неправильная практика, но в определенных сценариях это может быть необходимо или полезно, и если это так, вы можете, по крайней мере, предотвратить что-то плохое, сделав только свойство базового класса и установив его через ctor подклассы.

public enum EmployeeType
{
    Manager,
    Secretary
}

public class Employee
{
    public Employee(EmployeeType type)
    {
        this.Type = type;
    }

    public string Code { get; set; }

    public EmployeeType Type { get; private set; }
}

public class Manager : Employee
{
    public Manager()
        : base(EmployeeType.Manager)
    {
    }

    public void Manage()
    {
        // Managing
    }
}

public class Secretary : Employee
{
    public Secretary()
        : base(EmployeeType.Secretary)
    {
    }

    public void SetMeeting()
    {
        // Setting meeting
    }
}

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

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

Ответ 5

Из вопроса, похоже, EmployeeType соответствует классу. В этом случае это не просто ответ, а тип сотрудника - тип класса. Это не должно быть свойство вообще, и наличие перечисления типов кажется бессмысленным. Может быть, список или массив, но не перечисление. Точные данные зависят от того, что используется в случае использования и общего дизайна. Или, по мнению 2, вероятно, должно быть свойство только для чтения для типа, которое дает публичный, возможно абстрактный, тип, даже если реальный тип экземпляра сотрудника является закрытым.

Затем метод factory должен взять тип класса сотрудника. Обратите внимание, что тип, видимый в API, может быть абстрактным, а метод factory может выбрать конкретный конкретный тип внутри. Кроме того, возможно использование версии метода factory, который принимает тип строки.


Однако, если EmployeeType не соответствует классу с таким же именем, то ответ немного отличается. Прежде всего, вы должны использовать разные имена, чтобы не было путаницы с перечислением и классом. 2nd, Возможно, было бы целесообразно преобразовать EmployeeType в класс или интерфейс. Затем у сотрудника есть тип, а не тип. Состав над наследованием.

Ответ 6

Я думаю, что это хороший дизайн для определенных сценариев, и это, безусловно, повторяющаяся картина, встречающаяся в разных частях библиотеки базового класса .NET.

Я думаю, что неправильное представление о том, что это может быть плохой дизайн, заключается в том, что иерархии классов должны быть расширяемы. Однако, в первую очередь, отношения наследования только выражают, что некоторый подтип является специализацией супертипа и что подтип не совпадает с супертипом, но совместим с (интерфейсом) супертипа.

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

Что касается выражения типа текущего класса со значением enum, ничто не говорит против этого, если иерархия наследования ограничена расширением для третьих сторон. Строго говоря, такое свойство не должно быть необходимым, но поскольку языки, такие как С#, не предлагают никакой возможности переключения типов, предлагая значение enum, улучшает читаемость кода, который должен обрабатывать различные типы семейства типов и, следовательно, представляет собой хороший дизайн API. В этом отношении он также улучшает ремонтопригодность, поскольку будущие версии семейства того же типа могут реструктурировать иерархию типов. Код, который проверяет исключительно на значения enum, будет несовместим с такими изменениями (и обходные методы, подверженные ошибкам, такие как проверка имени типа (string), а не самого объекта Type, не будут введены в первую очередь).

.NET BCL использует этот шаблон в разных местах, например: