Абстрактные классы используются не по назначению. Но у них есть несколько веских целей.

Абстрактные классы - это основная функция многих объектно-ориентированных языков, таких как Java.

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

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

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

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

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

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

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

Приведем небольшой пример:

public abstract class Base {
    public void doSomething() {
        System.out.println("Doing something...")
    }
    public abstract void doSomethingElse();
}

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

Base b = new Base();

Вместо этого вам нужно создать подкласс Base следующим образом:

public class Sub extends Base {
    public abstract void doSomethingElse() {
        System.out.println("Doin' something else!");
    }
}

Обратите внимание на требуемую реализацию метода doSomethingElse().

Не во всех объектно-ориентированных языках есть понятие абстрактных классов.

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

Швейцарский армейский контролер

Давайте рассмотрим распространенное злоупотребление абстрактными классами, с которым я часто сталкивался. Я виновен в том, что увековечил его; у вас, вероятно, тоже есть.

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

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

Контроллеры швейцарской армии обычно возникают следующим образом:

1. Разработчики начинают создавать веб-приложение, используя фреймворк MVC, такой как Jersey.

2. Поскольку они используют инфраструктуру MVC, они поддерживают свою первую ориентированную на пользователя веб-страницу методом конечной точки внутри класса UserController.

3. Разработчики создают вторую веб-страницу и поэтому добавляют в контроллер новую конечную точку.

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

4. Команда начинает работу над страницами, ориентированными на продукт. Разработчики создают второй контроллер ProductController, чтобы не втискивать все методы в один класс.

5. Разработчики понимают, что в новом контроллере может также потребоваться использование метода constructUrl(). В то же время они понимают: «Эй! Эти два класса - контроллеры! » и поэтому должны быть естественным образом связаны.

Итак, они создают абстрактный класс BaseController, перемещают в него constructUrl() и добавляют extends BaseController к определению класса UserController и ProductController.

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

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

Первая проблема связана с дизайном. На самом деле все эти разные контроллеры не связаны друг с другом.

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

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

String url = new UserController().constructUrl(key, value);

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

String url = UserController.constructUrl(key, value);

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

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

Здесь важно подчеркнуть поток взаимодействия.

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

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

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

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

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

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

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

Шаблонный метод

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

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

Давайте посмотрим, как это связано с абстрактными классами и как его можно применить в реальном мире.

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

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

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

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

Любые отличающиеся детали будут отнесены к абстрактным методам, которые будут реализовывать конкретные подклассы:

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

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

Давайте сравним взаимодействие здесь с взаимодействием, которое мы наблюдали с командующим швейцарской армией.

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

Это становится понятным в том факте, что мы сделали метод setRoles(), который реализован в подклассах, защищенным.

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

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

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

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

Спросите себя: «Будут ли вызывающие ваши классы вызывать методы, реализованные в вашем абстрактном базовом классе, или методы, реализованные в ваших конкретных подклассах?»

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