Чи створення підкласів для конкретних випадків є поганою практикою?


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 ;-)
Doc Brown

1
Ось такий собі пов’язаний питання, коли ОП, здається, вважає протилежне звичайній мудрості. programmers.stackexchange.com/questions/253612/…
Данк,

11
Створюйте свої класи залежно від поведінки, а не даних. Для мене підкласи не додають нової поведінки до ієрархії.
Songo

2
Це широко використовується в анонімних підкласах (таких як обробники подій) на Java до того, як лямбдас прийшов до Java 8 (що те саме з різним синтаксисом).
marczellm

Відповіді:


77

Немає необхідності мати спеціальні підкласи для кожної людини.

Ви маєте рацію, натомість це повинні бути екземпляри.

Мета підкласів: розширення батьківських класів

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

  • Батьківський клас, Batteryякий може Power()щось мати і може мати Voltageвластивість,

  • І підклас RechargeableBattery, який може успадковувати Power()і Voltage, але також може бути Recharge()d.

Зауважте, що ви можете передавати екземпляр RechargeableBatteryкласу як параметр будь-якому методу, який приймає Batteryяк аргумент. Це називається принципом заміщення Ліскова , одним із п’яти принципів SOLID. Так само в реальному житті, якщо мій MP3-плеєр приймає два батарейки AA, я можу замінити їх двома акумуляторними батареями АА.

Зауважте, що іноді важко визначити, чи потрібно використовувати поле чи підклас, щоб представити різницю між чимось. Наприклад, якщо вам доведеться працювати з батареями AA, AAA та 9-вольтовими, ви створили б три підкласи або використали перерахунок? "Замінити підклас на поля" в " Рефакторинг " Мартіна Фаулера, стор. 232, може дати вам кілька ідей та способів переходу від одного до іншого.

У вашому прикладі, Karlі Johnнічого не поширюйте, і вони не надають додаткової цінності: ви можете мати точно таку ж функціональність, використовуючи Personклас безпосередньо. Мати більше рядків коду без додаткового значення - це ніколи добре.

Приклад ділової справи

Що може бути бізнесом, коли насправді має сенс створити підклас для конкретної людини?

Скажімо, ми створюємо додаток, який керує особами, які працюють у компанії. Додаток також керує дозволами, тому Хелен, бухгалтер, не може отримати доступ до сховища SVN, але Томас і Мері, двоє програмістів, не мають доступу до документів, пов’язаних із бухгалтерським обліком.

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

Найбіднішою моделлю для такого застосування є такі класи, як:

          Діаграма класів показує класи Хелен, Томас, Мері та Джиммі та їх спільного батька: абстрактний клас Особи.

тому що дублювання коду виникне дуже швидко. Навіть у самому базовому прикладі чотирьох працівників ви будете дублювати код між класами Томаса та Мері. Це підштовхне вас до створення загального батьківського класу Programmer. Оскільки у вас може бути кілька бухгалтерів, ви, ймовірно, також створите Accountantклас.

          Діаграма класу показує абстрактну Особу з трьома дітьми: абстрактного бухгалтера, абстрактного програміста та Джиммі.  Бухгалтер має підклас Олени, а програміст - два підкласи: Томас і Мері.

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

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

Отже, ви видалите класи, які ви не використовуєте:

          Діаграма класів містить підкласи Бухгалтер, Програміст та Джиммі із загальним батьківським класом: абстрактною Особою.

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

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
Якщо це не було зрозуміло з останнього абзацу: Навіть якщо у вас є лише одна людина з необмеженим доступом, ви все одно повинні зробити цю особу екземпляром окремого класу, який реалізує необмежений доступ. Перевірка самої людини призводить до взаємодії з кодовими даними та може відкрити цілу банку глистів. Наприклад, що робити, якщо Рада директорів вирішить призначити тріумвірата генеральним директором? Якщо у вас не було класу "Без обмежень", вам не доведеться просто оновлювати існуючого генерального директора, а й додати ще 2 чеки для інших членів. Якщо у вас це було, ви просто встановите їх як "Без обмежень".
Nzall

15

Нерозумно використовувати такий тип структури класу просто для зміни деталей, які, очевидно, повинні бути налаштовані поля для екземпляра. Але це особливо для вашого прикладу.

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

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


