Slide 1

Slide 1 text

Объектная модель Ruby а также метапрограммирование и создание DSL Дмитрий Цепелев, Backend в Evil Martians

Slide 2

Slide 2 text

DmitryTsepelev Saint P Ruby Meetup Winter’22 😷 Что почитать 2

Slide 3

Slide 3 text

Объектная модель Ruby

Slide 4

Slide 4 text

DmitryTsepelev Saint P Ruby Meetup Winter’22 😷 Виртуальная машина и байткод • Ruby – интерпретируемый язык; • компиляция — процесс перевода кода из одного языка в другой; • интерпретация – запуск кода из исходников; • интерпретатор Ruby (>=1.9) преобразует исходный код в байткод перед исполниенем; • байткод выполняется виртуальной машиной; • виртуальная машина знает как запустить байткод на конкретной машине; • Ruby – интерпретируемый язык? 🙂 4

Slide 5

Slide 5 text

DmitryTsepelev Saint P Ruby Meetup Winter’22 😷 Виртуальная машина и байткод 5 code = < < CODE puts 2+2 CODE puts RubyVM : : InstructionSequence.compile(code).disasm 0000 putself ( 1)[Li] 0001 putobject 2 0003 putobject 2 0005 opt_plus 0007 opt_send_without_block 0009 leave

Slide 6

Slide 6 text

DmitryTsepelev Saint P Ruby Meetup Winter’22 😷 Объект в Ruby • инкапсуляция — данные и методы работы с ними хранятся в одной сущности; • метод — набор инструкций байткода, хранится в памяти; • копирование байткода в каждый объект класса — дорого; • объект — указатель на класс и массив переменных объекта. 6

Slide 7

Slide 7 text

DmitryTsepelev Saint P Ruby Meetup Winter’22 😷 Класс в Ruby • объект, который содержит объявления методов, имена атрибутов, указатель на суперкласс и таблицу констант; • объект класса Class. 7 irb(main) : 003 : 0> Integer.class = > Class

Slide 8

Slide 8 text

DmitryTsepelev Saint P Ruby Meetup Winter’22 😷 Наследование • у каждого класса есть опциональный базовый класс; • методы базового класса доступны в наследнике; • базовый класс по умолчанию – Object. 8

Slide 9

Slide 9 text

DmitryTsepelev Saint P Ruby Meetup Winter’22 😷 Наследование 9 class ParentClass def parent_method; 'parent_method'; end end class ChildClass < ParentClass def child_method; 'child_method'; end end ParentClass.new.methods # = > [:parent_method, . . . ] ChildClass.superclass # = > ParentClass ChildClass.new.methods # = > [:child_method, :parent_method, . . . ] # child has access to parent methods ChildClass.new.parent_method # = > 'parent_method'

Slide 10

Slide 10 text

DmitryTsepelev Saint P Ruby Meetup Winter’22 😷 Где хранятся методы? • инстанс методы хранятся в объекте SomeClass; • методы класса должны быть где–то в базовом классе; • мы не можем добавить их прямо в Class; • Ruby генерирует для нас singleton_class/metaclass/eigenclass. 10 class SomeClass def self.class_method; 'class_method'; end def instance_method; 'instance_method'; end end instance = SomeClass.new instance.methods # = > [:instance_method, . . . ] SomeClass.singleton_class # = > # instance.singleton_class.methods # = > [:class_method, . . . ]

Slide 11

Slide 11 text

DmitryTsepelev Saint P Ruby Meetup Winter’22 😷 Переменные класса Инстанс переменные создаются в указанном контексте 11 class SomeClass @class_var = 'value' def self.class_var @class_var end end SomeClass.new.instance_variables # = > [] SomeClass.instance_variables # = > [ : @class_var] class SomeClass @@class_var = 'value' def self.class_var @@class_var end end SomeClass.new.instance_variables # = > [] SomeClass.instance_variables # = > [] SomeClass.class_variables # = > [ : @@class_var]

Slide 12

