Паттерны проектирования без ООП

от автора

Во времена, когда я писал на Лиспе и совсем не был знаком с ООП, я пытался найти паттерны проектирования, которые мог бы применить у себя в коде. И всё время натыкался на какие-то жуткие схемы классов. В итоге сделал вывод, что эти паттерны в функциональном программировании не применимы.

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

Наблюдатель

Нужно обеспечить возможность каким-то объектам подписываться на сообщения, а каким-то эти сообщения отсылать.
Реализуется словарём, который и представляет собой «почту». Ключами будут названия рассылок, а значениями списки подписчиков.

from collections import defaultdict  mailing_list = defaultdict(list)  def subscribe(mailbox, subscriber):     # Подписывает функцию subscriber на рассылку с именем mailbox     mailing_list[mailbox].append(subscriber)  def notify(mailbox, *args, **kwargs):     # Вызывает подписчиков рассылки mailbox, передавая им параметры     for sub in mailing_list[mailbox]:         sub(*args, **kwargs)  

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

def fun(insert):     print 'FUN %s' % insert  def bar(insert):     print 'BAR %s' % insert 

Подписываем наши функции на рассылки:

>>> subscribe('insertors', fun) >>> subscribe('insertors', bar) >>> subscribe('bars', bar) 

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

>>> notify('insertors', insert=123) FUN 123 BAR 123  >>> notify('bars', 456) BAR 456 
Шаблонный метод

Нужно обозначить каркас алгоритма и дать возможность пользователям переопределять определенные шаги в нём.
Функции высшего порядка, такие как map, filter, reduce по сути и являются такими шаблонами. Но давайте посмотрим, как можно провернуть такое же самому.

def approved_action(checker, action, obj):     # Шаблон, который выполняет над объектом obj действие action,     # если проверка checker дает положительный результат     if checker(obj):         action(obj)  import os def remove_file(filename):     approved_action(os.path.exists, os.remove, filename)  import shutil def remove_dir(dirname):     approved_action(os.path.exists, shutil.rmtree, dirname) 

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

def approved_action(obj, checker=lambda x: True, action=lambda x: None):     if checker(obj):         action(obj)  from functools import partial remove_file = partial(approved_action, checker=os.path.exists, action=os.remove) remove_dir = partial(approved_action, checker=os.path.exists, action=shutil.rmtree)  import sys printer = partial(approved_action, action=sys.stdout.write) 
Состояние

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

from random import randint # Функции, выполняющие работу в каждом из состояний. # Аргументом ко всем является обрабатываемая заявка # Вызовы randint эмулируют логику, принимающую какие-то решения в зависимости от внешних обстоятельств  def start(claim):     print u'заявка подана'     claim['state'] = 'analize'  def analize(claim):     print u'анализ заявки'     if randint(0, 2) == 2:         print u'заявка принята к исполнению'         claim['state'] = 'processing'     else:         print u'требуется уточнение'         claim['state'] = 'clarify'  def processing(claim):     print u'проведены работы по заявке'     claim['state'] = 'close'  def clarify(claim):     if randint(0, 4) == 4:         print u'пользователь отказался от заявки'         claim['state'] = 'close'     else:         print u'уточнение дано'         claim['state'] = 'analize'  def close(claim):     print u'заявка закрыта'     claim['state'] = None   # Определение конечного автомата. Какие функции в каком состоянии вызывать state = {'start': start,          'analize': analize,          'processing': processing,          'clarify': clarify,          'close': close}  # Запуск заявки в работу def run_claim():     claim = {'state': 'start'} # Новая заявка     while claim['state'] is not None: # Крутим машину, пока заявка не закроется         fun = state[claim['state']] # определяем запускаемую функцию         fun(claim) 

Как видим, основную часть кода занимает «бизнес-логика», а не оверхед на применение паттерна. Автомат легко расширять и изменять, просто добавляя/заменяя функции в словаре state.

Запустим пару раз, чтобы убедиться в работоспособности:

>>> run_claim() заявка подана анализ заявки требуется уточнение уточнение дано анализ заявки заявка принята к исполнению проведены работы по заявке заявка закрыта  >>> run_claim() заявка подана анализ заявки требуется уточнение пользователь отказался от заявки заявка закрыта 
Команда

Задача – организовать «обратный вызов». То есть, чтобы вызываемый объект мог из своего кода обратиться к вызывающему.
Этот паттерн видимо возник из-за ограничений статичных языков. Функциональщики бы его даже звания паттерна не удостоили. Есть функция – пожалуйста, передавай её куда хочешь, сохраняй, вызывай.

def foo(arg1, arg2): # наша команда     print 'FOO %s, %s' (arg1, arg2)  def bar(cmd, arg2):     # Приемник команды. Ничего не знает о функции foo...     print 'BAR %s' % arg2     cmd(arg2 * 2) # ...но вызывает её 

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

>>> from functools import partial >>> bar(partial(foo, 1), 2) BAR 2 FOO 1, 4 

…либо заворачиванием в lambda

>>> bar(lambda x: foo(x, 5), 100) BAR 100 FOO 200, 5 
Общий вывод

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

ссылка на оригинал статьи http://habrahabr.ru/post/184156/


Комментарии

Добавить комментарий

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