Swift with SOLID Principles — improve with iterations

Let’s build a fictitious HR system in Swift

PTeng
Better Programming

--

SOLID principles in software parlance is not just a buzzword but something that scalable software development cannot live without.

In a software’s lifetime, most of the cost of the software development is spent on maintaining the software. So as developers we want to build something that is not brittle but easy to maintain.

These principles may seem academic when you hear about them first but after spending time developing software in a team you quickly realize their importance. Some of us may have even not heard of these principles but maybe following some of them already in some form or the other.

In this article we will try to build a fictitious HR system, We will iterate over its development lifecycle as would happen in the real world. Each iteration will improve the code by implementing one of the SOLID principles. The completed example can be found here.

How it started

One day Lisa walked into Springfield Technologies. The manager summoned the team and announced that they were going to build an in-house HR system that would be a path-breaking way to manage the company’s HR processes.

The manager wanted the system to be able to do the following:

  • It should be able to store and retrieve the employee details
  • It should be able to email pay slips

Lisa in her excitement seconded himself to do the job and the next day she came up with the following:

Lisa put this up for peer review and her colleague raised an issue that her code was violating the Single Responsibility principle.

The module that Lisa has built currently has two stakeholders: HR for maintaining the employees and Payroll for generating the payroll.

Imagine if HR wanted to change the way the Payroll is generated (they wanted that instead of just emailing the Payslip they want to print it as well and send it by snail mail).

This would mean that under the current design the whole module will need to be recompiled affecting Payroll as well. Similarly, if the IT department wanted to change the internal workings of the database the whole module will need recompilation affecting HR!

Single Responsibility Principle (SRP)

This principle has a few meanings, among which the following stand out:

A function or module should have only one reason to change.

A function or module should have only one stakeholder (eg: HR, Payroll etc).

Lisa went back to the drawing board and came up with the following:

We can already see that the code is much better now. The internal workings of the EmployeeDatabase and Payroll are extracted out of HRSystems .

Any change that will need to be made to them will not directly affect HRSystems module.

Open close principle (OCP)

This principle states that:

A software artefact should be open for extension but closed for modification.

While Lisa is happy with her last round of refactoring to conform to SRP, her boss comes up to her and says that he wants the Payroll module to be able to print the payslip to be posted to the employee along with the existing email that it generates.

So Lisa came up with the following change:

We can see that because of our adoption to SRP we were able to modify the Payroll module without affecting the HRSystems module. We were able to extend the capabilities of the HRSystem module without having to modify it directly.

Liskov Substitution Principle (LSP)

Having made the previous change, Lisa’s colleague Moe has made a comment on her peer-review request that he has heard from the manager that they want the ability to swap to a different database system in case the negotiations with the existing database vendor don’t go as expected.

What this will mean is that Lisa will need to build her code so that the modules that are not database-related should be isolated from any changes to the database and the implementations of the database layer.

Liskov substitution principle states that if there are two objects O1 of type T and O2 of type U , then a function F defined in terms of O1 should behave unchanged if O1 is replaced by O2 , when U is type of T.

Lisa and Moe pair together and come up with the following refactored code:

For the first time, Lisa and Moe have introduced a protocol for what a Database should do.

Similarly, any references of EmployeeDatabase have been replaced by its abstraction,Database . We have made the RelationalDatabase and NoSQLDatabase adopt and conform to the Database protocol.

This allows us to be able to initialize the HRSystems module with a database implementation of our choice (RDBMS or NoSQL) without affecting the system behaviour.

We are able to substitute the database with either RDBMS or NoSQL without affecting the system behaviour.

Interface Segregation Principle (ISP)

The system is now coming up nicely, there is still some improvement that can be done to the HRSystems module to satisfy the ISP.

This module can potentially be used by people who are just interested in “Employee operations” similarly there can be another group of people who are interested in just generating “Payroll”.

As it stands if any of the employee operations need change it will also affect the payroll operations as the module will need to be rebuilt and vice versa.

To overcome this problem we will have to compose the HRSystems module out of individual modules. These modules will either do EmployeeOperations or PayrollOperations .

We now have been able to isolate Employee operations and Payroll operations into their own modules such that change in each of them does not directly affect the HRSystems module (even the modules themselves are decoupled).

This helps to satisfy the ISP. The HRSystems module is composed of EmployeeOperations & PayrollOperations .

The HRModuleBuilder helps us to build a HRSystems module by injecting into it the Employee and the Payroll modules at run time.

Dependency Inversion Principle (DI)

If you have managed to follow on till now you will be happy to know that we have already satisfied the Dependency Inversion principle which states that

Source code modules should depend on abstractions and not implementations. HRSystem module depends only on the abstract types (protocol) EmployeeOperations and PayrollOperations thus inverting the dependency we would have otherwise had.

Similarly EmployeeModule and PayrollModule depend on Database abstraction and not an actual Database implementation.

If we draw an arrow from a module to its dependency we see the inversion.

HRSystems → EmployeeOperations ← EmployeeModule (inverted dependency)HRSystems → PayrollOperations ← PayrollModule (inverted dependency)

The Result

Below is the class relation diagram that we have finally arrived at after implementing the SOLID principles. The complete example can be found here.

A SOLID System

Observations

  • The system is divided into modules each having a Single responsibility (SRP).
  • Modules like Emp, Pay & DB can be extended in isolation without affecting the other modules (OCP)
  • The modules depend on abstraction of the Database so they can be instantiated with any choice of implementation (LSP).
  • The HRSystems module is composed of Employee and Payroll module, changes to either can be done in isolation without affecting each other (ISP).
  • There is a good use of protocols in the system as modules depend on abstractions and not implementations. HRSystems module depends on EmployeeOperations & PayrollOperations . Similarly EmployeeModule & PayrollModule depend on Database which is an abstraction of the database system. This helps to implement Dependency Inversion (DI).

Conclusion

For any reasonably complex codebase adapting SOLID principles is not a choice but a necessity. It allows us to

  • To build a system that is modular.
  • The system components have low coupling.
  • Individual modules become easy to understand and reason about.
  • Changes to one part of the system do not easily affect the other parts (module isolation).
  • Unit testing of individual modules becomes easier (due to the ability to inject mock implementations into modules as the modules depend on abstractions and not implementations).
  • Changes can be made with confidence (due to low coupling and greater testability of the code at a lower level rather than higher up in the testing pyramid).
  • Software maintenance costs are high so we want to be able to have the above features.

--

--

Swift developer (iOS, tvOS & Chromecast) who can also write Javascript. Occasional writer on Medium. A speck in the universe 🪐