Single responsibility, Open-closed, Liskov substitution, Interface segregation и Dependency inversion
Don’t repeat yourself
Приложение необходимо проектировать (писать) так, что бы в него легко было вносить изменения. Далее по тексту дано описание общих принципов проектирования кода, позволяющих добиться этой цели.
Каждый класс и каждый метод должны иметь только одну ответственность (обязанность). Как проверить - описание того, что делает класс (метод) должно поместиться в одном предложении без слов “и” “или”.
Внутри классов не стоит использовать имена переменных экземпляра, а использовать методы, возвращающие значения переменных.
Вместо примитивных типов данных лучше использовать обертки - объекты представляющие данные. Например, вместо массива, где каждому индексу соответствует свойство использовать структуру, свойства которой ссылаются на индексы.
class Класс
def метод
good_struct = GoodStruct.new(arr)
good_struct.метод
end
GoodStruct = Struct.new(arr) do
def поле1
arr[0]
end
def поле2
arr[1]
end
end
end
Вместо использования имени одного класса внутри других классов следует использовать абстракцию - переменную (или метод) в которой содержится внешний класс. Сам внешний класс передается при инициировании класса как параметр.
class Класс
attr_reader :external_klass
def initialize(external_klass)
@external_klass = external_klass
end
def метод
external_klass.method
end
end
Если инъекция класса не реализуема, то надо изолировать создание экземпляра внешнего класса. По сути надо стараться сконцентрировать весь код использующий зависимость от внешнего класса в одном месте кода, так его легче найти и, при необходимости, откорректировать.
class Класс
attr_reader :external_klass
def initialize(external_klass)
@external_klass = external_klass
end
def external_klass
@external_klass ||= ВнешнийКласс.new
end
end
Принцип тут тот же, что и при изоляции создания экземпляра внешнего класса - вызовы методов внешних классов локализовать в отдельном месте кода (методах - обёртках).
class Класс
def метод
оболочка.external_method
end
def оболочка
external_klass.external_method
end
end
Для этого можно использовать хэши (по сути используются именованые параметры).
class Класс
def initialize(хэш)
@параметры = defaults.merge(хэш)
end
def defaults
{параметр: 'значение по умолчанию'}
end
end
Если аргументы в используемом внешнем методе зависят от порядка их следования, то можно создать метод-обёртку, в котором уже использовать для параметров хэш.
При проектировании следует добиваться того, что бы классы зависели от классов (абстрактных), которые редко меняются.
Определение сигналов, передаваемых между объектами. Сигналы, посылаемые объекту, должны говорить ему что сделать, а не как.
У объектов должны быть простые контексты.
Закон Деметры (LoD) гласит - нельзя посылать сообщение одному объекту через другой объект, тип которого отличается от первого объекта (разговор возможен только с ближайшими соседями).
Другими словами - нельзя передавать сообщения по цепочке (аналогия: методы - это вагоны состава, если в его любом месте произошел сбой, раваливается весь состав).
some_object1.some_object2.method
Не будет нарушением закона Деметра, если результат отправки сообщения в цепочке одно типа, например Enumerable
some_hash.keys.each { |k| puts k}
Вместо выбора посылаемого метода на основе типа объекта
case some_object
when String
some_object.some_method1
when Integer
some_object.some_method2
end
необходимо реализовать в объекте неявный тип, то есть заменить селектор класса на открытый интерфейс (метод) неявного типа.
Абстрактный класс должен содержать в себе не изменяемую в потомках часть алгоритма. Соответственно в реализации шаблонных методов в потомках должна содержаться изменяющаяся часть алгоритма.
Этот алгоритм определяется с помощью шаблонных методов.
В ходе проектирования лучше выделять из конкретных классов абстрактный класс, а не наоборот, так как забытая общая часть в конкретном классе не так страшна, как забытая конкретная часть в абстрактном.
В родительский (абстрактный) класс вносится метод, конкретная реализация которого может быть переопределена в классах потомках.
Таким образом в абстрактном классе алгоритм задается с помощью абстрактных методов, возможно без детализации.
В абстрактном классе должны быть все шаблонные методы потомков.
Шаблонный метод в абстрактном классе должен возвращать ошибку NotImplementedError, что бы облегчить жизнь разработчику подкласса.
class SomeAbstractClass
def some_action # общий алгоритм некой операции
template_method1
template_method2
end
def template_method
raise NotImplementedError, "subclass did not define #template_method"
end
end
class SomeSubclass < SomeAbstractClass
def template_method1
# конкретная реализация метода
end
def template_method2
# конкретная реализация метода
end
end
Что бы не полагаться на вызов метода super в подклассах рекомендуется использовать hook методы.
Смысл таких методов в том, что используются они в родительском (абстрактном) классе, а определяются в дочернем (конкретном).
class SomeAbstractClass
def initialize
some_hook_method
end
def som_hook_method; end
end
class SomeSubclass < SomeAbstractClass
def some_hook_method
# специфичная реализация метода, без необходимости вызова родительского метода (super)
end
end
В Ruby для наделения объектов сходным поведением используются миксины - включение модуля в классы, объекты которых необходимо наделить сходным поведением.
Объединять объекты в приложение можно с помощью наследования или композиции. Выбор между наследованием и композицией обуславливается тем, что для решения задачи важнее - плюсы и минусы одного подхода или плюсы и минусы другого.
Необходимо тестировать
Название в переводе - Ruby. Объектно-ориентированное проектирование