Four Rules of Simpler iOS Software Design
In the late 1990s, while developing Extreme Programming, famous software developer Kent Beck came up with a list of rules for simple software design.
According to Kent Beck, a good software design:
- Runs all the tests
- Contains no duplication
- Expresses the intent of the programmer
- Minimizes the number of classes and methods
In this article, we will discuss how these rules can be applied to the iOS development world by giving practical iOS examples and discussing how we can benefit from them.
Runs all the tests
Software design helps us create a system that acts as intended. But how can we verify that a system will act as intended initially by its design? The answer is by creating tests that validate it.
Unfortunately, in the iOS development universe tests are most of the times avoided… But in order to create a well-designed software, we should always write Swift code with testability in mind.
Let ’s discuss two principles that can make test writing and system design simpler. And they are Single Responsibility Principle and Dependency Injection.
Single Responsibility Principle (SRP)
SRP states that a class should have one, and only one reason to change. The SRP is one of the simplest principles, and one of the hardest to get right. Mixing responsibilities is something that we do naturally.
Let’s provide an example of some code that it's really hard to test and after that refactor it by using SRP. Then discuss how it made the code testable.
Suppose that we currently need to present a
PaymentViewController from our current view controller,
PaymentViewController should configure its view with depending on our payment product price. In our case, the price is variable depending on some external user events.
The code for this implementation currently looks like the following:
How can we test this code? What should we test first? Is the price discount calculated correctly? How can we mock the payment events to test the discount?
Writing tests for this class would be complicated, we should find a better way to write it. Well, firstly let’s address the big problem. We need to untangle our dependencies.
We see that we have logic for loading our product. We have Payment Events that make the user eligible for a discount. We do have Discounts, a discount calculation and the list goes on.
So let’s try to simply translate these into Swift code.
We created a
PaymentManager that manages our logic related to payments, and Separate
PriceCalculator that it’s easily testable. Also, a data-loader that is responsible for the network or database interaction for loading our products.
We also mentioned that we need a class responsible for managing the discounts. Let's call it
CouponManager and let it as well manages user discount coupons.
Our Payment view controller then can look like the following:
We can write now tests like
and many other ones! By creating separate objects now we avoid unneeded duplication and also created a code that it’s easy to write tests for.
The second principle is Dependency Injection. And we saw from the examples above that we already used dependency injection on our object initializers.
There are two major benefits of injecting our dependencies like above. It makes it clear on what dependencies our types rely and it allows us to insert mock objects when we want to test instead of the real ones.
A good technique is to create protocols for our objects and provide concrete implementation by the real and the mock object like the following:
Now we can easily decide which class we want to inject as a dependency.
Tight coupling makes it difficult to write tests. So, similarly, the more tests we write, the more we use principles like DIP and tools like dependency injection, interfaces, and abstraction to minimize coupling.
Making the code more testable not only eliminates our fear of breaking it (since we will write the test that will back us up) but also contributes to writing cleaner code.
This part of the article was concerned more on how to write code that will be testable than writing the actual unit test. If you want to learn more about writing the unit test, you can check out this article where I create the game of life using test-driven development.
Contains no duplication
Duplication is the primary enemy of a well-designed system. It represents additional work, additional risk, adds unnecessary complexity.
In this section, we will be discussing how we can use the Template design pattern for removing common duplication in iOS. In order to make it easier to understand we are going to refactor implementation of a real-life chat.
Suppose that we have currently in our app a standard chat section. A new requirement comes up and now we want to implement a new type of chat — a live-chat. A chat that should contain messages with maximum 20 number of characters and this chat will be disappearing when we dismiss the chat view.
This chat will have the same views as our current chat but will have a few different rules:
- Network request for sending chat messages will be different.
2. Chat messages have to be short, no more than 20 characters for message.
3. Chat messages should not be persisted in our local database.
Suppose that we are using
MVP architecture and we currently handle the logic for sending chat messages in our presenter. Let's try to add new rules for our new chat type named live-chat.
A naive implementation would be like the following:
But what happens if in future we will be having much more chat types?
If we continue to add
else that check the state of our chat in every function, the code will become messy hard to read and maintain. Also, it’s hardly testable and state checking would be duplicated all over the presenter's scope.
This is where the Template Pattern comes in use. The Template Pattern is used when we need multiple implementations of an algorithm. The template is defined and then built upon with further variations. Use this method when most subclasses need to implement the same behavior.
We can create a protocol for Chat Presenter and we separate methods that will be implemented differently by concrete objects in Chat Presenter Phases.
We can now make our presenter conform to the
Our Presenter now handles the message sending by calling common functions inside itself and delegates the functions that can be implemented differently.
Now we can provide Create objects that conform to the presenter phases based and configure these functions based on their needs.
If we use dependency injection in our view controller we can now reuse the same view controller in two different cases.
By using Design Patterns we can really simplify our iOS code. If you want to know more about that, the following article provides further explanation.
In software development, a design pattern is a general reusable solution to a problem. A design pattern is a…medium.com
The majority of the cost of a software project is in long-term maintenance. Writing easy to read and maintain code is a must for software developers.
We can offer more expressive code by using good Naming, Using SRP and Writing test.
Number one thing that makes the code more expressive — and it is naming. It’s important to write names that:
- Reveal intention
- Avoid disinformation
- Are easily searchable
When it comes to naming classes and functions, a good trick is to use a noun or noun-phrase for classes and user verbs or verb phrase names for methods.
Also when using different Design Patterns sometimes its good to append the pattern names such as Command or Visitor in the class name. So the reader would know immediately what pattern is used there without having the need to read all of the code to find out about that.
Another thing that makes code expressive is using Single Responsibility Principle that was mentioned from above. You can express yourself by keeping your functions and classes small and for a single purpose. Small classes and functions are usually easy to name, easy to write, and easy to understand. A function should serve only for one purpose.
Writing tests also brings lots of clarity, especially when working with legacy code. Well-written unit tests are also expressive. A primary goal of tests is to act as documentation by example. Someone reading our tests should be able to get a quick understanding of what a class is all about.
Minimize the number of classes and methods
The functions of a class must remain short, a function should always perform only one thing. If a function has too many lines that that might be the case that it’s performing actions that can be separated into two or more separate functions.
A good approach is to count physical lines and try to aim for maximum four to six lines of functions, in most cases anything that goes more than that number of lines it can become hard to read and maintain.
A good idea in iOS is to chop the configuration calls that we usually do on
In this way, each of the functions would be small and maintainable instead of one mess
viewDidLoad function. Same should also apply for app delegate. We should avoid throwing every configuration on
didFinishLaunchingWithOptions method and separate configuration functions or even better configuration classes.
With functions, it’s a bit easier to measure whether we keeping it long or short, we can most of the times just rely on counting the physical lines. With classes, we use a different measure. We count responsibilities. If a class has only five methods it does not mean that the class is small it might be that it has too many responsibilities with only those methods.
A known problem in iOS is the large size of
UIViewControllers. It is true that by apple view controller design, it's hard to keep these objects to serve a single purpose but we should try our best.
There are many ways to make
UIViewControllers small my preference is to use an architecture that has better separation of concerns something like
MVP but that does not mean that we can’t make it better in apple
MVC better as well.
By trying to separate as many concerns we can reach pretty decent code with any architecture. The idea is to create single-purpose classes that can serve as helpers to the view controllers and make the code more readable and testable.
Some things that can be simply avoided with no excuse in view controllers are:
- Instead of writing network code directly there should be a
NetworkManagera class that is responsible for network calls
- Instead of manipulating data in view controllers we can simply create a
DataManagera class that is responsible for that.
- Instead of playing with
UIViewControllerwe can create a facade over that.
I believe that we should compose software from components that are accurately named, simple, small, responsible for one thing and reusable.
In this article, we discussed four rules for simple design by Kent Beck and gave practical examples of how we can implement them in iOS Development environment.
If you enjoyed this article make sure to clap to show your support. Follow me to view many more articles that can take your iOS Developer skills to a next level.
If you have any questions or comments feel free to leave a note here or email me at email@example.com.