![](https://habrastorage.org/getpro/habr/post_images/4fc/b4e/dbc/4fcb4edbc68753c51ff17680123187ec.jpg)
Объектная гимнастика (англ. Object Calisthenics) — это упражнения в программировании, которые состоят из 9 правил, которые Джефф Бей описал в своей книге «The ThoughWorks Anthology». Пытаясь как можно точней следовать этим правилам, вы измените свои привычки написания кода. Это не значит, что вы должны постоянно соблюдать все эти правила. Найдите баланс и используйте только те, которые вам удобны.
Эти правила сфокусированы на читаемости, тестируемости, понятности и поддерживаемости вашего кода. Если вы уже пишите код, который читаем, тестируем, понятен и поддерживаем, тогда эти правила помогут сделать его более читаемым, тестируемым. Понятным и поддерживаемым.
Ниже я прокомментирую этих 9 правил:
- Только один уровень отступа в методе
- Не используйте Else
- Оберните все примитивные типы и строки
- Коллекции первого класса
- Одна точка на строку
- Не используйте сокращения
- Сохраняйте сущности короткими
- Никаких классов с более чем 2 атрибутами
- Никаких геттеров, сеттеров и свойств
1. Только один уровень отступа в методе
Много уровней отступа в вашем коде ухудшают читаемость и поддерживоемость. Большую часть времени вы не можете понять код, не компилируя его в своей голове, особенно, если у вас есть условия на разных уровнях или цикл внутри цикла, как показано в этом примере:
class Board {
public String board() {
StringBuilder buf = new StringBuilder();
// 0
for (int i = 0; i < 10; i++) {
// 1
for (int j = 0; j < 10; j++) {
// 2
buf.append(data[i][j]);
}
buf.append("\n");
}
return buf.toString();
}
}
Чтобы следовать этому правилу вы должны разделить ваши методы. Мартин Фаулер в книге «Рефакторинг», приводит паттерн выделение метода (Extract Method), который как раз то, что вам надо сделать.
У вас не будет меньше строк, но вы существенно улучшите их читаемость:
class Board {
public String board() {
StringBuilder buf = new StringBuilder();
collectRows(buf);
return buf.toString();
}
private void collectRows(StringBuilder buf) {
for (int i = 0; i < 10; i++) {
collectRow(buf, i);
}
}
private void collectRow(StringBuilder buf, int row) {
for (int i = 0; i < 10; i++) {
buf.append(data[row][i]);
}
buf.append("\n");
}
}
2. Не используйте Else
Ключевое слово
Else
известно многим, так как конструкция if/else
есть почти во всех языках программирования. Вы помните, когда вы в последний раз встречали вложенные условия? Вам понравилось их читать? Я так не думаю, и считаю это как раз то, чего надо избегать. Это так просто — добавить еще одну ветку вместо рефакторинга — что зачастую в конце у вас оказывается действительно плохой код.public void login(String username, String password) {
if (userRepository.isValid(username, password)) {
redirect('homepage');
} else {
addFlash('error', 'Bad credentials');
redirect('login');
}
}
Просто способ убрать
else
— это положиться на ранний возврат (early return).public void login(String username, String password) {
if (userRepository.isValid(username, password)) {
return redirect('homepage');
}
addFlash('error', 'Bad credentials');
return redirect('login');
}
Условие может быть оптимистическим — когда у вас есть условия ошибок и остаток метода придерживается сценария по умолчанию, или вы можете принять защитный подход (немного относящийся к Защитному Программированию) — когда вы помещаете сценарий по умолчанию в условие, и если оно не выполняется, тогда вы возвращаете статус ошибки. Этот вариант лучше, так как он защищает от потенциальных проблем, о которых вы не подумали.
Альтернативой может быть введение переменой, чтобы сделать ваш возврат параметрическим. Хотя последнее не всегда возможно.
public void login(String username, String password) {
String redirectRoute = 'homepage';
if (!userRepository.isValid(username, password)) {
addFlash('error', 'Bad credentials');
redirectRoute = 'login';
}
redirect(redirectRoute);
}
Также, следует напомнить, что ООП дает нам мощные возможности, таки как полиморфизм. Паттерны Объект Null, Состояние и Стратегия также могут вам помочь.
Например, вместо использования
if/else
для определения действия в зависимости от статуса (например RUNNING, WAITING и т.д.) отдайте предпочтение патерну Состояние, так как он используется для инкапсулирования различного поведения в зависимости от объекта состояния.![](http://habrastorage.org/storage3/ac2/c74/7f9/ac2c747f93a4b6fb0d16fb6776bf89f4.gif)
Источник: sourcemaking.com/design_patterns/state
3. Оберните все примитивные типы и строки
Следовать этому правилу довольно легко, вы просто должны инкапсулировать все примитивы в объекты, чтобы избежать анти-паттерна одержимости примитивами (Primitive Obsession).
Если у переменной вашего примитивного типа есть поведение, вы должны его инкапсулировать. И особенно это справедливо для Проблемно-ориентированного проектирование (Domain Driven Design). DDD описывает Объекты Значений (Value Objects), например: Деньги, Часы.
4. Коллекции первого класса
Любой класс, который содержит коллекцию, не должен содержать другие атрибуты. Если у вас есть набор элементов, и вы хотите манипулировать ими, создайте класс, который специально предназначен для этого набора.
Каждая коллекция оборачивается в свой собственный класс, так что поведения, относящиеся к коллекции, теперь имеют свое место (например, методы отбора, применения правила к каждому элементу).
5. Одна точка на строку
Точка — это та, которую вы используете для вызова методов в Java или C#. В PHP — это будет стрелка.
В основном это правило гласит, что вы не должны вызывать методы по цепочке. Однако, это не касается Текучего интерфейса (Fluent Interfaces), и в общем всего, что реализует паттерн цепочки методов (например, построитель запросов).
Для других классов вы должны придерживаться этого правила. Это прямое следствие закона Деметры, который предписывает обращаться только к непосредственным друзьям и не обращаться к незнакомцам:
Посмотрите на эти классы:
class Location {
public Piece current;
}
class Piece {
public String representation;
}
class Board {
public String boardRepresentation() {
StringBuilder buf = new StringBuilder();
for (Location loc : squares()) {
buf.append(loc.current.representation.substring(0, 1));
}
return buf.toString();
}
}
Нормально иметь публичные атрибуты в классах
Piece
(участок) и Location
(расположение). На самом деле публичный атрибут и приватный с сеттерами/геттерами — это одно и тоже (смотри 9 правило). Однако, метод
boardRepresentation()
– ужасен. Взгляните на его первую строку:buf.append(loc.current.representation.substring(0, 1));
Метод берет
Location
, потом его текущий Piece
, потом представление Piece
, с которым он и производит действие. Это слишком далеко от одной точки на строку.К счастью, закон Деметры говорит вам обращаться только к друзьям. Давайте сделаем это:
class Location {
private Piece current;
public void addTo(StringBuilder buf) {
current.addTo(buf);
}
}
Сделав экземпляр
Piece
приватным, вы удостоверлись, что не будете пытаться сделать что-нибудь плохое. Однако так как вам необходимо провести действие с этим атрибутом, вам необходимо добавить новый метод addTo()
. Это не ответственность класса Location
как добавлять класс Piece, поэтому надо спросить у него:class Piece {
private String representation;
public String character() {
return representation.substring(0, 1);
}
public void addTo(StringBuilder buf) {
buf.append(character());
}
}
Теперь вы должны изменить видимость атрибута. Напоминаю, что принцип открытости/закрытости говорит, что программные сущности (классы, модули, функции) должны быть открыты к расширению, но закрыты для модификации.
Также выделение кода получения первого символа
representation
в новый метод выглядит как хорошая идея, так как он может быть использован на некотором этапе. Наконец, обновленный код класса Board:class Board {
public String boardRepresentation() {
StringBuilder buf = new StringBuilder();
for (Location location : squares()) {
location.addTo(buf);
}
return buf.toString();
}
}
Намного лучше, правда?
6. Не используйте сокращения
Правильный вопрос, зачем вам надо использовать сокращения?
Вы можете ответить, что это потому, что вы пишите одно и то же имя снова и снова. И я отвечу, если этот метод используется много раз, то это похоже на дублирование кода.
Вы можете сказать, что название метода слишком длинное в любом случае. Тогда я скажу вам, что возможно у этого класса слишком много обязанностей, и это плохо, потому что нарушает принцип единственной обязанности.
Я часто говорю, что если вы не можете найти подходящее имя для класса или метода, значит что-то не так. Это правило я использую, когда следую разработке через название.
Не сокращайте, точка.
7. Сохраняйте сущности короткими
Класс не больше 50 строк и пакет не больше 10 файлов. Хорошо, это зависит от вас, но я думаю, вы можете увеличить это число с 50 до 150.
Идея этого правила, что длинные файлы сложнее читать, сложнее понимать и сложнее поддерживать.
8. Никаких классов с более чем 2 атрибутами
Я думал, люди будут смеяться, когда я буду рассказывать об этом правиле, но этого не произошло. Это правило, возможно, наиболее сложное, но он способствует высокой связности, и лучшей инкапсуляции.
Картинка стоит тысячи слов, поэтому вот вам объяснение этого правила на картинке. Обратите внимание, что это основано на третьем правиле.
![](http://habrastorage.org/storage3/0f1/b34/e92/0f1b34e92ea08b2c113bc8c2b9d13dac.png)
Источник: github.com/TheLadders/object-calisthenics#rule-8-no-classes-with-more-than-two-instance-variables
Главный вопрос — почему два атрибута? Мой ответ — почему бы и нет? Не лучшее объяснение, но по моему, главная идея это разделить два типа классов, те которые обслуживают состояние одного атрибута, и те которые координируют две отдельные переменные. Два — это произвольный выбор, который заставляет сильно разделять ваши классы.
9. Никаких геттеров, сеттеров и свойств
Мое любимое правило. Оно может быть перефразировано, как указывай, а не спрашивай.
Нормально использовать свойства, чтобы получать состояние объекта, до того пока вы не используете результат чтобы принимать решение за пределами объекта. Любые решения основанные полностью на состоянии одного объекта должны быть сделаны внутри самого объекта.
Вот почему герттеры/сеттепры считаються злом. И опять-таки, они нарушают принцип открытости/закрытости.
Например:
// Game
private int score;
public void setScore(int score) {
this.score = score;
}
public int getScore() {
return score;
}
// Usage
game.setScore(game.getScore() + ENEMY_DESTROYED_SCORE);
В коде выше
getScore()
используется для принятия решения. Вы решаете, как увеличить ваш счет (score
), вместо того, чтобы оставить эту ответственность самому классу Game
.Лучшее решение — это убрать геттеры и сеттеры и предоставить методы, которые имеют смысл. Помните, вы указываете классу что делать, а не спрашиваете его. Ниже, вы указываете классу обновить ваш счет, так как вы уничтожили
ENEMY_DESTROYED_SCORE
врагов.// Game
public void addScore(int delta) {
score += delta;
}
// Usage
game.addScore(ENEMY_DESTROYED_SCORE);
Это ответственно класса game как обновить счет.
В этом случае, вы можете оставить
getScore()
так как вы захотите его вывести где-то в пользовательском интерфейсе, но помните, что сеттеры запрещены. Выводы
Если вам не нравятся эти правила — это нормально. Но верьте мне, когда я говорю, что они могут быть использованы в реальной жизни. Попробуйте их в свободное время, например для рефакторинга ваших открытых проектов. Я думаю, это просто вопрос практики. Некоторые правила просты в соблюдении и могут вам помочь.