본문 바로가기
Practical Python Design Patterns

Template Method Pattern

by 자동매매 2023. 3. 29.

CHAPTER 17 Template Method Pattern

Success without duplication is merely future failure in disguise.

Randy Gage in How to build a multi-level money machine: the science of network marketing

In life, as in coding, there are patterns, snippets of actions that you can repeat step by step and get the expected result. In more complex situations, the details of the unique steps may vary, but the overall structure remains the same. In life, you get to write standard operating procedures or devise rules of thumb (or mental models, if that is how you roll). When we write code, we turn to code reuse.

Code reuse is the promised land that ushered in the almost ubiquitous obsession with object-oriented programming. The dream is that you would follow the DRY principle not only within a specific project, but also across multiple projects, building

up a set of tools as you went along. Every project would not only make you a better programmer, but would also result in another set of tools in your ever-growing arsenal. Some would argue that the content of this very book is a violation of the DRY principle since the existence of a set of patterns that come up enough times to be codified and solved means that such problems should not need solving again.

Complete code reuse has yet to be achieved, as any programmer with more than

a couple of years worth of experience can testify. The proliferation and constant reimagining of programming languages indicate that this is far from a solved problem.

I do not want to demoralize, but you need to be aware that we as developers need

to be pragmatic, and often we need to make decisions that do not fit the way the world should be. Being irritated with these decisions and desiring a better solution is not a problem unless it keeps you from doing the actual work. That said, using patterns like the strategy pattern allows us to improve the way we do things without needing to rewrite the whole program. Separation of concern, as we have been talking about throughout this book, is yet another way of enabling less work to be done whenever things change and things always change.

257

' Wessel Badenhorst 2017

W. Badenhorst, Practical Python Design Patterns, https://doi.org/10.1007/978-1-4842-2680-3_17
CHAPTER 17 TEMPLATE METHOD PATTERN

What are we to do when we identify a solid pattern of actions that need to be taken in

a variety of contexts, each with its own nuances?

Functions are one form of a recipe. Consider the pattern for calculating n!, where

n! = n * n -1 * * 1 where n > 2 else n! is 1. Solving this for the case where n = 4, we could simply write:

fact_5 = 5 * 4 * 3 * 2 * 1

That is fine if the only factorial you are ever going to be interested in is that of five, but it is way more helpful to have a general solution to the problem, such as this:

def fact(n):

if n < 2:

return 1 return n * fact(n-1)

def main():

print(fact(0)) print(fact(1)) print(fact(5))

if __name__ == "__main__":

main()

What we see here is an illustration where a specific set of steps leads to a predictable result. Functions are really great at capturing this idea of an algorithm or set of steps leading to an expected result for some set of input values. It does become more difficult when we begin to think about situations that might be more complicated than simply following a predefined set of instructions. Another consideration is what you would do should you want to use the same steps in a different way, or implement the steps using

a more efficient algorithm. It might not be clear from the trivial case of the factorial function, so let s look at a more involved example.

Your point of sales system suddenly became popular, and now customers want

to have all kinds of funny things. The chief of these requests is to interface with some third-party system they have been using forever to keep track of stock levels and pricing changes. They are not interested in replacing the existing system, because there are customers who are already using it, so you have to do the integration.

258
CHAPTER 17 TEMPLATE METHOD PATTERN

You sit down and identify the steps needed to integrate with the remote system.

Sync stock items between the point of sale and the third-party

system.

Send transactions to the third party.

This is a simplified set of steps, but it will serve us well enough.

In the world of simple functions we had before, we could write code for each step in the process and place each step s code in separate functions, each of which could be called at the appropriate time. See here:

def sync_stock_items():

print("running stock sync between local and remote system") print("retrieving remote stock items")

print("updating local items")

print("sending updates to third party")

def send_transaction(transaction):

print("send transaction: {0!r}".format(transaction))

def main():

sync_stock_items()

send_transaction(

{

"id": 1, "items": [

{

"item_id": 1, "amount_purchased": 3, "value": 238

}

],

}

)

if __name__ == "__main__":

main()

259
CHAPTER 17 TEMPLATE METHOD PATTERN

The result looks something like this:

