Better Object Oriented Code - Improving maintainability

Every programmer feels at least once in their coding career that a piece of code was really hard to change. The same might happen while trying to extend an application with a new feature. There is no more unnerving feeling while writing code than the terror that changing a class might break others. It’s frustrating and it decreases productivity and more bugs may get into the code as a poorly designed code has the risk of causing multiple changes in other parts while trying to rectify one unit.

However, maintaining a well-designed application, where people actually thought about maintainability and flexibility, requires less efforts to update (in comparison at least), even when the application is mature enough. It is also easier to understand even when the functionality is complex enough.

Writing flexible and easy to maintain code is a “hard to develop” skill. There are great recipes out there (most of them based on the great work by the Gang of Four Design Patterns: Elements of Reusable Object-Oriented Software). 
However, to apply those patterns successfully, programmers need to understand why they are good solutions to a problem. And, the only way to do that is to analyze code from the design perspective, and know how to measure design quality.
There are programmers that know some of those design patterns and try to apply them wherever they deem fit. As the saying goes, "If your only tool is a hammer, every problem looks like a nail".
Applying those recipes everywhere is a bad choice as it may end in a more complex design or code may get designed for flexibility in the unintended part.

Nothing remains the same

Even when the engineering team gets all detailed requirements up front (which rarely happens), there would still be things that might change later.
They might be driven by a change in:

  • Business needs
  • Technology
  • Law

Or It might even change because people responsible for defining the requirements skipped something important.

Changes are the main source of bugs. When things change they might introduce some unexpected errors. Having a flexible and well-designed code will help us in reducing that possibility and will control the damage in case of any possible error.

SOLID principles

Considering the above-mentioned problems, Robert C. Martin framed the following five design principles (under the SOLID acronym) to make software more comprehensibleflexible and maintainable:

  • Single Responsibility Principle
  • Open/Closed Principle
  • Liskov Substitution Principle
  • Interface Segregation Principle
  • Dependency Inversion Principle

I have seen developers follow these principles blindly and trying to adapt their code accordingly, no matter what.

SOLID is not an end itself. Maintainability is.

Understanding SOLID principles will help you design better classes, but these are not laws you have to follow. However, they are great concepts to know and take into account when thinking about class design.

The Single Responsibility Principle (SRP)

A class should have one, and only one, reason to change.

In other words, each class should have just one responsibility. We should split responsibilities between classes, so they can work together to achieve more complex tasks.

The idea of this principle is to think about the reasons that may cause a class to change, and keep all those reasons together, in the same class. We should segregate things that change for different reasons, in different classes.

It may be difficult to understand how this principle helps. Robert Martin has tried to clarify that as following by mentioning a case where a person asked for a change and the code broke for a feature of a different business function:

When you write a software module, you want to make sure that when changes are requested, those changes can only originate from a single person, or rather, a single tightly coupled group of people representing a single narrowly defined business function. You want to isolate your modules from the complexities of the organization as a whole, and design your systems such that each module is responsible (responds to) the needs of just that one business function.

You may wonder how this principle helps maintainability. Well, when there is a change request, developers need to change multiple classes in the application. Following the Single Responsibility Principle will not only help to reduce the number of places to change, but it will also help in preventing errors in classes where responsibility is not related.

Open/Closed Principle

Software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification.

Even though all these principles are important, this one can be a great aid to keep the system easily maintainable.
It means designing your entities in a way that when you need to extend your system functionality, you should be able to build on top of what you already have, instead of modifying existing entities. Based on that premise, we could also add that the only reason to change an existing entity should be to change an existing functionality.

How do we actually design entities to be open for extension and closed for modification?

The answer is abstraction. An entity will be open for extension only if all its dependencies are abstractions. If your entity relies on concrete entities, then you will be stuck with those dependencies whenever you’d want to extend your class. However, if your class depends on abstractions and you need to extend the functionality, you will just replace the dependency with another concrete class that answers the same messages as the abstract.

For example, an e-commerce client asks us to add a feature to notify the team when the client change the shipping address on an active purchase the day before the package is shipped:

class ShippingAddressChanged
  def initialize(purchase)
    @purchase = purchase
  end
      
  def notify!
    if @purchase.active? && @purchase.shipping_date.tomorrow?
      TeamMailer.new.send("Short Notice: Shipping address for P-#{@purchase.id} changed.")
    end
  end
end

That works great and later the client notices when a user changes the shipping address, the invoice (which is generated through a different Application) is not printed with the updated value. We need to do something about that.

