Разделение функциональности: наследование, модули, примеси

Андрей Васильев

2019

Принцип DRY

Do not Repeat Yourself - при разработке программного обеспечения необходимо устранять ненужное дублирование

Если логика изменится, то нужно исправить только в 1 месте

Примеры задач с общей логикой

В каждом из этих случаев следует избегать дублирования

Классы

Классы описывают общую логику действий для множества различных объектов: методы класса доступны всем объектам

Множество объектов одного класса

Базовая логика обработки сообщений

Вызов метода в Ruby - передача сообщения, состоящего из

Когда объект получает сообщение, то оно передаётся классу данного объекта для обработки

Поиск метода происходит только по имени, из аргументов учитывается только лишь их количество

Наследование и сообщения

Механизм наследования позволяет создавать новые версии класса, являющиеся его уточнением или специализацией

Пример наследования

Синтаксис наследования

class Parent
  ...
end
class Child < Parent
end

Пример кода

class Parent
  def say_hello
    puts "Hello from #{self}"
  end
end
parent = Parent.new
puts parent.say_hello

class Child < Parent
end
child = Child.new
puts child.say_hello

Дерево наследования

puts "Child superclass: #{Child.superclass}"
puts "Parent.superclass: #{Parent.superclass}"
puts "Object.superclass: #{Object.superclass}"
puts "BasicObject.superclass "\
     "#{BasicObject.superclass.inspect}"

Последовательность вызова метода

Напомним, что общение между объектами в Ruby напоминает отправку сообщения: в сообщении указывается название метода и его аргументы.

Когда объект получает сообщение, то он начинает поиск метода, которому можно передать данное сообщение:

Первый найденный метод в данной цепочке будет вызван и ему будут переданы все переданные аргументы

Перекрытие методов в подклассах

Метод to_s класса Object

class Person
  def initialize(name)
    @name = name
  end
end
person = Person.new("Michael")
puts person # <Person:0x007fc812839550>

class Person
  def initialize(name)
    @name = name
  end
  def to_s
    "Person named #{@name}"
  end
end
person = Person.new("Michael")
puts person # Person named Michael

Проблема роста приложений

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

В Java проблема решается обязательным выделением пакетов, а в последней версии и модулей со встроенными версиями.

Подходы к решению проблемы

Модули в языке Ruby

Модуль - замкнутое именованное пространство

module Trig
  PI = 3.141592654
  def Trig.sin(x) # Определяем метод модуля
    # ..
  end
  def Trig.cos(x)
    # ..
  end
end

puts Trig::PI # Доступ к константе
puts Trig.sin(Trig::PI/4) # Вызов метода

module Moral
  VERY_BAD = 0
  BAD = 1
  def Moral.sin(badness)
    # ...
  end
end
y = Trig.sin(Trig::PI/4)
wrongdoing = Moral.sin(Moral::VERY_BAD)

Определение методов в модуле

Определение методов модулей похоже на определение методов классов (не методов экземпляров классов). Можно даже использовать синтаксис с ключевым словом self

module ModuleWithBigName
  def ModuleWithBigName.greet(name)
    puts "Hello, #{name.upcase}"
  end
  def self.info
    'I am working'
  end
end
ModuleWithBigName.greet('mariya')
puts ModuleWithBigName.info

Оба метода в примере являются методами одного модуля

Определение констант в модуле

К константам, которые можно определить в модуле, относятся также модули и классы

module External
  class Internal
    ...
  end
end
object = External::Internal.new

Ввиду того, что модули тоже являются константами, то их содержимое можно определять в нескольких файлах, эффективно создавая замкнутые пространства имён

Описание модуля в нескольких файлах

Содержимое файла lib/application/car.rb

module Application
  class Car
    ...
  end
end

Содержимое файла lib/application/house.rb

require_relative 'car'
module Application
  class House
    def initialize
      @car = Car.new
    end
  end
end

Проблемы множественного наследования

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

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

Mixin (Примеси, Миксины, Агрегация)

Модули позволяют решить вопросы множественного наследования, реализуя возможность агрегирования: модули можно примешивать к определениям классов

module Debug
  def who_am_i? # Экземпляр метода
    "#{self.class.name} (id: #{self.object_id})"
  end
end
class Test
  include Debug # Примешивание модуля Debug
end
Test.new.who_am_i?

Особенности примеси модулей

Пример использования extend

module Mixin
  def self.class_method
    puts 'In the class method'
  end
  def instance_method
    puts 'In the instance method'
  end
end
class Test
   extend Mixin
end
Test.class_method
test = Test.new
test.instance_method # Не сработает

Другие схемы наследования

Множественное наследование (C++)

Один класс-родитель (Java < 8, C#)

В Java 8 интерфейсы могут содержать код, который может выполняться. Это открывает возможности по использованию их в качестве примесей, однако такой практики пока ещё не сложилось.

Использование примеси Comparable

Язык Ruby включает в себя ряд полезных примесей, которые могут пригодится при создании собственных объектов

Примесь Comparable опирается на то, что в классе будет реализован метод сравнения <=>. Данный метод берёт ссылку на другой объект, сравнивает и возвращает

Большинство встроенных типов данных реализуют данный метод.

Используя этот метод примесь добавляет в ваш класс следюущие методы: <, <=, ==, >, >=, between, clamp.

Пример использования примеси Comparable

class SizeMatters
  include Comparable
  attr :str
  def <=>(other)
    str.size <=> other.str.size
  end
  def initialize(str)
    @str = str
  end
end

s1 = SizeMatters.new("Z")
s2 = SizeMatters.new("YY")
s3 = SizeMatters.new("XXX")
s1 < s2                       #=> true
s4.between?(s1, s3)           #=> false

Использование примеси Enumerable

Модуль Enumerable предоставляет большое число методов для работы с данными, которые можно перечислить.

Классу, использующему модуль Enumerable необходимо реализовать следующие методы:

После примеси модуля становятся доступными все его методы:

Проектирование примесей

Из рассмотренных выше примесей мы можем вынести следующие уроки:

Ошибки в проектировании примесей

Методы, примешанные к классу, могут взаимодействовать не только с методами, но также и с переменными экземпляра

module First
  def foo
    @abc = 10
  end
end
module Second
  def bar
    @abc = 'abc'
  end
end

Не следует разрабатывать модулей, которые оперируют состоянием переменных экземпляра напрямую

Какой метод будет вызван?

Когда у вас появляется возможность использовать примеси, то должен встать вопрос: как происходит поиск методов среди всех связных компонент? Порядок достаточно простой:

Вопрос для самоизучения

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

Использование примесей и наследования

Механизм наследования позволяет определить чёткую иерархию свойств объектов. Для любого класса-ребёнка должно выполняться правило: класс-ребёнок является классом-родителем. Т.е. в программе можно заменить класс-родитель на класс-ребёнок и приложение должно продолжить правильно функционировать.

В большинстве случаев реального мира мы имеем дело с предметами, обладающими множеством различных свойств и множеством контрагентов. То есть объект живёт в некоторой среде и комбинирует в себе качества множества различных компонент. Такое поведение обычно сложно представить в виде подмножества какого-то конкретного класса.

Простое правило, которое следует использовать при проектировании классов: «композиция лучше наследования»