Как использовать полиморфизм во время выполнения

Одним из преимуществ объектно-ориентированного дизайна является способность объектов разделять одни модели поведения и одновременно отличаться друг от друга. Как правило, это достигается посредством наследования - когда многие подклассы наследуют свойства от родительского класса, но при необходимости могут переопределять определенное поведение. Это очень полезный и распространенный шаблон проектирования; однако бывают ситуации, когда полиморфизм через наследование неуместен. Рассмотрим, например, когда вам нужно изменить только одно поведение, но в остальном вы хотите, чтобы объект оставался прежним. Или, когда вы хотите, чтобы объект изменил свое поведение во время выполнения на основе некоторого внешнего (но непредсказуемого) фактора. В таких случаях схема наследования может вызвать излишне раздутый код, который трудно поддерживать (особенно при увеличении количества подтипов). Однако есть лучший подход: шаблон стратегии.

Полиморфизм через стратегии

Шаблон стратегии, также известный как шаблон политики, представляет собой шаблон проектирования поведения, который позволяет объекту выполнять некоторый алгоритм (стратегию) на основе внешнего контекста, предоставляемого во время выполнения. Этот шаблон особенно полезен, когда у вас есть объект, который должен иметь возможность выполнять одно поведение разными способами в разное время. Используя шаблон стратегии, вы можете определить набор алгоритмов, которые могут быть динамически предоставлены конкретному объекту, если / когда они необходимы. Этот шаблон имеет ряд преимуществ, в том числе: инкапсуляцию определенных алгоритмов в их собственные классы; изоляция знаний о том, как реализованы алгоритмы; и код, который является более гибким, мобильным и удобным в обслуживании. К последнему пункту, вы можете заметить, что это те же самые атрибуты, которые возникают из кода, который следует Принципу Открытия / Закрытия (OCP), и действительно, шаблон стратегии является отличным способом написания кода, придерживающегося OCP.

При реализации паттерна стратегии вам понадобятся три основных элемента:

  1. клиент, который знает о существовании некоторой абстрактной стратегии, но может не знать, что делает эта стратегия и как она это делает.
  2. Набор стратегий, которые клиент может использовать при наличии одной из них. Они могут иметь форму первоклассных функций, объектов, классов или какой-либо другой структуры данных.
  3. Необязательный контекст, который клиент может предоставить своей текущей стратегии для использования при выполнении.

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

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

Полиморфизм через чистое наследование

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

Здесь у нас есть Runner родительский класс и три подкласса, которые наследуются от него: Jogger; Sprinter; и Marathoner. Каждый подкласс заменяет метод run родительского класса своей собственной реализацией. Впоследствии, когда мы создаем экземпляры бегунов каждого типа и передаем их новому объекту Race, мы можем видеть, что каждый из них использует свое собственное поведение при запуске гонки.

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

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

Простые стратегии и поток управления

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

В этой версии у нас есть единственный Runner класс с конструктором, который принимает новый аргумент: strategy. В этом случае наша стратегия - это просто символ, который мы затем используем в рефакторинговом run методе. Новый метод run содержит оператор case, который проверяет атрибут strategy данного экземпляра и соответственно выполняет некоторый бит кода. Действительно, когда мы начинаем гонку на этот раз, мы получаем тот же результат, что и раньше.

В некотором смысле эта версия программы является улучшением по сравнению с нашей более ранней версией, хотя в других случаях это шаг назад. С другой стороны, теперь мы можем изменить наивную стратегию данного бегуна, используя сеттер, чтобы назначить ему новый символ, как в alice_ruby.strategy = :marathon. Таким образом мы можем эффективно изменить поведение конкретного объекта, не меняя его класс. Однако использование длинного регистра в методе Runner#run проблематично. Такой поток управления является явным нарушением OCP, потому что мы не можем расширить run метод, не открыв его для модификации. Итак, что нам делать, если мы хотим иметь возможность динамически изменять стратегии, при этом придерживаясь OCP?

Шаблон стратегии в действии

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

Как и во втором фрагменте, наш класс Runner принимает аргумент strategy при построении, а также имеет сеттер для изменения этой стратегии, если это необходимо. Однако вместо того, чтобы передавать простой символ в Runner для использования в структуре управления, мы вместо этого передаем ему один из нескольких классов стратегии, определенных в модуле RunStrategies. Каждая из этих стратегий имеет run метод, что означает, что наши клиентские объекты могут выполнять любую из них с одним и тем же кодом. Поскольку в Ruby нет формальных интерфейсов, мы предоставляем собственный простой механизм проверки ошибок, унаследовав каждую стратегию от класса RunStrategyInterface, который вызывает ошибку при вызове метода run класса. (Если стратегия не может реализовать версию этого метода сама по себе, тогда метод RunStrategyInterface run класса выполнит и вызовет ошибку, которую мы затем можем проверить перед развертыванием.)

Когда эта программа запускается, каждому бегуну предоставляется желаемая стратегия при создании экземпляра. Затем во время выполнения программы участники могут использовать эти стратегии по мере необходимости, передавая свое собственное имя в качестве контекста стратегии. И если бы мы хотели обновить стратегию конкретного бегуна в середине программы, мы могли бы легко сделать это с помощью метода установки, как в alice_ruby.strategy = RunStrategies::Marathon.

Используя шаблон стратегии, мы дали нашей программе возможность динамически изменять алгоритмы во время выполнения в зависимости от контекста. Кроме того, наш Runner#run метод совместим с OCP, потому что мы можем создавать новые модели поведения, просто реализуя новые стратегии (вместо того, чтобы изменять структуру управления в методе выполнения).

TL;DR

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

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

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

Ссылки

  1. Блог: Стратегия; Гуру рефакторинга
  2. Блог: Стратегия; OODesign
  3. Блог: Как использовать шаблон разработки стратегии в Ruby; RubyGuides
  4. Википедия: шаблон стратегии
  5. Википедия: Паттерны дизайна