We talk to the Invoice Application development team, and they create a Web Service for us so we can notify changes on the shipping address.

To add this feature, we would need to change the old ShippingAddressChanged class, as It depends on the TeamMailer class.

In order for this class to be open for extension, we would need to get an abstraction from the dependency. Here, an abstraction from TeamMailer would be Notifier.

class ShippingAddressChanged
  def initialize(purchase)
    @purchase = purchase
  end
      
  def notify!(notifier)
    if @purchase.active?
      notifier.shipping_address_changed!(@purchase)
    end
  end
end

Besides, the "one day to ship" condition is specific to the mailer notifier, as we would always need to update the invoice application even if the shipping is in two weeks. That is why that condition belongs to each notifier.
In the above example, we successfully replaced a concrete class (TeamMailer) by an abstraction (Notifier). And we also extracted the responsibility of deciding whether or not to send the mail, to the mailer:

class TeamMailer
  def shipping_address_changed!(purchase)
    if purchase.shipping_date.tomorrow?
      send("Short Notice: Shipping address for P-#{purchase.id} changed.")
    end
  end
end

Then we can use TeamMailer and any other Notifier indifferently, as long as they answer to the shipping_address_changed! message:

address_changed = ShippingAddressChanged.new(purchase)
address_changed.notify!(TeamMailer.new)
address_changed.notify!(InvoiceAPI.new)

Where InvoiceAPI would send an HTTP request to the web service when receiving the message shipping_address_changed!.

As you can see, if we wanted to notify someone (or something) else when the shipping address changed, we would not have to modify our main class. It would be enough to create another class that answers the shipping_address_changed! message. In other words, our class is now open for extension, because we would not need to change our existing class to extend functionality.

Liskov Substitution Principle (LSP)

If S is a subtype of T, then objects of type T in a program may be replaced with objects of type S without altering any of the desirable properties of that program.

The principle states that if you are dealing with an object of a given class, then replacing that object with another which is a subtype of the same class, should not cause any unexpected behaviour.

For example, we create a class to store multiple Http Requests and trigger them all at some point:

class HttpRequestList
  def initialize(requests = [])
    @requests = requests
  end
      
  def add(request)
    @requests << request
  end
      
  def trigger!
    @requests.each(&:send)
  end
end

Then we need a feature for triggering requests in batches, so we extend the class:

class HttpRequestBatchList < HttpRequestList
  BATCH_SIZE = 3.freeze

  def trigger!
    @requests.shift(BATCH_SIZE).each(&:send)
  end
end

The Liskov Substitution Principle states that we may replace the HttpRequestList with HttpRequestBatchList without altering the desirable behaviour of the program.

An object sending the trigger! message might expect  the requests to be sent all at once. But, in the case of HttpRequestBatchList, only the first three requests will be sent. That will not be an expected behaviour.

If we are building a model domain with decoupled classes, we’ll need to send a message to an object and trust it to do the expected.

Consequences of breaking the principle

We build a Monitor class for example, which will be responsible for triggering all requests and printing a "finished" message:

class Monitor
  def self.run(requests)
    requests.trigger!
    puts "We sent all the requests!"
  end
end

Here, the problem is that the Monitor expects the requests object to send all HttpRequests when receiving the trigger! message.
That will work when the Monitor receives an instance of HttpRequestList. However, when receiving an HttpRequestBatchList, it will just send the first three requests (batch size), and the Monitor will display the finished message even though there might be pending requests in the queue.

This is the principle I see being broken the most when maintaining inheritance hierarchies (and in dynamically-typed languages, also including classes with the same expected contract). That is because inheritance causes the members of the hierarchy (classes) to be tightly coupled. Having subclasses tightly coupled with their parents means they will have more constraints when they need to change.

How to follow Liskov?

Not breaking this principle might be tricky, as it is probably not easy to see the problem until the bug appears, or even when it is already too late and you need to change the whole hierarchy.

When you break this principle, it will probably be because you have the wrong abstraction.
In that case, a generic trigger! message does not fulfill every expectation, as when dealing with batches, it will trigger a single batch and not all requests.
For example, for keeping the principle intact, we could set the abstraction to be trigger_batch! and the class that should trigger all requests, could set the batch size as the requests.count (all requests). Any client using any of these classes would call trigger_batch! multiple times until there are no more pending requests.