running stock sync between local and remote system

retrieving remote stock items

updating local items

sending updates to third party

send transaction: {’items’: [{’amount_purchased’: 3, ’item_id’: 1, ’value’: 238}], ’id’: 1}

Next, we will evaluate the code in terms of what happens in the real world. If there is one third-party system you need to integrate with, there will be others. What if you are handed not one but two other third-party applications to integrate with?

You could do the easy thing and create a couple of sprawling if statements to direct traffic inside of the function to each of the three systems you need to cater to, resulting in something like the incomplete code snippet that follows, which is included only to demonstrate the effect of the sprawling if on the neatness of the code.

def sync_stock_items(system):

if system == "system1":

print("running stock sync between local and remote system1") print("retrieving remote stock items from system1") print("updating local items")

print("sending updates to third party system1")

elif system == "system2":

print("running stock sync between local and remote system2") print("retrieving remote stock items from system2") print("updating local items")

print("sending updates to third party system2")

elif system == "system3":

print("running stock sync between local and remote system3") print("retrieving remote stock items from system3") print("updating local items")

print("sending updates to third party system3")

else:

print("no valid system")

260
CHAPTER 17 TEMPLATE METHOD PATTERN

def send_transaction(transaction, system):

if system == "system1":

print("send transaction to system1: {0!r}".format(transaction)) elif system == "system2":

print("send transaction to system2: {0!r}".format(transaction)) elif system == "system3":

print("send transaction to system3: {0!r}".format(transaction)) else:

print("no valid system")

The test cases we included in the main function result in output similar to the one that follows. Just be aware that since dictionaries are not ordered in terms of their keys, the order of the items in the dictionary that is printed may vary from machine to machine. What is important is that the overall structure remains the same, as do the values of the keys.

==========

running stock sync between local and remote system1

retrieving remote stock items from system1

updating local items

sending updates to third party system1

send transaction to system1: ({’items’: [{’item_id’: 1, ’value’:

238, ’amount_purchased’: 3}], ’id’: 1},) ==========

running stock sync between local and remote system2

retrieving remote stock items from system2

updating local items

sending updates to third party system2

send transaction to system2: ({’items’: [{’item_id’: 1, ’value’:

238, ’amount_purchased’: 3}], ’id’: 1},) ==========

running stock sync between local and remote system3

retrieving remote stock items from system3

updating local items

sending updates to third party system3

send transaction to system3: ({’items’: [{’item_id’: 1, ’value’:

238, ’amount_purchased’: 3}], ’id’: 1},)

261
CHAPTER 17 TEMPLATE METHOD PATTERN

Not only do you have to pass in the arguments needed to execute the specific functionality, but also you have to pass around the name of the service relevant to the current user of the system. It is also obvious from our previous discussions that this

way of building a system of any non-trivial scale will be a disaster in the long run. What options are left? Since this is a book on design patterns, we want to come up with a solution that uses design patterns to solve the problem in question a solution that

is easy to maintain, update, and extend. We want to be able to add new third-party providers without any alterations to the existing code. We could try to implement the strategy pattern for each of the three functions, as follows:

def sync_stock_items(strategy_func):

strategy_func()

def send_transaction(transaction, strategy_func):

strategy_func(transaction)

def stock_sync_strategy_system1():

print("running stock sync between local and remote system1") print("retrieving remote stock items from system1") print("updating local items")

print("sending updates to third party system1")

def stock_sync_strategy_system2():

print("running stock sync between local and remote system2") print("retrieving remote stock items from system2") print("updating local items")

print("sending updates to third party system2")

def stock_sync_strategy_system3():

print("running stock sync between local and remote system3") print("retrieving remote stock items from system3") print("updating local items")

print("sending updates to third party system3")

def send_transaction_strategy_system1(transaction):

print("send transaction to system1: {0!r}".format(transaction))

def send_transaction_strategy_system2(transaction):

print("send transaction to system2: {0!r}".format(transaction))

262
CHAPTER 17 TEMPLATE METHOD PATTERN

def send_transaction_strategy_system3(transaction):

print("send transaction to system3: {0!r}".format(transaction))

def main():

