【问题标题】:Nesting modules in classes and directly calling module functions在类中嵌套模块并直接调用模块函数
【发布时间】:2020-07-04 02:07:56
【问题描述】:

出于测试和管理目的,我希望构建一个类来与 API 进行通信。我已经关闭了连接和身份验证,但正在努力解决类的基本结构和大小。

我的主要目标是保持每个应用程序域的拆分,但仍然可以通过一个类/连接轻松访问。 我已经做了一个更简单的例子来说明我正在寻找的东西。实际上,每个域都有自己的一组业务规则要遵循,这就是为什么我想将它们分开,而 API 连接保持不变。

例如,在 CLI 级别我想调用:

$ client_one = Api.new("one")
$ client_two = Api.new("two")

$ client_one.Bikes.delete(1)
> deleted bike 1 from one

$ client_two.Phones.new(phone)
> posted phone iPhone to two

我的想法是将模块嵌套在 Api 类中,但我无法让它工作或找到正确的语法。

class Api
  def initialize(client)
    @client = client
    @connection = Authentication.get_connection(@client)
  end

  #preferable put each submodule in a separate file
  module Authentication
    def get_connection(client)
      #code to get Faraday connection
    end
  end

  module Bikes
    def new(object)
      #code to post new bike
      @connection.post(object)
      puts "posted bike #{object.name} to #{@client}"
    end
    def delete(id)
      #code to delete old bike
      @connection.delete(id)
      puts "deleted bike #{id} from #{@client}"
    end
  end

  module  Phones
    def new(object)
      #code  to post new phone
      @connection.post(object)
      puts "posted phone #{object.name} to #{@client}"
    end
  end
end

这会导致如下错误:

NoMethodError: undefined method `Bikes' for #<Api:0x0000000003a543a0>

是否有可能实现我的目标,还是有更好的“Ruby”方法来实现它?

此外,是否可以将子模块拆分为不同的文件?例如:

api.rb
modules
  + -- authentication.rb
  + -- bikes.rb
  + -- phones.rb

【问题讨论】:

    标签: ruby


    【解决方案1】:

    对于您的示例中 Ruby OOP 的工作方式存在一些基本的误解,如果没有完整的代码示例,也没有机会询问您要完成的工作,很难指导您找到最合适的答案.我给出的任何答案都将部分基于经验,部分基于意见,因此您也可能会看到其他答案。

    在高层次上,您应该在模块中包含类,而不是在类中包含模块。虽然您可以将模块放入类中,但您最好在执行此操作之前对why you're doing that 有一个很好的了解。

    接下来,您在其中定义的模块和方法不会自动被父类的实例访问,因此 client.Bikes 将永远无法工作,因为 Ruby 期望在 @987654327 中找到名为 Bikes 的实例方法@ 班级;它不会寻找具有该名称的模块。

    访问您定义的模块和模块方法的唯一方法是在类/模块级别使用它们。所以如果你有这个:

    class Foo
      module Bar
        def baz
          puts 'foobarbaz'
        end
      end
    end
    

    您可以在类/模块级别执行此操作:

    Foo::Bar.baz
    foobarbaz
    => nil
    

    但是你不能在实例级别做任何事情:

    Foo.new::Bar.baz
    TypeError: #<Foo:0x00007fa037d39260> is not a class/module
    
    Foo.new.Bar.baz
    NoMethodError: undefined method `Bar' for #<Foo:0x00007fa037162e28>
    

    因此,如果您到目前为止了解示例的结构为何不起作用,那么您可以着手构建一些更明智的东西。让我们从命名和类/模块结构开始。

    首先,Api 在这里是个糟糕的名字,因为您通常会将Api 用于提供 API 的东西,而不是连接到 API,所以我建议稍微命名更具描述性并使用模块来表明您正在封装一个或多个相关类:

    module MonthyApiClient
    end
    

    接下来,我建议添加一个 Client 类来封装与实例化用于连接 API 的客户端相关的所有内容:

    module MonthyApiClient
      class Client
        def initialize
          @client = nil # insert your logic here
          @connection = nil # insert your logic here
        end
      end
    end
    

    您的代码示例中 clientconnection 之间的关系尚不清楚,因此为简单起见,我将假设它们可以组合成一个类 (Client),并且我们正在删除完全模块Authentication

    接下来,我们需要一种合理的方式将module Bikesmodule Phones 集成到这段代码中。将它们转换为类是没有意义的,因为不需要实例化它们。这些纯粹是帮助函数,为Client 的实例做一些事情,所以它们应该是该类中的实例方法:

    module MonthyApiClient
      class Client
        def initialize
          # insert your logic here
          @client = nil
          @connection = nil
        end
    
        def create_bike
          # insert your logic here
          # e.g., @connection.post(something)
        end
    
        def delete_bike
          # insert your logic here
          # e.g., @connection.delete(something)
        end
    
        def create_phone
          # insert your logic here
          # e.g., @connection.post(something)
        end
      end
    end
    

    请注意,我们已将 new 替换为 create;你不想在 Ruby 中命名一个方法 new,在上下文中我们使用这个 new 意味着 实例化但不保存新对象create 意味着实例化并保存一个新对象

    现在我们已经到了这里,并且我们已经通过将它们的逻辑移到其他地方来消除所有嵌套模块,我们可以看到我们最初设置的父模块是不必要的冗余,并且可以消除它:

    class MonthyApiClient
      def initialize
        # insert your logic here
        @client = nil
        @connection = nil
      end
    
      def create_bike
        # insert your logic here
        # e.g., @connection.post(something)
      end
    
      def delete_bike
        # insert your logic here
        # e.g., @connection.delete(something)
      end
    
      def create_phone
        # insert your logic here
        # e.g., @connection.post(something)
      end
    end
    

    那么你就可以完成你最初的目标了:

    client_one = MonthyApiClient.new
    client_one.create_bike
    client_two = MonthyApiClient.new
    client_two.create_phone
    

    通过这个解释,我认为您的原始代码是一个花费大量时间试图过早过度优化的示例。最好先规划好你的业务逻辑,让它尽可能简单。 https://softwareengineering.stackexchange.com/a/80094 有一些很好的信息可能有助于解释这个概念。

    我什至没有尝试优化我在这里展示的代码,因为我不知道创建和删除自行车和手机之间有多少共同点。有了这个功能类,并且更好地理解了这个应用程序中的其他代码,我可能会尝试 DRY 它(这可能意味着回到拥有一个带有 Client 类和模块方法或其他类的模块封装 DRY 逻辑),但现在尝试还为时过早。

    您的最后一个问题是关于如何为模块和类构建文件和目录,我建议您参考Ideal ruby project structure(以及本网站上的许多其他问题)以获取更多信息。

    【讨论】:

    • 谢谢另一个!我想我理解你的解释以简化结构。在de方法名中使用域名解决了我的过度思考。为了进一步拆分域,我可以将它们放在模块中(在不同的文件中)并将它们包含在 MonthyApiClient 类中,对吗?
    • 这是一种可能的方法,是的。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-07-24
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-01-28
    • 1970-01-01
    相关资源
    最近更新 更多