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

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

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

    Наблюдатель

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

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