- rtshkmr's digital garden/
- Readings/
- Books/
- Fluent Python: Clear, Concise, and Effective Programming – Luciano Ramalho/
- Chapter 10. Design Patterns with First-Class Functions/
Chapter 10. Design Patterns with First-Class Functions
Table of Contents
design pattern: general recipe for solving common design problems
language independent; however practically some languages already have inbuilt recipes for some of these patterns
e.g. Generators in python are the inbuilt version of the Iterator pattern.
in the context of languages that support first-class functions, the fact that we can leverage functions as first-class objects is useful to make code simpler.
the following classic patterns might need a rethink because functions can do the same work as classes while improving readability and reducing boilerplate:
Strategy Pattern
Command Pattern
Template Method
Visitor Pattern
What’s New in This Chapter #
Case Study: Refactoring Strategy #
- the objective of this case study is to see how we can leverage functions as first-class objects.
Classic Strategy #
- what it is:
- “Define a family of algorithms, encapsulate each one, and make them interchangeable. Strategy lets the algorithm vary independently from clients that use it.”
- participants:
Context
- Provides a service by delegating some computation to interchangeable components that implement alternative algorithms.
- in the example, this is the
Order
Strategy
interface common to the components that implement the different algorithms.
it’s the
Promotionabstract class
Concrete Strategy
- one of the concrete classes that implement the abstract class
Function-Oriented Strategy #
useful characteristics form the class-based implementation:
concrete strategies have a single useful method
strategy instances are stateless and hold no instance attributes
It’s because of these reasons, we can consider replacing the concrete strategies with simple functions, and removing the abstract class.
“strategy objects often make good flyweights” is the advice, wherein the cons of the Strategy pattern, which is its runtime cost (e.g. when instantiating the strategy) is addressed by using a Flyweight pattern.
now we end up getting more boilerplate
the python way of using first class functions works well in general because:
in most cases, concrete strategies don’t need to hold internal state because they deal with data injected by the context \(\implies\) good enough to use plain old functions.
a function is more lightweight than an instance of a user-defined class + we can just create each function once and use it.
Choosing the Best Strategy using MetaStrategy: Simple Approach #
- Once you get used to the idea that functions are first-class objects, it naturally follows that building data structures holding functions often makes sense.
Finding Strategies in a Module #
Modules are also first-class objects
globals()returns the current global symbol table. We can inspect attributes of the class object and get the function attributes defined within it like so:promos = [func for _, func in inspect.getmembers(promotions, inspect.isfunction)]I see this as a “pull method” almost where we try to pull together attributes that might make sense. Naturally a registration decorator approach makes more sense already so that we can do a “pull method” approach
Decorator-Enhanced Strategy Pattern #
Here’s the example:
| |
Advantages of using this decorator:
promo strategy functions don’t need special names, flexibility in naming
the registration decorator also becomes a highlighting of the purpose of the function being decorated
- also makes it easy to just comment out the decorator
registration can be done from any other module, anywhere in the system as long as we use the same registering decorator
The Command Pattern #
The goal of Command Pattern is to decouple an object that invokes an operation (the invoker) from the provider object that implements it (the receiver).
put a Command object between the two, implementing an interface with a single method,
execute, which calls some method in the receiver to perform the desired operation.Invoker doesn’t need to know the interface of the receiver
different receivers can be adapted through different
Commandsubclassesthe invoker is configed with a concrete command and calls its
executemethod to operate it.
some pointers from the example:
we have commands and command receivers.
Command receivers are the objects that implement the action specific to a command.
There can be multiple receivers that may respond to a command.
“Commands are an object-oriented repalcement for callbacks”. Nice. Depends on use-case but we could directly implement the callbacks if we want.
How to use simple callback functions directly?
Instead of giving the invoker a
Commandinstance, we can simply give it a function. Instead of callingcommand.execute(), the invoker can just callcommand(). TheMacroCommandcan be implemented with a class implementing__call__. Instances ofMacroCommandwould be callables, each holding a list of functions for future invocation.if we need more complex command usage (e.g. with undo) then we just need to keep necessary state, we could put it within classes like
MacroCommandand we can use a closure to hold the internal state of a function between calls.
Chapter Summary #
the GOF book’s patterns should be seen as steps in the design process of a system rather than end-points or structures that have to be implemented.
this will allow us to not mindlessly add in boilerplate or structures that actually would have better ways of getting implemented if we had thought about the language’s idioms
- In python’s case, functions or callable objects provide a more natural way of implementing callbacks in Python than mimicking the Strategy or the Command patterns
Further Reading #
not many options available for python and design patterns in pythonic fashion, there’s a list here in this book
funfact: If functions have a
__call__method, and methods are also callable, do__call__methods also have a__call__method?YES!!!