mixin 是否应该对它们包含 class 做出假设?

Should mixins make assumptions about their including class?

我找到了一个 mixin 的例子,它假设包含 class 的实例变量有哪些。像这样:

module Fooable
  def calculate
    @val_one + @val_two
  end
end

class Bar
  attr_accessor :val_one, :val_two
  include Fooable
end

我找到了支持和反对这是否是一个好习惯的论点。明显的替代方法是将 val_oneval_two 作为参数传递,但这似乎并不常见,并且具有更多参数化方法可能是一个缺点。

是否存在关于 mixin 对 class 状态的依赖的传统观点?从实例变量读取值与将它们作为参数传递有什么 advantages/disadvantages?或者,如果您开始修改实例变量而不是仅仅读取它们,答案会改变吗?

在模块中假设 includes/prepends 的 class 的某些属性完全不是问题。通常是这样做的。事实上,Enumerable 模块假定 class 那个 includes/prepends 它有一个 each 方法,并且有很多方法依赖于它。同样,Comparable 模块假设 including/prepending class 有 <=>。我不能立即想出一个实例变量的例子,但是在这一点上,方法和实例变量之间没有关键的区别;关于实例变量也应该这样说。

不使用实例变量传递参数的缺点是您的方法调用会冗长且不够灵活。

经验法则:Mixin 永远不应该对它们可能包含的 classes/modules 做出任何假设。但是,通常情况下,任何规则都有例外。

但首先,让我们谈谈第一部分。具体来说,访问(取决于)包括 class 个实例变量。如果你的 mixin 依赖于 class 中的任何内容,那么这意味着你不能在父 class 中更改 "anything" 并保证它不会破坏某些东西。此外,您必须不仅在与 mixin 相关的文档中,而且在包含 mixin 的 class/module 的文档中记录 mixin 的依赖性。因为在未来,需求可能会发生变化,或者有人可能会看到重构您的 class/module 代码的机会。显然,那个人不会挖掘那个 class 的文档,也不会知道那个特定的 class/module 在你的文档中有一个部分。

无论如何,通过依赖包含 class 内部结构,不仅你的 mixin 使自己成为依赖项,而且最终使包含它的任何 class/module 成为依赖项。这绝对不是一件好事。因为,您无法控制谁或哪个 class/module 包含了您的 mixin,您将永远没有信心引入更改。没有信心在不担心破坏任何东西的情况下进行更改是项目耗尽!

"workaround" 可能是 - “用测试覆盖它”。但是,考虑一下你自己或其他人在 2 年内维护该代码。你会记得覆盖你的新 class,它包括 mixin,以确保它符合所有 mixin 依赖要求吗?我相信你或新的维护者不会。

因此,从维护或基本 OOP 原则来看,您的 mixin 不得依赖 任何包括 class/module.

现在,让我们谈谈规则位总是有例外。

你可以做一个例外,前提是 mixin 依赖性不会在你的代码中引入 "surprises"。所以,如果 mixin 依赖项在你的团队中是众所周知的或者它们是约定俗成的,那也没关系。另一种情况可能是在内部使用 mixin 并且您控制谁使用它(基本上,当您在自己的项目中使用它时)。

OOP 在开发可维护系统方面的主要优势在于它能够 hide/encapsulate 实现细节。让你的 mixin 依赖于包含它的任何 class,就是把多年的 OOP 经验丢掉 window.

我想说 mixin 不应该对它包含的具体 class 做出假设,但是对一个共同的父class(分别是它的 public 方法)。

很好的例子:可以在将包含在控制器中的混合中调用 params

或者,根据您的示例更准确地说,我认为这样的事情完全没问题:

class Calculation
  attr_accesor :operands
end

module SumOperation
  def sum
    self.operands.sum
  end
end

class MyCustomCalculation < Calculation
  include SumOperation
end

当情况需要时,你应该毫不犹豫地在你的 mixin 模块中包含实例变量,哪怕是一秒钟。

假设,例如,您写道:

class A
  def initialize(h)
    @h = h
  end

  def confirm_colour(colour)
    @h[:colour] == colour
  end

  def confirm_size(size)
    @h[:size] == size
  end

  def confirm_all(colour, size)
    confirm_colour(colour) && confirm_size(size)
  end
end

a = A.new(:colour=>:blue, :size=>:medium, :weight=>10)  
a.confirm_all(:blue, :medium)
  #=> true
a.confirm_all(:blue, :large)
  #=> false

现在假设有人要求也检查重量。我们可以添加方法

def confirm_weight(weight)
  @h[:weight] == weight
end

并将confirm_all更改为

def confirm_all(colour, size)
  confirm_colour(colour) && confirm_size(size) && confirm_weight(size)
end

但有更好的方法:将所有检查放在一个模块中。

module Checks
  def confirm_colour(g)
    @h[:colour] == g[:colour]
  end

  def confirm_size(g)
    @h[:size] == g[:size]
  end

  def confirm_weight(g)
    @h[:weight] == g[:weight]
  end
end

然后通过所有检查将模块包含在A和运行中。

class A
  include Checks
  def initialize(h)
    @h = h
  end

  def confirm_all(g)
    Checks.instance_methods.all? { |m| send(m, g) } 
  end
end

a = A.new(:colour=>:blue, :size=>:medium, :weight=>10) 
a.confirm_all(:colour=>:blue, :size=>:medium, :weight=>10)
  #=> true
a.confirm_all(:colour=>:blue, :size=>:large, :weight=>10)
  #=> false

这样做的好处是,当要添加或删除检查时,只有模块受到影响;无需对 class 进行任何更改。诚然,这个例子是人为设计的,但它是现实世界中的一小步。

虽然做出这些假设很常见,但您可能需要考虑一种不同的模式,以便制作更具可组合性和可测试性的代码:依赖注入。

module Fooable
  def add(one, two)
    one + two
  end
end

class Bar
  attr_accessor :val_one, :val_two
  include Fooable

  def calculate
    add @val_one, @val_two
  end
end

虽然它增加了一个额外的间接层,但通常它是值得的,因为它有可能在更多 类 中使用关注点,并使测试代码更容易。