Thor's website

My Journey Towards Cleaner Code

Software Development

02 December 2025

When it comes to designing and writing software systems there's a lot of factors to take into account.
A developer has to balance all of these to create a flexible system, that balance is rarely easy.

Having the "perfect" codebase doesn't exist. Every codebase has its weaknesses.
Quick fixes, hacks, legacy code, etc...

In this blog post I talk about my personal experience towards trying to make that codebase at least... a little bit cleaner.

First Hello World

I started my programming journey wanting to create games on ROBLOX.
As a kid, I consulted YouTube videos on how to program in Lua.

Back then, I didn't really know anything about programming let alone how to keep everything clean and maintainable.
Did it really matter? Hell no.

After that, I tried web development which was my first introduction to JavaScript.
I created simple applications or API's which ran on Node.js.

And looking back, oh boy.
Immediately I recognize:

  • Incorrect indentation
  • God files/objects Wikipedia:
    In object-oriented programming, a god object is an object that references a large number of distinct types, has too many unrelated or uncategorized methods, or some combination of both.
    (literally all of the logic in 1 file)
  • Magic numbers Wikipedia:
    In computer programming, a magic number or file signature is a numeric literal in source code that has a special, particular meaning that is less than clear to the reader.
  • Nesting to the point it wouldn't fit on the screen
  • Terrible variable naming
  • No comments

Among more things, I now recognize as code smells.

To be fair, these are the mistakes most beginners make.

Goal

That makes me question, what have I learned so far?
It's not like I wrote it down somewhere.
I just know from experience that certain things work, and certain things don't.

That's what this blog post is about.

This blog post is not a tutorial or overview of clean code, software design and the likes.
Trust me, I am not experienced enough to know what I'm talking about.

Instead, this blog post is more or less my personal experience when it comes to clean code and software design.
What's worked, what hasn't, and what I'm still figuring out.

Table of contents

To composite or to abstract?

Favor composition over inheritance.

Inheritance is a powerful tool, but it has it's drawbacks.

Which brings me to the not-so-distant past.
In highschool I got my first interaction with an OOP Wikipedia:
Object-oriented programming (OOP) is a programming paradigm based on objects – software entities that encapsulate data and function(s).
language: C#.

And whilst I didn't exactly start using OOP structures in the beginning, I eventually learned to use them.

Fast-forward a bit later to college, for a class we had to make a game in the MonoGame engine whilst using SOLID principles and design patterns.

I can't say I'm terribly proud of the code I wrote there, it's a bit of a mess.
But I was on a pretty tight deadline to get things done, so instead of properly thinking how to design the project I just quickly put something together with what little I knew.

Anyways, when starting development everything went fine, until about halfway through the project.

What was the problem?
I overused inheritance to reuse code.

Back then, I would have said: "but, what's the problem with inheritance?"
And well, nothing, if it is used correctly.

Inheritance is very much a practical way of reusing code, it's simple and can easily be implemented.

But it backs the developer into a corner later on by making the system extremely coupled.
If you've used inheritance before, you've surely felt how tightly coupled Wikipedia:
In software engineering, coupling is the degree of interdependence between software modules, a measure of how closely connected two routines or modules are, and the strength of the relationships between modules.
the system becomes.

Let's take a look at how inheritance broke down for me.

Example

Focusing on an example from the game I was making.
The game is a carbon-copy of Chip's Challenge Wikipedia:
Chip's Challenge is a top-down tile-based puzzle video game originally published in 1989 by Epyx as a launch title for the Atari Lynx.
.

Chip's Challenge is basically a tile-based puzzle game, where you have to solve logic puzzles to get to the end of the level.
These "tiles" can be enemies, items that can be picked up, doors, etc...

Tilesheet from Chip's Challenge.

Tilesheet from Chip's Challenge.

Practical

My game loop is basically an array of Entities, it loops over those entities and calls Draw() and Update() on them.
The entities are themselves responsible for handling their own actions, handling objects they collide with, etc...

