Taha

Software design patterns and principles are not the goal

The worst thing programmers have to deal with is fixing a bug or adding a feature in code that is hard to understand and change.

The only solution to this is to write code that is easy to change.

Easier said than done.

Countless books and articles teach about design patterns and design principles (like SOLID or DRY) to help achieve that. The issue with them is that they work as shortcuts, and the problem with shortcuts is that they are not enough to properly learn the skill. You also have to practice.

Writing easy-to-change code is not easy!

It's not easy because it's a skill. Skills require practice and time to improve, and, more importantly, knowing how to improve them.

For most skills, there are clear steps to take for improvement. For example, if you want to learn how to play the piano, you need to familiarize yourself with the keyboard, practice hand posture and finger positions, learn about note names and positions, all the way until you train your muscle memory to play the song you want without thinking.

Unfortunately, for programmers, it's not that straightforward. There are plenty of books that talk about best practices and principles, but none of them guarantee writing good code that is easy to understand and change six months later.

You can learn all the design patterns, principles, and refactoring techniques out there, but when it comes to writing real code in a real job, you don't know how to apply them.

So, are they useless, and should you not learn them? Of course not! Most of them are good and were created by smart people for real-world scenarios.

But the problem is that you don't have enough experience and failures to know which ones to use.

Having enough experience doesn't mean having worked for 10 years as a programmer. There are programmers who have been in the industry for a very long time, and they are still writing code that is hard to change.

The key here is to be conscious and deliberate about practicing writing good code. You do this by taking a few minutes before writing code to think about the best way to write it. You have to predict how this function (or object, or whatever) will be used by other parts of the software and how changing it would affect the other parts.

Sometimes, a certain design pattern will come to your mind, and you would try it. And most of the time, you will realize that it wasn't appropriate and it's making the code more complex than necessary.

Other times, you would think that this code should be simple and just include it in a single function along with other code. But the more you add, the more difficult your code will be to read and change. Then, you might start to think about breaking this function into multiple ones or even introduce new objects.

But you will never know what is better until you try that. And it will be painful to see how spending too much time on something to make it cleaner turns out to be even worse. And that's okay. Actually, that's the only true way to learn how to write good code.

The more you fail at writing good code, the more you will be able to predict what will work and what won't in the future. Other developers would be impressed when you know right away what pattern to use or how splitting function makes things much easier. And they will think you are a code wizard. But you are not! It's only because you practiced this a lot that you instantly know if something will be good or not.

We always hear developers say, 'Make things simpler. Don't use patterns or principles, or your code will end up worse.' But that's not true. It all depends on the context of your code and how it will be used.

Writing simple code doesn't mean avoiding patterns or OOP principles. Instead, it means using the best techniques and tools to make the code easier to read and change. It might require a design pattern or not.

Let me give you a real example.

An example that doesn't need a design pattern

If I want to write a function that returns the discount percentage based on the user type, I wouldn't use the abstract factory pattern to add an object for each case. So, I wouldn't write it like the following 'to replace conditionals with polymorphism'.

function getDiscountForUser(user) {
  return createUserDiscount(user).getDiscount()
}

function createUserDiscount(user) {
  switch (user.type) {
    case 'premium':
      return new PremiumUserDiscount()
    case 'gold':
      return new GoldUserDiscount()
    case 'banned':
      return new BannedUserDiscount()
    default:
      return new UserDiscount()
  }
}

class PremiumUserDiscount {
  getDiscount() {
    return 0.4
  }
}

class GoldUserDiscount {
  getDiscount() {
    return 0.6
  }
}

class BannedUserDiscount {
  getDiscount() {
    return 0
  }
}

class UserDiscount {
  getDiscount() {
    return 0.2
  }
}

I will instead write it like this:

function getDiscountForUser(user) {
  switch (user.type) {
    case 'premium':
      return 0.4
    case 'gold':
      return 0.6
    case 'banned':
      return 0
    default:
      return 0.2
  }
}

For this case, this is good enough. If, in the future, I need to add more logic to it, then I will consider the best refactoring needed to keep it simple while remaining easy to change.

An example that needs a design pattern

In the next example, using the abstract factory pattern would significantly simplify the code.

To provide some context, I was working on Veloxi, a JavaScript library for creating smooth interactions. A major aspect of it involved implementing animations for specific properties, like position, rotation, or size.

Some properties, such as position and size, are represented with two values: x and y. This means we can represent them with a Vector2D object. However, for other properties, like rotation, only one number is needed to represent them (e.g. rotation degrees).

In Veloxi, I needed to support various types of animations, including tween animations and spring animations.

Each animation type requires its own implementation. In fact, for each animation type, I needed to implement two different versions: one for numbers and one for Vector2D objects.

The relationships between different parts of this code are more complex than in most cases. I have different properties, each with a different value representation (2D vector or number). For each value type, I need to implement all the animation types available.

In this scenario, it's clear that I need a well-structured way to represent these relationships and the way to create them. After thinking, I decided that using the abstract factory pattern would be the best choice.

// ************************************************************
// ** Both PositionProp and RotationProp don't know
// ** what type of animator they are using.
// ** It can be for Vector2D or Number.
// ** And it can be tween or spring.
// ************************************************************

class PositionProp {
  // currentValue is a Vector2D object
  currentValue

  // targetValue is a Vector2D object
  targetValue

  constructor(animatorFactory) {
    this.animatorFactory = animatorFactory
    this.animator = animatorFactory.createAnimatorByName('tween')
  }

