20 декабря 2013 в 20:42

Объектная гимнастика перевод

В первых двух абзацах оригинального текста автор описывает как он пил пиво с друзьями. Я заменил их пятничной картинкой о гимнастике из детства.

Объектная гимнастика (англ. Object Calisthenics) — это упражнения в программировании, которые состоят из 9 правил, которые Джефф Бей описал в своей книге «The ThoughWorks Anthology». Пытаясь как можно точней следовать этим правилам, вы измените свои привычки написания кода. Это не значит, что вы должны постоянно соблюдать все эти правила. Найдите баланс и используйте только те, которые вам удобны.

Эти правила сфокусированы на читаемости, тестируемости, понятности и поддерживаемости вашего кода. Если вы уже пишите код, который читаем, тестируем, понятен и поддерживаем, тогда эти правила помогут сделать его более читаемым, тестируемым. Понятным и поддерживаемым.

Ниже я прокомментирую этих 9 правил:
  1. Только один уровень отступа в методе
  2. Не используйте Else
  3. Оберните все примитивные типы и строки
  4. Коллекции первого класса
  5. Одна точка на строку
  6. Не используйте сокращения
  7. Сохраняйте сущности короткими
  8. Никаких классов с более чем 2 атрибутами
  9. Никаких геттеров, сеттеров и свойств


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 и т.д.) отдайте предпочтение патерну Состояние, так как он используется для инкапсулирования различного поведения в зависимости от объекта состояния.


Источник: 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 атрибутами



Я думал, люди будут смеяться, когда я буду рассказывать об этом правиле, но этого не произошло. Это правило, возможно, наиболее сложное, но он способствует высокой связности, и лучшей инкапсуляции.

Картинка стоит тысячи слов, поэтому вот вам объяснение этого правила на картинке. Обратите внимание, что это основано на третьем правиле.

Источник: 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() так как вы захотите его вывести где-то в пользовательском интерфейсе, но помните, что сеттеры запрещены.

Выводы



Если вам не нравятся эти правила — это нормально. Но верьте мне, когда я говорю, что они могут быть использованы в реальной жизни. Попробуйте их в свободное время, например для рефакторинга ваших открытых проектов. Я думаю, это просто вопрос практики. Некоторые правила просты в соблюдении и могут вам помочь.

Ссылки


Перевод: William DURAND
Денис Потапов @PatapSmile
карма
264,0
рейтинг 0,0
Обо всём и ни о чём
Самое читаемое Разработка