At the start of the project I worked on the Player.

The Player can be killed, and can move. I knew enemies could also do this.
So I put this in an abstract class called Entity

 1public abstract class Entity : Animator
 2{
 3    protected Entity(/*...*/) : base (/*...*/)
 4    {
 5        //...
 6    }
 7    public virtual void Kill(Objects killedBy) {
 8        //...
 9    }
10    public virtual bool Move(Vector2 velocity)
11    {
12        //...
13    }
14}

The game needed to know if the player had been killed.
So I overrode the Kill function to add some functionality.

 1public class Player : Entity
 2{
 3    public Player() : base(/*...*/)
 4    {
 5    }
 6
 7    public override void Kill(Objects killedBy)
 8    {
 9        //Let the game know the player died
10        base.Kill(killedBy);
11    }
12}

And honestly that's just the amazing power of inheritance.
Being able to: extend, reuse and alter functionality in a simple way.

If I changed anything to the default Kill inside of the Entity class, that would affect anything in the classes inheriting it.

What I was doing is called white-box Wikipedia:
A white box is a subsystem whose internals can be viewed but usually not altered.
reuse.

So good, so far.

Enemies

I then started work on implementing enemies into the game.

Bug

The bug is an enemy in the game that moves in the following way:
If the bug wants to go to a tile, but there is an object in the way it will

  • Try to go left, if it can't it will
  • Try to go up, then
  • Right, then
  • Down
The movement of the bug enemy.

The movement of the bug enemy.

Now there are other enemies like: fireball, rocket, paramecium, etc.
These enemies move in almost the same way as the bug does, eg:

  • Fireball moves:
    • UP, RIGHT, LEFT, DOWN
  • Rocket moves:
    • UP, LEFT, RIGHT, DOWN
  • Paramecium moves:
    • RIGHT, UP, LEFT, DOWN

So, since I knew that nearly all the bugs in the game move in this way, I abstracted this away into the Enemy class:

 1public abstract class Enemy : Entity
 2{
 3    public Enemy(Objects code, List<Direction> directions)
 4        : base(/*...*/)
 5    {
 6        //...
 7    }
 8    public override bool Move(Vector2 velocity)
 9    {
10        //...
11    }
12    public override void Kill(Objects killedBy)
13    {
14        //...
15    }
16    public virtual void Update()
17    {
18        //...
19    }
20    public virtual bool CheckMovement(Vector2 position)
21    {
22        //...
23    }
24    public virtual bool CanMoveTo(Objects code, Vector2 movingTo)
25    {
26        //...
27    }
28}

And the bug class...

1public class Bug : Enemy
2{
3    public Bug()
4        : base((Objects)Enemies.BUG,
5              new List<Direction> { Direction.LEFT, Direction.UP, Direction.RIGHT, Direction.DOWN })
6    {
7    }
8}
UML structure of implementation

UML structure of implementation

At the time it really simplified development.
The other enemies I implemented worked in the same way, so I could just inherit from Enemy, specify the directions and voila!

Not so fast

In the game there's an enemy called " teeth".

Now teeth doesn't move in the same way the other enemies do.
Instead of going a predetermined route he will instead chase the Player.

So, well, now my generic Enemy class didn't really work anymore for teeth.
I would have to pass the directions to the base() Enemy constructor... which teeth doesn't have, because its AI doesn't work like other enemies.

Teeth can't inherit Enemy.

Teeth can't inherit Enemy.

What now?
Make the Enemy even more abstract, and split it off into even more abstractions?
Change the Enemy class to allow customization like this?

Which... is what I ended up doing, a lot of the game code proceeded to became a spaghetti mess.

This, among other issues is what marked the downfall of inheritance for me.

