Какой код должен быть включен в абстрактный класс?

10

В последнее время меня беспокоит использование абстрактных классов.

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

Иногда случается, что абстрактный класс создается после создания некоторых «производных» классов (поскольку родительского / абстрактного класса там нет, они еще не были получены, но вы понимаете, что я имею в виду). В этих случаях абстрактный класс обычно используется как место, где вы можете поместить любой тип общего кода, который содержат текущие производные классы.

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

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

Позвольте мне привести пример: у абстрактного класса A есть метод a () и абстрактный метод aq (). Метод aq () в обоих производных классах AB и AC использует метод b (). Должен ли b () переместиться в A? Если да, то если кто-то смотрит только на A (давайте притворимся, что AB и AC не существуют), существование b () не имело бы особого смысла! Это плохо? Должен ли кто-то иметь возможность взглянуть на абстрактный класс и понять, что происходит, не посещая производные классы?

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

Что ты думаешь / практикуешь?

Александрос Гугоусис
источник
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.
user1118321
1
@ user1118321, отлично, приятно думать нестандартно. Имеет особый смысл, если не b()нуждается ни в каком долговечном состоянии экземпляра.
Эрик Эйдт
В (3) меня беспокоит то, что абстрактный класс больше не описывает самодостаточную часть поведения. Это кусочки кода, и я не могу понять код абстрактного класса, не переходя назад и вперед между этим и всеми производными классами.
Александрос Гугоусис
Можете ли вы сделать базовый / дефолтный acвызов bвместо acабстрактного?
Эрик Эйдт
3
Для меня самый важный вопрос: почему и AB, и AC оба используют один и тот же метод b (). Если это просто совпадение, я бы оставил две аналогичные реализации в AB и AC. Но, вероятно, это потому, что существует некоторая общая абстракция для AB и AC. Для меня DRY - это не столько ценность, сколько подсказка, что вы, вероятно, пропустили какую-то полезную абстракцию.
Ральф Клеберхофф,
2

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

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

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

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

Однако всегда есть вопросы об изменениях, что изменится и как это изменится и почему это изменится.

Наследование может привести к хрупкому и хрупкому телу источника (см. Также Наследование: просто прекратите его использовать! ).

Проблема хрупкого базового класса является фундаментальной архитектурной проблемой систем объектно-ориентированного программирования, в которых базовые классы (суперклассы) считаются «хрупкими», поскольку кажущиеся безопасными модификации базового класса, если они наследуются производными классами, могут вызывать сбои в работе производных классов. , Программист не может определить, безопасно ли изменение базового класса, просто изучая методы базового класса изолированно.

Ричард Чемберс
источник
Я уверен, что любая концепция ООП может привести к проблемам, если она используется не по назначению или чрезмерно или неправильно! Более того, допущение, что b () является библиотечным методом (например, чистой функцией без других зависимостей), сужает область моего вопроса. Давайте избегать этого.
Александрос Гугоусис
@AlexandrosGougousis Я не предполагаю, что b()это чистая функция. Это может быть функтоид или шаблон или что-то еще. Я использую фразу «метод библиотеки» как обозначение некоторого компонента, будь то чистая функция, объект COM или что-либо еще, что используется для функциональности, которую оно предоставляет решению.
Ричард Чемберс
Извините, если бы я не был ясен! Я упомянул чистую функцию как один из многих примеров (вы дали больше).
Александрос Гугоусис
1

Ваш вопрос предлагает либо подход, либо подход к абстрактным классам. Но я думаю, что вы должны рассматривать их как еще один инструмент в вашем наборе инструментов. И тогда возникает вопрос: для каких работ / задач абстрактные классы являются правильным инструментом?

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

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

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

Иногда случается, что абстрактный класс создается после создания некоторых «производных» классов (поскольку родительского / абстрактного класса там нет, они еще не были получены, но вы понимаете, что я имею в виду). В этих случаях абстрактный класс обычно используется как место, где вы можете поместить любой тип общего кода, который содержат текущие производные классы.

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

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

