Является ли создание подклассов для конкретных случаев плохой практикой?

37

Рассмотрим следующий дизайн

public class Person
{
    public virtual string Name { get; }

    public Person (string name)
    {
        this.Name = name;
    }
}

public class Karl : Person
{
    public override string Name
    {
        get
        {
            return "Karl";
        }
    }
}

public class John : Person
{
    public override string Name
    {
        get
        {
            return "John";
        }
    }
}

Вы думаете, что-то здесь не так? Для меня классы Карла и Джона должны быть просто экземплярами, а не классами, поскольку они точно такие же, как:

Person karl = new Person("Karl");
Person john = new Person("John");

Зачем мне создавать новые классы, когда экземпляров достаточно? Классы ничего не добавляют к экземпляру.

Игнасио Солер Гарсия
источник
17
К сожалению, вы найдете это в большом количестве производственного кода. Убери это, когда сможешь.
Адам Цукерман
14
Это отличный дизайн - если разработчик хочет сделать себя незаменимым для каждого изменения в бизнес-данных, и у него нет проблем, чтобы быть доступными 24/7 ;-)
Док Браун
1
Вот своего рода смежный вопрос, в котором ОП, похоже, считает, что это противоречит общепринятому мнению. programmers.stackexchange.com/questions/253612/…
Данк
11
Дизайн ваших классов в зависимости от поведения, а не данных. Для меня подклассы не добавляют никакого нового поведения в иерархию.
Сонго
2
Это широко используется с анонимными подклассами (такими как обработчики событий) в Java до того, как лямбды пришли в Java 8 (то же самое с другим синтаксисом).
marczellm

Ответы:

77

Нет необходимости иметь конкретные подклассы для каждого человека.

Вы правы, это должны быть примеры.

Цель подклассов: расширить родительские классы

Подклассы используются для расширения функциональности, предоставляемой родительским классом. Например, вы можете иметь:

  • Родительский класс, Batteryкоторый может Power()что-то и может иметь Voltageсвойство,

  • И подкласс RechargeableBattery, который может наследовать Power()и Voltage, но также может быть Recharge()d.

Обратите внимание, что вы можете передать экземпляр RechargeableBatteryкласса в качестве параметра любому методу, который принимает Batteryв качестве аргумента. Это называется принцип замещения Лискова , один из пяти принципов SOLID. Точно так же, в реальной жизни, если мой MP3-плеер принимает две батарейки АА, я могу заменить их двумя перезаряжаемыми батарейками АА.

Обратите внимание, что иногда трудно определить, нужно ли вам использовать поле или подкласс для представления различия между чем-либо. Например, если вам приходится работать с батареями типа AA, AAA и 9-вольтовым, вы бы создали три подкласса или использовали enum? «Замените подкласс на поля» в статье « Рефакторинг » Мартина Фаулера, стр. 232, может дать вам некоторые идеи и способы перехода от одного к другому.

В вашем примере, Karlи Johnничего не распространяются, они не предоставляют каких - либо дополнительную ценность: вы можете иметь точно такую же функциональность , используя Personкласс непосредственно. Наличие большего количества строк кода без дополнительного значения никогда не бывает хорошо.

Пример бизнес-кейса

Что может быть деловым случаем, когда имеет смысл создать подкласс для конкретного человека?

Допустим, мы создаем приложение, которое управляет людьми, работающими в компании. Приложение также управляет разрешениями, поэтому Хелен, бухгалтер, не может получить доступ к репозиторию SVN, но Томас и Мэри, два программиста, не могут получить доступ к документам, связанным с бухгалтерским учетом.

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

Самая плохая модель для такого применения - иметь такие классы, как:

          Диаграмма классов показывает классы Елены, Томаса, Мэри и Джимми и их общего родителя: абстрактный класс Person.

потому что дублирование кода возникнет очень быстро. Даже в самом простом примере с четырьмя сотрудниками вы будете дублировать код между классами Томаса и Мэри. Это подтолкнет вас к созданию общего родительского класса Programmer. Поскольку у вас также может быть несколько бухгалтеров, вы, вероятно, создадите и Accountantкласс.

          Диаграмма классов показывает абстрактную личность с тремя детьми: абстрактный бухгалтер, абстрактный программист и Джимми.  У бухгалтера есть подкласс Хелен, а у программиста есть два подкласса: Томас и Мэри.

Теперь вы замечаете, что наличие класса Helenне очень полезно, а также хранение Thomasи Mary: большая часть вашего кода работает на верхнем уровне - на уровне бухгалтеров, программистов и Джимми. Сервер SVN не заботится о том, должен ли Томас или Мэри получать доступ к журналу, ему нужно только знать, программист это или бухгалтер.

