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_one
和 val_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
虽然它增加了一个额外的间接层,但通常它是值得的,因为它有可能在更多 类 中使用关注点,并使测试代码更容易。
我找到了一个 mixin 的例子,它假设包含 class 的实例变量有哪些。像这样:
module Fooable
def calculate
@val_one + @val_two
end
end
class Bar
attr_accessor :val_one, :val_two
include Fooable
end
我找到了支持和反对这是否是一个好习惯的论点。明显的替代方法是将 val_one
和 val_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
虽然它增加了一个额外的间接层,但通常它是值得的,因为它有可能在更多 类 中使用关注点,并使测试代码更容易。