  setAnimator(animatorName) {
    this.animator = this.animatorFactory.createAnimatorByName(animatorName)
  }

  // Update loop
  update() {
    this.currentValue = this.animator.update(
      this.currentValue,
      this.targetValue
    )
  }
}

class RotationProp {
  // currentValue is a number value
  currentValue

  // targetValue is a number value
  targetValue

  constructor(animatorFactory) {
    this.animatorFactory = animatorFactory
    this.animator = animatorFactory.createAnimatorByName('tween')
  }

  setAnimator(animatorName) {
    this.animator = this.animatorFactory.createAnimatorByName(animatorName)
  }

  // Update loop
  update() {
    this.currentValue = this.animator.update(
      this.currentValue,
      this.targetValue
    )
  }
}

// ************************************************************
// ** Tween Animators for both Vector2D and numbers
// ************************************************************

class Vec2TweenAnimator {
  update(currentValue, targetValue) {
    // ...
  }
}

class NumberTweenAnimator {
  update(currentValue, targetValue) {
    // ...
  }
}

// ************************************************************
// ** Spring Animators for both Vector2D and numbers
// ************************************************************

class Vec2SpringAnimator {
  update(currentValue, targetValue) {
    // ...
  }
}

class NumberSpringAnimator {
  update(currentValue, targetValue) {
    // ...
  }
}

// ************************************************************
// ** Animator factories for both Vector2D and number
// ************************************************************

class Vec2AnimatorFactory {
  createAnimatorByName(animatorName) {
    switch (animatorName) {
      case 'tween':
        return new Vec2TweenAnimator()
      case 'spring':
        return new Vec2SpringAnimator()
      default:
        return new Vec2TweenAnimator()
    }
  }
}

class NumberAnimatorFactory {
  createAnimatorByName(animatorName) {
    switch (animatorName) {
      case 'tween':
        return new NumberTweenAnimator()
      case 'spring':
        return new NumberSpringAnimator()
      default:
        return new NumberTweenAnimator()
    }
  }
}

// ************************************************************
// ** Here's the code for creating the property objects
// ************************************************************

const positionProp = new PositionProp(new Vec2AnimatorFactory())
const rotationProp = new RotationProp(new NumberAnimatorFactory())

I think this code is well-structured for this use case. If, for example, I need to update how the spring animation is implemented for 2D vectors, I know exactly what to modify, and the same is true for adding a new animation type.

How to know what pattern to use?

In the example above, I didn't know I needed to use this pattern just because I read about it in the past. I recognized the need because of numerous past failures in implementing similar code—code where there is a complex relationship between its parts, and I needed a clear way to set up those components.

So, in this example, the focus is on the creation of those parts (objects) and the relationships between them. After exploring popular creational patterns, I found that the abstract factory fits well here.

It's important to note that I'm not claiming that using this pattern here is the absolute best way to implement this code. Others might come up with better solutions. However, there's no doubt that using the abstract factory pattern here fits well.

When to look for patterns to use in your code?

Every time I encounter difficulty modifying the code, it's a hint for me that it can be written better. Does it have to be rewritten using a certain design pattern or following some principle? Of course not! The goal is not to know every pattern out there; the goal is to make the code easy to change, even if I have to invent my own patterns.

I typically view design patterns as inspiration for solving design problems in code. They are great for helping me recognize design problems faster.

The best way to implement a pattern

The implementation is not the important thing here. In most cases, following their exact way of implementation doesn't help due to differences in programming languages, use cases, and the domain of the project. Only with practice will you know the best way to implement the pattern—or maybe create your own pattern.

It's not about using design patterns or principles

A constant reminder I keep telling myself is that the goal is not about using design patterns or principles. The ultimate goal is to write code that is easy to change.

Sometimes breaking the DRY principle and duplicating the code is the best choice for your current implementation. I can think of two examples. The first one is when the code will start the same in two places, but you know that one of them will change drastically in the future. Another example is when making the code reusable requires workarounds that will make your code end up worse.

Many people feel bad that they don't understand all the patterns and principles out there, but they shouldn't.

Design patterns are the result of other people's experiences. Others found that a certain design problem occurs frequently, and they gave it a label to make it easier to recognize them in the future.

It's very likely that their solutions won't fit well in your problems. However, knowing about them will make it easier for you to recognize similar problems and come up with similar solutions.

Design principles, on the other hand, work as checks. They are a great way to ask ourselves some questions before we write code. For example: "Do I need to make this function reusable?" or "Should I split this into multiple modules to improve responsibility?" They are just checks; you don't have to follow them. Do what's best for your current implementation.

The goal here is to build intuition for writing scalable and easy-to-change code; following a design pattern or not is not the point.

A practical advice

Read about patterns. Read about principles. Read others' open-source code for inspiration.

Before you start writing code, take a few minutes to consider the best way to write it. Don't use a pattern just because it exists. Instead, think about the simplest solution you can come up with at the moment. Use it.

When you revisit that code for modifications, assess how difficult it is to modify. Based on that, you'll know if your solution was good. If not, that's okay; you've learned a new way not to write code. Try another solution and evaluate.

The more you practice, the more it will become ingrained in your muscle memory, like learning the piano. And soon, you'll appear as a code wizard! Let's be code wizards!

Taha Shashtari

I'm Taha Shashtari, a full-stack web developer. Building for the web is passion. Teaching people how to do that is what I like the most. I like to explore new techniques and tools to help me and others write better code.

Subscribe to get latest updates about my work
©2024 Taha Shashtari. All rights reserved.