Slide 12 text

DmitryTsepelev Saint P Ruby Meetup Winter’22 😷 Значение переменной класса общее для класса и его наследников 12 class SomeClass @class_var = 'value' def self.class_var @class_var end end class ChildClass < SomeClass @class_var = 'child_value' end SomeClass.class_var # = > 'value' ChildClass.class_var # = > 'child_value' class SomeClass @@class_var = 'value' def self.class_var @@class_var end end class ChildClass < SomeClass @@class_var = 'child_value' end SomeClass.class_var # = > 'child_value' ChildClass.class_var # = > 'child_value' Переменные класса

Slide 13

Slide 13 text

DmitryTsepelev Saint P Ruby Meetup Winter’22 😷 Модули: include • модуль — объект, который содержит объявления методов, указатель на базовый класс и таблицу констант; • при включении модуля создается новый класс и добавляется в цепочку наследования как базовый класс для текущего. 13

Slide 14

Slide 14 text

DmitryTsepelev Saint P Ruby Meetup Winter’22 😷 14 module Raptor def name "🦖" end end class User attr_reader :name include Raptor def initialize(name) @name = name end end user = User.new("Ivan") puts user.name # => ???

Slide 15

Slide 15 text

DmitryTsepelev Saint P Ruby Meetup Winter’22 😷 Модули: класс переопределяет метод из включенного модуля 15 module Raptor def name "🦖" end end class User attr_reader :name include Raptor def initialize(name) @name = name end end user = User.new("Ivan") puts user.name # => Ivan

Slide 16

Slide 16 text

DmitryTsepelev Saint P Ruby Meetup Winter’22 😷 Модули: включение модуля в модуль 16 module Module1 def another_method 'another_method' end end module Module2 include Module1 def some_method 'some_method' end end class SomeClass include Module2 end SomeClass.new.some_method # = > "some_method" SomeClass.new.another_method # = > "another_method" module Module1 def another_method 'another_method' end end module Module2 def some_method 'some_method' end end class SomeClass include Module2 end Module2.include(Module1) SomeClass.new.some_method # = > "some_method" SomeClass.new.another_method # = > NoMethodError ⚠ разница ⚠

Slide 17

Slide 17 text

DmitryTsepelev Saint P Ruby Meetup Winter’22 😷 Модули: prepend 17 module Raptor def name “#{super} (🦖)” end end class User attr_reader :name prepend Raptor def initialize(name) @name = name end end user = User.new("Ivan") puts user.name # => “Ivan (🦖)" Prepend добавляет класс в цепочку наследования перед текущим

Slide 18

Slide 18 text

DmitryTsepelev Saint P Ruby Meetup Winter’22 😷 Модули: extend extend = include в singleton_class 18 class OtherClass extend M end # то же самое: class OtherClass class < < self include M end end OtherClass.new.methods # = > [ : …] OtherClass.new.singleton_class.methods # = > [:method_from_module, . . . ]

Slide 19

Slide 19 text

Инструменты метапрограммирования

Slide 20

Slide 20 text

DmitryTsepelev Saint P Ruby Meetup Winter’22 😷 Лексическая область видимости 20 class User end первая область видимости вторая область видимости • секция кода внутри синтаксической структуры программы; • не принимает во внимание структуру файлов, модулей или наследование.

Slide 21

Slide 21 text

DmitryTsepelev Saint P Ruby Meetup Winter’22 😷 Обычное объявление метода class User def full_name [f i rst_name, last_name].compact.join(" ") end end 21

Slide 22

Slide 22 text

DmitryTsepelev Saint P Ruby Meetup Winter’22 😷 Объявление метода класса с помощью префикса class User def self.admins where(is_admin: true) end end 22 def+pre fi x понимает, что self — это User и добавляет метод в метакласс

Slide 23

Slide 23 text

DmitryTsepelev Saint P Ruby Meetup Winter’22 😷 class User class < < self def admins where(is_admin: true) end end end 23 объявляем новую область видимости метод попадает в метакласс Объявление метода класса внутри новой лексической области видимости

