Статьи

Решение анти-паттернов дизайна в Ruby: исправление фабрики

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

Фабричная ошибка

Разработчики, пришедшие в Ruby из Java, склонны особенно любить фабричные классы и методы. Многие новые Ruby-истцы будут писать свои фабрики так:

class Shape def initialize(*args) # code for dynamically creating attributes from args list end def draw raise "not allowed here" end end class Triangle < Shape def draw "drawing triangle" end end class Square < Shape def draw "drawing square" end end # ...more shapes here class ShapeFactory def self.build(shape, *args) case shape when :triangle Triangle.new(*args) when :square Square.new(*args) when :circle Circle.new(*args) end end end 

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

 puts ShapeFactory.build(:triangle, 3, 2, 45) puts ShapeFactory.build(:square, 5) 

factory_classic

В этом подходе нет ничего серьезного, кроме, разумеется, того, что он совершенно не нужен. Подсказки есть: у нас есть базовый класс Shape , который на самом деле не добавляет никакого значения нашему коду, кроме как для обслуживания иерархии классов. Кроме того, тот факт, что у нас есть отдельный класс ( ShapeFactory ) для чего-то, что любой класс Ruby может легко сделать сам (т.е. динамически создавать экземпляры самого себя) оставляет плохой вкус во рту опытного Ruby-ist. За этим стилем разработки часто следуют сторонники ориентированных на классы языков, таких как C # или Java, где все должно вписываться в иерархию классов. Ruby, с другой стороны, является объектно-ориентированным, поэтому все — даже классы — являются объектами, и иерархии классов не всегда необходимы. Имея это в виду, мы можем думать о фабричной модели следующим образом:

Мы хотим создавать объекты общего типа, но иметь специфичное для объекта поведение.

Или даже:

Мы хотим создавать специализированные объекты в абстрактной манере.

Модули как декораторы объектов

Модуль Ruby находится где-то между интерфейсом Java и абстрактным классом C #, но более гибок, чем любой из них. Давайте перепроектируем наше решение Shapes, используя модули:

 class Shape def initialize(*args) end end module Triangle def draw "drawing triangle" end end module Square def draw "drawing square" end end 

Мы можем расширить объект Shape с помощью специального поведения:

 triangle = Shape.new( 3, 2, 45).extend(Triangle) square = Shape.new(5).extend(Square) 

object_decorator

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

 puts triangle.draw => drawing triangle 

В процессе мы покончили с иерархией классов, классом Factory и создали более чистый и компактный код. Сладкий.

Симптомы отмены типа

Некоторые люди могут чувствовать себя неловко из-за того, что треугольник — это Форма, которая ведет себя как Треугольник, а не как «реальный» Треугольник:

 p triangle => #<Shape:0x00000000956d98> 

Если вы один из этих людей, будьте уверены: Ruby достаточно гибок, чтобы удовлетворить любые потребности. Мы можем легко отследить «type», используя методы хука модуля:

 class Shape attr_accessor :type def initialize(*args) @type = [] end end module Triangle def draw "drawing triangle" end def self.extended(mod) mod.type << :Triangle end end 

Теперь мы можем точно сказать, с каким «типом» мы имеем дело:

 triangle = Shape.new( 3, 2, 45).extend(Triangle) puts triangle.type => Triangle 

Возможно, вы заметили, что атрибут type является массивом. Это потому, что мы потенциально можем расширить Shape более чем одним модулем. Следующее семантически верно, хотя концептуально бессмысленно:

 my_shape = Shape.new( 3, 2, 45).extend(Triangle).extend(Square) puts my_shape.type => Triangle Square 


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

Я ^ 3 ^ (Наследование препятствует реализации)

Давайте еще раз посетим наш новый дизайн Shape. На этот раз нам нужно создавать трехмерные фигуры, а также 2D-фигуры. Самый основной подход будет следующим:

hierarchy_1

Хотя этот дизайн работает, он ставит нас перед проблемой обслуживания. А именно, это эффективно удваивает нашу кодовую базу. Мы не только удваиваем количество поддерживаемых форм, но и наша фабрика также удваивается в размере. Более наблюдательные новички попытаются смягчить эту проблему, заметив, что многие трехмерные фигуры — это просто двумерные фигуры, вытянутые вдоль оси Z. Куб — это просто 3D-квадрат, Цилиндр — это только 3D-круг и так далее. Таким образом, они могут добавить дополнительный метод в 2D фигуры, который преобразует их в 3D.

class hierarchy 1

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

  1. Если у нас есть метод #transform в нашем базовом классе, то каждый производный класс будет нести этот метод, даже если он не может его использовать (т.е.Pyramid), поэтому мы получаем избыточность в нашем проекте.
  2. Мы можем устранить избыточность, добавив метод #transform только к тем классам, которые в нем нуждаются, но тогда мы получим много дублирования .
  3. Мы, вероятно, нарушим принцип подстановки Лискова (это L в Принципах разработки SOLID )). Преобразуя 2D Shape в 3D, мы лишаем законной силы его метод draw , что означает, что мы не можем заменить 2D 3D-объектом, если сначала не переопределим #draw .

Эти проблемы вызваны основной проблемой, заключающейся в том, что, хотя поведение draw относится ко всем типам фигур, его реализация в основном зависит от размеров фигур. Кажется, нет способа преодолеть это, не возвращаясь к нашему предыдущему дизайну многоотраслевого класса. Похоже, мы не можем иметь гибкий и гибкий код при правильном моделировании нашей проблемной области. Или мы можем? Еще раз, Модули приходят на помощь.

Инъекция метода

Мы будем использовать модуль ThreeD для обеспечения правильного поведения наших фигур. Когда мы расширяем объект формы с помощью него, модуль ThreeD правильную реализацию метода #draw в объект, переопределяя существующую реализацию. Мы превращаем предыдущую слабость в наше преимущество:

 class Shape attr_accessor :type def initialize(*args) @type = [] end end module Triangle def draw "drawing triangle" end def self.extended(mod) mod.type << :Triangle end end module Square def draw "drawing square" end def self.extended(mod) mod.type << :Square end end module ThreeD def self.extended(mod) mod.type << :ThreeD case mod.type.first when :Triangle mod.instance_eval do def draw(depth) puts "drawing a Wedge" end end when :Square mod.instance_eval do def draw(depth) puts "drawing a Box" end end end end end 

method_injector

Модуль ThreeD использует атрибут type чтобы определить, какой тип Shape он расширяет, и динамически создает соответствующий метод draw для него. Любые другие методы, уже смешанные с предыдущими модулями, остаются на месте. Проверьте это:

 sq = Shape.new.extend(Square) puts sq.draw => drawing square sq.extend(ThreeD) puts sq.draw(4) => drawing a Box puts sq.type => Square ThreeD 

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

Резюме

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

Есть ли какие-либо анти-паттерны, которые вы часто встречаете в коде? Как вы смягчаете эти анти-паттерны?