How Long Should a Class Be — When Clean Code Isn’t Really Clean
How rules for clean code can sometimes undermine its actual quality
Not long ago, I was looking for a new job, meticulously sifting through a sea of job adverts, each designed to catch the eye and encourage candidates to send off their CVs.
In this vast array of promises, one posting stood out. It wasn’t just the usual jargon — it resonated. This company wasn’t merely searching for a developer; they wanted a real craftsman. “We value clean code,” they stated.
For a brief moment, I was genuinely chuffed.
But, as always, there was a catch, slipped into the next paragraph: “Every class we create is capped at a hundred lines.”
Assessing Class Length
For the record, my primary language is PHP, and the job posting was for a PHP Web Developer position.
The thing with assessing class length is that it’s very difficult.
Every programming language has its own quirks and idiosyncrasies. There are loosely typed and strongly typed languages. Some support multithreading, while others are single-threaded. The list goes on.
Then there are the conventions.
It’s been nearly a decade since I touched Java, but if I recall correctly, the usual thing is to place the opening bracket at the end of a line. In PHP, however, the convention is to give that bracket its own line.
Here’s a simple example:
public class Calculator {
public double add(double number1, double number2) {
return number1 + number2;
}
}
class Calculator
{
public function add(float $number1, float $number2): float
{
return $number1 + $number2;
}
}
At first glance, these snippets are virtually identical in functionality. But, if one adheres to each language’s coding standards, the PHP version will inherently be longer than its Java counterpart. And this doesn’t even account for additional lines, such as imports, comments, and intentional white space — all of which contribute to class length.
Some might say that class length shouldn’t factor in these extras; others reckon that if it’s in the file, it’s part of the tally.
So, what happens when you tie a developer’s hands with a stringent rule of no more than a hundred lines of code per file? Let’s explore the possible outcomes.
Encouraging Bad Practices
Defining a function in PHP, by standard, requires at least four lines — one for the function header, two for opening and closing brackets, and at least one for a statement in the body.
If the number of lines is limited, the temptation to save a few by writing overly complex functions may arise.
When you’re constrained by a line count, you might find yourself devising some questionable strategies to save space. My favorite? Overly complex one-liners.
Here’s a snippet from my integration library. It’s part of a class responsible for building and managing addresses to remote resources:
class Address implements AddressContract, Stringable
{
//...
/**
* Checks if the collection contains parameters with
* the given names and validates them. This method
* accepts an arbitrary number of arguments, each of
* which should be a string representing a parameter
* name.
*
* @param string ...$names The names of the parameters
* to check for.
*
* @return bool Returns true if all parameters are
* found, false otherwise.
*/
public function hasValid(string ...$names): bool
{
foreach ($names as $name) {
if (!isset($this->parameters[$name]) || !$this->parameters[$name]->isValid()) {
return false;
}
}
return true;
}
//...
}
To give you some context, the Address in my system allows parameters such as {id} in https://example.com/articles/{id}. This code specifically checks if an instance of Address contains valid parameters.
There’s a somewhat explanatory PHPDoc (it’s a work in progress, by the way), and the code is easy to follow. Loop through the provided names and check if each exists and is valid. If not, then return false. Unless the if statement asserts true, you’ll get true.
Now, to save some space, I could write this code instead:
class Address implements AddressContract, Stringable
{
//...
public function hasValid(string ...$names): bool
{
return array_reduce($names, fn($carry, $name) => { return $carry && isset($this->parameters[$name]) && $this->parameters[$name]->isValid(); }, true);
}
//...
}
Five lines of code saved.
Making Everyone Waste Their Time
Obsessing over the length of a class is simply counterproductive.
Instead of channeling their energy into delivering high-quality functionalities, developers count lines and devise strategies to fit within the confines.
While one might argue that this line-counting process could be automated, it’s a double-edged sword. Automation could inadvertently promote bad practices as developers might attempt to outwit the system.
Even if the system is designed to enforce best practices, it’s a cat-and-mouse game; developers will continuously devise new ways to circumvent the rules to save time.
Let’s not forget that such automation must either be triggered somehow or be a part of the CI/CD pipeline.
Either way, there would be no instant feedback on class length, making the software development process much longer than it should be.
Creating Rat Kings
A rat king is a collection of rats whose tails are intertwined and bound together.
— Wikipedia
Forcing classes to be broken down can sometimes lead you to unintentionally craft what I like to call a rat king: a tangled mess of tiny classes that can’t live without each other.
This is likely the most prevalent issue and demands significant attention.
With one comprehensive class, users need to know only about that class. However, if you fragment that into many smaller, tightly coupled classes, users must be aware of all of them.
This is advantageous because every component can be replaced. This is valid, but it’s only beneficial if there’s a genuine requirement for such replacements.
John Ousterhout highlights this when discussing old Java I/O handling. In essence, to proficiently read serialized objects from a file, you had to instantiate three nested objects. Yet, you rarely engaged with the first two directly.
// You rarely interact with these two objects,
// except when injecting them into the next one.
FileInputStream fileStream = new FileInputStream(fileName);
BufferedInputStream bufferedStream = new BufferedInputStream(fileStream);
// You actually interact with this one.
ObjectInputStream objectStream = new ObjectInputStream(bufferedStream);
This problem often comes from going overboard with the Single Responsibility Principle. Trying to break a big class into a bunch of smaller ones can create classes that aren’t that useful.
Setting a strict limit on class length can push this kind of design even more.
The Better Way
Counting the lines in a class is not only difficult to track precisely, but it also doesn’t necessarily reflect the quality of the code.
Surely, a high line count can serve as a code smell — a hint that maybe the code needs refactoring. However, decisions to refactor based on this alone should be made with caution.
Take Laravel’s Model class as an example. It spans over 2.4k lines of code, supplemented by a few closely related traits. While this might seem extensive at first glance, it’s intentionally designed this way. It ensures that developers can extend the base model class, and voilà, their default model springs to life effortlessly.
When crafting code, the primary consideration should be its usability, not length. Be sure to make the most frequent use cases as straightforward as possible.
This article is influenced by my personal experience and John Ousterhout’s “A Philosophy of Software Design.” The book delves deeply into crafting good code and is a must-read for those serious about software design.
Want to connect?
For more content and to stay up to date, follow me on Twitter.
How Long Should a Class Be — When Clean Code Isn’t Really Clean was originally published in Better Programming on Medium, where people are continuing the conversation by highlighting and responding to this story.