【问题标题】:Whats a good ruby idiom for breaking up a large class into modules?将大类分解为模块的好 ruby​​ 习惯用法是什么?
【发布时间】:2010-12-30 23:59:45
【问题描述】:

我的班级很大,有很多方法,而且开始变得有点杂乱无章,难以导航。我想把它分解成模块,每个模块都是类和实例方法的集合。也许是这样的:

更新:我现在意识到这是一个非常糟糕的例子。您可能不想将验证或属性移出核心类。

class Large
  include Validations
  include Attributes
  include BusinessLogic
  include Callbacks
end

在阅读了 Yehuda 关于Better Ruby Idioms 的帖子后,我很好奇其他人是如何解决这个问题的。这是我能想到的两种方法。

第一种方法

module Foo
  module Validations
    module ClassMethods
      def bar
        "bar"
      end
    end

    module InstanceMethods
      def baz
        "baz"
      end
    end
  end

  class Large
    extend Validations::ClassMethods
    include Validations::InstanceMethods
  end
end

第二种方法

module Foo
  module Validations
    def self.included(base)
      base.extend ClassMethods
    end

    module ClassMethods
      def bar
        "bar"
      end
    end

    def baz
      "baz"
    end
  end

  class Base
    include Validations
  end
end

我的问题是:

  • 有没有更好的方法来做到这一点?
  • 如何以最少的魔法为一组类/实例方法获得单线模块 mixin?
  • 如何在不给类本身命名空间的情况下将这些模块命名为基类?
  • 您如何组织这些文件?

【问题讨论】:

标签: ruby refactoring module


【解决方案1】:

将一个类分解成模块,虽然很诱人(因为在 Ruby 中很容易),但这很少是正确的答案。我通常认为打破模块的诱惑是代码告诉我它想要被拆分为更集中的类的方式。一个大到你想把它分成多个文件的类几乎肯定会违反单一职责原则。

编辑:详细说明为什么将代码分解成模块是一个坏主意:这会让读者/维护者感到困惑。一个类应该代表一个单一的紧密关注的概念。当您必须滚动数百行才能找到在长类文件的另一端使用的实例方法的定义时,这已经够糟糕的了。当您遇到实例方法调用并不得不在另一个文件中查找它时,情况会更糟。

【讨论】:

  • 我同意你的观点,这可能是代码异味。我还认为可读性的关键是将代码组织成小的、紧密关注的部分。这是我将一个类分成更小的子部分的动机。以 Rails 为例,许多人通过将身份验证逻辑从模块中包含到他们的模型和控制器中,从而将身份验证逻辑与其他代码分开。
  • 如果您只想将代码拆分为文件,则不必使用模块;您可以重新打开每个文件中的类。不过,这仍然是个坏主意。最好将功能剥离到较小的类中,每个类都是与较大类的特定部分相关的一组紧密的业务逻辑、验证、回调等。毕竟,验证也是业务规则的一部分;将它们从其他业务逻辑中移出 更远 是没有意义的。
【解决方案2】:

按照 Avdi 所说的,在将任何东西放入模块之前,我会做这些事情:

  1. 此模块是否可以或将用于任何其他类?
  2. 将这些模块的功能提取到不同的类或基类中是否有意义?

如果 1 的答案是否定的,而 2 的答案是肯定的,那么恕我直言,这表明最好有一个类而不是一个模块。

另外,我认为将属性放在模块中在概念上是错误的,因为类从不与任何其他类共享它们的属性或实例变量,或者换句话说,它们的内部状态。一个类的属性只属于那个类。

业务逻辑确实属于类本身,如果 A 类的业务逻辑与 C 类有一些共同的职责,则需要将其提取到基类中以使其清晰,而不是仅仅将其放入模块中。

【讨论】:

  • 在 Ruby 中,“属性”一般不是指外界可以看到的关于对象的什么吗?
【解决方案3】:

标准的成语好像是

foo.rb
foo/base.rb
foo/validations.rb
foo/network.rb
foo/bar.rb

而 foo.rb 会是这样的

class Foo
  include Foo::Base
  include Foo::Validations
  include Foo::Network
  include Foo::Bar
end