Slide 24

Slide 24 text

DmitryTsepelev Saint P Ruby Meetup Winter’22 😷 Объявление метода на объекте class User end user = User.new def user.full_name [f i rst_name, last_name].compact.join(" ") end 24 метод попадает в метакласс

Slide 25

Slide 25 text

DmitryTsepelev Saint P Ruby Meetup Winter’22 😷 class User end user = User.new class < < user def full_name [f i rst_name, last_name].compact.join(" ") end end 25 Объявление метода на объекте внутри новой лексической области видимости объявляем новую область видимости

Slide 26

Slide 26 text

DmitryTsepelev Saint P Ruby Meetup Winter’22 😷 define_method 26 class User def i ne_method(:full_name) do [f i rst_name, last_name].compact.join(" ") end end

Slide 27

Slide 27 text

DmitryTsepelev Saint P Ruby Meetup Winter’22 😷 eval: выполнение кода из строки 27 eval "2 + 2"

Slide 28

Slide 28 text

DmitryTsepelev Saint P Ruby Meetup Winter’22 😷 28 def build_closure(x, y) binding end eval "x + y", build_closure(1, 2) binding: замыкание без функции

Slide 29

Slide 29 text

DmitryTsepelev Saint P Ruby Meetup Winter’22 😷 binding: замыкание без функции 29 User = Struct.new(:f i rst_name, :last_name) user = User.new('John', 'Doe') def full_name [f i rst_name, last_name].join(' ') end m = method(:full_name) m.class # = > # um = m.unbind um.class # = > UnboundMethod um.bind_call(user) # = > "John Doe"

Slide 30

Slide 30 text

DmitryTsepelev Saint P Ruby Meetup Winter’22 😷 instance_eval: меняем получателя 30 class User; end user = User.new user.instance_eval do [f i rst_name, last_name].compact.join(' ') end

Slide 31

Slide 31 text

DmitryTsepelev Saint P Ruby Meetup Winter’22 😷 Refinements • re fi nement добавляет методы; • добавленные методы доступны только в текущей лексической области видимости; • полезно для портирования фич в OSS и изменения поведения классов Ruby для нужд приложения. 31 module EmailCheck ref i ne String do def is_email? include?('@') end end end using EmailCheck puts '[email protected]'.is_email? # = > true puts '123'.is_email? # = > false

Slide 32

Slide 32 text

DmitryTsepelev Saint P Ruby Meetup Winter’22 😷 Refinements 32 module EmailCheck ref i ne String do def is_email? include?('@') end end end class User using EmailCheck def has_valid_email? 'asd'.is_email? end end puts User.new.has_valid_email? # = > false puts '[email protected]'.is_email? # = > NoMethodError

Slide 33

Slide 33 text

DmitryTsepelev Saint P Ruby Meetup Winter’22 😷 Как доступиться до переменных? • instance_variables • instance_variable_set • instance_variable_get 33

Slide 34

Slide 34 text

Демо: генератор методов

Slide 35

Slide 35 text

DmitryTsepelev Saint P Ruby Meetup Winter’22 😷 Цель 35 class User my_attr_accessor :email def initialize(email: nil) @email = email end end user = User.new user.email = '[email protected]' puts user.email # = > [email protected] * https://gist.github.com/DmitryTsepelev/ab1797e7d00b484973615ba1782e0e36# fi le-02_method_generator-rb

Slide 36

Slide 36 text

DmitryTsepelev Saint P Ruby Meetup Winter’22 😷 Делаем метод доступным везде 36 module MyAttrAccessor def my_attr_accessor(attr_name) my_attr_reader(attr_name) my_attr_writer(attr_name) end # . . . end Object.extend(MyAttrAccessor) * https://gist.github.com/DmitryTsepelev/ab1797e7d00b484973615ba1782e0e36# fi le-02_method_generator-rb все классы наследуются от Object

Slide 37

Slide 37 text

