20th October 2025
Benjamin Askew-Gale
- Software Engineer at NatWest
- GitHub Profile
- Introduce the code to refactor
- Dependency Injection
- Primitive Obsession
- Strategy Pattern
- Any other improvements
- Downloads raw files from GitHub.
- Breaks it into sections based on the file type.
- How might it change in the future?
- How might someone use the output?
- How easy is it to understand?
- How easy is it to test?
Definition
Dependency injection is a technique in which an object or function receives other objects or functions that it requires, as opposed to creating them internally.
- Wikipedia
Rule of Thumb
Avoid initialising objects inside the function or class that is using them. Particularly if they handle Input/Output (IO) tasks.
- Easier to test
- Easier to customise behaviour
- Reduces coupling
# Before
class PieMaker:
def make(self) -> str:
return "fruit pie"
class TakeAway:
def __init__(self) -> None:
self.food_maker = PieMaker()
def order(self, customer: str):
food = self.food_maker.make()
print(f"Making {food} for {customer}")
take_away = TakeAway()
take_away.order("Ben")
# After
class FruitPieMaker:
def make(self) -> str:
return "fruit pie"
class MeatPieMaker:
def make(self) -> str:
return "meat pie"
class TakeAway:
def __init__(self, food_maker) -> None:
self.food_maker = food_maker
def order(self, customer: str):
food = self.food_maker.make()
print(f"Making {food} for {customer}")
take_away = TakeAway(MeatPieMaker())
take_away.order("Ben")
Definition
Primitive obsession is a programming anti-pattern where only primitive data structures, such as integers, tuples, strings, lists and maps, are used to organise data.
Rule of Thumb
If you create a dictionary that always has the same keys, consider using a class instead.
If common operations associated with that primitive don’t apply to the business object, consider using a class instead.
- Allows validation logic to be encapsulated with the type.
- Enforces business logic rules.
- Behaviour relevant to that type can be grouped with it using methods.
- Enables you to define interfaces, making code more flexible.
# Before
pie = "fruit pie"
if "pie" not in pie:
raise ValueError("Expected a type of pie.")
# After
class Pie:
def __init__(self, pie_type: str) -> None:
if "pie" not in pie_type:
raise ValueError("Expected a type of pie.")
self.pie_type = pie
valid_pie = Pie("fruit_pie")
invalid_pie = Pie("burger")
Definition
The strategy pattern enables selecting an algorithm at runtime.
Rule of Thumb
If you have an if statement, that could theoretically be infinitely long, to select a particular algorithm within a function, consider using the strategy pattern.
- Easier to test
- Easier to customise behaviour
- Separates algorithm selection from usage
# Before
def make_order(order: list[str], notify: str) -> None:
print(f"Making order")
if notify == "sms":
print(f"Order summary to SMS: {order}")
elif notify == "email":
print(f"Order summary to email: {order}")
elif notify == "social_media"
print(f"Order summary to socials: {order}")
else:
raise ValueError("Unrecognised notification system")
make_order(["pie"], "email")
# After
import typing as t
Notifier = t.Callable[[list[str]], None]
def sms_notifier(order: list[str]) -> None:
print(f"Order summary to SMS: {order}")
def email_notifier(order: list[str]) -> None:
print(f"Order summary to email: {order}")
def social_media_notifier(order: list[str]) -> None:
print(f"Order summary to socials: {order}")
def make_order(order: list[str], notifier: Notifier) -> None:
print(f"Making order")
notifier(order)
make_order(["pie"], email_notifier)