Object calisthenics (in PHP)
In classical Greek, calisthenics (καλός-σθένος) etymologically means beautiful of strength. In general, calisthenics were "physical exercises usually done repeatedly to keep your muscles in good condition and improve the way you look or feel"1.
Despite its explicit reference to physical activity, the concept of "object calisthenics" exists in software engineering. What does it mean?
Object calisthenics' aims to improve code. Concretely, to improve the maintainability, readability, testability and comprehensibility of your code. By repeating a set of 9 exercises, you will naturally change how you code and you will improve it.
As the definition says, object calisthenics consists of 9 exercises to apply when you code whenever possible, not a dogma you must follow no matter what.
1. Only one level of indentation per method
This rule establish that there should only be one indentation level for each of the methods we have within a class.
Suppose we have the following class:
final readonly class ProcessVideoAudioElements
{
public function execute(array $videoElements, array $audioElements): void
{
foreach($videoElements as $videoElement) {
foreach ($audioElements as $audioElement) {
$this->api->call($videoElement, $audioElement);
}
}
}
}
Applying this rule, we could refactor the code as follows:
final readonly class ProcessVideoAudioElements
{
public function execute(array $videos, array $audios): void
{
$this->executeApiCalls($videos, $audios);
}
public function executeApiCalls(array $videos, array $audios): void
{
foreach ($videos as $video) {
$this->handleVideo($video, $audios);
}
}
public function handleVideo(Video $video, array $audios): void
{
foreach($audios as $audio) {
$this->api->call($video, $audio);
}
}
}
2. Do not use else
This rule proposes not using the else
keyword in our conditions.
We have two possible ways to apply this rule.
2.1. Early return
Having the following method:
public function login(string $user, string $password): Response
{
if ($this->repo->isValid($user, $password)) {
return new Response('ok', 200);
} else {
return new Response('error', 400);
}
}
we could apply the rule and refactor the code as follows:
public function login(string $user, string $password): Response
{
if ($this->repo->isValid($user, $password)) {
return new Response('ok', 200);
}
return new Response('error', 400);
}
2.2. Return parametrized
With the following code:
public function link(string $status): string
{
if ($status === 'subscribed') {
$link = 'enabled';
} else {
$link = 'not-enabled';
}
return $link;
}
we could refactor it to:
public function link(string $status): string
{
$link = 'not-enabled';
if ($status === 'subscribed') {
$link = 'enabled';
}
return $link;
}
3. Wrap all primitives and string
This rule proposes to use Value Objects to encapsulate all primitives within objects that have their own meaning.
For example, we could have the following class:
final readonly class Order
{
private string $id;
private int $numLines;
private float $totalAmount;
}
Applying the rule, we could have a Value Object for each of the primitives:
final readonly class Order
{
private Id $id;
private Quantity $numLines;
private Money $totalAmount;
}
4. First class collections
This rule proposes to wrap all array
constructions within their own class.
Imagine we have this class:
final readonly class Order
{
/** Line[] */
private array $lines;
private function calculateTotalAmount(): void
{
foreach($this->lines as $line) {
// Line may actually be anything!
}
}
}
$this->lines
could be anything, even with a PHPDoc comment.
Thus, we could have a LineCollection
that contains all those elements:
final class LineCollection
{
/** @var Line[] */
private array $elements = [];
private int $count = 0;
public function add(Line $line): void
{
$this->elements[] = $line;
$this->count++;
}
}
We would then refactor the main class to:
final readonly class Order
{
private LineCollection $lines;
private function calculateTotalAmount(): void
{
foreach($this->lines as $line) {
// Line can only be of type Line!
}
}
}
5. One dot per line
This rule is also known as Demeter's Law: don't talk to strangers, because it breaks code encapsulation.
Suppose we have the following classes:
final readonly class Language
{
private Code $code;
public function code(): Code
{
return $this->code;
}
}
final readonly class Audio
{
private Language $language;
public function language(): Language
{
return $this->language;
}
}
final readonly class Response
{
private Audio $audio;
public function audio(): Audio
{
return $this->audio;
}
}
We could then have the following chain of calls:
echo $response->audio()->language()->code();
However, what would happen if some of the methods changed its return type, in order to accept null
, for example? Code will break, as we are breaking code encapsulation.
Applying this rule, we could refactor Audio
and Response
classes:
final readonly class Audio
{
private Language $language;
public function languageCode(): Code
{
return $this->language->code();
}
}
final readonly class Response
{
private Audio $audio;
public function audioLanguageCode(): Code
{
return $this->audio->languageCode();
}
}
We could then have the following call, that does not break code encapsulation:
echo $response->audioLanguageCode();
6. Don't abbreviate
This rule talks on its own.
Suppose we have the following class:
final readonly class AudioStrResCol
{
// ...
}
What does AudioStrResCol
mean?
AudioStringResourceColumn
?AudioStringResultColumn
?AudioStringResultCollation
?AudioStrongResultColumn
?
Actually, the class name should be:
final readonly class AudioStreamResponseCollection
{
// ...
}
7. Keep all entities small
Files with lots of lines of code are hard to read. Thus, this rule proposes to keep all classes between 50 and 150 lines,
Nonetheless, this rules is not always easy to follow, as it depends on the programming language you use.
8. No classes with more than two instance variables
This rule, as the previous one, is not always easy to follow.
Suppose we have the following class:
final class Customer
{
private Id $id;
private Email $email;
private PasswordHash $passwordHash;
private FirstName $firstName;
private LastName $lastName;
private Name $address;
private City $city;
private PostalCode $postalCode;
}
Applying the rule, we could refactor the code to be:
final class Customer
{
private Id $id;
private UserData $userData;
}
final class UserData
{
private AuthData $authData;
private PersonalData $personalData;
}
final class AuthData
{
private Email $email;
private PasswordHash $passwordHash;
}
final class PersonalData
{
private NameData $nameData;
private AddressData $addressData;
}
final class NameData
{
private FirstName $firstName;
private LastName $lastName;
}
final class AddressData
{
private Name $address;
private CityData $cityData;
}
final class CityData
{
private City $city;
private PostalCode $postalCode;
}
9. No getters, setters, properties
This rule is also known as "tell, don't ask". It consists on not asking (get) before performing an action, but to encapsulate the behavior within the object.
For example, if we have the following code:
if ($cart->getAmount() >= 100) {
$cart->setDiscount(20);
}
We could refactor for it to be:
final class Cart
{
private const DISCOUNT_THRESHOLD = 100;
private const DISCOUNT_AMOUNT = 20;
public function applyDiscount(): void
{
if ($this->totalAmount < self::DISCOUNT_THRESHOLD) {
return; // Throw exception
}
$this->discount = self::DISCOUNT_AMOUNT;
}
}
Then, it could be called with:
$cart->applyDiscount();
Summary
Object calisthenics consists of 9 exercises/rules to apply when writing code to improve the maintainability, readability, testability and comprehensibility of your code.
Not all rules are always applicable, but it is important to keep them in mind as a guide to follow.