transaction = { "id": 1,

"items": [ {

"item_id": 1, "amount_purchased": 3, "value": 238

}

],

},

print("="*10) sync_stock_items(stock_sync_strategy_system1) send_transaction(

transaction, send_transaction_strategy_system1

)

print("="*10) sync_stock_items(stock_sync_strategy_system2) send_transaction(

transaction, send_transaction_strategy_system2

)

print("="*10) sync_stock_items(stock_sync_strategy_system3) send_transaction(

transaction, send_transaction_strategy_system1

)

if __name__ == "__main__":

main()

263
CHAPTER 17 TEMPLATE METHOD PATTERN

We have the same results with the test cases included in the main function as we had for the version using multiple if statements.

==========

running stock sync between local and remote system1

retrieving remote stock items from system1

updating local items

sending updates to third party system1

send transaction to system1: ({’items’: [{’item_id’: 1, ’amount_purchased’:

3, ’value’: 238}], ’id’: 1},)

==========

running stock sync between local and remote system2

retrieving remote stock items from system2

updating local items

sending updates to third party system2

send transaction to system2: ({’items’: [{’item_id’: 1, ’amount_purchased’:

3, ’value’: 238}], ’id’: 1},)

==========

running stock sync between local and remote system3

retrieving remote stock items from system3

updating local items

sending updates to third party system3

send transaction to system1: ({’items’: [{’item_id’: 1, ’amount_purchased’:

3, ’value’: 238}], ’id’: 1},)

Two things bother me about this implementation. The first is that the functions we are following are clearly steps in the same process, and, as such, they should live in a single entity instead of being scattered. We also don t want to pass in a strategy based on the system that is being targeted for every step of the way, as this is another violation of the DRY principle. Instead, what we need to do is implement another design pattern called the template method pattern.

The template method does exactly what it says on the box. It provides a method template that can be followed to implement a specific process step by step, and then that template can be used in many different scenarios by simply changing a couple of details.

264
CHAPTER 17 TEMPLATE METHOD PATTERN

In the most general sense, the template method pattern will look something like this when implemented:

import abc

class TemplateAbstractBaseClass(metaclass=abc.ABCMeta):

def template_method(self):

self._step_1() self._step_2() self._step_n()

@abc.abstractmethod def _step_1(self): pass

@abc.abstractmethod def _step_2(self): pass

@abc.abstractmethod

def _step_3(self): pass

class ConcreteImplementationClass(TemplateAbstractBaseClass):

def _step_1(self): pass

def _step_2(self): pass

def _step_3(self): pass

This is the first instance where the use of Python s Abstract base class library is truly useful. Up until now, we got away with ignoring the base classes that are so often used by other, less dynamic languages because we could rely on the duck-typing system. With the template method, things are a little different. The method used to execute the process is included in the Abstract base class (ABC), and all classes that inherit from this class are forced to implement the methods for each step in their own way, but all child classes will have the same execution method (unless it is overridden for some reason).

265
CHAPTER 17 TEMPLATE METHOD PATTERN

Now, let s use this idea to implement our third-party integrations using the template method pattern.

import abc

class ThirdPartyInteractionTemplate(metaclass=abc.ABCMeta):

def sync_stock_items(self):

self._sync_stock_items_step_1() self._sync_stock_items_step_2() self._sync_stock_items_step_3()

self._sync_stock_items_step_4()

def send_transaction(self, transaction):

self._send_transaction(transaction)

@abc.abstractmethod

def _sync_stock_items_step_1(self): pass @abc.abstractmethod

def _sync_stock_items_step_2(self): pass @abc.abstractmethod

def _sync_stock_items_step_3(self): pass @abc.abstractmethod

def _sync_stock_items_step_4(self): pass

@abc.abstractmethod

def _send_transaction(self, transaction): pass

class System1(ThirdPartyInteractionTemplate):

def _sync_stock_items_step_1(self):

print("running stock sync between local and remote system1")

def _sync_stock_items_step_2(self):

print("retrieving remote stock items from system1")

def _sync_stock_items_step_3(self):

print("updating local items")

266
CHAPTER 17 TEMPLATE METHOD PATTERN

def _sync_stock_items_step_4(self):

