Хотите повысить возможность повторного использования кода? Как-нибудь примените наследование и АБРАКАДАБРА! Код теперь можно использовать повторно как никогда! — Вот во что я верил, когда впервые узнал о наследовании. Итак, когда наш преподаватель курса ООП сказал нам, что наследование не всегда делает код пригодным для повторного использования, а иногда делает прямо противоположное, для меня это едва ли имело смысл.

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

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

Давайте создадим приложение!

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

Мозговой штурм решения

Чтобы сделать необходимые классы, наивным подходом было бы создание одного класса для каждого типа уток. Например, мы можем создать классы MallardDuck, MarbledDuck, DomestickDuck и реализовать каждый из их атрибутов и свойств (например, их способность летать, плавать и т. д.).

Очевидно, что это создаст много повторяющихся кодов.

Итак, мы можем воспользоваться помощью наследования. Мы можем создать класс «Утка», который реализует стандартное поведение (например, полет, плавание и т. д.), и использовать его в качестве базового (родительского) класса. Производные (дочерние) классы, такие как MallardDuck, CanvasDuck и т. д., могут наследовать класс Duck. Таким образом не будет дублирования кода.

Давайте рассмотрим класс Duck примерно так (на C#):

public class Duck
{
    public void Swim()
    {
        Console.WriteLine("I'm swimming!"); 
    }
    public void Fly()
    {
        Console.WriteLine("I'm flying!");
    }
    public void Quack()
    {
        Console.WriteLine("Quack! Quack!");
    }
}

Все готово! Теперь мы можем создать любой класс вида утки и расширить класс утки. Дублированного кода не будет.

Но увы! Возникла проблема.

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

Все утки летают. Даже «резиновые» утки!

Не все утки должны уметь летать, особенно резиновые. Но так как все утки расширяют класс Duck, и в этом классе метод Fly() дает возможность летать, а опция no can't fly дает возможность уткам не летать.

Рассмотрение альтернативного подхода

Почему бы нам не переопределить метод Fly? Почему бы нам не создать интерфейс с именем IFly и не реализовать его во всех дочерних классах Duck? Опять же, нельзя ли объявить утку абстрактным классом и реализовать метод Fly() в дочерних классах?

Да, мы можем, и это решает текущую проблему. Но эти решения приводят к другой проблеме — «дубликатам кодов». Представьте, как сложно будет переопределить метод Fly() для 10 и более классов. Также, продолжая работу над проектом, вы можете столкнуться с той же проблемой другим методом. В этом случае количество строк кода будет увеличиваться в геометрической прогрессии. Вот почему наследование не улучшает возможности повторного использования кода.

В подобных ситуациях единственное, что может помочь, — это «Стратегический паттерн».

Паттерн стратегии

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

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

Шаблон стратегии следует трем принципам проектирования. И первый принцип:

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

Другими словами, мы должны отделять то, что меняется, от того, что остается неизменным.

В нашем приложении все утки умеют плавать. Таким образом, мы можем сохранить метод Swim() в классе Duck. Но поведение Fly() и Quack() варьируется от утки к утечке. Вот по этому принципу и надо их от Дака изолировать. И с этой целью мы можем создать два набора классов, один набор для летающего поведения, а другой для шарлатанского поведения.

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

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

Программа для интерфейса, а не для реализации

Следует отметить, что здесь программирование интерфейса фактически означает программирование супертипа. И это можно сделать как с интерфейсом java (или любым другим языком), так и без него. Программирование реализации выглядит примерно так:

Dog d = new Dog();

С другой стороны, программирование для супертипа выглядит так:

Animal a = new Dog();

Таким образом, не обязательно использовать интерфейс, чтобы «запрограммировать интерфейс». Но для нашего текущего решения мы используем интерфейс.

Взгляните на следующую диаграмму классов, которая представляет набор классов поведения полета:

Наша цель состоит в том, чтобы запрограммировать супер-тип. Реализуя классы в соответствии с диаграммой выше, мы можем создать атрибут типа FlyBehavior в классе Duck. И в этом атрибуте мы можем назначить поведение CanFly или CanNotFly.

При этом мы применили еще один принцип проектирования:

Предпочтение композиции наследованию

Если все выглядит немного запутанным, взгляните на более широкую картину того, что мы сделали до сих пор. Мы удалили метод Fly() из класса Duck и создали набор классов, которые представляют различные типы поведения при полете (могут летать и не могут летать). Вместо метода Fly() мы используем экземпляр FlyBehavior в классе Duck. И теперь класс Duck ничего не наследует, он содержит переменную экземпляра другого класса (FlyBehavior), и после расширения класса Duck мы можем реализовать его желаемую функциональность. Вот что такое композиция.

После выполнения этих задач наша окончательная диаграмма классов должна выглядеть примерно так:

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

Недостаток

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

Вывод

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

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

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