In the world of software development, maintaining clean, readable, and scalable code is a significant challenge. As projects grow in complexity, developers often struggle with code that is difficult to manage and prone to bugs, hindering productivity and innovation.
Enter object calisthenics, a set of programming exercises designed to improve developers object-oriented programming skills.
By adhering to a series of rules and practices, object calisthenics encourages developers to write cleaner, more maintainable code. These practices promote better design patterns and help avoid common pitfalls in coding, ultimately leading to more robust software solutions.
What is Object Calisthenics
Object Calisthenics are programming exercises, formalized as a set of 9 rules invented by Jeff Bay in his book The ThoughtWorks Anthology. The word Object is related to Object-Oriented Programming. The word Calisthenics is derived from greek, and means exercises in the context of gymnastics.
Here are all these 9 rules in order:
- Only One Level Of Indentation Per Method
- Don’t Use The ELSE Keyword
- Wrap All Primitives And Strings
- First Class Collections
- One Dot Per Line
- Don’t Abbreviate
- Keep All Entities Small
- No Classes With More Than Two Instance Variables
- No Getters/Setters/Properties
When to use Object Calisthenics
By trying to follow these rules as much as possible, you will naturally change how you write code. It doesn’t mean you have to follow all these rules, all the time. Find your balance with these rules, use some of them only if you feel comfortable with them.
These rules focus on maintainability, readability, testability, and comprehensibility of your code. If you already write code that is maintainable, readable, testable, and comprehensible, then these rules will help you write code that is more maintainable, more readable, more testable, and more comprehensible.
1. One level of indentation per method
Indentation in your code is often detrimental to readability and maintainability. Usually, you need to compile the code in your mind to understand it. This is especially true if you have different conditions at different levels or a loop in another loop. Take a look at the example below:
public class Grid {
public String displayGrid(char[][][] data) {
StringBuilder buffer = new StringBuilder();
// level 0
for (int i = 0; i < 10; i++) {
// level 1
for (int j = 0; j < 10; j++) {
// level 2
buffer.append(data[i][j]);
}
buffer.append("\\n");
}
return buffer.toString();
}
}
To follow this rule, you have to split up your methods up. Martin Fowler in his book Refactoring, introduce the extract method pattern, which is what we will use.
public class Grid {
public String displayGrid(char[][][] data) {
StringBuilder buffer = new StringBuilder();
collectRows(buffer,data);
return buffer.toString();
}
private void collectRows(StringBuilder buffer, char[][][] data) {
for (int i = 0; i < 10; i++) {
collectRow(buffer, data, i);
}
}
private void collectRow(StringBuilder buffer, char[][][] data, int row) {
for (int i = 0; i < 10; i++) {
buffer.append(data[row][i]);
}
buffer.append("\\n");
}
}
By doing so, all our method now have only one level of indentation.
2. Dont use the ELSE keyword
The else
keyword is well-known as the if/else
construct is built into nearly all programming languages.
Do you remember the last time you saw a nested conditional?
Did you enjoy reading it?
I don't think so, and that is precisely why it should be avoided. Since it is much easier to add a new branch to existing code than to refactor it to a better solution, you often end up with very ugly code.
public class Authentication {
private final UserRepository userRepository = new UserRepository();
public void login(String username, String password) {
if (userRepository.isValid(username, password)) {
redirect("homepage");
} else {
throwException("errorCode", "Bad credentials");
redirect("loginRoute");
}
}
void throwException(String key, String value) {
*//...*
}
void redirect(String url) {
*//...*
}
}
A good way to avoid using the else clause, it to rely on the early return.
public class Authentication {
private final UserRepository userRepository = new UserRepository();
public void login(String username, String password) {
String redirectRoute = "homepage";
if (!userRepository.isValid(username, password)) {
throwException("errorCode", "Bad credentials");
redirectRoute = "login";
}
redirect(redirectRoute);
}
void throwException(String key, String value) {
*//...*
}
void redirect(String url) {
*//...*
}
}
3. Wrap all primitives and Strings
This one is pretty tricky, because you can’t always recognize when and how to use it. But the rule say, you have to encapsulate all the primitive within objects, in order to avoid Primitive Obsession anti-pattern. The rule can be summarize by one simple sentence tho:
If the variable of your primitive type has a behavior, you MUST encapsulate it!
Take a look at the class below:
public class Runner {
private int meter = 1;
private int kilometer = meter * 1000;
}
We see that the kilometer variable have a behavior. We then need to encapsulate these variable within a class where the class name must therefore have a business/domain meaning.
To make it into practice, we can encapsulate it into a class called Distance
.
class Distance {
private int value;
private String unit;
Distance(int value, String unit) {
this.value = value;
this.unit = unit;
}
public Distance toKilometer() {
return new Distance(this.value * 1000, "kilometer");
}
}
Then change the primitive variables of Runner class to Distance.
public class Runner {
private Distance meter = new Distance(1, "meter");
private Distance kilometer = meter.toKilometer();
}
4. First class collection
A First Class Collection in Java is a design principle where a class is created to encapsulate a collection and any associated behavior, without holding additional fields besides the collection itself. This helps in enhancing the management and operations related to that collection by encapsulating all functionalities in a single, cohesive unit.
I have already done a well structured blog post about this rule, so you can read it with the link below.
5. Use one dot per line
The dot is what we use to call methods in Java, but if you’re a PHP developer, you can consider it as →
(arrow).
Anyway, the method basically says, you should not chain method calls.
This rule doesn’t apply to Fluent interfaces or generally to anything implementing the Method Chaining Pattern (e.g a Query Builder)
This rule is the real implementation of the well-known the Law of Demeter
, which says, you should only talk to your direct friends, don’t talk to strangers.
Now let take an example of 3 classes :
- Employee
- Manager
- Service
public class Employee {
private Department department;
Department getDepartment() {
return department;
}
}
class Department {
private String manager;
public String getManager() {
return manager;
}
}
And a Service class that call the employee methods
class Service {
public String getEmployeeManager() {
Employee employee = new Employee();
return employee.getDepartment().getManager();
}
}
So to validate the rule of one dot per line, (The Demeter Law), we will create another method in the employee class called getEmployeeManager()
and call it in the Service class.
public class EmployeeValid {
// ...
String getEmployeeManager() {
return department.getManager();
}
}
class Service {
public String getEmployeeManager() {
Employee employee = new Employee();
return employee.getEmployeeManager();
}
}
6. Dont Abbreviate
The rule speaks for itself, do not abbreviate and use descriptive names.
if you come across the case where the method name is too long, then it's probably indicative of a method that does several things, so the method have multiple responsibilities.
So refactor your method, and for the sake of God, don’t abbreviate !! (please)
7. Keep all entities small
No class over 50 lines and no package over 10 files. Well, it depends on you, but I think you could change the number of lines from 50 to 150.
The idea behind this rule is that long files are harder to read, harder to understand, and harder to maintain.
8. No class with more than two instances variables
Now consider a scenario, you need to handle information about books and authors.
public class Book {
private final String title;
private final Author author;
private final LocalDate publicationYear;
private final String isbn;
public Book(String title, Author author, LocalDate publicationYear, String isbn) {
this.title = title;
this.author = author;
this.publicationYear = publicationYear;
this.isbn = isbn;
}
}
class Author {
private final String name;
private final String bio;
private final String birthDate;
public Author(String name, String bio, String birthDate) {
this.name = name;
this.bio = bio;
this.birthDate = birthDate;
}
}
To comply with the object calisthenics rule, we can refactor as follows
public class Book {
private final String title;
private final Author author;
public Book(String title, Author author) {
this.title = title;
this.author = author;
}
}
class Author {
private final String name;
private final String birthDate;
public Author(String name, String birthDate) {
this.name = name;
this.birthDate = birthDate;
}
}
The goal of this rule is by limiting the number of instance variables to two, we ensure that each class has a clear, singular responsibility, simplifying the code overall. The design leads to better encapsulation and easier unit testing, making it clearer how different parts of the system relate to one another.
9. No getter and no setter
This is a significant one, it can be rephrased as Tell, Don’t Ask.
It is okay to use accessors to get the state of an object, as long as you don’t use the result to make decisions outside the object. Any decisions based entirely upon the state of one object should be made inside the object itself.
That why Getters and Setters are evil, and they break the open / close principle.
Take a look at this example
public class Wallet {
private BigDecimal balance;
public Wallet(BigDecimal balance) {
this.balance = balance;
}
void setBalance(BigDecimal balance) {
this.balance = balance;
}
public BigDecimal getBalance() {
return balance;
}
}
// USAGE
wallet.setBalance(wallet.getBalance() + NEW_AMOUNT)
In the code above, getBalance
is used to make a decision, you choose how to increase your wallet balance instead of leaving this responsibility to the Wallet instance.
What I will recommend instead is to remove the getter/setter, and provide some method that make sense. You must tell the class to do something, and you should not ask it.
So to apply that to the code above, we will get something like this:
public class Wallet {
private BigDecimal balance;
public Wallet(BigDecimal balance) {
this.balance = balance;
}
void increaseMoney(BigDecimal balance) {
this.balance.add(balance);
}
public BigDecimal totalAmount() {
return balance;
}
}
// USAGE
wallet.increaseMoney(NEW_AMOUNT)
It is the wallet instance responsibility to determine how to increase money.
Note that renaming the method getBalance() to totalAmount() was not really necessary, you could have let the getBalance() but the setter should not be allowed.
These rules have been really beneficial for me in my projects, and I recommend you to try them to see the benefit for you as well.
I hope you enjoyed reading this, and I'm curious to hear if this tutorial helped you. Please let me know your thoughts below in the comments. Don't forget to subscribe to my newsletter to avoid missing my upcoming blog posts.
You can also find me here LinkedIn • Twitter • GitHub or Medium