Який код слід включити до абстрактного класу?


10

Нещодавно мене турбує використання абстрактних занять.

Іноді абстрактний клас створюється заздалегідь і працює як шаблон того, як працювали похідні класи. Це означає, більш-менш, що вони забезпечують функціональність високого рівня, але залишають певні деталі, які можна реалізувати у похідних класах. Абстрактний клас визначає потребу в цих деталях, застосовуючи деякі абстрактні методи. У таких випадках абстрактний клас працює як креслення, опис функціональності на високому рівні або все, що ви хочете назвати. Він не може використовуватися самостійно, але повинен бути спеціалізований для визначення деталей, що залишилися поза впровадженням на високому рівні.

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

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

Чи повинен код, який не може мати значення для самого абстрактного класу, лише тому, що він є загальним для похідних класів?

Наведу приклад: абстрактний клас A має метод a () та абстрактний метод aq (). Метод aq () в обох похідних класах AB і AC використовує метод b (). Чи повинен b () перейти до A? Якщо так, то у випадку, якщо хтось дивиться лише на A (зробимо вигляд, що AB та AC там немає), існування b () не мало б сенсу! Це погано? Чи повинен хтось мати змогу заглянути в абстрактний клас і зрозуміти, що відбувається, не відвідуючи похідні класи?

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

Що ви думаєте / практикуєте?


7
Чому повинно існувати "правило"?
Роберт Харві

1
Writing an abstract class that makes sense without having to look in the derived classes is a matter of clean code and clean architecture.- Чому? Хіба не можливо, що під час проектування та розробки програми я виявляю, що декілька класів мають спільну функціональність, яка, природно, може бути перероблена на абстрактний клас? Чи повинен я бути ясновидцем, щоб завжди передбачати це перед тим, як писати будь-який код для похідних класів? Якщо я не буду такою проникливою, мені забороняється виконувати таке рефакторинг? Повинен я скинути свій код і почати спочатку?
Роберт Харві

Вибачте, якщо мене зрозуміли неправильно! Я намагався сказати, що вважаю (на даний момент) кращою практикою, не маючи на увазі, що має бути абсолютне правило. Більше того, я не маю на увазі, що будь-який код, що належить до абстрактного класу, слід писати лише заздалегідь. Я описував, як на практиці абстрактний клас закінчується кодом високого рівня (який виступає як шаблон для похідних), а також кодом низького рівня (що ви не можете зрозуміти, що це зручність, якщо ви не забуваєте похідні класи).
Олександрос Гугусіс

@RobertHarvey для загальнодоступного базового класу, вам забороняється переглядати похідні класи. Для внутрішніх занять це не має значення.
Френк Гілеман

Відповіді:


4

Наведу приклад: абстрактний клас A має метод a () та абстрактний метод aq (). Метод aq () в обох похідних класах AB і AC використовує метод b (). Чи повинен b () перейти до A? Якщо так, то у випадку, якщо хтось дивиться лише на A (зробимо вигляд, що AB та AC там немає), існування b () не мало б сенсу! Це погано? Чи повинен хтось мати змогу заглянути в абстрактний клас і зрозуміти, що відбувається, не відвідуючи похідні класи?

Ваше запитання - де його розмістити b(), а в іншому сенсі питання - чи Aнайкращий вибір як безпосередній суперклас для ABта AC.

Здається, є три варіанти:

  1. залишити b()в обох ABіAC
  2. створити проміжний клас, ABAC-Parentякий успадковує Aі який вводить b(), а потім використовується як безпосередній суперклас для ABіAC
  3. помістити b()в A(не знаючи інший клас в майбутньому ADзахоче b()чи ні)

  1. страждає від того, що не БУДЕ .
  2. страждає ЯГНІ .
  3. так, що залишає цей.

Поки інший клас AD, який не хоче, b()представляє себе (3) здається правильним вибором.

У такий час, як ADподарунки, ми можемо відмовитись від підходу в (2) - адже це програмне забезпечення!