12

Чому я б створював нові класи, коли екземплярів достатньо?

У більшості випадків ви цього не зробили. Ваш приклад справді хороший випадок, коли така поведінка не додає реальної цінності.

Він також порушує принцип відкритого закриття , оскільки підкласи в основному не є розширенням, а змінюють внутрішню роботу батьків. Більше того, публічний конструктор материнської програми тепер дратує в підкласах, і тим самим API стає менш зрозумілим.

Однак іноді, якщо ви коли-небудь матимете одну-дві спеціальні конфігурації, які часто використовуються у всьому коді, іноді зручніше і забирає менше часу просто підкласи батьків, які мають складний конструктор. У таких особливих випадках я не бачу нічого поганого в такому підході. Назвемо це конструктор currying j / k


Я не впевнений, що згоден з пунктом OCP. Якщо клас виставляє сеттер властивостей своїм нащадкам, то - припускаючи, що він керується правильними принципами дизайну - властивість не повинна бути частиною його внутрішнього функціонування
Бен Ааронсон,

@BenAaronson До тих пір, поки поведінка власності не буде змінена, ви не порушите OCP, але тут вони є. Звичайно, ви можете використовувати цю власність у своїх внутрішніх роботах. Але ви не повинні змінювати існуючу поведінку! Вам слід лише поширювати поведінку, а не змінювати її у спадок. Звичайно, є винятки з кожного правила.
Сокіл

1
Тож будь-яке використання віртуальних членів є порушенням OCP?
Бен Аронсон

3
@Falcon Не потрібно отримувати новий клас просто, щоб уникнути повторюваних складних викликів конструктора. Просто створіть заводи для загальних випадків і параметризуйте невеликі варіанти.
Кін

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

8

Якщо це ступінь практики, то я згоден, це погана практика.

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

У цьому випадку має сенс мати їх власний підклас із визначеною поведінкою. Є кращі способи досягти того ж (наприклад, CleaningBehaviorклас), але принаймні цей спосіб не є страшним порушенням принципів ОО.


Немає різної поведінки, просто різні дані. Спасибі.
Ігнасіо Солер Гарсія

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

Це виняток із "повинні бути екземпляри" тут - хоча і трохи крайні.

Якщо ви хотіли, щоб компілятор застосував дозволи для доступу до методів, заснованих на тому, чи це Карл чи Джон (у цьому прикладі), а не запитувати реалізацію методу, щоб перевірити, який екземпляр передано, ви можете використовувати різні класи. Застосовуючи різні класи, а не інстанціювання, ви робите перевірки диференціації між "Karl" та "John" можливими під час компіляції, а не час виконання, і це може бути корисно - особливо в контексті безпеки, де компілятору можна довіряти, ніж виконувати час виконання перевірка коду (наприклад) зовнішніх бібліотек.


2

Дурною практикою вважається дублювання коду .

Це хоча б частково тому, що рядки кодів і, отже, розмір кодової бази корелює з витратами на технічне обслуговування та дефектами .

Ось відповідна логіка здорового глузду для цієї конкретної теми:

1) Якщо мені потрібно змінити це, скільки місць мені потрібно відвідати? Тут менше краще.

2) Чи є причина, чому це робиться саме так? У вашому прикладі - не могло бути - але в деяких прикладах може бути якась причина для цього. Одне, про що я можу придумати голову, знаходиться в таких рамках, як ранній Грааль, де успадкування не обов'язково грає добре з деякими плагінами для GORM. Мало і далеко між ними.

3) Чи чистіше і легше це зрозуміти? Знову ж таки, це не у вашому прикладі - але є деякі моделі успадкування, які можуть бути більше розтягненням, коли розділені класи насправді простіше у підтримці (певні звички моделей команд чи стратегій приходять до уваги).


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

Див. Пункт №2. Звичайно, час від часу є причини. Ось чому вам потрібно запитати, перш ніж витягувати чийсь код.
dcgregorya

0

Існує випадок використання: формування посилання на функцію чи метод як звичайний об'єкт.

Це можна побачити в Java GUI-коді багато:

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

Тут вам допоможе компілятор, створивши новий анонімний підклас.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.