Fork me on GitHub

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

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

Имея некоторый опыт и скорее всего несознательно приходя к решениям которые уже где то описаны как готовые, я начал знакомиться с паттернами проектирования по книге Эрика Фримен и Элизабет Фримен “Паттерны проектирования”.

Саму книгу можно купить тут

Итак, некоторые принципы проектирования упомянутые авторами в первой главе:

  1. Выделить аспекты приложения которые изменяются и отделить их от тех которые не изменяются. “Отделять изменяемое от постоянного”

  2. Программировать на уровне интерфейсов, а не реализации.

  3. Отдавать предпочтение композиции, а не наследованию.

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

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

Паттерн СТРАТЕГИЯ

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

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

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

strategy_duck.png

Поведение уточек вынесено (инкапсулировано) и представлено двумя различными интерфейсами FlyBehavior и QuackBehavior. Сущности FlyWithWings, FlyNoWay реализуют уже настоящие поведения - “уточка летит с помощью крыльев”, “уточка не летает”. Сущности Quack, Squeak, MuteQuack реализуют интерфейс QuackBehavior и соответственно реализуют уже какие то реальные качества, “крякает”, “пищит”, “не издает звуков”. Эти сущности и рассматриваются как алгоритмы, ведь действительно они определяют некие разные поведения.

В абстрактный класс Duck “вмонтированы” (композиция) два объекта, представленные переменными типа интерфейса FlyBehavior и QuackBehavior, flyBehavior и quackBehavior соответственно. Получается клиент (Duck) использует инкапсулированные алгоритмы (сущности). Вдобавок Duck содержит набор методов позволяющих оперировать (менять) поведения, вызывать конкретные методы поведений.

Из абстрактного класса Duck могут быть уже наследованы реальные “утки” MallardDuck, RubberDuck в которых может быть переопределен допустим метод отображения (как утка выглядит) и из которых уже можно создавать реальные экземпляры уток.

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

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

Для примера пусть будет два персонажа Рыцарь (Knight) и Вор (Thief). Отделим (инкапсулируем) поведение оружия и применив композицию встроим оружие в обьект который будет представлять персонаж.

Диаграмма классов будет выглядеть так:

strategy_characters.png

Чтобы реализовать интерфейс на Python я использую абстрактный класс с абстрактным методом, таким образом клиент не сможет создать экземпляр и будет вынужден переопределить абстрактный метод. Второй способ который я нашел - это способ основанный полностью на соглашении, т.е. объявляется обычный класс, методы которого возвращают NotImplemented. Таким образом клиентский код должен наследоваться и переопределять эти методы. К сожалению в Python нет специальной конструкции вроде interface как в Java, но способы реализовать абстракции к счастью имеются.

Итак интерфейс и классы Knife, Sword реализующие оружие - выглядят так:

from abc import ABCMeta, abstractmethod


class IWeaponBehaviour(metaclass=ABCMeta):
    @abstractmethod
    def use_weapon(self):
        raise NotImplementedError()

# 2nd approach
#class IWeaponBehaviour:
#    def use_weapon(self):
#        raise NotImplementedError()


class KnifeBehaviour(IWeaponBehaviour):
    def use_weapon(self):
        print("Knife hit...")
        print("Damage 2 ...")


class SwordBehaviour(IWeaponBehaviour):
    def use_weapon(self):
        print("Sword hit...")
        print("Damage 5 ...")

Классы реализуют метод интерфейса use_weapon(), уникальный для каждого типа оружия.

Абстрактный класс персонажа и конкретные классы персонажей выглядят так:

class AbstractCharacter(metaclass=ABCMeta):

    @property
    @abstractmethod
    def weapon(self): # IWeaponBehaviour object
        raise NotImplementedError()

    def set_weapon(self, wb):
        self.weapon = wb

    @abstractmethod
    def fight(self):
        raise NotImplementedError()

    def use_weapon(self):
        self.weapon.use_weapon()

class Thief(AbstractCharacter):
    weapon = KnifeBehaviour()

    def fight(self):
        print("Thief do 1 step")
        self.use_weapon()


class Knight(AbstractCharacter):
    weapon = SwordBehaviour()

    def fight(self):
        print("Knife do 2 steps")
        self.use_weapon()

Класс конкретного персонажа использует конкретное поведение оружия, у абстрактного класса AbstractCharacter есть метод set_weapon() позволяющий менять оружие персонажа “на лету” и метод use_weapon() позволяющий использовать оружие. Поведение оружия используется как свойство, тут используется композиция, мы используем инкапсулированый объект Оружие в объекте Персонаж.

Полный код выглядит вот так:

# -*- coding: utf-8 -*-
"""
Created on Thu Jul 27 09:36:35 2017

@author: biceps
"""
from abc import ABCMeta, abstractmethod


class IWeaponBehaviour(metaclass=ABCMeta):
    @abstractmethod
    def use_weapon(self):
        raise NotImplementedError()

# 2nd approach
#class IWeaponBehaviour:
#    def use_weapon(self):
#        raise NotImplementedError()


class KnifeBehaviour(IWeaponBehaviour):
    def use_weapon(self):
        print("Knife hit...")
        print("Damage 2 ...")


class SwordBehaviour(IWeaponBehaviour):
    def use_weapon(self):
        print("Sword hit...")
        print("Damage 5 ...")


class AbstractCharacter(metaclass=ABCMeta):

    @property
    @abstractmethod
    def weapon(self): # IWeaponBehaviour object
        raise NotImplementedError()

    def set_weapon(self, wb):
        self.weapon = wb

    @abstractmethod
    def fight(self):
        raise NotImplementedError()

    def use_weapon(self):
        self.weapon.use_weapon()

class Thief(AbstractCharacter):
    weapon = KnifeBehaviour()

    def fight(self):
        print("Thief do 1 step")
        self.use_weapon()


class Knight(AbstractCharacter):
    weapon = SwordBehaviour()

    def fight(self):
        print("Knife do 2 steps")
        self.use_weapon()



thief = Thief()    
thief.fight()
thief.set_weapon(SwordBehaviour())
thief.fight()

knight = Knight()
knight.fight()

в конце приведен небольшой проверочный код:

thief = Thief() # cоздадим персонаж Thief   
thief.fight() # ударим оружием Thief - Knife по умолчанию 
thief.set_weapon(SwordBehaviour()) # дадим Thief другое оружие - Sword
thief.fight() # попросим Вора ударить Мечом

knight = Knight() # создадим Рыцаря
knight.fight() # попросим Рыцаря ударить Мечом 

Результаты выглядят так:

Thief do 1 step
Knife hit...
Damage 2 ...
Thief do 1 step
Sword hit...
Damage 5 ...
Knife do 2 steps
Sword hit...
Damage 5 ...

Выводы

Итак общий принцип используемый в паттерне СТРАТЕГИЯ это отделение неких сущностей в отдельные классы, которые можно использовать в клиентском коде с помощью композиции. Таким образом появляется возможность модифицировать эти сущности отдельно от кода клиента. В этом заключается гибкость и простота добавления новых сущностей и простота использования на стороне клиента.


Comments

comments powered by Disqus