Главная

Когда использовать абстрактные классы

Часто абстрактные классы используются либо чрезмерно, либо неправильно.

Поделиться:
 

25+

На самом деле, они должны использоваться только в нескольких случаях.

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

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

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

Определение абстрактных классов

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

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

Обратите внимание, что doSomething() — неабстрактный метод, ибо в нем реализовано тело, а doSomethingElse() — абстрактный. Вы не можете напрямую создать экземпляр класса Base. Если вы попробуете это сделать, ваш компилятор будет жаловаться:

Вместо этого вам нужно создать подкласс для класса Base:

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

Swiss Army Controller

Давайте рассмотрим распространенное злоупотребление абстрактными классами, с которым я часто сталкиваюсь. Признаюсь, я тоже страдал этим … да и вы, наверное, тоже.

Хотя этот антипример может проявляться практически везде, я часто вижу его в Model-View-Controller (MVC) фреймворках на уровне контроллера. По этой причине я стал называть это Swiss Army Controller (контролером швейцарской армии).
Антипаттерн прост: ряд подклассов, связанных только тем, где они находятся в стеке, наследуются от общего абстрактного базового класса. Этот абстрактный базовый класс содержит любое количество общих «служебных» методов. Подклассы вызывают служебные методы из своих собственных методов.

Swiss Army Controllers обычно появляются так:
1. Разработчики начинают создавать веб-приложения, используя какой-то MVC-фреймворк, например Jersey.
2. Поскольку они используют MVC, они поддерживают свою первую ориентированную на пользователя веб-страницу с помощью метода конечной точки внутри класса UserController.

3. Разработчики создают вторую веб-страницу и, следовательно, добавляют новую конечную точку в контроллер.
Один разработчик замечает, что обе конечные точки выполняют одну и ту же логику (скажем, создание URL-адреса с учетом набора параметров) и поэтому перемещает эту логику в отдельный метод constructUrl() внутри UserController.

4. Команда начинает работу над продукт-ориентированными страницами. Разработчики создают второй контроллер ProductController, чтобы не объединять все методы в один класс.
5. Разработчики понимают, что новому контроллеру может также понадобиться метод constructUrl(). В то же время до них доходит : «Эй! Эти два класса являются контроллерами!» И поэтому должны быть естественно связаны.
Таким образом, они создают абстрактный класс BaseController, перемещают в него constructUrl() и добавляют расширения BaseController к определению классов UserController и ProductController.

6. Этот процесс повторяется до тех пор, пока BaseController не будет иметь десять подклассов и 75 общих методов.

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

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

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

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

Скажу честно, этот подход не намного лучше, а возможно даже, немного хуже. Если вы не создаете экземпляр контроллера, вы все равно привязываете контроллер к другим классам.

А что если вам нужно использовать метод в DAO? Ваш уровень DAO не должен знать о ваших контроллерах. Хуже того, введя несколько статических методов, вы значительно усложнили тестирование и макетирование (с англ. mocking). Здесь важно подметить поток взаимодействия.

В этом примере вызов выполняется непосредственно к одному из методов конкретного подкласса. Затем в какой-то момент этот метод вызывает один или несколько служебных методов в абстрактном базовом классе.

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

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

Теперь есть набор служебных методов, которые действительно могут быть использованы любым классом, которому они могут понадобиться. Кроме того, мы можем разбить эти методы на связанные группы. Приведенная выше диаграмма изображает класс с именем UrlUtility, который может содержать только методы, связанные с созданием и анализом URL-адресов. Мы также могли бы создать один класс с методами, связанными с манипулированием строками, другой с методами, связанными с текущим аутентифицированным пользователем приложения и т. д.

Также обратите внимание, что этот подход также хорошо согласуется с композицией по принципу наследования.
Наследование и абстрактные классы являются мощной конструкцией. Многочисленные примеры изобилуют его неправильным использованием, частым примером является Swiss Army Controller. Я обнаружил, что наиболее типичное использование абстрактных классов можно рассматривать как антипаттерны, и что в действительности существует мало хороших применений абстрактным классам.

Шаблонный метод (The template method)

С учетом вышесказанного давайте теперь рассмотрим одно из лучших применений абстрактных классов, описанного шаблоном “the template method”. Я обнаружил, что “The template method” является одним из менее известных, но наиболее полезных шаблонов проектирования.

Первоначально шаблон был описан в книге «Банда четырех» (с англ. the Gang of Four Design Patterns); Многие описания теперь можно найти и в Интернете. Ладно, давайте посмотрим, как этот шаблон относится к абстрактным классам и как его можно применять в реальном мире.

Для согласованности опишу еще один сценарий, в котором используются контроллеры MVC. В нашем примере у нас есть приложение, для которого существует несколько разных типов пользователей (сейчас мы определим два: employee и admin). При создании нового пользователя любого типа есть небольшие различия в зависимости от того, какого типа пользователя мы создаем. Например, назначение ролей должно выполняться по-разному.

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

Затем нам нужно просто расширить BaseUserController один раз для каждого типа пользователя:

Каждый раз, когда нам нужно поддерживать новый тип пользователя, мы просто создаем новый подкласс BaseUserController и соответствующим образом реализуем метод setRoles().
Давайте сопоставим взаимодействие здесь с взаимодействием, которое мы видели в случае с Swiss Army Controller.

Используя метод шаблонного подхода, мы видим, что вызывающая(caller) сторона (в данном случае само MVC, отвечающее на веб-запрос) вызывает метод в абстрактном базовом классе, а не в конкретном подклассе.Это является следствием того факта, что мы сделали метод setRoles(), который реализован в подклассах, защищенным.

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

Правило большого пальца

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

Задайте себе вопрос: «Будут ли пользователи(с англ. callers) ваших классов вызывать методы, которые реализованы в абстрактном базовом классе, или методы, реализованные в ваших конкретных подклассах?»
1 ) Если первое, то вы намереваетесь представить только методы, реализованные в вашем абстрактном классе, и есть вероятность, что вы создали хороший, поддерживаемый набор классов.
2) Если последнее, то “callers“ будут вызывать методы, реализованные в ваших подклассах, которые, в свою очередь, будут вызывать методы в абстрактном классе. Есть большая вероятность того, что вы проектируете неподдерживаемый антипаттерн.

25+