7
Є 4-й варіант. Не кладіть b()жодного класу. Зробіть це безкоштовною функцією, яка сприймає всі його дані як аргументи і має обидва ABі ACназиває їх. Це означає, що вам не потрібно переміщувати його чи створювати більше класів, коли ви додаєте AD.
користувач1118321

1
@ user1118321, відмінне, приємне мислення поза коробкою. Має особливий сенс, якщо b()не потрібна інстанція, яка довго живе.
Ерік Ейдт

Що мене турбує в (3), це те, що абстрактний клас більше не описує самодостатню частину поведінки. Це біт-фрагменти коду, і я не можу зрозуміти абстрактний код класу, не переходячи між цим та всіма похідними класами.
Олександрос Гугусіс

Чи можете ви створити базову / за замовчуванням, acщо дзвінки bзамість того, щоб acбути абстрактними?
Ерік Ейдт

3
Для мене найважливіше питання: ЧОМУ і AB, і AC використовують один і той же метод b (). Якщо це просто збіг, я б залишив дві подібні реалізації в AB та AC. Але, ймовірно, це тому, що існує якась загальна абстракція для AB та AC. Для мене DRY - це не стільки цінність, а натяк на те, що ви, мабуть, пропустили якусь корисну абстракцію.
Ральф Клеберхофф

2

Абстрактний клас не має бути демпінгом для різних функцій або даних, які передаються до абстрактного класу, оскільки це зручно.

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

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

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

Однак завжди є запитання щодо зміни, що буде змінюватися і як це зміниться, і чому це зміниться.

Спадкування може призвести до крихких та крихких тіл джерела (див. Також Спадщина: просто припиніть його вже використовувати! ).

Неміцна проблема базового класу - це фундаментальна архітектурна проблема об'єктно-орієнтованих систем програмування, де базові класи (надкласи) вважаються "крихкими", оскільки, здавалося б, безпечні модифікації базового класу при успадкуванні похідних класів можуть призвести до несправності похідних класів. . Програміст не може визначити, чи безпечна зміна базового класу, просто вивчивши ізольовано методи базового класу.


Я впевнений, що будь-яка концепція ООП може призвести до проблем, якщо її неправильно застосовувати, або використовувати або зловживати! Більше того, припущення, що b () є бібліотечним методом (наприклад, чиста функція без інших залежностей), звужує сферу мого питання. Давайте уникати цього.
Олександрос Гугусіс

@AlexandrosGougousis Я не припускаю, що b()це чиста функція. Це може бути функтоїд, шаблон або щось інше. Я використовую фразу "метод бібліотеки" як сенс чиєї складової, будь то чиста функція, об'єкт COM, або все, що використовується для функціоналу, який він надає рішенню.
Річард Чемберс

Вибачте, якщо мені не було зрозуміло! Я згадав про чисту функцію як один із багатьох прикладів (ви дали більше).
Олександрос Гугусіс

1

Ваше запитання пропонує будь-який або підхід до абстрактних класів. Але я думаю, що ви повинні розглядати їх як просто ще один інструмент у вашій панелі інструментів. І тоді виникає питання: для яких завдань / проблем абстрактні класи правильний інструмент?

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

Іноді абстрактний клас створюється заздалегідь і працює як шаблон того, як працювали похідні класи. Це означає, більш-менш, що вони забезпечують функціональність високого рівня, але залишають певні деталі, які можна реалізувати у похідних класах. Абстрактний клас визначає потребу в цих деталях, застосовуючи деякі абстрактні методи. У таких випадках абстрактний клас працює як креслення, опис функціональності на високому рівні або все, що ви хочете назвати. Він не може використовуватися самостійно, але повинен бути спеціалізований для визначення деталей, що залишилися поза впровадженням на високому рівні.

Я думаю, що ваш перший приклад - це, по суті, опис шаблону методу шаблону (виправте мене, якщо я помиляюся), тому я вважав би це ідеально правильним випадком використання абстрактних класів.

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

