Во времена, когда я писал на Лиспе и совсем не был знаком с ООП, я пытался найти паттерны проектирования, которые мог бы применить у себя в коде. И всё время натыкался на какие-то жуткие схемы классов. В итоге сделал вывод, что эти паттерны в функциональном программировании не применимы.
Теперь я пишу на Питоне и с ООП знаком. И паттерны мне теперь намного понятней. Но меня по-прежнему воротит от развесистых схем классов. Многие паттерны прекрасно работают в функциональной парадигме. Опишу несколько примеров.
Классические реализации паттернов приводить не буду. Те, кто с ними не знаком, могут поинтересоваться в Википедии или в других источниках.
Наблюдатель
Нужно обеспечить возможность каким-то объектам подписываться на сообщения, а каким-то эти сообщения отсылать.
Реализуется словарём, который и представляет собой «почту». Ключами будут названия рассылок, а значениями списки подписчиков.
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
Общий вывод
Не обязательно городить огород из абстрактных классов, конкретных классов, интерфейсов и т.д. Минимальные возможности обращения с функциями как с объектами первого класса, уже позволяют довольно лаконично применять те же шаблоны проектирования. Иногда даже не замечая этого :)
Автор: Yoschi