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

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

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

    Наблюдатель

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


    Общий вывод

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

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

                            Самое читаемое