ruby - Ruby:使用模块部分共享初始化逻辑
问题描述
我想在两个不相互继承的类之间共享一些初始化逻辑(所以我不能super
在里面调用initialize
)。
例如,注意Person
andDog
类共享age
and name
kwargs 和初始化逻辑:
class Person
def initialize(age: , name: , height: )
@age = age.to_i
@name = name.to_sym
@height = height
end
end
class Dog
def initialize(age: , name: , breed: )
@age = age.to_i
@name = name.to_sym
@breed = breed
end
end
为了保持代码干燥,我不想在两个类中重复这一点;相反,我想将该共享逻辑移至模块并将其包含在两个类中。
但是,我不想将初始化参数更改为options = {}
(散列),所以我想仍然对两个类的初始化方法使用关键字参数。在某种程度上,我们需要将共享的 kwargs 与特定于类的 kwargs 合并def initialize
。
如何在两个不同的类之间共享这个初始化逻辑(关键字参数和初始化方法)?
更新
实现一半目标(共享初始化逻辑)的一种方法是使用binding
:
module AgeAndNameInitializationConcern
def self.included(base)
base.class_eval do
attr_reader :name, :age
end
end
def initialize_age_and_name(binding)
code_string = <<~EOT
@age = age.to_i
@name = name.to_sym
EOT
eval(code_string, binding)
end
end
class Person
include AgeAndNameInitializationConcern
def initialize(age: , name: , height: )
initialize_age_and_name(binding)
@height = height
end
end
class Dog
include AgeAndNameInitializationConcern
def initialize(age: , name: , breed: )
initialize_age_and_name(binding)
@breed = breed
end
end
解决方案
super
与模块一起工作得很好。用于**
忽略其他关键字参数。
module Being
def initialize(age: , name: , **)
@age = age.to_i
@name = name.to_sym
end
end
class Person
include Being
def initialize(height:, **)
super
@height = height
end
end
class Dog
include Being
def initialize(breed: , **)
super
@breed = breed
end
end
#<Dog:0x00007fb0fe80f7f8 @age=6, @name=:"Good Boy", @breed="Good Dog">
#<Person:0x00007fb0fe80f2a8 @age=42, @name=:Bront, @height="6' 2\"">
p Dog.new(age: 6, name: "Good Boy", breed: "Good Dog")
p Person.new(age: 42, name: "Bront", height: %q{6' 2"})
您可能会在super
与模块混合时遇到一些麻烦,因为并不总是很清楚super
将调用哪个祖先方法。您可以使用 . 检查您的完整继承树Module#ancestors
。这包括类,因为所有类都是模块。
# [Dog, Being, Object, Kernel, BasicObject]
# [Person, Being, Object, Kernel, BasicObject]
p Dog.ancestors
p Person.ancestors
为避免这种情况,请使用组合。由几个不同的对象组成您的类,并将方法调用委托给它们。在这种情况下,有一个 Being 对象和对它的委托方法调用。我们将用于Forwardable
将方法调用转发到 Being 对象。
require 'forwardable'
class Being
attr_accessor :age, :name
def initialize(age:, name:)
@age = age.to_i
@name = name.to_sym
end
def greeting
"Hello, my name is #{name} and I am #{age} years old."
end
end
class Person
extend Forwardable
def_delegators :@being, :greeting
def initialize(height:, **args)
@being = Being.new(**args)
@height = height
end
def to_s
self
end
end
class Dog
extend Forwardable
def_delegators :@being, :greeting
def initialize(breed:, **args)
@being = Being.new(**args)
@breed = breed
end
def to_s
self
end
end
#<Dog:0x00007fb87702c060 @being=#<Being:0x00007fb87702e400 @age=6, @name=:"Good Boy">, @breed="Good Dog">
#<Person:0x00007fb87a02f870 @being=#<Being:0x00007fb87a02f7f8 @age=42, @name=:Bront>, @height="6' 2\"">
p dog = Dog.new(age: 6, name: "Good Boy", breed: "Good Dog")
p person = Person.new(age: 42, name: "Bront", height: %q{6' 2"})
# Hello, my name is Good Boy and I am 6 years old.
# Hello, my name is Bront and I am 42 years old.
puts dog.greeting
puts person.greeting
# [Dog, Being, Object, Kernel, BasicObject]
# [Person, Being, Object, Kernel, BasicObject]
p Dog.ancestors
p Person.ancestors
def_delegators :@being, :greeting
说当greeting
被调用时,@being.greeting
改为调用。
继承很容易,但它可能导致难以找到复杂性。组合需要更多的工作,但发生的事情更明显,它允许更灵活的类。您可以换掉委派给的内容。
例如,假设您需要从网上获取东西。您可以从 Net::HTTP 继承。或者您可以委托给 Net::HTTP 对象。然后在测试中,您可以将 Net::HTTP 对象替换为执行虚拟网络调用的对象。