On writing maintainable code

If I was to name one of the tasks a software developer is least excited about, it would probably be maintaining a “smelly” codebase. Imagine a five-year-old piece of software written by an inexperienced developer on a very tight schedule. It’s not a pretty sight and most of us wouldn’t even want to touch this code with a stick. To make matters worse, requirements changed over the years and new features were added on top of a very fragile foundation. Not by the original developers, but by new ones, who made progress by poking a black box and measuring the response.

Unfortunately situations like the one described above are quite common. Especially at companies working on multiple projects for multiple different clients, where developers jump from one codebase to another. That is where code quality is of uttermost importance and writing well structured and easy-to-understand software makes a quickly discernible difference. But where do we start? How can we make sure that our colleagues get positively surprised when they inherit a codebase we’ve been working on?

What makes our code easy to maintain?

Let’s start by quickly defining what we mean by “maintaining” a codebase. We should understand it as adding new features, modifying existing ones or refactoring a system that we do not have a deep understanding of. This can, for example, be an iPhone app written by a colleague who does not work at the company any more. It can also be a project we have worked on ourselves, but it was a long time ago and we do not remember its inner workings.

In my opinion, code that is easy to maintain has three following qualities:

  • It is easy to understand
  • It minimizes the time it takes to make changes to the system and extend it
  • It can be tested with little effort

This is fairly straightforward. The first thing that we do, when starting to work on a new codebase, is to try to understand the structure and what the existing classes are responsible for. Once we have a good grasp on what is happening in the system, we begin to make changes to add a new feature, or modify one that is already present. Last but not least, we need to make sure that our new addition behaves as expected and that we didn’t introduce any bugs in the old code (a so-called  “regression”).

Now is a good time to ask the question which is the reason this blog post exists: What can we do to make sure that our codebase has all of the above-mentioned qualities? The short answer is that we have to focus on our coding style (this includes naming classes, variables and functions as well as code formatting) and the architecture of the system at hand. Let’s work through some examples while focusing on each of the code qualities mentioned before.

Project structure

What makes things easy to understand is structure. It gives us a high level view of the subject without the need to focus on details. This rule applies to the way we organize software projects as well. The way we name folders and directories should tell us what kind of a system we’re dealing with. That’s why it makes sense to group classes according to the features they belong to and how closely they work with each other. For example, if I open an app that manages my mobile phone plan, I might see folders like “Top up”, “Add-ons”, “Bills”. I should definitely not see folders named “Views” or “View controllers”.

This topic is discussed by Robert C. Martin in one of his talks from 2013. You can find it on Youtube.

Short class description

Based on my personal experience, I find it very useful when class files contain a short description of what the class is responsible for and how it fits in the bigger picture. Most of the time 2-3 sentences are enough and they make it much easier to understand the actual code, as we already have certain expectations towards the given class. Here is what such a description might look like:

This class is responsible for generating a PDF version of a bill, based on a corresponding “Bill” object. Used, for example, when the user wants to send a bill to an email address.

Don’t Repeat Yourself

One of the fundamental rules of software development. Repeating identical code in multiple places is a waste of time (even though it may not seem so at first) and can lead to bugs in the future. New developers working on your codebase may be unaware of the fact that they need to introduce a change in multiple places, because we were too lazy and decided to copy a bunch of code. The same goes for excessive use of literals, for example, representing the dimensions of a view or a key in a dictionary. In such a case it’s always better to define a constant.

Composition over inheritance

Composition over inheritance is an often-stated principle of Object-Oriented Programming. Its premise is, that it’s more beneficial to try to achieve code reuse and polymorphic behavior by composing objects from smaller, specialized objects, than it is by creating deep class hierarchies. This has a positive influence on the architecture of the system and makes it easier to follow the flow of control. I’ve always found it quite confusing when I had to jump between a superclass and a subclass to be able to understand the behavior of the latter. The reason for that was the fact that subclasses often override some of the methods defined in the superclass.

Composition is even better when used together with dependency injection via a constructor. This means, that the objects class A is composed of, are not created by class A itself, but by another class, and then passed in via class A’s constructor. As an example, let’s consider a view controller managing a list of todos. The controller needs to be able to fetch a list of todos from the server, as well as parse the received response. We would like everyone, who uses our view controller, to be immediately aware of what the controller’s dependencies are. It should also be impossible to create an instance of the controller without the objects it cooperates with. Therefore we decide to write the following constructor (in Swift):

