Python: Refactoring to Patterns
Photo by Patric Ho I’ll keep this brief to save your time — here’s the TL;DR: This table in Python contains a list of code smells and the design patterns that address them. class CodeSmells: Duplicated_Code = [ form_template_method, introduce_polymorphic_creation_with_factory_method, chain_constructors, replace_one__many_distinctions_with_composite, extract_composite, unify_interfaces_with_adapter, introduce_null_object, ] Long_Method = [ compose_method, move_accumulation_to_collecting_parameter, replace_conditional_dispatcher_with_command, move_accumulation_to_visitor, replace_conditional_logic_with_strategy, ] Conditional_Complexity = [ # Complicated conditonal logic replace_conditional_logic_with_strategy, move_emblishment_to_decorator, replace_state_altering_conditionals_with_state, introduce_null_object, ] Primitive_Obssession = [ replace_type_code_with_class, replace_state_altering_conditionals_with_state, replace_conditional_logic_with_strategy, replace_implict_tree_with_composite, replace_implicit_language_with_interpreter, move_emblishment_to_decorator, encapsulate_composite_with_builder, ] # Lack of "information hiding" [Parnas] Indecent_Exposure = [encapsulate_classes_with_factory] # The logic/responsibility is sprawled in multiple places # (classes, methods) Solution_Sprawl = [move_creation_knowledge_to_factory] # [Fowler and Beck] Interfaces of classes different, # but classes are similar Alternative_Classes_with_Different_Interfaces = unify_interfaces_with_adapter # [Fowler and Beck] A class the doesn't do enough to pay itself Lazy_Class = [inline_singleton] Large_Class = [ replace_conditional_dispatcher_with_command, replace_state_altering_conditionals_with_state, replace_implict_tree_with_composite, ] Switch_Statements = [ # Complicated switches replace_conditional_dispatcher_with_command, move_accumulation_to_visitor, ] # Code that do the same with different types or quantity of data # (similar to duplication) Combination_Explostion = [replace_implicit_language_with_interpreter] # The same problem being solved in many ways in the system # (similar to duplication) Oddball_Solutions = [unify_interfaces_with_adapter] Refactorings After nearly a year of effort, I’ve finally completed my self-imposed goal of writing all the refactoring examples from the book Refactoring to Patterns by Joshua Kerievsky in Python. This book broadened my understanding of how to apply design patterns in production code. Each example includes a brief explanation of the original code and its context, followed by the refactored code and the benefits gained through the refactoring. For instance, the refactoring to “Compose Method” transforms difficult-to-read code into a simple, streamlined implementation. Lets get this example and examine the original code. # Original code # It is not easy to understand the code def add(element): readonly = False size = 0 elements = [] if not readonly: new_size = size + 1 if new_size > len(elements): new_elements = [] for i in range(size): new_elements[i] = elements[i] elements = new_elements size += 1 elements[size] = element It is possible to see that the code is not easy to understand. It has many nested conditions and loops. Now lets go to the refactored code. # Code Refactored # The new code has meaningfull names for blocks of code and is not nested. # The Compose Method is a refactoring to simplificate the code def at_capacity(new_size, elements): new_size > len(elements) def grow(size): new_elements = [] for i in range(size): new_elements[i] = elements[i] elements = new_elements def add_elements(elements, element, size): size += 1 elements[size] = element def add_refac(element): readonly = False if readonly: return if at_capacity: grow() add_elements(element) The idea of the refactoring is reduce the complication with meaningful methods and remove the nested branches. Notice it was necessary to extract blocks of code to methods. While working through the book and writing the examples, I had to interpret UML diagrams and understand the mechanics in detail. This required intense focus and mental effort. Many times, I had to rebuild the examples from scratch because converting code from Java to Python is not straightforward. Native Python doesn’t support cyclic imports, constructor overloads, or interfaces well, so some adaptations were necessary. I added comments to these areas to help future consultation of the code. Through
Photo by Patric Ho
I’ll keep this brief to save your time — here’s the TL;DR:
This table in Python contains a list of code smells and the design patterns that address them.
class CodeSmells:
Duplicated_Code = [
form_template_method,
introduce_polymorphic_creation_with_factory_method,
chain_constructors,
replace_one__many_distinctions_with_composite,
extract_composite,
unify_interfaces_with_adapter,
introduce_null_object,
]
Long_Method = [
compose_method,
move_accumulation_to_collecting_parameter,
replace_conditional_dispatcher_with_command,
move_accumulation_to_visitor,
replace_conditional_logic_with_strategy,
]
Conditional_Complexity = [ # Complicated conditonal logic
replace_conditional_logic_with_strategy,
move_emblishment_to_decorator,
replace_state_altering_conditionals_with_state,
introduce_null_object,
]
Primitive_Obssession = [
replace_type_code_with_class,
replace_state_altering_conditionals_with_state,
replace_conditional_logic_with_strategy,
replace_implict_tree_with_composite,
replace_implicit_language_with_interpreter,
move_emblishment_to_decorator,
encapsulate_composite_with_builder,
]
# Lack of "information hiding" [Parnas]
Indecent_Exposure = [encapsulate_classes_with_factory]
# The logic/responsibility is sprawled in multiple places
# (classes, methods)
Solution_Sprawl = [move_creation_knowledge_to_factory]
# [Fowler and Beck] Interfaces of classes different,
# but classes are similar
Alternative_Classes_with_Different_Interfaces = unify_interfaces_with_adapter
# [Fowler and Beck] A class the doesn't do enough to pay itself
Lazy_Class = [inline_singleton]
Large_Class = [
replace_conditional_dispatcher_with_command,
replace_state_altering_conditionals_with_state,
replace_implict_tree_with_composite,
]
Switch_Statements = [ # Complicated switches
replace_conditional_dispatcher_with_command,
move_accumulation_to_visitor,
]
# Code that do the same with different types or quantity of data
# (similar to duplication)
Combination_Explostion = [replace_implicit_language_with_interpreter]
# The same problem being solved in many ways in the system
# (similar to duplication)
Oddball_Solutions = [unify_interfaces_with_adapter]
Refactorings
After nearly a year of effort, I’ve finally completed my self-imposed goal of writing all the refactoring examples from the book Refactoring to Patterns by Joshua Kerievsky in Python. This book broadened my understanding of how to apply design patterns in production code.
Each example includes a brief explanation of the original code and its context, followed by the refactored code and the benefits gained through the refactoring. For instance, the refactoring to “Compose Method” transforms difficult-to-read code into a simple, streamlined implementation.
Lets get this example and examine the original code.
# Original code
# It is not easy to understand the code
def add(element):
readonly = False
size = 0
elements = []
if not readonly:
new_size = size + 1
if new_size > len(elements):
new_elements = []
for i in range(size):
new_elements[i] = elements[i]
elements = new_elements
size += 1
elements[size] = element
It is possible to see that the code is not easy to understand. It has many nested conditions and loops. Now lets go to the refactored code.
# Code Refactored
# The new code has meaningfull names for blocks of code and is not nested.
# The Compose Method is a refactoring to simplificate the code
def at_capacity(new_size, elements):
new_size > len(elements)
def grow(size):
new_elements = []
for i in range(size):
new_elements[i] = elements[i]
elements = new_elements
def add_elements(elements, element, size):
size += 1
elements[size] = element
def add_refac(element):
readonly = False
if readonly:
return
if at_capacity:
grow()
add_elements(element)
The idea of the refactoring is reduce the complication with meaningful methods and remove the nested branches. Notice it was necessary to extract blocks of code to methods.
While working through the book and writing the examples, I had to interpret UML diagrams and understand the mechanics in detail. This required intense focus and mental effort. Many times, I had to rebuild the examples from scratch because converting code from Java to Python is not straightforward. Native Python doesn’t support cyclic imports, constructor overloads, or interfaces well, so some adaptations were necessary. I added comments to these areas to help future consultation of the code.
Through this process, I realized my previous understanding of design patterns was mostly theoretical and limited to trivial scenarios. For example, while I understood that “Polymorphism” addresses development problems, the book showed its application in test automation by abstracting the setup phase and reusing the remaining test implementation.
Here are both versions of the code. The difference between the original and new code is the setup of the test.
# Original code
# Similar methods differs from the object instantiation.
# All the rest is the same
class TestCase:
pass
class DOMBuilder:
def __init__(self, orders) -> None:
pass
def calc(self):
return 42
class XMLBuilder:
def __init__(self, orders) -> None:
pass
def calc(self):
return 42
class DOMTest(TestCase):
def run_dom_test(self):
expected = 42
builder = DOMBuilder("orders") # different object created
assert builder.calc() == expected
class XMLTest(TestCase):
def run_xml_test(self):
expected = 42
builder = XMLBuilder("orders") # different object created
assert builder.calc() == expected
# Code refactored
# The instantiation of the DOMBuilder or XMLBuilder is the only difference
# in both tests.
# It was created an OutputBuilder like an interface for both classes
# (it is not necessary given that Python uses duck type).
# In TestCase a new method called "create_builder" was introduced to be
# implemented by the children classes.
# This is the step executed in runtime for each type of test. This is the
# polymorphism. When both tests (DOMTest and XMLTest) are executed,
# the instance returned from the "create_builder" depends on the
# implementation. Is can be DOMBuilder or XMLBuilder.
class OutputBuilder:
def calc(self):
raise NotImplementedError()
class DOMBuilderRefac(OutputBuilder):
def calc(self):
return 42
class XMLBuilderRefac(OutputBuilder):
def calc(self):
return 42
class TestCaseRefac:
def create_builder(self):
raise NotImplementedError()
def run_test(self):
expected = 42
builder = self.create_builder() # different object created
assert builder.calc() == expected
class DOMTestRefac(TestCaseRefac):
def create_builder(self) -> OutputBuilder:
return DOMBuilderRefac()
class XMLTestRefac(TestCaseRefac):
def create_builder(self):
return XMLBuilderRefac()
def run():
dom_tc = DOMTestRefac()
dom_tc.run_test()
xml_tc = XMLTestRefac()
xml_tc.run_test()
The “Visitor” pattern was the most difficult for me to understand. I read about the pattern in the Design Patterns book before attempting the refactoring. It was only after seeing the original (unrefactored) code being transformed into the new version that I realized the pattern isn’t as complex as it initially seems. Essentially, the pattern decouples classes from their methods. Again, both codes for comparison. The implementation of the pattern is “by the book”.
# Original code
# The TextExtractor has lots of conditons to handle Nodes, like StringNode
# The idea ofthe rectoring is distribute the logic into Visitor classes
# Interface
class Node:
pass
class LinkTag(Node):
pass
class Tag(Node):
pass
class StringNode(Node):
pass
class TextExtractor:
def extract_text(self, nodes: list[Node]):
result = []
for node in nodes:
if isinstance(node, StringNode):
result.append("string")
elif isinstance(node, LinkTag):
result.append("linktag")
elif isinstance(node, Tag):
result.append("tag")
else:
result.append("other")
return result
# Code refactored
# Interface (the visitor)
class NodeVisitorRefac:
def visit_link_tag(self, node):
return "linktag"
def visit_tag(self, node):
return "tag"
def visit_string_node(self, node: object):
return "string"
class NodeRefac:
def accept(self, node: NodeVisitorRefac):
pass
class LinkTagRefac(NodeRefac):
def accept(self, node: NodeVisitorRefac):
return node.visit_link_tag(self)
class TagRefac(NodeRefac):
def accept(self, node: NodeVisitorRefac):
return node.visit_tag(self)
class StringNodeRefac(NodeRefac):
def accept(self, node: NodeVisitorRefac):
return node.visit_string_node(self)
# The concret visitor
class TextExtractorVisitorRefac(NodeVisitorRefac):
def extract_text(self, nodes: list[NodeRefac]):
result = []
for node in nodes:
result.append(node.accept(self))
return result
def run_refac():
# The original object calling its method
result1 = TextExtractor().extract_text([StringNode()])
# The new object accepting visitors
result2 = TextExtractorVisitorRefac().extract_text([StringNodeRefac()])
return result1, result2
Conclusion
I highly recommend the book to everyone. The first time I read it, I found it boring and difficult to grasp the concepts just by following static code examples. However, when you actively write the code, the ideas gradually come to life. Errors will occur, and addressing them requires understanding the underlying concepts. This process transforms theory into practice and solidifies your knowledge.