这是标准的成语,它可以很好地让你把事情分开。不要做类方法与实例方法。这些通常是相当随意的区别,您最好将处理类似主题的代码放在一起。这将最大限度地减少您为任何给定更改而必须接触的文件数量。

注意:Rails 可能会被这样的嵌套模型弄糊涂,至少如果一切都是类的话。我认为所有嵌套文件都只是模块,它会做得更好,但你必须看看。我仍然建议这样做,因为它是 Ruby 社区使用的常规习惯用法,但是您可能必须避免在 Rails 模型中同时包含 foo.rb 和 foo/ 目录(如果这是您正在谈论的那种类)。

【讨论】:

    【解决方案4】:

    虽然包含不同的模块会起作用,但通常比简单地在多个地方重新打开类更麻烦。

    有一个(非常简单的)gem 可以用来让它尽可能漂亮:concerned_with

    示例(来自自述文件)

    # app/models/user.rb
    class User < ActiveRecord::Base
      concerned_with :validations,
                     :authentication
    end
    
    # app/models/user/validations.rb
    class User < ActiveRecord::Base
      validates_presence_of :name
    end
    
    #app/models/user/authentication.rb
    class User < ActiveRecord::Base
      def self.authenticate(name, password)
        find_by_name_and_password(name, password)
      end
    end
    

    【讨论】:

    • 谢谢,这很有趣。看起来它利用了 ActiveSupport::Dependencies#require_dependency,这并不简单。从概念上讲,非常简单。重新打开课程。
    【解决方案5】:

    我倾向于使用 Ruby 的 duck typing 接口方法,它基本上允许您向任何对象发送任何消息,然后评估如何处理它。

    这种方法使我能够坚持 Avdi 提到的相同模式,保持课程小而简洁——只负责一件事。 Ruby 的伟大之处在于您可以将职责委托给其他简洁的类,而无需将任何逻辑混为一谈。例如:

    class Dog
      def initialize(name)
        @name = name
      end
    
      def bark  
        "woof"
      end
    
      def fetch(object)
        "here's that #{object}"
      end
    
      def sit
        "sitting down"
      end
    
      private
      attr_accessor :name
    end
    

    这里有我的狗类,其中包含大量与狗相关的方法。它们都是特定于狗的,所以可以愉快地住在这里。但是,如果这些方法有点复杂,调用其他方法或者这只狗学习了一堆新技巧,就会出现问题!?所以我可以将它们分离到它们自己的类中,然后将责任委托给它们,如下所示:

    class Tricks
      def initialize(name)
        @name = name
      end
    
      def fetch(object)
        "here's that #{object}"
      end
    
      def sit
        "sitting down"
      end
    
      def come_when_called(my_name)
        "I'm coming" if my_name == name
      end
    
      def put_toy_away(object)
        "#{fetch(object)}, I'll put it away"
      end
    
      private 
      attr_reader :name
    end
    
    class Dog
      def initialize(name)
        @name = name
      end
    
      delegate :sit, :fetch, :come_when_called, :put_away_toy, to: :tricks_klass
    
      def bark  
        "woof"
      end
    
      private
      attr_accessor :name
    
      def tricks_klass
        @tricks_klass ||= Tricks.new(name)
      end
    end
    

    所以现在,Dog 类真正开始表现得像与狗相关的行为的接口,而这些技巧不再与之耦合。这将使测试更容易,因为它能够实例化一个 Tricks 对象并更通用地测试它,而不需要 Dog(因为它们并不总是听)。

    现在,我们可以有一个 Cat 类,它也将责任委托给这个 Tricks 类 - 不过,那将是一只聪明的 Cat!

    您现在也可以单独使用 Tricks 类 - 这是将单个行为封装在其自己的类中的强大功能。您甚至可以进一步区分这些行为 - 但只有作为开发人员的您才知道这样做是否值得!

    【讨论】:

      猜你喜欢
      • 2017-08-13
      • 1970-01-01
      • 1970-01-01
      • 2020-01-03
      • 1970-01-01
      • 1970-01-01
      • 2021-02-09
      • 2017-12-09
      • 1970-01-01
      相关资源
      最近更新 更多