DEV Community

Cover image for Object calisthenics (in PHP)
Rubén Rubio
Rubén Rubio

Posted on

Object calisthenics (in PHP)

Table of Contents

 1. Only one level of indentation per method
 2. Do not use else
      2.1. Early return
      2.2. Return parametrized
 3. Wrap all primitives and string
 4. First class collections
 5. One dot per line
 6. Don't abbreviate
 7. Keep all entities small
 8. No classes with more than two instance variables
 9. No getters, setters, properties
 Summary

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);
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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);
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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);
    }
}
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

we could refactor it to:

public function link(string $status): string
{
    $link = 'not-enabled';

    if ($status === 'subscribed') {
        $link = 'enabled';
    }

    return $link;
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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!
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

$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++;
    }
}
Enter fullscreen mode Exit fullscreen mode

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!
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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;
    }
}
Enter fullscreen mode Exit fullscreen mode

We could then have the following chain of calls:

echo $response->audio()->language()->code();
Enter fullscreen mode Exit fullscreen mode

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();
    }
}
Enter fullscreen mode Exit fullscreen mode

We could then have the following call, that does not break code encapsulation:

echo $response->audioLanguageCode();
Enter fullscreen mode Exit fullscreen mode

6. Don't abbreviate

This rule talks on its own.

Suppose we have the following class:

final readonly class AudioStrResCol
{
    // ...
}
Enter fullscreen mode Exit fullscreen mode

What does AudioStrResCol mean?

  • AudioStringResourceColumn?
  • AudioStringResultColumn?
  • AudioStringResultCollation?
  • AudioStrongResultColumn?

Actually, the class name should be:

final readonly class AudioStreamResponseCollection
{
    // ...
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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;
    }
}
Enter fullscreen mode Exit fullscreen mode

Then, it could be called with:

$cart->applyDiscount();
Enter fullscreen mode Exit fullscreen mode

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.


  1. Cambridge dictionary 

Top comments (1)

Collapse
 
nestorgonzalezqfplus profile image
Néstor González

Very good blog and efficient way to program, thanks!