From Messy to SOLID: First Steps Towards Clean Architecture

From Messy to SOLID: First Steps Towards Clean Architecture

Publish Date: Jun 30
5 0

Quick intro

Hi, I'm Matheus. I'm fairly new to the SOLID principles of object-oriented design (you could say I recently joined the cult of clean code after watching a video about it 🤪). This post is a short case study of how I applied some SOLID ideas to improve a messy view function in one of my personal projects.

This is a short scenario study about my approach to a certain problematic involving SOLID principles, so stick until the end to judge my solution and also comment if you find a better one.

The problem

To give some context, I’ll use an application I'm currently building as an example. This app is a web-based guessing game. Each level shows an image, and the user must guess the location it represents.

Before I jump right into the problem, here’s a summary of the SOLID principles using (check out this BEAUTIFUL article for a deeper dive).

S – Single Responsibility: an entity has to deal with one, and only one, responsibility, it shouldn't do multiple tasks.

O – Open/Closed: you should be able to extend an entity with new stuff, without modifying what already exists.

L – Liskov Substitution: extended entities should act just like their parent when needed.

I – Interface Segregation: each entity can have its own interface, to utilize only the variables it really needs.

D – Dependency Inversion: entities depend on abstractions between them, not knowing exactly how one thing is done, just doing it.

What was I doing wrong

So, back to my app, I had a function-based view that was handling (already a red flag) a lot of things at the same time. It was generating a random level while checking if the user who made the request doesn't already have an open session and if not it creates a new session.

Basically I had:

@api_view(["GET"])
@permission_classes([IsAuthenticated])
def getRandomLevel(request, pk=None):

    # checking if the run has been passed as a parameter
    if not pk:

        # 1- do stuff to create a new session
        # 2- append a level to the session

    else:

        # 1- fetch the open session
        # 2- check for a real run
        # 3- check for a valid run (not already finished)
        # 4- append a level to the session 
Enter fullscreen mode Exit fullscreen mode

I will not show the entire code here for the sake of brevity, but there's a specific part I want to comment about.

When the request is called with a pk parameter, we need to check if thats a real Run. So in this bit of code I handle the run existence check:

try:

    # [...]

    current_run = Run.objects.get(id=pk)

    if current_run.levels_left == 0:
        return Response(
            data={"error": "Run finished"},
            status=status.HTTP_301_MOVED_PERMANENTLY,
        )

    # [...] a lot of code here

except Run.DoesNotExist:
    return Response(
        data={"error": "Invalid run"}, status=status.HTTP_400_BAD_REQUEST
    )
Enter fullscreen mode Exit fullscreen mode

The refactor

Now, can you identify (among other problems) which SOLID principles is this code violating? Heres a quick breakdown:

  • Right away you can tell that this view is handling a lot of logic (or responsibilities) that it really shouldn't. This breaks the Single Responsibility Principle, the view should only do one thing and one thing only, which is generate the next random level.

  • The error handling has two possibilities, one at the try/except wrapper and the other in the middle of the logic. This complicates extension and violates the Open/Closed Principle when new exceptions are required in the future.

So with that in mind, we can move to the refactored bit and take a look at my choices to solve these problems:


    # [...]

    # creating a processor to handle it properly
    run_processor = RunProcessor(pk) 

    # handling one exception
    if not run_processor.is_valid_run(): 
        return Response(
            data={"error": "Invalid Run"},
            status=status.HTTP_400_BAD_REQUEST,
        )

    # handling the other exception
    if run_processor.is_finished_run(): 
        return Response(
            data={"error": "Run has finished"},
            status=status.HTTP_301_MOVED_PERMANENTLY,
        )

    # [...]
Enter fullscreen mode Exit fullscreen mode

Key changes that I need to highlight:

  • 1. Removal of try-except wrapper. This makes the code a lot easier to read.

  • 2. Delegation of the responsibility to check the run to a separate processor (Single Responsibility Principle).

  • 3. Addition of separate method for each error handling allowing easy implementation of new checks in the future like run_processor.is_user_banned() (Open/Closed Principle).

The other SOLID principles like Liskov Substitution, Interface Segregation, and Dependency Inversion aren’t directly relevant here yet, since I'm only working with a single class and not a full inheritance chain or interface architecture. That said, as the app grows, these principles may become more applicable.

Conclusion

This small refactor might seem simple, but for me it marked a shift in mindset. Instead of just getting code to work, I started thinking about how to make it clean, maintainable, and extendable. Applying the SOLID principles — even just SRP and OCP in this case — helped me break apart tangled logic and structure my code with more intention.

I’m still learning, and I know there’s plenty of room for improvement. But this was a solid (pun intended) first step toward better architecture in my projects. If you’ve got feedback, questions, or even a better way to tackle the same problem, I’d love to hear from you!

Let’s keep writing code that future-us won’t hate. 🚀

Comments 0 total

    Add comment