Для вашого другого прикладу я вважаю, що використання абстрактних класів не є оптимальним вибором, оскільки існують чудові методи для роботи зі спільною логікою та дублюваним кодом. Скажімо , у вас є абстрактний клас A, похідні класи Bі Cі обидві похідні класи поділяють певну логіку у вигляді методу s(). Щоб визначитися з правильним підходом до позбавлення від дублювання, важливо знати, метод s()є частиною публічного інтерфейсу чи ні.

Якщо це не так (як у вашому власному конкретному прикладі з методом b()), справа досить проста. Просто створіть з нього окремий клас, також витягуючи контекст, необхідний для виконання операції. Це класичний приклад композиції над успадкуванням . Якщо контексту мало або взагалі не потрібно, простої допоміжної функції вже може бути достатньо, як запропоновано в деяких коментарях.

Якщо s()є частиною загальнодоступного інтерфейсу, він стає дещо складнішим. Згідно з припущенням, s()яке не має нічого спільного Bі Cпов'язане з ним A, ви не повинні ставити s()всередину A. То куди його потім поставити? Я б заперечував за оголошення окремого інтерфейсу I, який визначає s(). Потім знову слід створити окремий клас, який містить логіку реалізації s()та і те, Bі Cзалежати від нього.

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

Хороший абстрактний клас зменшить кількість коду, який потрібно переписати, оскільки його функціональність або стан можуть бути спільними.


Я погодився б, що частина (-и) публічного інтерфейсу робить речі набагато складнішими. Я б взагалі уникав визначати публічні методи, що називають інші публічні методи того ж класу.
Олександрос Гугусіс

1

Мені здається, вам не вистачає точки орієнтації на об'єкт, як логічно, так і технічно. Ви описуєте два сценарії: групування загальної поведінки типів у базовому класі та поліморфізм. Це обидва законні програми абстрактного класу. Але чи слід робити клас абстрактним чи ні, залежить від вашої аналітичної моделі, а не від технічних можливостей.

Ви повинні визнати тип, який не має втілення в реальному світі, але закладає основу для конкретних типів, які існують. Приклад: тварина. У тварини немає такого поняття. Це завжди собака чи кішка чи що завгодно, але справжньої тварини немає. І все ж тварини обрамляють їх усіх.

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

Назва вашого абстрактного класу разом з його членами має мати сенс. Він повинен бути незалежним від будь-якого похідного класу, як технічно, так і логічно. Як і тварина, може бути абстрактний метод Eat (який був би поліморфним) та булеві абстрактні властивості IsPet та IsLifestock, що має сенс, не знаючи про котів чи собак чи свиней. Будь-яка залежність (технічна чи логічна) повинна йти лише одним шляхом: від нащадка до основи. Базові класи самі не повинні мати знань про низхідні класи.


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

"Базові класи самі по собі також не повинні мати знань про низхідні класи". Я з цим повністю згоден. Батьківський клас (абстрактний чи ні) повинен містити самостійний фрагмент ділової логіки, який не потребує перевірки виконання дочірнього класу, щоб мати сенс.
Олександрос Гугусіс

Є ще одна річ у тому, що базовий клас знає його підтипи. Кожного разу, коли розробляється новий підтип, базовий клас потребує модифікації, який є зворотним і порушує принцип відкритого закриття. Нещодавно я стикався з цим у існуючому коді. Базовий клас мав статичний метод, який створював екземпляри підтипів на основі конфігурації програми. Намір був заводським зразком. Це таким чином також порушує SRP. З деякими твердими пунктами я думав "так, це очевидно", або "мова автоматично подбає про це", але я знайшов людей більш творчими, ніж я міг собі уявити.
Мартін Мейт

0

Перший випадок із вашого запитання:

У таких випадках абстрактний клас працює як креслення, опис функціональності на високому рівні або все, що ви хочете назвати.

Другий випадок:

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

Тоді ваше запитання :

Мені цікаво, який із цих двох випадків має бути правилом. ... Чи повинен код, який може не мати значення для самого абстрактного класу, тільки тому, що він є загальним для похідних класів?

IMO у вас є два різних сценарії, тому ви не можете скласти єдиного правила, щоб вирішити, який дизайн потрібно застосовувати.

