📜 ⬆️ ⬇️

Design patterns without OOP

At a time when I was writing on Lisp and was not at all familiar with OOP, I tried to find design patterns that I could apply in my code. And all the time I ran across some kind of creepy class schemes. As a result, I concluded that these patterns in functional programming are not applicable.

Now I am writing on Python and with OOP sign. And the patterns are much clearer to me now. But I still turn up from the spreading class schemes. Many patterns work fine in the functional paradigm. I will describe a few examples.
I will not give classical implementations of patterns. Those who are not familiar with them, may be interested in Wikipedia or other sources.

Observer

It is necessary to provide an opportunity for some objects to subscribe to messages, and to send some messages.
It is implemented by a dictionary, which is the "mail". The keys are the names of the mailing lists, and the values ​​are the lists of subscribers.
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) 

Now you can subscribe to any functions. The main thing is that the interface functions included in the same distribution group, is compatible.
 def fun(insert): print 'FUN %s' % insert def bar(insert): print 'BAR %s' % insert 

')
We sign our functions on the mailing list:
 >>> subscribe('insertors', fun) >>> subscribe('insertors', bar) >>> subscribe('bars', bar) 


At any place in the code, we trigger notifications for these mailings and observe that all subscribers respond to the event:
 >>> notify('insertors', insert=123) FUN 123 BAR 123 >>> notify('bars', 456) BAR 456 


Template Method

It is necessary to designate the framework of the algorithm and allow users to override certain steps in it.
Higher-order functions such as map, filter, reduce are, in essence, such patterns. But let's see how you can crank the same.
 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) 

We have the functions of deleting a file and a folder, which checks in advance whether we have something to delete.
If the “template” call directly seems to contradict the pattern, functions can be defined using currying. Well, to introduce to the heap the possibility of "overriding" not all parts of the algorithm.
 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) 


condition

It is necessary to provide different behavior of the object depending on its state.
Let's imagine that we need to describe the process of completing the application, which may require several rounds of approvals.
 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) 

As you can see, the main part of the code is taken by “business logic”, and not by the overhead of pattern application. The automaton is easy to expand and change by simply adding / replacing functions in the state dictionary.

Run a couple of times to make sure it works:
 >>> run_claim()                     >>> run_claim()             


Team

The task is to organize a “callback”. That is, so that the callee can refer to the caller from its code.
This pattern apparently arose due to the limitations of static languages. Functionalists would not even be honored with the title of pattern. There is a function - please pass it where you want, save, call.
 def foo(arg1, arg2): #   print 'FOO %s, %s' (arg1, arg2) def bar(cmd, arg2): #  .      foo... print 'BAR %s' % arg2 cmd(arg2 * 2) # ...   


In the original tasks of the pattern Command there is also the possibility to transfer some parameters to the command object in advance. Depending on convenience, it is solved either by currying ...
 >>> from functools import partial >>> bar(partial(foo, 1), 2) BAR 2 FOO 1, 4 

... either by wrapping in lambda
 >>> bar(lambda x: foo(x, 5), 100) BAR 100 FOO 200, 5 


General conclusion

It is not necessary to fence a vegetable garden from abstract classes, concrete classes, interfaces, etc. The minimal possibilities of handling functions as objects of the first class already allow using the same design patterns quite concisely. Sometimes without even noticing it :)

Source: https://habr.com/ru/post/184156/


All Articles