While there are many great design patterns available to us, I wanted to highlight a few of them that I think are relevant to object oriented programming and to build maintainable and scalable applications.
I want to bring some extra attention to Event Driven Architecture, Application Routing, Command and Strategy Patterns as they provide some great ways of building scalable architectures. Consider these as your starting point when building an application which needs to handle navigation, input commands, routing or events.
SOLID Principles
These are five principles that guide object-oriented design and promote maintainability, scalability, and robustness. We will go through each of them separately but SOLID is an acronym for these patterns:
- Single Responsibility
- Open-Closed
- Liskov Substitution
- Interface Segregation
- Dependency Inversion
Single Responsibility Principle (SRP)
A class should have one and only one reason to change, meaning that a class should have only one job. This helps us encapsulates the responsibility and makes the code and class easier to maintain and re-use. E.g. a class handling user profile data should not handle user authentication (login/logout).
It’s easy to forget, especially if two features look very similar and we don’t want to waste effort reinventing the wheel. It’s tempting to reuse some existing code, but look again, are they really achieving the same thing? What if one of those features changes a little, e.g. a remote API evolves to add new features, can you easily add support for that in your code? Or perhaps you should copy, or subclass the code instead?
It’s much easier to introduce bugs or confusing code when we reuse the same code for similar features - and that’s why SRP is so important.
Open-Closed Principle
Objects or entities should be open for extension but closed for modification. Another super important principle for maintainability and scalability. This also applies to APIs; for example if we have an API, it’s ok to add new fields to an endpoint or new endpoints to an API. It’s not OK to remove or change the input or output, then it’s time to add a new version of that API. This also means when we implement APIs our systems need to be flexible to allow new fields or output of that API.
Liskov Substitution
Every subclass or derived class should be substitutable for their base or parent class.
For example, think about a program that uses a Shape
class with methods like getArea()
. If you have a subclass Circle
and another subclass Square
, the Liskov Substitution Principle ensures that you can use a Circle
object or a Square
object wherever you would use a Shape
object without breaking the program. I.e. getArea()
is implemented for both, it’s not optional.
Interface Segregation
A client should never be forced to implement an interface that it doesn’t use, or clients shouldn’t be forced to depend on methods they do not use.
Dependency Inversion Principle (DIP)
High-level modules must depend on abstractions, not on concrete solutions. It states that the high-level module must not depend on the low-level module, but they should depend on abstractions.
If you have a high-level module that needs to interact with a database, you would define an abstract way to interact with that databases (an interface), instead of communicating with the concrete database. This allows you to switch out the database implementation without changing the high-level module.
Dependency Injection (DI)
A technique used to implement the Dependency Inversion Principle, where one object supplies the dependencies of another object, rather than the object itself creating them. This promotes loose coupling and testability.
Arguably one of the most important patterns to follow. It allows us to abstract away black box solutions and external systems, like push notifications, emails or databases.
This in turn allows us to write simple unit tests for our code. We don’t want to test that the database works, we just want to test that the database gets the correct command or values. This speeds up tests runs. Integration tests are for end-to-end testing.
Continuing the example; Instead of creating a specific database object directly in your code, with Dependency Injection your code would be provided with a database object from outside.
Event Driven Architecture
Also known as Observables or Pub-Sub
Using an event driven pattern is an absolut game changer when ut comes to building scalable applications. Implementing an event driven architecture can at first look quite complicated and in many cases it’s over the top, but many OS SDKs already moved in this direction; iOS has Combine, Android has Rx or Coroutines Flow, on web we have React and in the backend world we’ve had message/event queues for ages.
Observer Pattern
Defines a one-to-many dependency between objects so that when one object changes state, all its dependents are notified and updated automatically. Observers are typically managed by the observable object creating a direct communication route between the observers and the observable object, if you don’t want this or want a many-to-many relationship look at Pub-Sub.
Observers ties nicely into Event Driven Architectures as this is a very basic way of sending events. Consider an application where you have a list of items; Instead of reloading the list every time you come back to look at the list - the object managing the list will inform the UI when a list item has changed, and then the UI can reload.
Publish-Subscribe (Pub-Sub)
Similar to the Observer Pattern, Pub-Sub builds on subscribing (instead of observering) for changes. In Pub-Sub however the Publishers and Subscribers should not have any direct knowledge of the sender.
This is another great pattern for Event Driven Architecture, when there is a need for communicating across components, for example sending commands or navigation requests to the correct invoker.
Application Router
Used to handle routing within an application. Especially relevant in web applications where you want to map a specific url to a piece of code, typically a controller and action/view. The Application Router is also useful in mobile/web (stateful) applications to achieve the same thing and handle navigation. Coupled with the Command Pattern and/or Event Driven Architecture you can create very scalable and flexible architectures where adding a new feature is as easy as creating a new controller and adding a route to it.
Command Pattern
Used when you want to encapsulate a request as an object allowing you to parameterize clients with different commands. This allows for queuing requests, undo and redo actions and more. For example if you have a text editor, when you mark some text as bold, that can be implemented as a Command. Same thing if you have a menu of commands in an app, each menu option can be a command which either does something under the hood, or navigates the user to a different place.
Strategy Pattern
Defines a family of algorithms, encapsulates each one, and makes them interchangeable. Strategy lets the algorithm vary independently from clients that use it.
One typical use case for the Strategy Pattern is during testing. Combined with Dependency Injection your code is programmed to accept an abstract object which does something. The object injected into your test code could be a simple mock object which adheres to the interface, but in the production code that object does the real thing - like send a push notification or email.
Facade Pattern
Provides a simplified, higher-level interface to a set of interfaces in a subsystem. It makes the subsystem easier to use for the client.
Adapter Pattern
Allows the interface of an existing class to be used as another interface. It is often used to make existing classes work with others without modifying their source code.
Factory Pattern
Provides an interface for creating instances of a class, but allows subclasses to alter the type of instances that will be created.
Singleton Pattern
Ensures a class has only one instance and provides a global point of access to it.
While Singletons have valid use-cases, like connecting to databases, it’s easy to pollute the global scope with singletons. Please be careful when creating Singletons.
Decorator Pattern
Attaches additional responsibilities to an object dynamically. Decorators provide a flexible alternative to subclassing for extending functionality.
This is a good pattern for following the composition over inheritance principle.
MVC (Model-View-Controller)
Separates the concerns of data representation (Model), user interface (View), and control flow and logic (Controller). This promotes modularity and maintainability.
I’ve read plenty of negative articles about MVC, especially regarding bloated “massive” controllers. MVC is not the only tool What many of those miss is that MVC is not the solution to all problems. MVC is great together with other patterns
MVVM (Model-View-ViewModel)
MVVM differs from MVC in several ways but one of the big advantages is the use of a ViewModel as a sort of converter between Model and View. This allows us to write Views which have no clue what the Model or data actually looks like. The ViewModel becomes the glue which pairs views with models. This allows us to write model-agnostic view components. Another similar way of doing this is called Presentation Model (or Presenter Pattern).
MVVM also defines how data should get pushed or updated. In short the ViewModel has the bindings to the Model and when there are changes in the Model the ViewModel forwards/notifies the View which can update the values.
CQRS (Command Query Responsibility Segregation)
Separate the responsibility for reading data (queries) from the responsibility for modifying data (commands). This can lead to a more scalable and maintainable architecture.