首页 > 解决方案 > Ruby:使用模块部分共享初始化逻辑

问题描述

我想在两个不相互继承的类之间共享一些初始化逻辑(所以我不能super在里面调用initialize)。

例如,注意PersonandDog类共享ageand namekwargs 和初始化逻辑:

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

标签: ruby

解决方案


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 对象替换为执行虚拟网络调用的对象。


推荐阅读