It's simple, promotes code reuse and prevents code duplication but it also causes a few side effects:

  • It causes the application to have tight coupling Wikipedia:
    In software engineering, coupling is the degree of interdependence between software modules, a measure of how closely connected two routines or modules are, and the strength of the relationships between modules.
    .
  • It breaks the L in SOLID, Liskov Substitution Principle Wikipedia:
    The Liskov substitution principle (LSP) is a particular definition of a subtyping relation, called strong behavioral subtyping, that was initially introduced by Barbara Liskov in a 1987 conference keynote address titled Data abstraction and hierarchy.
    .
  • It can cause the diamond problem Wikipedia:
    Multiple inheritance is a feature of some object-oriented computer programming languages in which an object or class can inherit features from more than one parent object or parent class.
    .

Let's reflect

Well, clearly that wasn't the most optimal solution.
I mean it looked to be working in the beginning, but once we started encountering edge cases Wikipedia:
An edge case is a problem or situation that occurs only at an extreme operating parameter.
the system fell apart.

Let's take a look at how I could have prevented this by using composition instead.

Composition

After a bit of browsing while writing this post I discovered the GameComponent class from the XNA library.
I wont be using it here, instead I'll show composition the same way it is used in Unity.

Usually when I'm trying to solve a problem, my first instinct now is to see if someone already had this problem before.
And, yes, game engines have solved this problem in their own creative ways.

For example, in Unity everything is represented using a GameObject you can attach functionality to that GameObject by using Components.

That's the major difference between composition and inheritance.

Composition uses a has-a relationship.
Whilst inheritance has an is-a relationship.

Example for MonoGame

First we define an interface all components will use, this satisfies the Dependency Inversion Principle Wikipedia:
In object-oriented design, the dependency inversion principle is a specific methodology for loosely coupled software modules.
.

1public interface IComponent {
2    void Update(GameTime gameTime);
3    void Draw(SpriteBatch spriteBatch);
4}

Then the IGameObject interface and GameObject concrete class that represents an object within the game.

1public interface IGameObject {
2    void AddComponent(IComponent component);
3    T GetComponent<T>() where T : class, IComponent;
4    void Update(GameTime gameTime);
5    void Draw(SpriteBatch spriteBatch)
6}
 1public class GameObject : IGameObject {
 2    private Dictionary<Type, IComponent> components = new Dictionary<Type, IComponent>();
 3
 4    public void AddComponent(IComponent component)
 5    {
 6        components.Add(component.GetType(), component);
 7    }
 8
 9    public T GetComponent<T>() where T : class, IComponent
10    {
11        components.TryGetValue(typeof(T), out IComponent component);
12        return component as T;
13    }
14
15    public void Update(GameTime gameTime)
16    {
17        foreach (var components in components.Values)
18        {
19            components.Update(gameTime);
20        }
21    }
22
23    public void Draw(SpriteBatch spriteBatch)
24    {
25        foreach (var components in components.Values)
26        {
27            components.Draw(spriteBatch);
28        }
29    }
30}

Now our main game loop:

 1public Game1 : Game {
 2    //...
 3
 4    private List<IGameObject> gameObjects = new List<IGameObject>();
 5    private SpriteBatch _spriteBatch;
 6
 7    //...
 8
 9    protected override void Update(GameTime gameTime)
10    {
11        foreach (var gameObject in gameObjects) {
12            gameObject.Update(gameTime);
13        }
14
15        base.Update(gameTime);
16    }
17
18    protected override void Draw(GameTime gameTime)
19    {
20        foreach (var gameObject in gameObjects) {
21            gameObject.Draw(_spriteBatch);
22        }
23
24        base.Draw(gameTime);
25    }
26}

And, to test, a component that just throws an exception:

 1public class TestComponent : IComponent
 2{
 3    public void Draw(SpriteBatch spriteBatch)
 4    {
 5    }
 6    public void Update(GameTime gameTime)
 7    {
 8        throw new Exception("HIT!");
 9    }
10}

Then if we want to add a gameObject to the game, we create a factory to create our Player, Bug, Teeth and all the other entities and tiles!
This way we create individual components, those components can then be reused for our enemies that share the same movement logic.

