DEV Community

Lucas Leal da Costa
Lucas Leal da Costa

Posted on • Updated on

Maintainable Code: Building Components of Large Scale Projects

Introduction

Every developer at some point has faced problems when it comes to implementing new features into legacy code. Your component needs to have another functionality implemented. What if the project that you are working on is getting bigger each day, how would you assure that creating new functionalities wouldn't be painful? By exploring a simple example we will understand what maintainable code really is.

PS.: I choose Python due to its simplicity, what will be shown can be easily applied in any other modern language.


Case Study 📝

So you are a developer building the back-end of a two numbers calculator. The most straightforward approach would be something like

def calculate(first_number, second_number, operation):

    result = 0

    if operation == "addition":
        result = first_number + second_number

    elif operation == "subtraction":
        result = first_number - second_number

    elif operation == "multiplication":
        result = first_number * second_number

    elif operation == "division":
        result = first_number / second_number

    return result
Enter fullscreen mode Exit fullscreen mode

What if you need to implement another operation, like raising the power of a number? Would you create another elif statement operation == "power"? What if you had to implement more complicated operations?

There are definitely better ways of writing the back-end of this calculator, that is what we are going to dive into.


Refactoring the Code 💻

Whenever we think about large scale projects, it is always important to build components that can easily scale modifying as little as possible the existing code. Your new feature should be an extension of what is already there, not a restructure of it. That is what the 'O' of the S.O.L.I.D. principles stands for:

software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification

Our goal is not to explain SOLID Principles or Design Patterns, but providing a solution for the problem we've pointed. For the moment let's keep these principles under the hood...


1. Creating Enum

It is important to define in our system which operations our calculator will handle, we can do it by creating an Enum.

from enum import Enum

class Operations(Enum):
    ADDITION="addition"
    SUBTRACTION="subtraction"
    MULTIPLICATION="multiplication"
    DIVISION="division"
Enter fullscreen mode Exit fullscreen mode

As soon as we have our Enum let's rewrite our function using the type we created

def calculate(first_number: float, second_number: float, operation: Operations) -> float:

    result = 0

    if operation == Operations.ADDITION:
        result = first_number + second_number

    elif operation == Operations.SUBTRACTION:
        result = first_number - second_number

    elif operation == Operations.MULTIPLICATION:
        result = first_number * second_number

    elif operation == Operations.DIVISION:
        result = first_number / second_number

    return result
Enter fullscreen mode Exit fullscreen mode

We are no longer manipulating strings to define which operation has to be performed. From now on every operation is going to be strictly defined in the Enum created.


2. Segregating Responsibilities

As we discussed earlier what if we want to implement more complicated operations? It does not seem like a good idea to let this function get bigger and bigger as we need to grow our project. So let's decouple the main calculator function from its functionalities

2.1. Operations

def addition(first_number: float, second_number: float) -> float:
    return first_number + second_number

def subtraction(first_number: float, second_number: float) -> float:
    return first_number - second_number

def multiplication(first_number: float, second_number: float) -> float:
    return first_number * second_number

def division(first_number: float, second_number: float) -> float:
    return first_number / second_number
Enter fullscreen mode Exit fullscreen mode

2.2. Calculator

def calculate(first_number: float, second_number: float, operation: Operations) -> float:

    if operation == Operations.ADDITION:
        return addition(first_number, second_number)

    if operation == Operations.SUBTRACTION:
        return subtraction(first_number, second_number)

    if operation == Operations.MULTIPLICATION:
        return multiplication(first_number, second_number)

    if operation == Operations.DIVISION:
        return division(first_number, second_number)

    raise ValueError(f"{Operations} value not supported => {operation}")
Enter fullscreen mode Exit fullscreen mode

As we can see, now it is much easier to create and apply new operations to our calculator. We also took the opportunity to create error handling. If an invalid value for operation is inputted, an error will be raised so we can more easily track unexpected arguments so avoiding unwanted behaviors.

However, our initial proposal was building it as if it were made for large scale projects, so let's take a step further and improve this function...


3. Taking a Step Further

In our example we have simple mathematical operations to be implemented. Nevertheless if we were building features which depend on complex business logic, we should make our code more generic, so to speak. We must structure our components so they rely on interfaces, not on concrete implementations. This is also a SOLID principle, so it's highly recommended to know these concepts by heart.

3.1. Creating Classes for Implementations

Since we are taking into account complex implementations, let's create classes to our calculator operations; these classes will have a public method called calculate. For the sake of simplicity only one example will be shown

class AdditionOperation(CalculatorOperationsInterface):

    def __init__(self, first_number: float, second_number: float):
        self.__first_number = first_number
        self.__second_number = second_number

    # Here you can create more methods to apply business logic    

    def calculate(self) -> float:
        return self.__first_number + self.__second_number
Enter fullscreen mode Exit fullscreen mode

One might be wondering what is CalculatorOperationsInterface. That is the interface we are applying to all of our operations.

3.2. Creating Interface

Now we need an interface for all our operations to conform. This interface requires a method which returns a float. That is how we create interfaces in Python

from abc import ABC, abstractmethod

class CalculatorOperationsInterface(ABC):

    @abstractmethod
    def calculate(init) -> float:
        '''calculate operations'''
Enter fullscreen mode Exit fullscreen mode

Every class that implements this interface must have the calculate method.

3.3. Factory Pattern

In order to make it more generic, we need to have a component that will create the objects of the implementation classes. The component just mentioned is a Factory Design Pattern. This is how it is gonna work: depending on the operation - Enum - we're going to provide, our Factory has to create an object in which we can call the method calculate.

Since new Python versions 3.10+ have introduced Switch statements, that is what we are going to use, however you are free to use if statements too

class CalculatorOperationsFactory:

    @staticmethod
    def create_operation_object(
        operation: Operations, 
        first_number: float, 
        second_number: float
    ) -> CalculatorOperationsInterface:

        match operation:

            case Operations.ADDITION:
                return AdditionOperation(
                    first_number=first_number,
                    second_number=second_number
                )

            case Operations.SUBTRACTION:
                return SubtrationOperation(
                    first_number=first_number,
                    second_number=second_number
                ) 

            case Operations.MULTIPLICATION:
                return MultiplicationOperation(
                    first_number=first_number,
                    second_number=second_number
                ) 

            case Operations.DIVISION:
                return DivisionOperation(
                    first_number=first_number,
                    second_number=second_number
                )

            case _: # default case
                raise ValueError(f"Enter a valid {Operations} => '{operation}' was entered")
Enter fullscreen mode Exit fullscreen mode

3.4. Refactored Calculator Function

Once we have created our components, we can present our new calculator function

def calculate(first_number: float, second_number: float, operation: Operations) -> float:

    operation_object = CalculatorOperationsFactory.create_operation_object(
        operation=operation,
        first_number=first_number,
        second_number=second_number,
    )

    return operation_object.calculate()
Enter fullscreen mode Exit fullscreen mode

4. What We Have Built

Okay, you might be thinking that this whole structure for a simple calculator is kind of overkill, and you are not completely wrong. However, what we built is much more than a calculator. What we have accomplished is a maintainable solution to apply to large scale systems whenever we need to implement new features to an existing component, as long as it is possible to gather those input conditions inside an Enum.

4.1. How to Create New Features?

Creating new features is as simple as:

1. Creating a new Enum value
2. Creating the feature class implementing the interface created
3. Creating a new case in Factory

As you can perceive almost no existing code is modified to implement new features. This is ideal when we are in a project in which dozens of developers are contributing to, it reduces the possibility of conflicts and makes it easier to maintain.

Top comments (0)