if (person is Programmer)
{
    this.AccessGranted = true;
}

В итоге вы удалите классы, которые не используете:

          Диаграмма классов содержит подклассы Accountant, Programmer и Jimmy с общим родительским классом: abstract Person.

«Но я могу оставить Джимми как есть, так как всегда будет только один генеральный директор, один большой босс - Джимми», - думаете вы. Более того, Джимми часто используется в вашем коде, который на самом деле выглядит примерно так, а не так, как в предыдущем примере:

if (person is Jimmy)
{
    this.GiveUnrestrictedAccess(); // Because Jimmy should do whatever he wants.
}
else if (person is Programmer)
{
    this.AccessGranted = true;
}

Проблема с этим подходом заключается в том, что Джимми все еще может сбить автобус, и будет новый генеральный директор. Или совет директоров может решить, что Мэри настолько хороша, что должна стать новым генеральным директором, а Джимми будет понижен до должности продавца, поэтому теперь вам нужно пройтись по всему вашему коду и изменить все.

Арсений Мурзенко
источник
6
@DocBrown: Я часто удивляюсь тому, что короткие ответы, которые не являются неправильными, но недостаточно глубокими, имеют гораздо более высокий балл, чем ответы, которые подробно объясняют предмет. Возможно, слишком много людей не любят много читать, поэтому очень короткие ответы выглядят для них более привлекательно. Тем не менее, я отредактировал свой ответ, чтобы дать хоть какое-то объяснение.
Арсений Мурзенко
3
Короткие ответы, как правило, публикуются первыми. Предыдущие посты получают больше голосов.
Тим Б
1
@TimB: я обычно начинаю с ответов среднего размера, а затем редактирую их, чтобы они были очень полными (вот пример , учитывая, что было, вероятно, около пятнадцати ревизий, только четыре показаны). Я заметил, что чем дольше становится ответом со временем, тем меньше голосов он получает; Подобный вопрос, который остается коротким, привлекает больше голосов.
Арсений Мурзенко
Согласитесь с @DocBrown. Например, хороший ответ сказал бы что-то вроде «подкласс для поведения , а не для контента», и ссылался бы на некоторые ссылки, которые я не буду делать в этом комментарии.
user949300
2
В случае, если из последнего абзаца было неясно: даже если у вас есть только 1 человек с неограниченным доступом, вы все равно должны сделать его экземпляром отдельного класса, который реализует неограниченный доступ. Проверка самого человека приводит к взаимодействию кода с данными и может открыть целую банку с червями. Например, что если совет директоров решит назначить триумвирата генеральным директором? Если у вас нет класса «Unrestricted», вам не нужно будет просто обновить существующего генерального директора, но также добавить еще 2 проверки для других участников. Если он у вас есть, вы просто устанавливаете его как «Неограниченный».
Nzall
15

Глупо использовать такую ​​структуру классов только для изменения деталей, которые, очевидно, должны быть настраиваемыми полями в экземпляре. Но это относится к вашему примеру.

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

Разница в том, как вы ожидаете, что сущности, представленные этим классом, будут различаться. Предполагается, что люди почти всегда приходят и уходят, и ожидается, что почти каждое серьезное приложение, имеющее дело с пользователями, сможет добавлять и удалять пользователей во время выполнения. Но если вы моделируете такие вещи, как разные алгоритмы шифрования, поддержка нового алгоритма, вероятно, в любом случае является серьезным изменением, и имеет смысл изобрести новый класс, myName()метод которого возвращает другую строку (и perform()метод которого предположительно делает что-то другое).

Килиан Фот
источник
12

Зачем мне создавать новые классы, когда экземпляров достаточно?

В большинстве случаев вы этого не сделаете. Ваш пример действительно хороший случай, когда это поведение не добавляет реальной ценности.

Это также нарушает принцип Open Closed , поскольку подклассы в основном не являются расширением, а изменяют внутреннюю работу родителя. Более того, открытый конструктор родителя теперь раздражает в подклассах, и API, таким образом, стал менее понятным.

Однако иногда, если у вас когда-либо будет одна или две специальные конфигурации, которые часто используются во всем коде, иногда удобнее и меньше времени просто создавать подкласс для родительского объекта со сложным конструктором. В таких особых случаях я не вижу ничего плохого в таком подходе. Давайте назовем это конструктор карри J / K

