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

в 10:15, , рубрики: python, паттерны проектирования, функциональное программирование, метки: , ,

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

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

Наблюдатель

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

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

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js