代码之家  ›  专栏  ›  技术社区  ›  Ben Marini

把一个大类分解成模块有什么好的Ruby习语?

  •  3
  • Ben Marini  · 技术社区  · 15 年前

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

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

    class Large
      include Validations
      include Attributes
      include BusinessLogic
      include Callbacks
    end
    

    读完耶胡达的帖子后 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
    

    我的问题是:

    • 有更好的方法吗?
    • 如何用最少的魔力为一组类/实例方法获得一行模块混合?
    • 在不使用类本身的名称空间的情况下,如何将这些模块命名为基类?
    • 你如何组织这些文件?
    5 回复  |  直到 7 年前
        1
  •  10
  •   Avdi    15 年前

    将一个类分解成模块,虽然很诱人(因为在Ruby中很容易做到),但这很少是正确的答案。我通常认为分解模块的诱惑是代码告诉我它希望拆分成更紧密关注的类的方式。如果一个类太大,你想把它分成多个文件,那么它肯定违反了单一责任原则。

    编辑:详细介绍一下 为什么 将代码分解成模块是一个坏主意:它会让读卡器/维护人员感到困惑。类应该表示一个集中度很高的概念。当您需要滚动数百行来查找长类文件另一端使用的实例方法的定义时,这已经够糟糕了。更糟糕的是,当您遇到一个实例方法调用并且必须在另一个文件中查找它时。

        2
  •  3
  •   nas    15 年前

    在完成了avdi所说的操作之后,在将任何内容放入模块之前,我会先完成以下操作:

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

    如果1的答案是“否”,2的答案是“是”,那么imho表示最好有一个类,而不是一个模块。

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

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

        3
  •  1
  •   Marc-André Lafortune    15 年前

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

    有一种宝石(非常简单),你可以用它来使它尽可能漂亮: 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
    
        4
  •  0
  •   edebill    15 年前

    标准的成语似乎是

    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/目录(如果这是您所讨论的类的话)。

        5
  •  0
  •   JayJay    7 年前

    我喜欢用红宝石的 鸭子打字 接近接口,它基本上允许您向任何对象发送任何消息,然后评估如何处理它。

    这种方法允许我坚持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
    

    这里我们有我的dog类,它有很多与dog相关的方法。它们都是特定于狗的,所以可以在这里愉快地居住。但是,如果这些方法变得有点复杂,调用其他方法,或者这只狗学习了一系列新的技巧,就会有问题!所以我可以把它们分成自己的类,然后把责任委托给那些类,比如:

    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类真的开始表现得像一个与dog相关行为的接口,而这些技巧不再与之耦合。这将使测试更容易,因为可以实例化一个技巧对象并更一般地测试它,而不需要一只狗(因为它们不总是听)。

    现在,我们可以有一个将责任委托给这个技巧类的cat类-尽管,那将是一个聪明的cat!

    现在您还可以单独使用tricks类——这是封装单个行为的强大功能,它是自己的类。您甚至可以进一步分离这些行为-但只有作为开发人员的您知道这是否值得!