сокол
источник
Я не уверен, что согласен с пунктом OCP. Если класс выставляет установщик свойства своим потомкам, то при условии, что он следует хорошим принципам проектирования, свойство не должно быть частью его внутренней работы
Бен Ааронсон,
@BenAaronson Пока поведение самого свойства не изменяется, вы не нарушаете OCP, но здесь это происходит. Конечно, вы можете использовать это свойство во внутренней работе. Но вы не должны изменять существующее поведение! Вы должны только расширять поведение, а не изменять его по наследству. Конечно, есть исключения из каждого правила.
Сокол
1
Таким образом, любое использование виртуальных членов является нарушением OCP?
Бен Ааронсон
3
@Falcon Нет необходимости извлекать новый класс только для того, чтобы избежать повторяющихся сложных вызовов конструктора. Просто создайте фабрики для общих случаев и параметризуйте небольшие вариации.
Кин
@BenAaronson Я бы сказал, что наличие виртуальных членов, которые имеют реальную реализацию, является таким нарушением. Абстрактные члены или, скажем, методы, которые ничего не делают, будут действительными точками расширения. По сути, когда вы смотрите на код базового класса, вы знаете, что он будет работать как есть, вместо того, чтобы каждый не финальный метод был кандидатом на замену чем-то совершенно другим. Или, говоря иначе: способы, с помощью которых наследующий класс может влиять на поведение базового класса, будут явными и будут ограничиваться «аддитивностью».
миллимус
8

Если это степень практики, то я согласен, что это плохая практика.

Если Джон и Карл ведут себя по-разному, все немного меняется. Возможно, у этого personесть метод, позволяющий cleanRoom(Room room)выяснить, что Джон - отличный сосед по комнате и очень эффективно убирает, а Карл - нет, и он не очень часто убирает в комнате.

В этом случае имело бы смысл иметь их в качестве собственного подкласса с определенным поведением. Есть лучшие способы достичь того же (например, CleaningBehaviorкласс), но по крайней мере этот способ не является ужасным нарушением принципов ОО.

corsiKa
источник
Нет другого поведения, просто разные данные. Спасибо.
Игнасио Солер Гарсия
6

В некоторых случаях вы знаете, что будет только определенное количество экземпляров, и тогда было бы нормально (хотя и на мой взгляд, некрасиво) использовать этот подход. Лучше использовать enum, если это позволяет язык.

Пример Java:

public enum Person
{
    KARL("Karl"),
    JOHN("John")

    private final String name;

    private Person(String name)
    {
        this.name = name;
    }

    public String getName()
    {
        return this.name;
    }
}
Адам
источник
6

Многие уже ответили. Думаю, я бы дал свою собственную точку зрения.


Когда-то я работал над приложением (и до сих пор занимаюсь), которое создает музыку.

Приложение было абстрактный Scaleкласс с несколькими подклассов: CMajor, DMinorи т.д. Scaleсмотрел что - то вроде этого:

public abstract class Scale {
    protected Note[] notes;
    public Scale() {
        loadNotes();
    }
    // .. some other stuff ommited
    protected abstract void loadNotes(); /* subclasses put notes in the array
                                          in this method. */
}

Музыкальные генераторы работали с конкретным Scaleэкземпляром для создания музыки. Пользователь будет выбирать масштаб из списка, из которого будет генерироваться музыка.

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

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

Часто интуитивно мыслить в терминах «суперклассов и подклассов». Почти все , что можно выразить через эту систему: суперкласс Personи подклассы Johnи Mary; суперкласс Carи подклассы Volvoи Mazda; суперкласс Missileи подклассы SpeedRocked, LandMineи TrippleExplodingThingy.

Это очень естественно думать, особенно для человека относительно новичка в ОО.

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

Это не работа подкласса, чтобы заполнить шаблон. Это работа объекта. Работа подкласса заключается в добавлении фактической функциональности или расширении шаблона .

И именно поэтому я должен был создать конкретный Scaleкласс с Note[]полем и позволить объектам заполнять этот шаблон ; возможно через конструктор или что-то. И в конце концов я так и сделал.

Каждый раз, когда вы разрабатываете шаблон в классе (например, пустой Note[]элемент, который необходимо заполнить, или String nameполе, которому необходимо присвоить значение), помните, что заполнение шаблона - это задача объектов этого класса ( или, возможно, те, кто создает эти объекты). Подклассы предназначены для добавления функциональности, а не для заполнения шаблонов.


У вас может возникнуть соблазн создать систему «суперкласса Person, подклассов Johnи Mary», как вы это делали, потому что вам нравится формальность, которую вы получаете.

Таким образом, вы можете просто сказать Person p = new Mary(), вместо Person p = new Person("Mary", 57, Sex.FEMALE). Это делает вещи более организованными и более структурированными. Но, как мы уже говорили, создание нового класса для каждой комбинации данных не очень хороший подход, так как он раздувает код даром и ограничивает вас с точки зрения возможностей времени выполнения.

Итак, вот решение: используйте базовую фабрику, возможно, даже статическую. Вот так:

public final class PersonFactory {
    private PersonFactory() { }