Okay... but this is really simplified, how do components access each other?
How can a component check collision if it doesn't have the context of the full game?

Here, the best way is to use dependency injection to give the class information it needs, when and if it needs it at all.

 1public class SomeLogicComponent : IComponent
 2{
 3    private ISomeSingleton someSingleton;
 4
 5    public SomeLogicComponent(ISomeSingleton someSingleton) {
 6        this.someSingleton = someSingleton;
 7    }
 8
 9    public void Draw(SpriteBatch spriteBatch)
10    {
11    }
12    public void Update(GameTime gameTime)
13    {
14
15    }
16}

In really performance tight scenario's you might not want to use this.
But, that's the exception here really.

When can we use inheritance then?

Inheritance is simply a tool to help you reuse code.
It's meant to be used in specific use cases.

The only valid reason I can think of from the top of my head is:
When you are working on an existing codebase and cannot change the entire architecture to take advantage of composition.

I myself, have learned to avoid inheritance whenever I can.

Code duplication

Let's talk code duplication.

I've always been a very lazy programmer, and that does come with its benefits and drawbacks.

For example, I absolutely hate it when I have to repeat logic.
You will never see me copy-pasting code. Ever.

Which comes with added benefits like:

  • Only having to fix bugs in once place.
  • No code be out of sync, there are no two places of code that need the same change. (AKA Single Source Of Truth Wikipedia:
    In information science and information technology, single source of truth (SSOT) architecture, or single point of truth (SPOT) architecture, for information systems is the practice of structuring information models and associated data schemas such that every data element is mastered in only one place, providing data normalization to a canonical form.
    )
  • Cleaner, smaller codebase, better to read

However like having mentioned before in my inheritance part, I usually created a lot of coupled and heavily abstracted systems.
Systems which increased complexity and made it harder to read and change logic in the code.

My conclusion

It's complicated.

It's best to look at the rule of three Wikipedia:
Rule of three is a code refactoring rule of thumb to decide when similar pieces of code should be refactored to avoid duplication.
here.
The rule of three is about refactoring, it says:

When you see code ...

  1. duplicated cringe at it, but do not fully abstract it.
  2. three times, abstract it.

And if I understand correctly "duplicated code" does not refer to:

  • Code that is exactly the same.
    • The rule of three applies to code that is somewhat the same, but has important changes to it.
  • Big chunks of duplicated code.
    • What we want to prevent is abstracting tiny pieces of code, which don't need to get abstracted.

It's a balance between coupling, complexity and code duplication.

Code that is literally copy-pasted should be immediately abstracted.

Why?

From what I've read and learned, when abstracting a problem so that it is not repeated people usually end up creating an abstraction that wont work for all use cases.

Abstractions create more coupling if the abstraction is not designed properly. By having three examples of different use cases in similar code, you can figure out how to properly abstract something which can be generally used for multiple purposes without having to do major refactors.

Again, it's difficult and it depends from scenario to scenario.

Complexity

KISS Wikipedia:
KISS is a design principle first noted by the U.
, and no not the band. It stands for Keep It Simple Stupid!

When writing software I've learned to strive for simplicity, less is better Wikipedia:
"Worse is better" is a term conceived by Richard P.
. This refers back to my original abstraction problem.

I tended to over-abstract systems.
Which makes them way more complex than they had to be.

This mainly happens because I tend to make my abstraction as big, reusable and functional as a Swiss Army Knife Wikipedia:
The Swiss Army knife is a pocketknife, generally multi-tooled, now manufactured by Victorinox.
.
This mainly links back to the previous topic.

Compare complex abstractions to Swiss Army Knives

Compare complex abstractions to Swiss Army Knives

A Swiss Army Knife is handy to have!
Because it can do so much at the same time.

However, I don't see people using these knives to do basic tasks, you use tools designed for that specific task, not a swiss knife.