DmitryTsepelev Saint P Ruby Meetup Winter’22 😷 Реализация геттера 37 module MyAttrAccessor # . . . def my_attr_reader(attr_name) instance_variable_name = attr_to_variable_name(attr_name) def i ne_method(attr_name) do instance_variable_get(instance_variable_name) end end # . . . private def attr_to_variable_name(attr_name) "@ # { attr_name}" end end * https://gist.github.com/DmitryTsepelev/ab1797e7d00b484973615ba1782e0e36# fi le-02_method_generator-rb объявляем метод динамически получаем значение переменной экземпляра динамически

Slide 38

Slide 38 text

DmitryTsepelev Saint P Ruby Meetup Winter’22 😷 Реализация сеттера 38 module MyAttrAccessor # . . . def my_attr_writer(attr_name) instance_variable_name = attr_to_variable_name(attr_name) def i ne_method(" # { attr_name}=") do |value| instance_variable_set(instance_variable_name, value) end end # . . . end * https://gist.github.com/DmitryTsepelev/ab1797e7d00b484973615ba1782e0e36# fi le-02_method_generator-rb присваиваем значение переменной экземпляра динамически

Slide 39

Slide 39 text

DmitryTsepelev Saint P Ruby Meetup Winter’22 😷 Альтернатива: class_eval 39 module MyAttrAccessor def my_attr_accessor(attr_name) class_eval < < ~RUBY def # { attr_name} @ # { attr_name} end def # { attr_name}=value @ # { attr_name} = value end RUBY end end Object.extend(MyAttrAccessor) * https://gist.github.com/DmitryTsepelev/ab1797e7d00b484973615ba1782e0e36# fi le-08_method_generator_class_eval-rb выполняем код в контексте класса

Slide 40

Slide 40 text

DmitryTsepelev Saint P Ruby Meetup Winter’22 😷 class_eval и ошибки 40 class ClassThatCanRaiseError class_eval < < ~RUBY def raise_error! 1 / 0 end RUBY end ClassThatCanRaiseError.new.raise_error! Traceback (most recent call last) : 1 : from class_eval.rb:15:in `' (eval) : 2:in `raise_error!': StandardError (StandardError)

Slide 41

Slide 41 text

DmitryTsepelev Saint P Ruby Meetup Winter’22 😷 41 class ClassThatCanRaiseError class_eval < < ~RUBY, _ _ FILE _ _ , _ _ LINE _ _ + 1 def raise_error_with_trace! 1 / 0 end RUBY end ClassThatCanRaiseError.new.raise_error_with_trace! Traceback (most recent call last) : 2 : from class_eval.rb:16:in `' 1 : from class_eval.rb:10:in `raise_error_with_trace!' class_eval.rb:10:in `/': divided by 0 (ZeroDivisionError) class_eval и ошибки

Slide 42

Slide 42 text

Демо: генератор модулей

Slide 43

Slide 43 text

DmitryTsepelev Saint P Ruby Meetup Winter’22 😷 Цель 43 class User include MyAttrAccessor[:email] def initialize(email: nil) @email = email end end user = User.new user.email = '[email protected]' puts user.email # = > [email protected] * https://gist.github.com/DmitryTsepelev/ab1797e7d00b484973615ba1782e0e36# fi le-03_module_generator-rb

Slide 44

Slide 44 text

DmitryTsepelev Saint P Ruby Meetup Winter’22 😷 Реализация 44 module MyAttrAccessor def self.[](attr_name) Module.new do instance_variable_name = "@ # { attr_name}" def i ne_method(attr_name) do instance_variable_get(instance_variable_name) end def i ne_method(" # { attr_name}=") do |value| instance_variable_set(instance_variable_name, value) end end end end * https://gist.github.com/DmitryTsepelev/ab1797e7d00b484973615ba1782e0e36# fi le-03_module_generator-rb dynamic module generation

Slide 45

Slide 45 text

Демо: роутер «как в рельсах»

Slide 46

Slide 46 text