print("sending updates to third party system1")

def _send_transaction(self, transaction):

print("send transaction to system1: {0!r}".format(transaction))

class System2(ThirdPartyInteractionTemplate):

def _sync_stock_items_step_1(self):

print("running stock sync between local and remote system2")

def _sync_stock_items_step_2(self):

print("retrieving remote stock items from system2")

def _sync_stock_items_step_3(self):

print("updating local items")

def _sync_stock_items_step_4(self):

print("sending updates to third party system2")

def _send_transaction(self, transaction):

print("send transaction to system2: {0!r}".format(transaction))

class System3(ThirdPartyInteractionTemplate):

def _sync_stock_items_step_1(self):

print("running stock sync between local and remote system3")

def _sync_stock_items_step_2(self):

print("retrieving remote stock items from system3")

def _sync_stock_items_step_3(self):

print("updating local items")

def _sync_stock_items_step_4(self):

print("sending updates to third party system3")

def _send_transaction(self, transaction):

print("send transaction to system3: {0!r}".format(transaction))

267
CHAPTER 17 TEMPLATE METHOD PATTERN

def main():

transaction = { "id": 1,

"items": [ {

"item_id": 1, "amount_purchased": 3, "value": 238

}

],

},

for C in [System1, System2, System3]:

print("="*10)

system = C() system.sync_stock_items() system.send_transaction(transaction)

if __name__ == "__main__":

main()

Once again, our test code results in the output we would hope for. As in the previous section, the order of the key value pairs in the dictionaries returned by your implementation might be different, but the structure and values will remain consistent.

==========

running stock sync between local and remote system1

retrieving remote stock items from system1

updating local items

sending updates to third party system1

send transaction to system1: ({’items’: [{’amount_purchased’: 3, ’value’:

238, ’item_id’: 1}], ’id’: 1},)

==========

running stock sync between local and remote system2

retrieving remote stock items from system2

updating local items

sending updates to third party system2

268
CHAPTER 17 TEMPLATE METHOD PATTERN

send transaction to system2: ({’items’: [{’amount_purchased’: 3, ’value’:

238, ’item_id’: 1}], ’id’: 1},)

==========

running stock sync between local and remote system3

retrieving remote stock items from system3

updating local items

sending updates to third party system3

send transaction to system3: ({’items’: [{’amount_purchased’: 3, ’value’:

238, ’item_id’: 1}], ’id’: 1},)

Parting Shots

Knowing which pattern to use where, and where to not rely on any of the patterns in this or any other book, is a form of intuition. As is the case with martial arts, it is helpful to practice implementing these patterns wherever you find the opportunity. When you implement them, be aware of what is working and what is not. Where are the patterns helping you, and where are they hampering your progress? Your goal is to develop a feeling for the kinds of problems that a specific pattern would solve using a clear and known implementation, and where the pattern would just get in the way.

Since I just mentioned knowledge and the need for experimentation, I suggest

you look at the documentation of your favorite editor and find out if it has some sort

of snippet function that you could use to handle some of the boilerplate code you find yourself writing repeatedly. This might include the preamble you use to start coding in a new file, or the structure around the main() function. This harkens back to the idea that if you are going to use a tool every day, you really need to master that tool. Being comfortable using snippets, and especially creating your own, is a good place to gain an extra bit of leverage over your tools and remove some wasteful keystrokes.

Exercises

Implement a fourth system, and this time see what happens when

you leave out one of the steps.

Explore the challenges associated with trying to keep two systems in sync

without being able to stop the whole world and tie everything together nicely; you will need to look at race conditions, among other things.

269
CHAPTER 17 TEMPLATE METHOD PATTERN

ink of some other systems where you know what steps you need to

take, but the specifics of what gets done in each of these steps differ from case to case.

Implement a basic template pattern based system to model the

situation you thought of in the previous exercise.

270

'Practical Python Design Patterns' 카테고리의 다른 글

Model-View-Controller Pattern  (0) 2023.03.29
Visitor Pattern  (0) 2023.03.29
Strategy Pattern  (0) 2023.03.29
State Pattern  (0) 2023.03.29
Observer Pattern  (0) 2023.03.29

댓글