class ToDoListViewController: UIViewController 
{
    let todoDownloader: ToDoListDownloader
    let todoParser: ToDoListJSONParser

    init(downloader: ToDoListDownloader, parser: ToDoListJSONParser){
        todoDownloader = downloader
        todoParser = parser
        super.init(nibName: "ToDoListViewController", bundle: nil)
    }
}

When it so happens, that someone else gets the pleasure of maintaining our code, they will see immediately what objects a ToDoListViewController needs to function correctly. On top of that, the constructor does not allow for the creation of controllers that do not have access to a downloader or a parser.

Single Responsibility Principle

Minimizing the time it takes to introduce a change to existing code is often about loose coupling and good isolation of responsibilities. If the classes in our codebase are too dependent on each other and know each other’s implementation details, then a small change can snowball into an intimidating refactoring. We should strive to create classes that specialize in doing one thing very well. Their implementation details should not be connected in any way to the rest of the system. It is said, that systems designed in this way follow the Single Responsibility Principle, which is one of the SOLID principles.

A classic example of the violation of SRP is a so-called “massive view controller” in iOS applications. Controllers like that grow to possibly thousands of lines of code, because they take on too many responsibilities. Some of them could be fetching data from a backend, saving objects to the database or performing business logic. In fact, all of these responsibilities should be moved to separate classes. A view controller is supposed to, as the name implies, be responsible for controlling the view and passing UI events on to other objects which respond to them.

Dependency Injection and use of protocols

We’ve already touched upon the concept of Dependency Injection in one of the previous paragraphs. This concept can be made even more powerful if we couple it with the use of protocols/interfaces. In our previous example involving ToDoListViewController, we wrote a constructor, which explicitly references a downloader class and a parser class. But what if we decide that the todo list returned by the server is formatted as XML and not JSON? We would have to make changes in ToDoListViewController. A better idea is to make the controller not know the concrete class it cooperates with, but only the protocol that declares the necessary methods. Here is our previous example again, this time using a protocol instead of a concrete class:

class ToDoListViewController: UIViewController 
{
    let todoDownloader: ToDoListDownloader
    let todoParser: ToDoListParserProtocol

    init(downloader: ToDoListDownloader, parser: ToDoListParserProtocol){
        todoDownloader = downloader
        todoParser = parser
        super.init(nibName: "ToDoListViewController", bundle: nil)
    }
}

Now we can implement a class called ToDoListXMLParser, which conforms to the ToDoListParserProtocol protocol. The new class can be used instead of ToDoListJSONParser without any need to change the controller. This kind of flexibility definitely makes our code more maintainable, but should only be introduced in places where it makes sense.

Another benefit of Dependency Injection is that it has a very positive impact on the testability of our classes. This is due to the fact, that we can replace real dependencies (like the parser) with mock objects, which we have configured to behave in a certain way. Such an approach allows us to have full control of the testing environment and does not force us to create objects that we are not interested in testing at the moment.

Accessing singletons view static methods

Singletons have always been a matter of contention in the iOS community. In my opinion there is nothing wrong with them as long as we remember how to handle them correctly. What I mean by that is mainly the way we access singleton objects. If we do it via static methods sprinkled all over our codebase, then we’re introducing unnecessary coupling and make testing harder. The classes that have a need to reference a singleton object should not know how to get a hold of it. They should receive a reference to the singleton object via a constructor. We achieve two things by following this strategy:

  • The objects that use the singleton are not aware of how to obtain it.
  • We can easily replace the singleton with a mock for the purpose of testing.

Of course we will need to have some classes, which know how to access the shared instance of the singleton class. It would be preferable if those classes were factories, which are responsible for constructing other objects and putting the object graph together. After all creating an object graph is a responsibility like any other, and it deserves a dedicated class.

Conclusion

The list we looked at in this article is by no means exhaustive. It describes some of the practices I found very useful while working on multiple iOS applications. If you have any other ideas that can help us all to write more maintainable code, it would be awesome if you could share them in the comments section below.

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google+ photo

You are commenting using your Google+ account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

w

Connecting to %s