DmitryTsepelev Saint P Ruby Meetup Winter’22 😷 Цель 1: делаем что работало 46 router = Router.draw do |builder| builder.get '/users', controller: :users, action: :index builder.post '/users', controller: :users, action: :create end puts router.route('GET /users') # = > {:controller= > : users, :action= > : index} puts router.route('POST /users') # = > {:controller= > : users, :action= > : create} puts router.route('GET /orders') # = > NOT_FOUND * https://gist.github.com/DmitryTsepelev/ab1797e7d00b484973615ba1782e0e36# fi le-04_builder_simple-rb

Slide 47

Slide 47 text

DmitryTsepelev Saint P Ruby Meetup Winter’22 😷 Router 47 * https://gist.github.com/DmitryTsepelev/ab1797e7d00b484973615ba1782e0e36# fi le-04_builder_simple-rb class Router NOT_FOUND = 'NOT_FOUND' def self.draw builder = Builder.new yield(builder) new(builder) end def initialize(builder) @builder = builder end def route(method_and_path) method, path = method_and_path.split(' ') @builder.routes[[method.downcase, path]] | | NOT_FOUND end end

Slide 48

Slide 48 text

DmitryTsepelev Saint P Ruby Meetup Winter’22 😷 Builder 48 * https://gist.github.com/DmitryTsepelev/ab1797e7d00b484973615ba1782e0e36# fi le-04_builder_simple-rb class Builder def get(path, * * args) routes[['get', path]] = args end def post(path, * * args) routes[['post', path]] = args end def routes @routes | | = {} end end

Slide 49

Slide 49 text

DmitryTsepelev Saint P Ruby Meetup Winter’22 😷 Цель 2: блок без аргумента 49 router = Router.draw do get '/users', controller: :users, action: :index post '/users', controller: :users, action: :create end puts router.route('GET /users') # = > {:controller= > : users, :action= > : index} puts router.route('POST /users') # = > {:controller= > : users, :action= > : create} puts router.route('GET /orders') # = > NOT_FOUND * https://gist.github.com/DmitryTsepelev/ab1797e7d00b484973615ba1782e0e36# fi le-05_builder_instance_eval-rb

Slide 50

Slide 50 text

DmitryTsepelev Saint P Ruby Meetup Winter’22 😷 Router v2: instance_eval 50 * https://gist.github.com/DmitryTsepelev/ab1797e7d00b484973615ba1782e0e36# fi le-05_builder_instance_eval-rb class Router NOT_FOUND = 'NOT_FOUND' def self.draw(&block) new.tap { |router| router.instance_eval(&block) } end def route(method_and_path) method, path = method_and_path.split(' ') routes[[method.downcase, path]] | | NOT_FOUND end def get(path, * * args) routes[['get', path]] = args end def post(path, * * args) routes[['post', path]] = args end def routes @routes | | = {} end end блок будет выполнен в контексте инстанса Router все эти методы доступны внутри блока, переданного instance_eval

Slide 51

Slide 51 text

DmitryTsepelev Saint P Ruby Meetup Winter’22 😷 Router v3: генерируем методы 51 * https://gist.github.com/DmitryTsepelev/ab1797e7d00b484973615ba1782e0e36# fi le-06_builder_generate_methods-rb class Router class < < self def draw(&block) new.tap { |router| router.instance_eval(&block) } end def generate_route_methods(*method_names) # . . . end end generate_route_methods :get, :post, :patch, :delete NOT_FOUND = 'NOT_FOUND' def route(method_and_path) method, path = method_and_path.split(' ') routes[[method.downcase, path]] | | NOT_FOUND end def routes @routes | | = {} end end

Slide 52

Slide 52 text

DmitryTsepelev Saint P Ruby Meetup Winter’22 😷 Router v3: генерируем методы 52 * https://gist.github.com/DmitryTsepelev/ab1797e7d00b484973615ba1782e0e36# fi le-06_builder_generate_methods-rb class Router class < < self # . . . def generate_route_methods(*method_names) method_names.each do |method_name| def i ne_method(method_name) do |path, * * args| routes[[method_name, path]] = args end end end end generate_route_methods :get, :post, :patch, :delete # . . . end