Если он s()является частью общедоступного интерфейса, он становится немного сложнее. В предположении, что s()это не имеет ничего общего Bи не Cимеет отношения к Aвам, вы не должны вкладывать s()внутрь A. Так где же это поставить? Я бы поспорил за объявление отдельного интерфейса I, который определяет s(). Опять же, вы должны создать отдельный класс, который содержит логику для реализации s()и то и другое Bи Cзависит от него.

Наконец, вот ссылка на отличный ответ на интересный вопрос о SO, который может помочь вам решить, когда переходить на интерфейс, а когда на абстрактный класс:

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

larsbe
источник
Я бы согласился с тем, что s (), являющаяся частью общедоступного интерфейса, усложняет задачу. Я бы вообще избегал определения открытых методов, вызывающих другие открытые методы в том же классе.
Александрос Гугоусис
1

Мне кажется, вы упускаете точку объектной ориентации, как логически, так и технически. Вы описываете два сценария: группировка общего поведения типов в базовом классе и полиморфизм. Оба они являются законными приложениями абстрактного класса. Но следует ли сделать класс абстрактным или нет, зависит от вашей аналитической модели, а не от технических возможностей.

Вы должны распознать тип, который не имеет воплощений в реальном мире, но закладывает основу для определенных типов, которые существуют. Пример: животное. Там нет такого понятия, как животное. Это всегда собака, кошка или что-то еще, но настоящего животного нет. Все же Животное создает их всех.

Тогда вы говорите об уровнях. Уровни не являются частью сделки, когда дело доходит до наследования. Признание общих данных или общего поведения также не является техническим подходом, который, скорее всего, не поможет. Вы должны распознать стереотип и затем вставить базовый класс. Если такого стереотипа нет, вам лучше использовать пару интерфейсов, реализованных несколькими классами.

Название вашего абстрактного класса вместе с его членами должно иметь смысл. Он должен быть независимым от любого производного класса, как технически, так и логически. Подобно Animal может иметь абстрактный метод Eat (который будет полиморфным) и логические абстрактные свойства IsPet и IsLifestock, которые имеют смысл, не зная о кошках, собаках или свиньях. Любая зависимость (техническая или логическая) должна идти только одним путем: от потомка к основанию. Сами базовые классы также не должны знать о нисходящих классах.

Мартин Маат
источник
Стереотип, который вы упоминаете, является более или менее тем же самым, что я имею в виду, когда говорю о функциональности более высокого уровня или когда кто-то говорит о обобщенных концепциях, описываемых классом выше в иерархии (по сравнению с более специализированными концепциями, описанными классами ниже в иерархии).
Александрос Гугоусис
«Сами базовые классы также не должны знать о нисходящих классах». Я полностью согласен с этим. Родительский класс (абстрактный или нет) должен содержать отдельную часть бизнес-логики, которая не нуждается в проверке реализации дочернего класса, чтобы понять это.
Александрос Гугоусис
Есть еще одна вещь в базовом классе, зная его подтипы. Всякий раз, когда разрабатывается новый подтип, базовый класс должен быть изменен, что является обратным и нарушает принцип открытого-закрытого типа. Я недавно столкнулся с этим в существующем коде. Базовый класс имел статический метод, который создавал экземпляры подтипов на основе конфигурации приложения. Намерение было фабричным образцом. Это также нарушает СРП. С некоторыми твердыми моментами я думал: «Да, это очевидно» или «Язык автоматически об этом позаботится», но я обнаружил, что люди более креативны, чем я мог себе представить.
Мартин Маат
0

Первый случай , из вашего вопроса:

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

Второй случай:

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

Тогда ваш вопрос :

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

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

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

Во втором случае вы привели пример абстрактного класса A, получающего метод ab (); IMO метод b () следует перемещать, только если он имеет смысл для ВСЕХ других производных классов. Другими словами, если вы перемещаете его, потому что он используется в двух местах, возможно, это неправильный выбор, потому что завтра может появиться новый конкретный класс Derived, для которого 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, пожалуйста, сделайте мне одолжение и прокомментируйте, что не так с этим ответом.
Вадим Самохин