Object Calisthenics
9 steps to better OO code
Agenda
Learn how to make our code more:
- readable
- reusable
- testable
- maintainable
Calisthenics
Cal • is • then • ics - /ˌkaləsˈTHeniks/
"Calisthenics are exercises consisting of a variety of gross motor movements; often rhythmical and generally without equipment or apparatus."
Wikipedia
Object Calisthenics
Written for Java
DRY SOLID KISS
Why bother?
Code is read more than it's written
Rule #1
Only one level of indentation per method
class Board {
public function __construct(array $data) {
$buf = '';
// 0
for ($i=0; $i<10; $i++) {
// 1
for ($j=0; $j<10; $j++) {
// 2
$buf .= $data[$i][$j]
}
}
return $buf;
}
}
class Board {
public function __construct(array $data) {
$buf = '';
collectRows($buf);
return $buf;
}
private function collectRows($buf) {
for ($i=0; $i<10; $i++) {
collectRow($buf, $i);
}
}
private function collectRow($buf, $row) {
for ($i=0; $i<10; $i++) {
$buf .= $data[$row][$i];
}
}
}
Benefits
- Single responsibility
- Better naming
- Shorter methods
- Reusable methods
Rule #2
Do not use else keyword
if (...) {
...
} elseif (...) {
...
} elseif (...) {
...
} elseif (...) {
...
} elseif (...) {
...
} elseif (...) {
...
} else {
...
}
public function login($username, $password) {
if ($this->userRepository->isValid($username, $password)) {
redirect("homepage");
} else {
addFlash("error", "Bad credentials");
redirect("login");
}
}
public function login($username, $password) {
if ($this->userRepository->isValid($username, $password)) {
return redirect("homepage");
}
addFlash("error", "Bad credentials");
return redirect("login");
}
Extract code
Default value
Polymorphism
Strategy pattern
State pattern
Benefits
- Avoids code duplication
- Lower complexity
- Readability
Rule #3
Wrap primitive types if it has behaviour
Value Object in DDD
public function checkDate(int $year, int $month, int $day)
{
...
}
// 10th of December or 12th of October?
$validator->checkDate(2016, 10, 12);
public function checkDate(Year $year,
Month $month,
Day $day)
{
...
}
$validator->checkDate(new Year(2016),
new Month(10),
new Day(12)
);
Benefits
- Encapsulation
- Type hinting
- Attracts similar behaviour
Rule #4
Only one -> per line
OK: Fluent interface
$validator->addFilter(new EmailFilter())
->addFilter(new NotEmptyFilter());
Not OK: getter chain
$token = $this->getService(Service::AUTH)
->authUser($user, $password)
->getResult()
->getToken();
// 1. What if non object is returned?
// 2. How about exceptions handling?
class Location {
/** @var Piece */
public current;
}
class Piece {
/** @var string */
public representation;
}
class Board {
public function boardRepresentation(array $board) {
$buf = '';
foreach ($board as $field) {
$buf .= substring($field->current->representation, 0, 1);
}
return $buf;
}
}
class Location {
/** @var Piece */
private $current;
public function addTo($buf) {
return $this->current->addTo($buf);
}
}
class Piece {
/** @var string */
private $representation;
public function character() {
return substring(representation, 0, 1);
}
public function addTo($buf) {
return $buf . $this->character();
}
}
class Board {
public function boardRepresentation(array $board) {
$buf = '';
/** @var Location $field */
foreach ($board as $field) {
$field->addTo($buf);
}
return $buf;
}
}
Benefits
- Encapsulation
- Demeter's law
- Open/Closed Principle
Rule #5
Do not abbreviate
Why abbreviate?
Name too long?
Too many responsibilities
Split & extract
Duplicated code?
Refactor!
Benefits
- Clear intentions
- Indicate underlying problems
Rule #6
Keep your classes small
What is small class?
- 15-20 lines per method
- 50 lines per class
- 10 classes per module
200 lines per class
10 methods per class
15 classes per namespace
Benefits
- Single Responsibility
- Smaller namespaces
Rule #7
No more than 2 5 instance variable per class
Class should handle single variable state
In some cases it might be two variables
class CartService {
private $userService;
private $logger;
private $cart;
private $translationService;
private $entityManager;
private $authService;
// ...
}
Benefits
- High cohesion
- Encapsulation
- Fewer dependencies
Rule #8
First class collections
Doctrine's ArrayCollection
Benefits
- Single Responsibility
Rule #9
Do not use setters/getters
Accessors are fine
Don't make decisions outside of class
Let class do it's job
Tell, don't ask
class Game {
/** @var int */
private score;
public function setScore(score) {
$this->score = score;
}
public function getScore() {
return $this->score;
}
}
// Usage
$game->setScore($game->getScore() + ENEMY_DESTROYED_SCORE);
class Game {
/** @var int */
private score;
public function addScore($delta) {
$this->score += $delta;
}
}
// Usage
$game->addScore(ENEMY_DESTROYED_SCORE);
Benefits
- Open/Closed Principle
Catch 'em all!
- Only one level of indentation per method,
- Do not use else keyword,
- Wrap primitive types if it has behavior,
- Only one dot per line,
- Don’t abbreviate,
- Keep your entities small,
- No more than two instance variable per class,
- First Class Collections,
- Do not use accessors
- ???
- PROFIT!
Homework
Create new project up to 1000 lines long
Apply presented rules as strictly as possible
Draw conculsions
Customize these rules
Final thoughts
These are not best practices
These are just guidelines
Use with caution!
Thank you!
Object Calisthenics
By Paweł Lewtak
Object Calisthenics
- 441