Seeing Responsibilities in Object-Oriented Code

Some thoughts on splitting the 1000-line class

Shaaz Ahmed
The Software Firehose
6 min readJan 15, 2018

--

Motivation

In the past few months, I’ve had to work with three different classes longer than a 1000 lines of code in Java and Ruby on Rails codebases. Although being a 1000-line class by itself is a superficial concern and not necessarily problematic, the consequences that often accompany it may be a latent productivity leech for programmers maintaining that code.

Exemplar negative consequences include code that is difficult to comprehend or reason about, often filled with intertwined concerns that make it harder to spot mistakes and redundancy in the code. Large classes also obscure the flow of the operations being executed and make it harder to decipher where a particular behaviour should be added. In the object-oriented paradigm where mutability is more a norm than a sin, this can also lead to difficulty in unit testing behaviours as it becomes more involved to replicate the state of the object in question to pull it into a test harness.

‘Rigidity’ of Object Models

In contrast with dynamically-typed functional programming languages where behaviours or functions can exist largely independently of the types or objects, the object-oriented programming languages require that your function is strongly associated to an entity. I believe these are two polar approaches to dealing with change — the former deals with it by embracing it, while the latter deals with it by rejecting it.

Functional programming deals with the problem of change by being very malleable and having fewer implicit constraints around where a piece of code belongs, while object-oriented programming deals with by leaning in the other direction — by being rigid and being less permissive of moving around code. However, leaning too far towards either direction can be damaging, and a programmer needs to exercise discipline to preserve structure in a codebase that doesn’t impose the coupling of behaviours and domain entities and to preserve flexibility in a codebase that imposes it. To go further down this train of thought without the specific comparisons of these paradigms, I would recommend you watch Zach Tellman’s fantastic talk “On Abstraction”.

However, in the rest of this post, I will be covering how to tackle the latter problem — identifying large classes and refactoring them to manageable sizes to improve the malleability and reusability of your code.

Of course, I’m in no way claiming that it’s impossible to move methods around in an object-oriented programming language. However, I admit that it is often more difficult to do so in such languages due to the overhead of creating an extra class to move it or due to the complications that mutable state brings with it.

‘Does this method belong here?’

While adding a certain method to a class, I often find myself asking the question “does this method belong here?”. In the case of a really large class, this question by itself is not sufficient. This innocuous litmus test overlooks the subjective nature of object models and the different levels of granularity possible while modelling domain objects. The fact that many correct models exist for the same problem arise from the fact that a higher level of abstraction can exist within object models to accommodate entities at a lower level of abstraction. Although many people are devout believers of the Single Responsibility Principle, they often slip up on the fact that the idea of a responsibility can be nebulous.

To refrain from talking entirely in the abstract, I’ll describe an example. Take for example, the order management system of a airline ticketing company. Initially, the company’s website takes bookings only via its website and app, and you decide to call a class Booking (one could argue that this is a bad choice). Later, the company expands into hotel bookings, which involves different business logic, and you ask yourself “does this method belong here?”. You answer, “yes, it’s related to the booking”. And as the company grows into other domains, your Booking class begins to do more than it was ever meant to, dealing with a wide variety of domains.

With the viewpoint that objects should model life-cycles and not real-word entities, one could have chosen a different class name. Also, another dimension in which the class can grow is the number of state transitions it undergoes. It is possible, and sometimes more manageable to create a separate class for a ConfirmedBooking or a CancelledBooking if there is a large amount of business logic associated with these state transitions. One could also drop in interfaces into this picture to deal with these different dimensions of change and growth, and increase the adaptability of the code.

However, I’m going to assume you’re already stuck with a 1000-line class (whether you knew it all along or whether it just snuck up on you) and describe a few approaches to tease apart concerns in a class by ‘seeing responsibilities’ where responsibilities are entangled and spread across more lines than your brain can hold on to.

Approaches

The approaches listed here are mentioned in Michael C. Feathers’ book Working Effectively with Legacy Code. Steve from Nilenso recommended that I read this book, and I’ve found the lessons from it extremely valuable. Before coming to the conclusion that you don’t really work with legacy code, do consider the fact that code that you yourself write can turn out to be legacy code when you come back to modify it a year later.

I recommend reading the relevant chapter from the book for more examples and a better understanding — it’s quite a lucid read. I’ll quickly go through the methods described in Feathers’ book here.

  • Group methods
  • Look at hidden methods
  • Identify decisions that can change
  • Identify and group internal relationships
  • Describe the main purpose

1. Group Methods

This was one of the most effective methods, for my use case. This method involves listing the different methods in your large class, and trying to group them in different ways. If you see different even-sized groups (or a mix of even-sized and tiny atomic units) emerging, you can try to identify different responsibilities and refactor your code for better separation.

2. Look at hidden methods

The private functions in your code can hide a lot of distinction in responsibilities, and it’s probably worth looking at them to see if you can tease out a large chunk of separate behaviours. I haven’t found this particularly useful yet.

3. Identify decisions that can change

It’s possible to overdo this method that lead premature abstractions, so tread with caution. Basically, look at your code, identify critical decisions around domain entities that have the possibility to change. The example of the ticketing company discussed above is a relevant one — you could sometimes foresee such changes.

4. Identify and group internal relationships

This method also proved to be very helpful for my use case. You look for how your instance methods and instance variables interact, and identify which groups of methods interact with which groups of instance variables. If you find that some instance variables are not at all used by some methods but are used a by another group of methods, then it’s likely that there’s a responsibility in there waiting to be teased out.

5. Describe the main purpose of the class

Although describing the main purpose of the class in a single sentence as a solution to nebulous responsibilities may seem like a cyclical argument, it may turn out to be effective if you didn’t get it right the first time around when you created the class or if you have developed a new perspective on your domain since you first created the class. Ensure that there are no conjunctions in the sentence that you use to describe the purpose, and that your implementation involves nothing more or nothing less than the described purpose.

--

--

Shaaz Ahmed
The Software Firehose

Every reader should ask himself periodically “Toward what end, toward what end?” — but do not ask it too often lest you pass up the fun of programming. — Perlis