Модули и миксины

Mixin

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

# Объявление модуля
module MyModule
  # метод модуля
  def print_hello
    puts 'hello world'
  end
end
class MyClass 
  # для того, чтобы подмешать модуль испольуется ключевое слово include
  include MyModule
end 
instance = MyClass.new
# Как можем заметить метод print_hello отсутствует в нашем классе
# но присутствует в нашем модуле
instance.print_hello # = hello world

Подмешивание на английском переводится как mix, отсюда и происходит слово mixin, поэтому можно сказать что mixin - это возможность использовать наши модули как инструмент расширения функциональности наших классов.

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

module ModuleOne
  def say_hi
    puts 'Hi'
  end
end
module ModuleTwo
  def say_bye
    puts 'Bye'
  end
end
class MyClass
  include ModuleOne
  include ModuleTwo
end
instance = MyClass.new
# метод полученый из модуля ModuleOne
instance.say_hi # = Hi
# метод полученный из модуля ModuleTwo
instance.say_bye # = Bye

В Ruby существуют встроенные модули, которые позволяют расширить функциональность наших классов. Например модуль Comparable, который дает возможность сравнивать объекты друг с другом, если мы подмешиваем этот модуль и переопределяем поведение метода <=>. Метод принимает в аргументах объект с которым будет происходить сравнение и должен:

  • вернуть 1, если наш объект больше объекта переданного в аргументе

  • вернуть -1, если наш объект меньше объекта переданного в аргументе

  • вернуть 0, если они равны

После этого нам становятся доступны такие логические операции как >, <, >= и <=.

class MagicBall
  include Comparable

  attr_reader :value

  def initialize(value)
    @value = value
  end

  def <=>(another_ball)
    # Если value нашего объекта меньше value у сравниваемого
    if value < another_ball.value
      -1 # то мы должны вернуть отрицательное число
    # Если value нашего объекта больше value у сравниваемого
    elsif value > another_ball.value
      1 # то мы должны вернуть положительное число
    else
      0 # иначе эти объекты равны, а значит мы должны вернуть 0
    end
  end
end

first_ball = MagicBall.new(1000)
second_ball = MagicBall.new(2000)

# > или < или >= или <= это тоже методы, которые добавляет в наш класс модуль Comparable
# Соответственно, без этого модуля мы получим ошибку о неизвестном методе
p first_ball > second_ball # = false

Все примитивные дата классы такие как строки, числа и тд уже имеют в себе переопределенный метод <=>, поэтому мы можем использовать этот метод у этих объектов для упрощения написания кода.

class MagicBall
  include Comparable

  attr_reader :value

  def initialize(value)
    @value = value
  end

  def <=>(another_ball)
    # такое же поведение как и выше, только логику мы переложили 
    # на переопределенный метод <=> внутри объекта Integer
    value <=> another_ball.value
  end
end

first_ball = MagicBall.new(1000)
second_ball = MagicBall.new(2000)

# > или < или >= или <= это тоже методы, которые добавляет в наш класс модуль Comparable
# Соответственно, без этого модуля мы получим ошибку о неизвестном методе
p first_ball > second_ball # = false

Помимо Comparable, есть также модуль Enumarable, который добавляет множество методов для нашего класса, чтобы работать с ним как с коллекцией. Для этого нам необходимо при подмешивании определить метод each, который должен применять блок кода к каждому элементу, который содержится внутри нашего класса. Блоки кода будут разбираться в дальнейших уроках, сейчас главное понимать не как реализовывать метод each, а что нам дает его переопределние при использовании модуля Enumarable.

Взамен мы получаем такие методы как count, min, max, find, map и тд. Думаю вы заметили, что такие методы вы уже встречали в массивах ? На самом деле любая коллекция в Ruby имеет внутри себя модуль Enumarable. К слову, цикл for работает с объектами благодаря наличию у них модуля Enumarable.

class Collection
  include Enumarable

  attr_reader :arr

  def initialize(arr)
    @arr = arr
  end

  # переопределяем метод each
  def each
    arr.each { |element| yield(element) }
  end
end

collection = Collection.new([0, 10, 15])

# благодаря Enumarable мы можем использовать объект внутри цикла for
for element in collection
  puts element # сначала распечатается 0, потом 10, потом 15
end

Более подробно с встроенными модулями вы сможете ознакомиться в дополнительном материале.

Константы

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

module Color
  GREEN = 0
  BLACK = 1
  RED = 2
end
p Color::GREEN # = 0

Структуризация

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

module School
  class User
    def initialize(name, class_number)
      @name = name
      @class_number = class_number
    end
  end
end
module University
  class User
    def initialize(name, course_number)
      @name = name
      @course_number = course_number
    end
  end
end
# объект класса User, который находится внутри модуля School
school_user = School::User.new('mike', 9)
# объект класса User, который находится внутри модуля University
university_user = University::User.new('john', 10)
# заметьте, оба классы абсолютно разные и не связаны между собой, кроме одинакового названия

задание animals

Внимание ! Это и последующие задания данного модуля с названием animals необходимо выполнять в одной и той же директории в корне модуля - 3_oop/animals

  • Изучите дополнительный материал про модуль Comparable и с помощью него научитесь сравнивать объекты Cat между собой по их весу.

  • Напишите в main.rb пример сравнения ваших котов, убедитесь, что оно работает

  • Попробуйте теперь сравнить объект класса Cat и класса Dog, какие выводы можно сделать ?

Задание company

Внимание ! Это и последующие задания данного модуля с названием company необходимо выполнять в одной и той же директории в корне модуля - 3_oop/company

  • Реализуйте в классе Company методы:

    • get_top_salary_staff(count) - возвращает список зарплат сотрудников, отсортированных по убыванию заданной длины

    • get_lowest_salary_staff(count) - возвращает список зарплат сотрудников, отсортированных по возрастанию заданной длины

    • выозвите эти методы до и после увольнения 50% сотрудников

Дополнительный материал

Last updated