Another possible issue is adding a lot of generic code to the base class, for example, to share the code of some complex logic, you will end up with a fat base class that causes big contracts to be inherited by all the subclasses. This, in turn, will raise the chances of overriding the base class behaviour, and ultimately breaking the principle. Therefore base classes should always be as simple as possible. Even though that will not be enough to avoid breaking this principle, it will help.

Interface Segregation Principle (ISP)

Clients should not be forced to depend upon interfaces that they don't use.

There are no interface entities in Ruby, therefore this principle is thought to be for languages like Java or C# where interfaces are elements of the language. However, the meaning of this principle can also be "translated" to other languages. In fact, we have a few different ways to analyze this principle.

The Interface Segregation Principle states that your classes should not need to respond to unnecessary messages. Working with bigger interfaces means you will have less flexibility when reusing code, and changing the interfaces will impact even more classes.

For example, we have a Notifiable module that has the logic to send the host record as an email:

module Notifiable
  def send_email!
    # ...
  end
end
    
class Message
  include Notifiable
end
    
class Invoice
  include Notifiable
end

Now we need the feature to send a Message by SMS as well:

module Notifiable
  def send_email!
    # ...
  end
      
  def send_sms!
    # ...
  end
end

This will add the SMS feature to the Invoice even though we know we will never send an Invoice by SMS. Here, we are breaking the principle, as Invoice need not know how to be sent by SMS.
As you can see, this principle is related to the Single Responsibility Principle, given if we had the module's responsibilities, we would not break the ISP:

module EmailNotifiable
  def send_email!
    # ...
  end
end
    
module SmsNotifiable
  def send_sms!
    # ...
  end
end

This way we have smaller modules and can combine them so our classes only have the interface they need:

class Message
  include EmailNotifiable, SmsNotifiable
end
    
class Invoice
  include EmailNotifiable
end

This allows us to have way more flexibility when reusing our modules into different classes.

Dependency Inversion Principle

High level modules should not depend on low level modules. Both of them should depend upon abstractions.

High level components require one or more external dependencies to complete a task. Here, “High level” is not related to their layer in the architecture.
The Low level components have specific knowledge of a task and, are used by High level components.

Regarding reusability, we should be more interested in High level components as they contain the business model decisions. When High level components are tightly coupled to the Low level components, it is difficult to reuse High level modules in different contexts.

For example, if we add a #xsl_export method to the Invoice class responsible for exporting the Invoice into an XSL file:

class Invoice
  def xsl_export
    XslHelper.create(invoice_number)
  
    items.each do |item|
      XslHelper.add_row [item.id, item.units, item.price, item.total_price]
    end
  end
end

Here, our High level module is the Invoice, which depends on the Low level module XslHelper. As you can see, it tightly couples Invoice#xsl_export to the XslHelper implementation. Any change in the XslHelper API would cause another change in our Invoice class. For example, if the method XslHelper#add_row was renamed to XslHelper#create_row, it would break our Invoice class and we would need to update it.
The principle says that our High level module (the Invoice) should depend on an abstraction, not an implementation. It means changing that would change the dependency from an implementation to an abstraction. Therefore, we would not need to change our High level module as long as the abstraction remains the same:

class Invoice
  def export(exporter)
    exporter.export(self)
  end
end

Here, we have created the exporter abstraction, which needs to answer to the #export method. If we had another format to export, we would only need to create a class which knows how to export the invoice when receiving the #export message.
That way, we would have removed the dependency between Invoice and XslHelper.

Last words

We have gone through all the SOLID principles. Just knowing these principles is not enough to build a flexible application. But you should always take them into account when designing a model as they will help in reducing fragility and rigidity and increasing flexibility.
All these principles are closely related to one another. Once you understand them it would only be a matter of time when you’ll stop thinking about the principles and apply them automatically.
SOLID principles are only a way to think about a design. There will be times when breaking a principle would make sense. And, there is no problem with that, as long as you’re clear about the reasons for making that decision.

The most important ideas to take home from this article are:

  • Be careful when using inheritance as it would make your hierarchy tightly coupled and reduce flexibility. Use composition instead.
  • Keep all dependencies upon abstractions instead of implementations. That will help to keep your code more flexible and reduce the scope of updates when introducing changes.
  • When you need to add a dependency, always try to make it injectable through an argument. It will be easier to test (as you will stub the dependency) and extend (as sending another object that answers to the same messages will be enough).
  • If you cannot fit together different objects to answer a set of messages, the wrong abstraction might be the reason. Think again!
« Go home