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

Недавно наткнулась на ютуб канал и блог Егора. Он создает контент, наполненный информацией, ориентированной на O.O.P. концепции и лучшие практики.

Одной из интересных тем в его блоге является обсуждение геттеров и сеттеров. Статья о «анемичных моделях предметной области» некоторое время находится в моих планах; но сейчас, прочитав мнение Егора о тупых классах данных, решил написать свои мысли по этому поводу.

Для примеров кода в этой статье мы будем использовать модель данных, представляющую шахматную фигуру слона:

class Bishop {
   private char column;
   private int row;
   private Color pieceColor;

   // getters and setters
}

«Геттеры/сеттеры. Зло. Период." — Егор Бугаенко

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

Переименование геттеров и сеттеров

Например, давайте посмотрим на поле «pieceColor». Наличие getPieceColor() подразумевает наличие целочисленного поля pieceColor и предполагает, что для него может быть установщик, что приводит к утечке деталей реализации.

Для изменяемых данных Егор предлагает просто удалить префикс «get» из геттера, что приведет к чему-то похожему на класс записей java17: pieceColor().

Хотя, поскольку это свойство класса chess Bishop, должно быть совершенно очевидно, что мы говорим о шахматной фигуре. Поэтому «color()» может быть достаточно.

Точно так же мы можем изменить установщик на «changeColor()» или даже удалить его — изменится ли когда-нибудь цвет элемента? Скорее всего, нет: если этот сеттер используется, у нас, вероятно, где-то больше запах дизайна.

Лучшее понимание модели предметной области

Сделать цвет неизменным? Переименовать getColorValue() в color() ? Это довольно большие изменения! Что ж команда не согласится?!

Конечно, обсудите это со своими сверстниками! Эти обсуждения всегда полезны, потому что они ведут к более глубокому пониманию модели предметной области: это одна из основных идей этой статьи!

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

Нарушение инкапсуляции

Предоставление всех внутренних полей через геттеры и сеттеры (или «аксессоры» и «мутаторы») создаст большую связь и нарушит инкапсуляцию. Предположим, мы хотим переместить слоновую шахматную доску на три клетки в правый верхний угол стола:

public void move() {
    Bishop bishop = // ... 
  
    char newCol = bishop.getColumn() + 3;
    int newRow = bishop.getRow() + 3;
  
    if(newCol > 'h' || newrow > 8) {
       throw new IllegalArgumentException("invalid move!");
    }
  
    bishop.setColumn(newCol);
    bishop.setRow(newRow);
}

Как мы можем заметить, существует тесная связь между этой функцией и внутренними компонентами класса Bishop. Функция знает, как класс Bishop хранит свою позицию, знает пределы шахматной доски и, что еще хуже, знает, как слону разрешено двигаться по доске.

Теперь давайте попробуем исправить эти проблемы и создать лучший API для класса епископа. Во-первых, мы инкапсулируем всю эту логику внутри класса епископа:

public class Bishop {
   private char column;
   private int row;
   private Color pieceColor;
   
   public enum MoveDirection {
       TOP_RIGHT, TOP_LEFT, BOTTOM_RIGHT, BOTTOM_LEFT;
   }
  
   public move(MoveDirection direction, int nrOfSquares) {
       // determine the new position
       // validate the new position
       // set the new position
   }

   // getters
}

В результате теперь мы можем перемещать фигуру, просто указав направление движения и желаемое количество квадратов для перемещения. Следовательно, сеттеры для полей строки и столбца можно полностью удалить.

Слабая связь

Теперь дизайн лучше, но его можно улучшить: давайте разберемся с геттерами! Мы можем добавить метод position(), который возвращает текущую позицию фигуры на шахматной доске. Тип возвращаемого значения — строка, соответствующая стандартной записи шахмат (например, положение фигуры в нижнем левом углу будет «a1»).

public String position() {
    return row + "" + column;
}

Теперь мы можем удалить геттер, и вызывающая сторона не знает (и ему все равно), хранятся ли эти координаты в виде пары чисел, представляющих индексы, строки, символы и т. д.

Эта слабая связь позволяет нам легко изменять/рефакторить реализацию класса Bishop. Например, мы можем поместить эти две координаты в небольшой неизменяемый класс «Square», и это не повлияет ни на модульный тест, ни на клиента этого класса.

