В предыдущей статье мы рассмотрели некоторые распространенные анти-паттерны кодирования, используемые программистами, плохо знакомыми с 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)
В этом подходе нет ничего серьезного, кроме, разумеется, того, что он совершенно не нужен. Подсказки есть: у нас есть базовый класс 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)
Теперь мы динамически украшаем наши фигуры нужным нам поведением, поэтому наш треугольник — это форма, которая ведет себя как треугольник.
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-фигуры. Самый основной подход будет следующим:
Хотя этот дизайн работает, он ставит нас перед проблемой обслуживания. А именно, это эффективно удваивает нашу кодовую базу. Мы не только удваиваем количество поддерживаемых форм, но и наша фабрика также удваивается в размере. Более наблюдательные новички попытаются смягчить эту проблему, заметив, что многие трехмерные фигуры — это просто двумерные фигуры, вытянутые вдоль оси Z. Куб — это просто 3D-квадрат, Цилиндр — это только 3D-круг и так далее. Таким образом, они могут добавить дополнительный метод в 2D фигуры, который преобразует их в 3D.
Этот подход, конечно, урежет иерархию классов и сохранит некоторое кодирование, но он представляет новый набор проблем:
- Если у нас есть метод
#transform
в нашем базовом классе, то каждый производный класс будет нести этот метод, даже если он не может его использовать (т.е.Pyramid), поэтому мы получаем избыточность в нашем проекте. - Мы можем устранить избыточность, добавив метод
#transform
только к тем классам, которые в нем нуждаются, но тогда мы получим много дублирования . - Мы, вероятно, нарушим принцип подстановки Лискова (это 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
Модуль 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, чтобы устранить фабрики и сложные или неадекватные иерархии классов. Надеюсь, вам понравилось.
Есть ли какие-либо анти-паттерны, которые вы часто встречаете в коде? Как вы смягчаете эти анти-паттерны?