Should mixins make assumptions about their including class?

好久不见. 提交于 2019-12-23 12:04:11

问题


I found examples of a mixin that makes assumptions about what instance variables an including class has. Something like this:

module Fooable
  def calculate
    @val_one + @val_two
  end
end

class Bar
  attr_accessor :val_one, :val_two
  include Fooable
end

I found arguments for and against whether it's a good practice. The obvious alternative is passing val_one and val_two as parameters, but that doesn't seem as common, and having more heavily parameterized methods could be a downside.

Is there conventional wisdom regarding a mixin's dependence on class state? What are the advantages/disadvantages of reading values from instance variables vs. passing them in as parameters? Alternatively, does the answer change if you start modifying instance variables instead of just reading them?


回答1:


It is not a problem at all to assume in a module some properties about the class that includes/prepends it. That is usually done. In fact, the Enumerable module assumes that a class that includes/prepends it has a each method, and has many methods that depend on it. Likewise, the Comparable module assumes that the including/prepending class has <=>. I cannot immediately come up with an example of an instance variable, but there is not a crucial difference between methods and instance variables regarding this point; the same should be said about instance variables.

Disadvantage of passing arguments without using instance variable is that your method call will be verbose and less flexible.




回答2:


Rule of thumb: Mixins should never make any assumptions about the classes/modules they may be included in. However, as it usually goes, any rule has exceptions.

But first, let's talk about the first part. Specifically, accessing (depending on) including class instance variables. If your mixin depends on anything within the including class, then it means that you can not change that "anything" in the parent class with a guarantee that it would not break something. Also, you will have to document that dependency of mixin not only in documentation related to mixin, but also in the documentation of the class/module that includes the mixin. Because, down the road, the requirements may change or someone might see an opportunity in refactoring your class/module code. Obviously, that person will not dig for that class's documentation or know that that specific class/module has a section in your documentation.

Anyhow, by depending on including class internals, not only your mixin made itself dependant, but also ended up making any class/module that includes it a dependant. Which is definitely not a good thing. Because, you can not control who or which class/module has included your mixin, you will never have a confidence to introduce a change. Not having that confidence to change without a fear of breaking anything is project drainer!

The "workaround" may be - "covering it with test". But, consider yourself or someone else maintaining that code in 2 years. Will you remember to cover your new class, that includes the mixin, to make sure it complies with all mixin dependency requirements? I am sure you or the new maintainer will not.

So, from the maintenance or basic OOP principles, your mixin must not depend on any including class/module.

Now, let's talk about that there is alway an exception to the rule bit.

You can make an exception, provided that mixin dependency does not introduce "surprises" to your code. So, it is ok, if the mixin dependencies are well known among your team or they are a convention. Another case could be when the mixin is used internally and you control who uses it (basically, when you are using it within your own project).

The key advantage of OOP in developing maintainable systems was its ability to hide/encapsulate the implementation details. Making your mixin dependant on any class that includes it, is throwing all the years of OOP experience out the window.




回答3:


I'd say a mixin should not make assumptions about a specific class it is included in, but it's totally fine to make assumptions about a common parent class (respectively its public methods).

Good example: It's fine to call params in a mixin that will be included in controllers.

Or, to be more precise according your example, I'd think something like this would totally be fine:

class Calculation
  attr_accesor :operands
end

module SumOperation
  def sum
    self.operands.sum
  end
end

class MyCustomCalculation < Calculation
  include SumOperation
end



回答4:


You should not hesitate, even for a second, to include instance variables in your mixin module when the situation calls for it.

Suppose, for example, you wrote:

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

Now suppose someone asks for the weight be checked as well. We could add the method

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

and change confirm_all to

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

but there is a better way: put all the checks in a module.

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

Then include the module in A and run through all the checks.

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

This has the advantage that when checks are to be added or removed, only the module is affected; no changes need to be made to the class. This example is admittedly contrived, but it is a short step to real-world situations.




回答5:


Although it is common to make these assumptions, you may want to consider a different pattern in order to make more composable and testable code: Dependency Injection.

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

Although it adds an extra layer of indirection, often times it will be worth it because of the potential to use the concern across a larger number of classes, as well as making it easier to test code.



来源:https://stackoverflow.com/questions/36723178/should-mixins-make-assumptions-about-their-including-class

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!