Applying SOLID principles in day to day architecture

Aleksander Kania
DAZN Engineering
Published in
6 min readApr 3, 2023

Why clean code/architecture is so important?

Imagine that you are an experienced developer and start to develop an e-commerce mobile app. You are in a team that has only 4 members. You start your work, implement features, you are very fast and want to show the results to the business asap. As a result, you finish the MVP in just 2 months.

Great? Yeah.. not really

Because then new requirements arrive. You have to add new functionality to orders list. Product Owner wants you to add sorting by price. Ok, so that’s easy but then you see that you have to add a few API methods because primary implementation does not assume sorting by product details (which is price in this case). And it takes much longer than you expected. And it’s still ok, these things happen in the programming world.

After next 2 months you have to add another feature and another… Complexity grows but there are still only 4 developers in the team so to make the work faster your team needs to hire another developer.

After a while, you see that the team adds more and more features and with each feature complexity grows. 5 team members is not enough. So again, you need to hire 2 developers more. Then strange things start to happen. You observe that the team’s work is not faster anymore even with more devs on board! Why is that? Because complexity still grows and you don’t know how to maintain that. Wrong decision made in the beginning of the project is now exposed. You didn’t plan architecture in a proper way and now you have to maitain huge code base with a lot of „ifs” and workarounds.

Could you avoid this situation?

Yes — by making good architecture decisions in the beginning. But is is too late now? Not exactly. You are awakened so your team still is able to manage this problem.

But the solution is not that easy. You cannot just start refactoring everything. Another approach is needed. First of all, start implementing new independent feature with well-planned architecture. Also, you should start refactoring small things in files/module you touched by current development. So, splitting things, improving them constantly and the most importantly — learning to do things better.

If your team grows you can select one of the crucial things in the app and try to rewrite it in another way in simple steps. Then in time you will see your code base is getting better but remember — it will take time.

How to start making things better?

It is kind of obvious, but believe me — not all programmers know about it or want to do it because… now everything works so why would I change it?

Ok, so the most obvious thing is to start using…

SOLID

SOLID principles are spread across programming world for a long time already. Many Software Engineers use them on a daily basis to keep code clean and easy to maintain.

Let’s summarize what SOLID is in a few simple words:

S — Single Resposibility Principle

Module (could be just source file or component) should be responsible only for one actor (action).

O — Open-Close Principle

Module should be open for extension but closed for modification

Liskov Substition Principle

Concrete components should be eaisly replaced by other. These components should respect the same specification. Speaking in programming language: conform to the same interface or use the same API call construction.

Interface Segregation Principle

No client should be forced to depend on methods it does not use

Dependency Inversion Principle

Base on abstraction. No concrete implementation .

Swift implementation

Lets look at the simple implementation in Swift language

S — Single Resposibility Principle

protocol FileProcessor {
func process()
}

class XLSFileProcessor: FileProcessor {
func process() {
print("Processing XLS file")
}
}

class PDFFileProcesor: FileProcessor {
func process() {
print("Processing PDF file")
}
}

final class FileProcessFacade {

func processXlsFile() {
XLSFileProcessor().process()
}

func processPDFFile() {
PDFFileProcesor().process()
}
}

Using above aproach programmer could eaisly implement another FileProcessors and don’t depend on others. If a new requirement will come only one class will be affected by changes so we have a very small possibility to break any other implementation.

O — Open-Close Principle

final class StandingsInteractor {
func fetchNewData() {}
func item(at index: IndexPath) {}
}

final class StandingsViewController: UIViewController {

private let interactor = StandingsInteractor()

override func viewDidLoad() {
super.viewDidLoad()
interactor.fetchNewData()
}

// Just mock
// If I change the data source provider for example to UICollectionView
// I don't have to change interactor logic. Just put the call to different
method
func tableView(didSelectItemAt indexPath: IndexPath) {

interactor.item(at: indexPath)
}
}

Above example of an implementation representing quite different approach to Open-Close principle. It is more like an architectural approach.

Let’s look at a better definition of Open-Close principle:

Module should be open for extension but close for modification — protect higher-level components from changes in lower-level components.

So how it is associated with this example?
Developer could make any changes in the ViewController but Interactor won’t be touched.

Higher level component (interactor) is protected from changes from lower level component (view). So — ViewController could have extended functionality without modifing the logic in interactor. Simple.

L — Liskov Substition Principle

protocol DriverLicence {
func licenceDetails()
}

class TaxiDriverLicence: DriverLicence {
func licenceDetails() {}
}

class BusDriverLicence: DriverLicence {
func licenceDetails() {}
}

// We fit licence protocol requirements
let concreteLicence: Licence = TaxiDriverLicence()
concreteLicence.licenceDetails()

// LSP violation
// Break the licenceDetails purpose
class BuildingDestroyLicence: DriverLicence {
func licenceDetails() {
print("Cannot be implemented like that.")
}
}

Above implementation shows in a simple way how LSP principle works. Is every DriverLicence a Car license? If this requirement is fit then LSP principle is fullfiled. In this example BuildingDestroyLicence is not an driver licence so this implementation is wrong.

Interface Segregation Principle

protocol PDFProcessor {
func processPDFFile()
}

extension FileProcessFacade: PDFProcessor {}

let processor: PDFProcessor = FileProcessFacade()

func processPDFRaport(processor: PDFProcessor) {
processor.processPDFFile()
}

Now we can see how ISP principle works. We expose only functionality we needed to fullfill API requirement to the end user (programmer). We don’t care about implemtation details of some component, also we don’t care about other methods, features. As programmer we only want to expose the PDFProcessor feature. In Swift we could do this using simple exntension to main component and use protocol (interface in other languages) to expose this methods.

Dependency Inversion Principle

let fileProcessor: FileProcessor = PDFFileProcesor()
fileProcessor.process()

class MockedFileProcessor: FileProcessor {
func process() {
print("Process file for tests")
}
}

let mockedFileProcessor: FileProcessor = MockedFileProcessor()
mockedFileProcessor.process()

This rule is probably the most liked by many programmers. DIP rule is a winner if we want to have abstraction on a production code, mocked services etc. We could inject any implementation we want and the main requirement is to only fit the protocol (interface) requirement.

The real world example could be unit tests — we want to test specific component, which requires from us other serves (eg. to provide some data). Then we could inject mocked network component and return some sample json instead of making a network call.

SOLID principles use case

The above examples are good for using in day to day code eg. for methods or classes. But Software Engineering is more than that. We are working on big projects — we have a lot of modules and even we share these modules as external dependencies. This requires from us to develop a readable and maintainable API. We could use SOLID principles to make this work done.

For example if we have Network module we don’t want to have specific services implementation in that module — only network layer — making GET, POST requests. And here we have a ‘S’ principle applied.

But there’s more than this.

Every frontend application has some view. Views are connected to interactors and view models which take data from services, parse them and return in a user-readable form. This kind of work could be done in many ways. One of them is to apply one of the Clean Architecture implementation introduced by Uncle Bob.

In the next article I will present SOLID principles at the architectural level to show how powerful tool we have in our hands to make our developer life eaisier.

Resources

I know that not every developer is an Apple fan so I’ve prepared JavaScript implementation of the SOLID principles available on my Github page:

https://github.com/aleksanderkania/JavaScript-SOLID

Look at this if you are interested.
Also if you want to read more about SOLID principles look at the Uncle Bob blog:
https://blog.cleancoder.com/uncle-bob/2020/10/18/Solid-Relevance.html

Enjoy!

--

--