    public static Person createJohn(){
        return new Person("John", 40, Sex.MALE);
    }

    public static Person createMary(){
        return new Person("Mary", 57, Sex.FEMALE);
    }

    // ...
}

Таким образом, вы можете легко использовать «предустановки» и «идти с программой», например, так:, Person mary = PersonFactory.createMary()но вы также оставляете за собой право динамически создавать новых людей, например, в случае, если вы хотите, чтобы пользователь мог это делать , Например:

// .. requesting the user for input ..
String name = // user input
int age = // user input
Sex sex = // user input, interpreted
Person newPerson = new Person(name, age, sex);

Или даже лучше: сделать что-то вроде этого:

public final class PersonFactory {
    private PersonFactory() { }

    private static Map<String, Person> persons = new HashMap<>();
    private static Map<String, PersonData> personBlueprints = new HashMap<>();

    public static void addPerson(Person person){
        persons.put(person.getName(), person);
    }

    public static Person getPerson(String name){
        return persons.get(name);
    }

    public static Person createPerson(String blueprintName){
        PersonData data = personBlueprints.get(blueprintName);
        return new Person(data.name, data.age, data.sex);
    }

    // .. or, alternative to the last method
    public static Person createPerson(String personName){
        Person blueprint = persons.get(personName);
        return new Person(blueprint.getName(), blueprint.getAge(), blueprint.getSex());
    }

}

public class PersonData {
    public String name;
    public int age;
    public Sex sex;

    public PersonData(String name, int age, Sex sex){
        this.name = name;
        this.age = age;
        this.sex = sex;
    }
}

Я увлекся. Я думаю, вы поняли идею.


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

Вы не должны создавать новый класс для каждой возможной комбинации данных. (Точно так же, как я не должен был создавать новый Scaleподкласс для каждой возможной комбинации Notes).

Это руководство: всякий раз, когда вы создаете новый подкласс, подумайте, добавляет ли он какие-либо новые функциональные возможности, которых нет в суперклассе. Если ответ на этот вопрос «нет», то вы, возможно, пытаетесь «заполнить шаблон» суперкласса, и в этом случае просто создайте объект. (И, возможно, Фабрика с «пресетами», чтобы облегчить жизнь).

Надеюсь, это поможет.

Авив Кон
источник
Это отличный пример, большое спасибо.
Игнасио Солер Гарсия
@SoMoS Рад, что мог помочь.
Авив Кон
2

Это исключение из «должно быть экземпляров» здесь - хотя и несколько экстремальное.

Если вы хотите, чтобы компилятор принудительно установил права доступа к методам на основе того, является ли это Карлом или Джоном (в этом примере), а не запрашивал реализацию метода, чтобы проверить, какой экземпляр был передан, вы можете использовать другие классы. Применяя различные классы вместо создания экземпляров, вы делаете возможным проверку различий между «Карлом» и «Джоном» во время компиляции, а не во время выполнения, и это может быть полезно, особенно в контексте безопасности, где компилятору можно доверять больше, чем во время выполнения проверка кода (например) внешних библиотек.

Дэвид Шолфилд
источник
2

Дублировать код считается плохой практикой .

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

Вот соответствующая логика здравого смысла для меня на эту конкретную тему:

1) Если бы мне нужно было это изменить, сколько мест мне нужно посетить? Чем меньше, тем лучше.

2) Есть ли причина, почему это делается таким образом? В вашем примере - не может быть - но в некоторых примерах может быть какая-то причина для этого. Один из них, который я могу придумать, находится в некоторых фреймворках, таких как ранние Grails, где наследование не обязательно хорошо сочеталось с некоторыми плагинами для GORM. По пальцам пересчитать.

3) Это чище и легче понять таким образом? Опять же, это не в вашем примере - но есть некоторые шаблоны наследования, которые могут быть более натянутыми, когда отдельные классы на самом деле проще поддерживать (некоторые виды использования шаблонов команд или стратегий приходят на ум).

dcgregorya
источник
1
плохо это или нет, не в камне. Например, существуют сценарии, когда дополнительные издержки на создание объектов и вызовы методов могут быть настолько вредными для производительности, что приложение больше не соответствует спецификациям.
С
Смотрите пункт № 2. Конечно, есть причины, почему иногда. Вот почему вы должны спросить, прежде чем выпустить чей-то код.
dcgregorya
0

Существует вариант использования: формирование ссылки на функцию или метод как обычный объект.

Вы можете увидеть это в коде Java GUI много:

ActionListener a = new ActionListener() {
    public void actionPerformed(ActionEvent arg0) {
        /* ... */
    }
};

Компилятор поможет вам, создав новый анонимный подкласс.

Саймон Рихтер
источник