У першому випадку у нас є такі речі, як шаблон шаблону методу шаблону, який уже згадувався в інших відповідях.

У другому випадку ви навели приклад абстрактного методу отримання ab () класу A; IMO метод b () слід переміщувати, лише якщо це має сенс для ВСІХ інших похідних класів. Іншими словами, якщо ви переміщуєте його, оскільки він використовується в двох місцях, мабуть, це не вдалий вибір, тому що завтра може з’явитися новий конкретний похідний клас, для якого b () взагалі не має сенсу. Крім того, як ви вже говорили, якщо дивитися на клас A окремо і b () також не має сенсу в цьому контексті, то, ймовірно, це натяк на те, що це також не вдалий вибір дизайну.


-1

У мене були такі самі питання певний час тому!

Що я прийшов, коли я натрапляю на це питання, я виявляю, що є якась прихована концепція, яку я не знайшов. І це поняття, швидше за все, не повинно виражатися деяким ціннісним об'єктом . У вас є дуже абстрактний приклад, тому я боюся, що я не можу продемонструвати, що я маю на увазі, використовуючи ваш код. Але ось випадок із моєї власної практики. У мене було два класи, які представляли спосіб надсилання запиту на зовнішній ресурс і аналізувати відповідь. Існували схожість у формуванні запитів та способах розбору відповідей. Ось так виглядала моя ієрархія:

abstract class AbstractProtocol
{
    /**
     * @return array Registration params to send
     */
    abstract protected function assembleRegistrationPart();

    /**
     * @return array Payment params to send
     */
    abstract protected function assemblePaymentPart();

    protected function doSend(array $data)
    {
        return
            (new HttpClient(
                [
                    'timeout' => 60,
                    'encoding' => 'utf-8',
                    'language' => 'en',
                ]
            ))
                ->send($data);
    }

    protected function log(array $data)
    {
        $header = 'Here is a request to external system!';
        $body = implode(', ', $this->maskData($data));
        Logger::log($header . '. \n ' . $body);
    }
}

class ClassicProtocol extends AbstractProtocol
{
    public function send()
    {
        $registration = $this->assembleRegistrationPart();
        $payment = $this->assemblePaymentPart();
        $specificParams = $this->assembleClassicSpecificPart();

        $dataToSend =
            array_merge(
                $registration, $payment, $specificParams
            );

        $this->log($dataToSend);

        $this->doSend($dataToSend);
    }

    protected function assembleRegistrationPart()
    {
        return ['hello' => 'there'];
    }

    protected function assemblePaymentPart()
    {
        return ['pay' => 'yes'];
    }
}

Такий код вказує на те, що я просто зловживаю спадщиною. Ось як це можна було відновити:

class ClassicProtocol
{
    private $request;
    private $logger;

    public function __construct(Request $request, Logger $logger, Client $client)
    {
        $this->request = $request;
        $this->client = $client;
        $this->logger = $logger;
    }

    public function send()
    {
        $this->logger->log($this->request->getData());
        $this->client->send($this->request->getData());
    }
}

$protocol =
    new ClassicProtocol(
        new PaymentRequest(
            new RegistrationData(),
            new PaymentData(),
            new ClassicSpecificData()
        ),
        new ClassicLogger(),
        new ClassicClient()
    );

class RegistrationData
{
    public function getData()
    {
        return ['hello' => 'there'];
    }
}

class PaymentData
{
    public function getData()
    {
        return ['pay' => 'yes'];
    }
}

class ClassicLogger
{
    public function log(array $data)
    {
        $header = 'Here is a request to external system!';
        $body = implode(', ', $this->maskData($data));
        Logger::log($header . '. \n ' . $body);
    }
}
class ClassicClient
{
    private $properties;

    public function __construct()
    {
        $this->properties =
            [
                'timeout' => 60,
                'encoding' => 'utf-8',
                'language' => 'en',
            ];
    }
}

Відтоді я дуже обережно ставлюся до спадщини, бо мене багато разів боляче.

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


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