Пользователь
0,0
рейтинг
21 июня 2013 в 14:15

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

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

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

Наблюдатель

Нужно обеспечить возможность каким-то объектам подписываться на сообщения, а каким-то эти сообщения отсылать.
Реализуется словарём, который и представляет собой «почту». Ключами будут названия рассылок, а значениями списки подписчиков.
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'] = 'analyze'

def analyze(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'] = 'analyze'

def close(claim):
    print u'заявка закрыта'
    claim['state'] = None


# Определение конечного автомата. Какие функции в каком состоянии вызывать
state = {'start': start,
         'analyze': analyze,
         '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
карма
23,0
рейтинг 0,0
Пользователь
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Реклама

Самое читаемое Разработка

Комментарии (35)

  • +11
    Ваш функциональный «наблюдатель» отличается от объектного только переносом состояния в область глобальных переменных.
    • +1
      Ну да, это простейший случай. Если нужно несколько наблюдателей, можно добавить ещё один параметр и в subscribe и в notify, обозначающий, какой из наблюдателей использовать.
      • 0
        Вообще, одна из главных особенностей ООП и Паттернов — отпадает необходимость править код при расширении функционала. Код не изменяется, а именно расширяется.
        • +1
          Боюсь, при переходе от глобального хранилища подписок к локальному, нам придется вносить изменения в код наблюдателя, независимо от парадигмы. Если вы об изменениях в коде клиентов, то они не обязательны. Хочу указываю доп.параметр, хочу нет — в дефолтном случае будет использоваться всё то же глобальное хранилище.
      • –1
        На Пайтоне вы используете ООП, даже когда вам кажется, что вы его не используете. Вот эти ваши «функции» можно вызвать как
        import имяфайла
        
        имяфайла.run_claim()
        
        • +7
          Т.е. по вашему наличие тут точки — признак ООП?
          • –2
            Именно это точка в Пайтоне и значит. Вы от этого класс-объекта можете наследоваться и так далее.
            • +5
              Ну вот возьмите для примера Haskell. Там можем написать
              import Module
              -- ...
              xxx = Module.function_name
              
              Тут тоже ООП? Или в чем отличие от Python?

              То, что модули в питоне объекты — это, условно говоря, деталь реализации.
              Ведь и функции объекты, и коллекции, и даже числа.
              Вы же не будете утверждать, что это ООП-код (дада, тут используются объекты)?
              print(1 + 2)
              

              • +2
                На самом деле, завит от контекста, в котором мы используем «объект».
                xxx = Module.function_name
                

                конечно не ООП.
                А когда мы применяем магию вроде
                from types import ModuleType
                class(ModuleType):
                    ...
                

                или
                function_name.newattr = xxx
                

                то резко замечаем, что и модуль и функция тоже объекты.
                Мне кажется, тут надо разделять «использование ООП» и «возможность использования ООП».
              • –3
                Причём тут Хаскель? В Пайтоне — ООП, в Хаскеле мне всё равно что.
                • +2
                  Да в принципе и не при чем. Главное не говорить на собеседованиях «ООП — это когда точку ставишь, а потом такой список доступных методов вылазит».
                  • –1
                    Я про Пайтон говорю, в Пайтоне это так.
                    • 0
                      Старше меня, а такую чушь несёте, да ещё так упор(но|ото). ООП — это объектно-ОРИЕНТИРОВАННОЕ программирование. Вызов обычной функции через точку, как «метод» модуля — не тот случай. Можно написать целый модуль из чистых (в смысле ФП) функций, и все их вызывать через точку, но это не будет ООП, т.к. ООП — это парадигма, а не синтаксис. На ассемблере тоже есть ООП, однако нет точки:

                      ;; object.method(arg1, arg2)
                      push object
                      push arg1
                      push arg2
                      call method
                      • –2
                        Младше меня, а такую чушь несёте. Причём тут возраст вообще?

                        Вы, вот, например, путаете ФП и процедурный стиль.

                        На Ассемблере, очевидным образом, ООП нет, так как нет инкапсуляции и наследования.

                        Так как написание файла с функциями почти равно на Пайтоне написанию класса с методами, то это, конечно же, ООП.

                        Если вы накидаете бессистемно класс в методами, отнаследовавшись от чего-либо — это не ООП будет что ли? Файл с функциями ровно так и выглядит внутри Пайтона (да и для программиста — тоже).
                        • 0
                          От класса с методами отнаследоваться далее можно. А от модуля с функциями уже нельзя. Вы не сможете (без магии) определить модуль, отсутствующие атрибуты которого будут искаться в «родительском» модуле. Так что модуль — это всё таки экземпляр класса, но не сам класс. Т.е. наследования, так необходимого для ООП, здесь нет.
                          • –1
                            О какой магии речь идёт?

                            Вы точно о Пайтоне говорите?

                            a.py:
                            attrib1="a1"
                            attrib2="a2"
                            
                            def method1():
                                print("a1")
                            
                            def method2():
                                print("a2")
                            
                            def _private():
                                print("private")
                            


                            b.py:
                            from a import *
                            
                            def method2():
                                print("b2")
                            
                            def method3():
                                print("b3")
                            
                            attrib2="b2"
                            
                            method1()
                            method2()
                            method3()
                            
                            print(attrib1, attrib2)
                            
                            _private()
                            


                            Запускаем:
                            a1
                            b2
                            b3
                            a1 b2
                            Traceback (most recent call last):
                              File "b.py", line 17, in <module>
                                _private()
                            NameError: name '_private' is not defined
                            


                            Вы тут не видите принципы ООП что ли?
                            • 0
                              Нет, не вижу. Вы просто скопировали атрибуты одного модуля в другой, наследственной связи не возникло.
                              Если после ваших манипуляций выполнить a.method1 = another_method, то b.method1 никак не изменится.
                              • 0
                                А какой принцип ООП требует, чтобы что-то изменилось в этом случае?
                                • 0
                                  Тот самый, который вы пытаетесь проиллюстрировать. Мы ведь о наследовании говорим?
                                  Все свойства подкласса, которые он не перезаписывал, должны всегда соответствовать свойствам родителя.
                                  По-русски: Если в классе «Гражданин» во время выполнения программы изменился метод «расчет НДФЛ», то это автоматически должно отразиться на всех подклассах — и «Программист» и «Врач» и всех остальных. Кроме, может быть, специального класса «Льготный гражданин», в котором определено, что он НДФЛ считает не как все.
                                  • 0
                                    Они и соответствуют. То, что вы потом на хочу что-то изменили «задним числом» ни о чём не говорит. Это уже особенности реализации, а не принципы ООП.
                                    • +1
                                      Это не «на хочу», а совешенно необходимая вещь. Вместо метода, там мог бы быть какой-нибудь счётчик, или ещё какое-то изменяемое свойство, которое должно быть доступно для всех подклассов и их экземпляров.

                                      Видимо у нас слишком разное понимание этих принципов. Похоже, что вы даже такой
                                      dct.update(another_dict)

                                      или такой
                                      lst[:] = another_list

                                      код тоже отнесёте к проявлениям ООП.
                                      Мне же ближе классическое определение от Алана Кея.
                                      Подчеркну — я знаю, что в Python'е всё есть объект. И это очень удобно. Но писать на нём все же можно по-разному. Статья написана о стиле кодирования, а не об особенностях терминологии. Наверное надо было её назвать «паттерны без классов», а не «без ООП». Меньше возражений было бы.
                            • 0
                              Различных способов реализовать наследование или его подобие при отсутствии встроенной в язык поддержке ООП очень много. Покажите где хотя бы один из них используется в статье. У вас написано «from a import *». У автора такого нет. Наличие возможности использования ООП тем или иным способам не делает любой код использующим ООП. Я, к примеру, могу написать пачку классов вида

                              from math import acos, atan, sin, cos
                              
                              class CoordCartezian:
                                  def __init__(self, x, y, z):
                                      self.x = x
                                      self.y = y
                                      self.z = z
                              
                                  def distance(self, c2):
                                      return ((self.x-c2.x)**2 + (self.y-c2.y)**2 + (self.z-c2.z)**2) ** 0.5
                              
                                  def spherical(self):
                                      r = (self.x*self.x + self.y*self.y + self.z*self.z) ** 0.5
                                      return CoordSpherical(
                                          r,
                                          acos(self.z/r),
                                          atan(self.y/self.x)
                                      )
                              
                              class CoordSpherical:
                                  def __init__(self, r, t, p):
                                      self.r = r
                                      self.t = t
                                      self.p = p
                              
                                  def angle(self, c2):
                                      return acos(sin(self.t)*sin(self.p)*sin(c2.t)*sin(c2.p) + sin(self.t)*cos(self.p)*sin(c2.t)*cos(c2.p) + cos(self.t)*cos(c2.t))
                              
                                  def cartezian(self):
                                      return CoordCartezian(
                                          self.r * sin(self.t) * cos(self.p),
                                          self.r * sin(self.t) * sin(self.p),
                                          self.r * cos(self.t)
                                      )
                              


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

                              Так что вопрос не в том, можно ли реализовать принципы ООП на модулях. Как уже сказано, ООП можно реализовать даже на ассемблере. Вопрос в том, где вы нашли ООП в статье?
            • +1
              Вы от этого класс-объекта можете наследоваться и так далее.

              Неа, не можем:
              Скрытый текст
              In [1]: import itertools
              
              In [2]: class MyItertools(itertools):
                 ...:     pass
                 ...: 
              ---------------------------------------------------------------------------
              TypeError                                 Traceback (most recent call last)
              <ipython-input-2-8cefe4e16e35> in <module>()
              ----> 1 class MyItertools(itertools):
                    2     pass
              
              TypeError: Error when calling the metaclass bases
                  module.__init__() takes at most 2 arguments (3 given)
              
              

              • 0
                Чуток не так надо делать.

                import itertools
                
                Module = type(itertools)
                
                class MyItertools(Module):
                    pass
                


                Хотя по-моему это уже жуткий оффтопик…
                • +1
                  Это понятно, я к
                  от этого класс-объекта можете наследоваться

                  придрался =)
    • +1
      НЛО прилетело и не оставило здесь никакого сообщения.
      • +4
        Он путает функциональщину с процедурным стилем.
      • 0
        Не вижу, в чем ерунда. Переменная mailing_list в примере кода в статье — глобальная. ixSci ниже правильно написал, то, что автор отказался от использования классов при реализации наблюдателя, не делает этот паттерн функциональным.
  • +14
    ООП это методология, классы лишь наиболее простое средства для реализации модели декларирующей объектные свойства. Это вовсе не означает, что ООП это классы. ООП можно реализовывать и на чистом С, в котором, как Вы наверное знаете, классов нет и в помине. Поэтому Ваш заголовок не соответствует действительности. Паттерн «Наблюдатель» это ООП паттерн по своей сути, и не важно какими средствами Вы реализовали эту абстрактную модель. Она от этого не перестанет быть ОО.
    • 0
      Я согласен, что паттерн остаётся самим собой независимо от реализации. Перестаёт ли он от этого быть объектно-ориентированным… это пожалуй спорный вопрос и он может легко развиться в холивар в стиле «это мы придумали», «а у нас это уже было» и т.д. :)
      Но пост как раз про реализацию. Паттерны в пособиях объясняются именно на примерах создания классов. И тот же человек пишущий на чистом C может не догадаться, что тоже может ими пользоваться. Поэтому я и решил предложить альтернативную версию. Для расширения понимания, так сказать.
  • 0
    Разумеется, в тех случаях, где функциональная парадигма изначально естественна, ее эмуляция методами ООП будет выглядеть более монстроподобно.

    У ООПшного подхода есть так плюс как самодокументируемость. Что такое «словарь функций»? — как я могу догадаться, что в данном конкретном случае это «почта»? Только поддерживая консистентные имена переменных? Если в какое-нибудь место ваша «почта» будет передана просто как arg3 — что я смогу понять про ее назначение?

    В то время как, скажем, интерфейс SubscriptionTopics говорит мне уже гораздо больше — независимо от того, каким путем он ко мне попал. Я даю имя сущности, которая мне нужна — в то время как вы называете реализацию сущности: в одном месте словарь функций может быть «почтой», в другом — частью конечного автомата — это очень разные сущности, а реализация одна.
    • 0
      > У ООПшного подхода есть так плюс как самодокументируемость

      class a{
      private $b,$c,$d;
      public $e,$f,$g;
      }
  • +3
    В «Состояние», таскаемый везде dict и есть объект. Это тот же ООП, только записанный иначе.
    И с классами оно немного лаконичней — gist.github.com/nvbn/5830627 =)
  • +1
    Кажется, что использование паттерна Команда как простого коллбэка — это один из наиболее редких юзкейзов.

    Основные, как мне кажется — объединить код и данные с целью манипуляции этими данными — например сохранить команды, чтобы иметь возможность повторно применить их к другому объекту; или иметь возможность откатить изменения (для этого в команде должен быть метод undo). Или сбросить команды на диск как «историю» изменений.
    Более хитрый юзкейз — слияние однотипных подряд идущих команд в одно действие — тоже может быть полезен.
  • –1
    Подписываем наши функции на рассылки:
    >>> subscribe('insertors', fun)
    >>> subscribe('insertors', bar)

    ммда, такие примеры можно называть паттернами, в каком-то узком смысле, просто потому, что они достаточно типичны. но на роль хороших архитектурных приемов, они, по-моему, не годятся уже лет как тридцать.)

    как выше заметил bolk, в статье перепутаны ФП и процедурное программирование — практически во всех примерах видны все стандартные грабли этого подхода, связанных с плохо инкапсулированным состоянием. как раз ООП с этим и борется, за счет «развесистых схем классов», а в ФП состояния не должно быть как такового (во всяком случае в таком совсем уж явном виде).

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.