🎉 Ура! После трёх лет работы, я наконец выпустил английскую версию книги о паттернах! Вот она »

Замена кодирования типа подклассами

Также известен как: Replace Type Code with Subclasses

Что такое кодирование типа? Это когда вместо отдельного типа данных вы имеете набор чисел или строк, который составляет список допустимых значений для какой-то сущности. Зачастую этим конкретным числам и строкам даются понятные имена с помощью констант, что и является причиной их широкого распространения.

Проблема

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

Решение

Для каждого значения закодированного типа, создайте подклассы. А затем, вынесите соответствующие поведения из исходного класса в эти подклассы. Управляющий код замените полиморфизмом.

До
Replace Type Code with Subclasses - Before
После
Replace Type Code with Subclasses - After

Причины рефакторинга

Данный рефакторинг является более сложным случаем замены кодирования типа классом.

Как и в первом рефакторинге, у вас есть какой-то набор простых значений, которые составляют все доступные значения для какого-то поля. Хотя эти значения зачастую заданы как константы и имеют понятные имена, их использование чревато появлением ошибок проверки типов, так как, в сущности, они всё-равно являются значениями примитивных типов. Например, у вас есть метод, принимающий в параметрах одно из таких значений. В определённый момент, вместо константы USER_TYPE_ADMIN со значением "ADMIN", в метод придёт та же строка, но в нижнем регистре "admin", что приведёт к выполнению чего-то другого, нежели планируемое автором поведение.

В данном рефакторинге мы имеем дело с управляющим кодом, таким как условные операторы if, switch или ?:. Другими словами, внутри условий этих операторов используются поля с закодированным значением (напр. $user->type === self::USER_TYPE_ADMIN). Если бы мы применяли здесь замену кодирования типа классом, то все эти управляющие конструкции следовало бы тоже перенести в класс, отвечающих за тип данных. Это, в конечном итоге, сделало бы класс типа очень похожим на исходный класс, и содержащим те же проблемы.

Достоинства

  • Удаление управляющего кода. Вместо большого switch в исходном классе, вы переносите код в соответствующие подклассы. Это улучшает разделение ответственности между классами и упрощает читабельность программы в целом.

  • Если вам потребуется добавить новое значение закодированного типа, все что нужно будет сделать — это добавить новый подкласс, не трогая существующий код (принцип открытости/закрытости).

  • Заменив кодирование типа классами, мы обеспечим возможность контроля и проверки типов значений (type hinting), передаваемых в методы и поля на уровне языка программирования. Чего не сделаешь при помощи простых численных или строковых значений, содержащихся в закодированном типе.

Когда нельзя применить

  • Этот рефакторинг невозможно применить, если у вас уже есть какая-то иерархия классов. Вы не можете создать двойную иерархию при помощи наследования в ООП. Тем не менее, замену кодирования типа можно осуществить, используя композицию вместо наследования. Для этого используйте замену кодирования типа состоянием/стратегией.

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

Порядок рефакторинга

  1. Используйте самоинкапсуляцию поля для создания геттера для поля, которое содержит кодирование типа.

  2. Сделайте конструктор суперкласса приватным. Создайте статический фабричный метод с теми же параметрами, что и конструктор суперкласса. Он обязательно должен содержать параметр, который будет принимать стартовые значения закодированного типа. В зависимости от этого параметра, фабричный метод будет создавать объекты различных подклассов. Для этого в его коде придётся создать большой условный оператор, но, по крайней мере, он будет единственным, который действительно необходим — обо всем остальном, в дальнейшем, смогут позаботиться подклассы и полиморфизм.

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

  4. Удалите поле с закодированным типом из суперкласса, его геттер сделайте абстрактным.

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

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