Slide 53

Slide 53 text

Демо: CoolRecord

Slide 54

Slide 54 text

DmitryTsepelev Saint P Ruby Meetup Winter’22 😷 Цель: добавить и методы экземпляра и методы класса 54 class User include CoolRecord attr_accessor :id, :email, :name def initialize(id: nil, email: nil, name: nil) @id = id @email = email @name = name end end user = User.new(email: '[email protected]', name: 'John') user.save puts User.where(name: 'John').inspect # = > [#] puts User.f i nd_by(name: 'John').inspect # = > # puts User.where(name: 'Jane').inspect # = > [] * https://gist.github.com/DmitryTsepelev/ab1797e7d00b484973615ba1782e0e36# fi le-07_instance_and_class_methods-rb

Slide 55

Slide 55 text

DmitryTsepelev Saint P Ruby Meetup Winter’22 😷 Хук included 55 module CoolRecord def self.included(base) base.extend(ClassMethods) end def save # . . . end module ClassMethods def where(conditions) # . . . end def f i nd_by(conditions) # . . . end end end * https://gist.github.com/DmitryTsepelev/ab1797e7d00b484973615ba1782e0e36# fi le-07_instance_and_class_methods-rb хук для вызова extend вместе с include модуль для статических методов этот метод будет добавлен как instance метод

Slide 56

Slide 56 text

DmitryTsepelev Saint P Ruby Meetup Winter’22 😷 Мок для БД 56 class DB def self.instance @instance | | = new end def users @users | | = {} end end * https://gist.github.com/DmitryTsepelev/ab1797e7d00b484973615ba1782e0e36# fi le-07_instance_and_class_methods-rb

Slide 57

Slide 57 text

DmitryTsepelev Saint P Ruby Meetup Winter’22 😷 #save 57 module CoolRecord # . . . def save record_id = if new_record? self.id = (DB.instance.users.map(&:id).max | | 0) + 1 else id end self.class.table[record_id] = { id: record_id, email: email, name: name } end def new_record? id.nil? end# . . . end * https://gist.github.com/DmitryTsepelev/ab1797e7d00b484973615ba1782e0e36# fi le-07_instance_and_class_methods-rb

Slide 58

Slide 58 text

DmitryTsepelev Saint P Ruby Meetup Winter’22 😷 .where/.find_by 58 module CoolRecord # . . . module ClassMethods # . . . def table DB.instance.public_send(table_name) end private def table_name " # { self.name.downcase}s" end end end * https://gist.github.com/DmitryTsepelev/ab1797e7d00b484973615ba1782e0e36# fi le-07_instance_and_class_methods-rb вывод названия таблицы из имени класса вызов публичного метода по имени

Slide 59

Slide 59 text

DmitryTsepelev Saint P Ruby Meetup Winter’22 😷 .where/.find_by 59 module CoolRecord # . . . module ClassMethods def where(conditions) table.values .select { |attrs| conditions.all? { |(attr_name, value)| attrs[attr_name] = = value } } .map { |attrs| self.new( * * attrs) } end def f i nd_by(conditions) where(conditions).f i rst end # . . . end end * https://gist.github.com/DmitryTsepelev/ab1797e7d00b484973615ba1782e0e36# fi le-07_instance_and_class_methods-rb

Slide 60

Slide 60 text

DmitryTsepelev Saint P Ruby Meetup Winter’22 😷 Еще раз • в Ruby все является объектом и ведет себя схожим образом; • метапрограммирование — код, который пишет код; • include, prepend, extend и re fi ne помогают добавлять методы в контекст; • статические методы живут в метаклассе; • классы, модули и методы могут добавляться динамически. 60

Slide 61

Slide 61 text

@dmitrytsepelev Спасибо! Вопросы? 61