Конструкторы

Если следить за видео, Егор пару раз упоминает о необходимости перегрузки конструктора. Это, конечно, заменяет необходимость создания пустого объекта и последующей установки различных полей.

Лично я предпочитаю использовать статический фабричный метод: таким образом мы полностью инкапсулируем понятия row и column от вызывающего объекта класса Bishop .

Например, в шахматной партии слоны всегда начинаются с файлов (или столбцов) "c" и "f". Одним из возможных решений будет создание двух статических фабричных методов для создания экземпляра фрагмента в одном из этих файлов только путем указания цвета:

public class Bishop {
   private char column;
   private int row;
   private Color pieceColor;

   public static Bishop cFileBishop(Color color) {
       return new Bishop('c', color == Color.WHITE? 1 : 8, color);
   }

   public static Bishop fFileBishop(Color color) {
      return new Bishop('f', color == Color.WHITE? 1 : 8, color);
   }

   // same as Lombok's @AllArgsConstructor
   private Bishop(char column, int row, Color pieceColor) {
       // ... 
   }
   
   public enum MoveDirection {
       TOP_RIGHT, TOP_LEFT, BOTTOM_RIGHT, BOTTOM_LEFT;
   }
  
   public move(MoveDirection direction, int nrOfSquares) {
       // determine the new position
       // validate the new position
       // set the new position
   }

   public Color color() {
       return color;
   }

   public String position() {
      return row + "" + column;
   }
}

Давайте сравним наш результат с нашей начальной, «глупой» версией:

@Data // => all getters + setters
public class Bishop {
   private char column;
   private int row;
   private Color pieceColor;
}

Как мы видим, строк кода в классе епископа стало больше — но это неплохо, если код связный. Более того, уменьшится глобальная сложность нашего приложения.

Другими словами, у нас больше нет кода: мы просто инкапсулировали логику создания и перемещения фрагмента внутри его собственного класса и предоставили интерфейс, допускающий слабое связывание.

DTO и записи Java 17

У Егора более радикальный подход, и он предлагает удалить все наши геттеры и сеттеры: это будет включать наши граничные объекты, такие как DTO. Это оставляет нам два возможных решения: одно из них — использовать очень простые структуры данных, без поведения и открытых полей:

public class BishoDto {
   public String color;
   public String position;
} 

Для Java альтернативой может быть использование новой функции Java17 — классов записей:

public record BishoDto (
   String color,
   String position,
){}

Однако следует помнить, что записи являются «прозрачными носителями неизменяемых данных». Поскольку они «прозрачны», они не позволяют инкапсулировать данные и нарушают многие идеи, описанные в этой статье. Таким образом, они могут быть хорошими вариантами, когда речь идет о DTO или небольших «объектах-значениях» без особой логики, но они не предназначены для использования в наших основных доменных объектах.

Заключение

В этой статье мы рассмотрели многое: мы начали с простого удаления префиксов «get» и «set» наших методов. Мы продолжили, спросив себя, действительно ли нужны сеттеры и должны ли некоторые поля быть неизменяемыми.

После этого мы начали исследовать API нашей модели предметной области (класс Bishop) и сумели инкапсулировать логику перемещения элемента и добавления некоторых статических фабричных методов. Следовательно, все геттеры и сеттеры исчезли, и мы превратили «тупую» модель данных в богатую модель с лучшим API.

Наконец, я настоятельно рекомендую вам посетить вебинар Егора, чтобы увидеть несколько иной взгляд на эту тему. Видео наполнено полезными советами и вдохновило меня на написание этой статьи:

Спасибо!

Спасибо за прочтение статьи и, пожалуйста, дайте мне знать, что вы думаете! Любая обратная связь приветствуется.

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

Наконец, если вы хотите стать участником Medium и поддержать мой блог, вот мой реферал.

Удачного кодирования!

Повышение уровня кодирования

Спасибо, что являетесь частью нашего сообщества! Перед тем, как ты уйдешь:

  • 👏 Хлопайте за историю и подписывайтесь на автора 👉
  • 📰 Смотрите больше контента в публикации Level Up Coding
  • 🔔 Подписывайтесь на нас: Twitter | ЛинкедИн | "Новостная рассылка"

🚀👉 Присоединяйтесь к коллективу талантов Level Up и найдите прекрасную работу