Have a look at this repository.

is-even

Whilst it's obviously a joke, it does go to show that you can solve problems in even the most complex ways.

The key note is that there's a million-and-one ways to get to a solution, keep it simple.
Experimenting and creating sloppy code is fine, as long as you find the best solution and refactor Wikipedia:
In computer programming and software design, code refactoring is the process of restructuring existing source code—changing the factoring—without changing its external behavior.
it!

From what I've learned is to make little reusable parts, keep these little parts simple. You must be able to explain this reusable part to a co-worker in one sentence.

These small parts must also have one reason Wikipedia:
The single-responsibility principle (SRP) is a computer programming principle that states that "A module should be responsible to one, and only one, actor.
for existing, and one reason only.

Then, just like LEGO blocks, you can piece them together to make a complex system that is stable, maintainable and testable.

Unix pipes

The Unix pipe Wikipedia:
In Unix-like computer operating systems, a pipeline is a mechanism for inter-process communication using message passing.
is a great example of this.

It defines a single interface, their standard stream Wikipedia:
In computer programming, standard streams are preconnected input and output communication channels between a computer program and its environment when it begins execution.
, which modules get built upon.
Then, if you want to apply a transformation to something you can combine these small processes to make something complex.

SOC

Separation Of Concern Wikipedia:
In computer science, separation of concerns (SoC) is a software engineering principle that allows software engineers to deal with one aspect of a problem so that they can concentrate on each individually.
(SOC) refers to segmenting (splitting) a software system into smaller, distinct parts.

Examples:

  • n-tier architecture Wikipedia:
    In software engineering, multitier architecture is a client–server architecture in which various levels of software architecture are physically separated.
    used in many client-server architectures.
  • IP-stack Wikipedia:
    The Internet protocol suite, commonly known as TCP/IP, is a framework for organizing the communication protocols used in the Internet and similar computer networks according to functional criteria.
    .

These are well defined architectures.
If I ask someone who knows about the n-tier architecture where a query error could be, they would know!

But SOC doesn't only apply to architectures, but also the way to tackle a project.

Now this may be a weird example, but one of my most favorite video's:

(I recommend it very much!)

... has a part where Scott Anderson, the senior project manager says:

When we told LEGO that it was going to take two years to create the product, they were stunned.
They were taken aback. They were not happy.
Ultimately what we decided to do in order to placate them, in a sense, was we created these little miniature parts of the program and we completed them, we brought them all the way to completion.
So we had the building of the jet ski, and we completed that, and then we put that onto a disc and we sent it off to LEGO.

MattKC then says:

The LEGO Island team actually had a better time than most.
Their method of completing the game piece-by-piece to appease LEGO also ended up making it a lot easier to plan and keep track of time throughout development.

This sounds eerily close to the SCRUM Wikipedia:
Scrum is an agile team collaboration framework commonly used in software development and other industries.
methodology.
In fact, this way of development is actually called vertical slicing Wikipedia:
A vertical slice (VS) is a type of milestone, benchmark, or deadline, with emphasis on demonstrating progress across all components of a project.
.

Making one big monolith of a project with no real planning and working on big badly-defined tasks makes it incredibly difficult to reason, test and write code about!

In reality though, it's not that easy to build a modular system.
I personally feel that this kind of work, software architecture Wikipedia:
Software architecture is the set of structures needed to reason about a software system and the discipline of creating such structures and systems.
, is the most difficult part of software development.

Fin

And there we go! This post covered most of the lessons I learned while programming when it comes to clean code / system design.

Originally I intended to add way more to this, covering documentation, collaboration, testing & CI/CD and budget & estimation.
But... this blog post is already becoming quite big.

In the grand scheme of software development, I'm still a beginner.
There is soooo much more to software development!

But, hey, I'm learning.

And it's nice to be able to reflect back on the past to see that:
every time I write software, I learn something new.

And with that, thank you for reading!

Resources

×