Upgrade to Pro — share decks privately, control downloads, hide ads and more …

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

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

Dmitry Tsepelev

February 24, 2022
Tweet

More Decks by Dmitry Tsepelev

Other Decks in Programming

Transcript

  1. DmitryTsepelev Saint P Ruby Meetup Winter’22 😷 Виртуальная машина и

    байткод • Ruby – интерпретируемый язык; • компиляция — процесс перевода кода из одного языка в другой; • интерпретация – запуск кода из исходников; • интерпретатор Ruby (>=1.9) преобразует исходный код в байткод перед исполниенем; • байткод выполняется виртуальной машиной; • виртуальная машина знает как запустить байткод на конкретной машине; • Ruby – интерпретируемый язык? 🙂 4
  2. 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 <calldata!mid:+, argc:1, ARGS_SIMPLE> 0007 opt_send_without_block <calldata!mid:puts, argc:1, FCALL|ARGS_SIMPLE> 0009 leave
  3. DmitryTsepelev Saint P Ruby Meetup Winter’22 😷 Объект в Ruby

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

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

    каждого класса есть опциональный базовый класс; • методы базового класса доступны в наследнике; • базовый класс по умолчанию – Object. 8
  6. 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'
  7. 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 # = > #<Class:SomeClass> instance.singleton_class.methods # = > [:class_method, . . . ]
  8. 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]
  9. 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' Переменные класса
  10. DmitryTsepelev Saint P Ruby Meetup Winter’22 😷 Модули: include •

    модуль — объект, который содержит объявления методов, указатель на базовый класс и таблицу констант; • при включении модуля создается новый класс и добавляется в цепочку наследования как базовый класс для текущего. 13
  11. 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 # => ???
  12. 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
  13. 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 ⚠ разница ⚠
  14. 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 добавляет класс в цепочку наследования перед текущим
  15. 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, . . . ]
  16. DmitryTsepelev Saint P Ruby Meetup Winter’22 😷 Лексическая область видимости

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

    class User def full_name [f i rst_name, last_name].compact.join(" ") end end 21
  18. 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 и добавляет метод в метакласс
  19. DmitryTsepelev Saint P Ruby Meetup Winter’22 😷 class User class

    < < self def admins where(is_admin: true) end end end 23 объявляем новую область видимости метод попадает в метакласс Объявление метода класса внутри новой лексической области видимости
  20. 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 метод попадает в метакласс
  21. 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 Объявление метода на объекте внутри новой лексической области видимости объявляем новую область видимости
  22. 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
  23. DmitryTsepelev Saint P Ruby Meetup Winter’22 😷 28 def build_closure(x,

    y) binding end eval "x + y", build_closure(1, 2) binding: замыкание без функции
  24. 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 # = > #<Method: main.full_name> um = m.unbind um.class # = > UnboundMethod um.bind_call(user) # = > "John Doe"
  25. 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
  26. 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
  27. 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
  28. DmitryTsepelev Saint P Ruby Meetup Winter’22 😷 Как доступиться до

    переменных? • instance_variables • instance_variable_set • instance_variable_get 33
  29. 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
  30. 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
  31. 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 объявляем метод динамически получаем значение переменной экземпляра динамически
  32. 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 присваиваем значение переменной экземпляра динамически
  33. 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 выполняем код в контексте класса
  34. 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 `<main>' (eval) : 2:in `raise_error!': StandardError (StandardError)
  35. 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 `<main>' 1 : from class_eval.rb:10:in `raise_error_with_trace!' class_eval.rb:10:in `/': divided by 0 (ZeroDivisionError) class_eval и ошибки
  36. 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
  37. 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
  38. 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
  39. 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
  40. 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
  41. 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
  42. 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
  43. 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
  44. 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
  45. 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 # = > [#<User:0 x 00007fa5f385c2f8 @id=1, @email="[email protected]", @name="John">] puts User.f i nd_by(name: 'John').inspect # = > #<User:0 x 00007fa5f3834398 @id=1, @email="[email protected]", @name="John"> puts User.where(name: 'Jane').inspect # = > [] * https://gist.github.com/DmitryTsepelev/ab1797e7d00b484973615ba1782e0e36# fi le-07_instance_and_class_methods-rb
  46. 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 метод
  47. 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
  48. 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
  49. 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 вывод названия таблицы из имени класса вызов публичного метода по имени
  50. 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
  51. DmitryTsepelev Saint P Ruby Meetup Winter’22 😷 Еще раз •

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