Комментарии (23)

  • +18
    Любые догмы — это булшит, головой надо думать. С одной стороны да, нужно ограничивать вложенность условий по возможности, но «не используйте else» — это уже сектанство какое-то. Ну и остальные пункты тоже из той серии.
    • 0
      Автор предлагает это как упражнения, а не как догмы. Просто попробовать на каком-нибудь небольшом проекте эти правила, чтобы улучшить свои навыки ООП.
      • +14
        И превратить небольшой проект в сферическое ООП в вакууме, с классами у которых не больше 2х аттрибутов?) Это упражнения чего? Как писать код в отрыве от реальности?

        Не бывает императивного, хоть сколько то серьезного кода, в котором в ветвлениях нет else, или в котором в классах только по 2 аттрибута (почему 2 вообще?). Это синтетические правила и синтетические упражнения, и я не очень понимаю как они должны помочь писать хороший реальный код.
        • +3
          Бывает. Вот пример enterprise-quality кода, который соответствует всем правилам, изложенным в статье. Проект уже обсуждался на хабре.

          • 0
            При всей абсурдности обсуждения шуточного кода, но даже там эти правила бы пригодились.

            Оригинал:
            ...
            public class NoFizzNoBuzzStrategy implements IsEvenlyDivisibleStrategy {
            
                    public boolean isEvenlyDivisible(int theInteger) {
                            if (!NumberIsMultipleOfAnotherNumberVerifier.numberIsMultipleOfAnotherNumber(theInteger, NoFizzNoBuzzStrategyConstants.NO_FIZZ_INTEGER_CONSTANT_VALUE)) {
                                    if (!NumberIsMultipleOfAnotherNumberVerifier.numberIsMultipleOfAnotherNumber(theInteger, NoFizzNoBuzzStrategyConstants.NO_BUZZ_INTEGER_CONSTANT_VALUE)) {
                                            return true;
                                    } else {
                                            return false;
                                    }
                            } else if (!NumberIsMultipleOfAnotherNumberVerifier.numberIsMultipleOfAnotherNumber(theInteger, NoFizzNoBuzzStrategyConstants.NO_BUZZ_INTEGER_CONSTANT_VALUE)) {
                                    if (!NumberIsMultipleOfAnotherNumberVerifier.numberIsMultipleOfAnotherNumber(theInteger, NoFizzNoBuzzStrategyConstants.NO_FIZZ_INTEGER_CONSTANT_VALUE)) {
                                            return true;
                                    } else {
                                            return false;
                                    }
                            } else {
                                    return false;
                            }
                    }
                    
            }
            


            Исправляем на один уровень вложенности в методе:
            ...
            public class NoFizzNoBuzzStrategy implements IsEvenlyDivisibleStrategy {
            
                    public boolean isEvenlyDivisible(int theInteger) {
                            if (NumberIsMultipleOfAnotherNumberVerifier.numberIsMultipleOfAnotherNumber(theInteger, NoFizzNoBuzzStrategyConstants.NO_FIZZ_INTEGER_CONSTANT_VALUE)) {
                                            return false;
                            }
            
                            if  (NumberIsMultipleOfAnotherNumberVerifier.numberIsMultipleOfAnotherNumber(theInteger, NoFizzNoBuzzStrategyConstants.NO_BUZZ_INTEGER_CONSTANT_VALUE)) {
                                            return false;
                            }
            
                            return true;
            }
            
          • +1
            И даже там я нашёл else
      • +1
        Ох уж эти теоретики от программирования (я не про вас, а про автора оригинальной статьи).

        Ага, например, как в первом примере взять и размазать вложенные циклы по однострочным функциям. И где тут ООП? Повышение читаемости? Спорно. Чтобы понять, что происходит, придётся попрыгать по функциям, восстановить контекст, помнить цепочку вызовов и держать размерность блока циклов в голове, вместо того, чтобы просто взять и увидеть всё это в трёх несчастных строчках кода с двумя отступами.

        Понимаю, что пример высосан из пальца, но именно его приводит автор. У Фаулера в книжке более адекватные примеры, хотя и не без перегибов, на мой взгляд.

        Что это вообще за бредовое правило «не больше одного отступа на метод», ну кто его придумал? Кто сказал и доказал, что два отступа уже не читаются и не поддерживаются? :)
        • +1
          Пора вводить понятие программистский радикализм)
  • +4
    1. Конкретно этот пример гораздо более читаем в одном методе. Если он разрастется, тогда уже разделяем. Думаю, многие со мной согласятся.
    8. У меня есть еще отчество. Тоже создавать отдельный класс? Мне кажется это самое бредовое правило. Нужно знать меру все же.
  • 0
    Пункт 3 вообще-то смущает, и при том сильно. Не получится ли в итоге, что в каждом модуле будет по своему типу строк и придется их друг к другу приводить? И вообще, это крайне плохо на производительности сказывается.
  • 0
    Пример с разнесением двух вложенных циклов по разным методам — откровенно дурацкий, IMHO. И там не улучшение читаемости, а явное ухудшение.
  • +3
    Многие пункты не актуальны из-за лишнего overhead'a, если оптимизировать код под производительность. Ну например для написания игры.
    • +2
      На C++ можно делать inline. Более того, сам компилятор решает, когда стоит сделать inline. Java умеет так делать? (просто интересно)
      • +1
        Умеет.
      • +1
        Дело не только в inline-ах, я про 2 поля в классе. Создание постоянно мелких объектов не очень хорошо для GC и кажется это самый бредовый совет.
  • 0
    Добавьте в список неизменяемость данных и можно на какой-нибудь хаскель переходить.
  • –1
    Боже, это бред какой-то. А early return я вообще считаю злом.
    • +3
      А можно поинтересоваться, почему? Это единственный совет из вышеприведенных, которому я следую в большинстве случаев и он уменьшает количество отступов тела функции и повышает читабельность. Конечно, это если его использовать только в начале функции; искать return'ы в середине никто не любит.
      • 0
        Читабельнее. Код пишется для людей, а не метрик. С else получается более «человеческое» выражение, сразу видно, что хотел сделать автор. ИМХО

        А теперь от моего любимого C# со сборкой мусора перейдем к С:
        void foo()
        {
          int *ptr = malloc(1234);
          ... // Тысячи строк кода
          ...
          if (bar(ptr))
            return; // Утечка
          foobar(ptr);
          ... // Тысячи строк кода
          ...
          free(ptr);
        }
        Пример сферический, но в целом мысль понятна. Чужой код, все очень сложно, а тебе надо чуток подправить код.
        • +3
          По моему пример неудачный. Здесь early return не пахнет (после первых-то тысяч строк кода).
          Ну и в подобных монстрах надо все перепроверять на двадцать рядов, чтобы хоть как-то быть уверенным, что не ошибся.
        • 0
          Даже если поставить else-блок, нельзя гарантировать, что в него случайно не попадет вызов free. А что если вызов free вообще в середине вторых тысяч строк кода, а не в конце? :) Все решает внимательность к деталям.
        • +1
          Пожалуй, можно поспорить, что лучше в плане читабельности обработки ошибок: колбаса из else или goto cleanup. Особенно, если ошибка в каком-то цикле.
  • 0
    Интересно, как быть с правилом «Никаких классов с более чем 2 атрибутами» для сложных сущностей? Например, что-то типа «счет», у которого десяток атрибутов, каждый из которых логически не пересекается ни с каким другим.
    Мне кажется, следуя этим правилам, можно написать отличные макароны из кода, чтобы понять которые придется сотни раз прыгать туда-сюда.

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.