在 rails 5 上 ruby 上重构 类

Stuck with refactoring classes on ruby on rails 5

我有一个 class 具有不同的方法,但在这些方法上我需要在进行一些调用之前检查访问令牌

class SomeClass
    def initialize
        @client = SomeModule::Client.new
    end
    def get_intervention_chart(subId:, projectId:, interventionId:)
        @client.check_presence_of_access_token()
        SomeModule::Service::Project.new(@client).get_intervention_chart(subId: subId, projectId: projectId, interventionId: interventionId)
    end
    
    def get_intervention_documents(subId:, projectId:, interventionId:)
        @client.check_presence_of_access_token()
        SomeModule::Service::Project.new(@client).get_intervention_documents(subId: subId, projectId: projectId, interventionId: interventionId)
    end
end

如您所见,我调用了方法“check_presence_of_access_token”,它检查访问令牌是否存在以及是否可以使用,如果没有,它会获取另一个并将其存储在文件中。

有我的客户 class :

class Client
        class Configuration
            attr_accessor :access_token 
            attr_reader :access_token_path, :endpoint, :client_id, :client_secret, :subId
    
            def initialize
                @access_token = ''
                @access_token_path = Rails.root.join('tmp/connection_response.json')
                @endpoint = ENV['TOKEN_ENDPOINT']
                @client_id    = ENV['CLIENT_ID']
                @client_secret = ENV['CLIENT_SECRET']
                @subId = "SOME_ID"
            end
        end
        def initialize
            @configuration = Configuration.new
        end

        # Check if the file 'connection_response' is present and if the token provided is still valid (only 30 min)
        def check_presence_of_access_token          
            if File.exist?(self.configuration.access_token_path.to_s)
                access_token = JSON.parse(File.read(self.configuration.access_token_path.to_s))["access_token"]
                if access_token
                    jwt_decoded = JWT.decode(access_token, nil, false).first
                    # we want to check if the token will be valid in 60s to avoid making calls with expired token
                    if jwt_decoded["exp"] > (DateTime.now.to_i + 60)
                        self.configuration.access_token = access_token
                        return
                    end
                end
            end
            get_token()
        end
        def get_token
            config_hash = Hash.new {}
            config_hash["grant_type"] = "client_credentials"
            config_hash["client_id"] = self.configuration.client_id
            config_hash["client_secret"] = self.configuration.client_secret

            response = RestClient.post(self.configuration.endpoint, config_hash, headers: { 'Content-Type' => 'application/x-www-form-urlencoded' })
            response_body = JSON.parse(response.body)
            self.configuration.access_token = response_body["access_token"]

            stock_jwt(response_body.to_json)
        end

        def stock_jwt(response_body)
            File.open(self.configuration.access_token_path.to_s, 'w+') do |file|
                file.write(response_body)
            end
        end
end

我不知道如何重构这个,你能帮我吗?

面向对象语言的一般原则是“惰性”,并尽可能晚地推迟决策。我们将使用该原则来重构您的代码,使令牌在过期时自动刷新,and/or 如果尚未刷新则自行获取。

还有一组原则统称为SOLID。我们也将使用这些原则。

关于方法和模块,我要提到的最后一个原则是“越小越好”。将其与 SOLID 中的“S”(单一职责)结合起来,您会发现重构包含更多但更小的方法。

撇开原则不谈,从问题陈述中不清楚令牌是短暂的 (只持续一个“会话”) 还是长期的 (例如:比单次会话持续时间更长).

如果令牌是长期存在的,那么将它存储到一个文件中是可以的,如果使用它的唯一进程在相同系统上。

如果多个 Web 服务器将使用此代码,那么除非每个服务器都有自己的令牌,否则令牌应使用某种数据存储在所有系统之间共享,例如 Redis,MySQL, 或 Postgres.

由于您的代码使用的是文件,我们假设同一系统上的多个进程可能正在共享令牌。

鉴于这些原则和假设,这里是对您的代码的重构,使用文件存储令牌,使用“惰性”延迟、模块化逻辑。

class Client
  class Configuration
    attr_accessor :access_token
    attr_reader :access_token_path, :endpoint, :client_id, :client_secret, :subId


    def initialize
      @access_token      = nil
      @access_token_path = Rails.root.join('tmp/connection_response.json')
      @endpoint          = ENV['TOKEN_ENDPOINT']
      @client_id         = ENV['CLIENT_ID']
      @client_secret     = ENV['CLIENT_SECRET']
      @sub_id            = "SOME_ID"
    end
  end
  
  attr_accessor :configuration
  delegate :access_token, :access_token_path, :endpoint, :client_id, :client_secret, :sub_id,
           to: :configuration

  TOKEN_EXPIRATION_TIME = 60 # seconds

  
  def initialize
    @configuration = Configuration.new
  end

  # returns a token, possibly refreshed or fetched for the first time
  def token
    unexpired_token || new_token
  end

  # returns an expired token
  def unexpired_token
    access_token unless token_expired?
  end

  def access_token
    # cache the result until it expires
    @access_token ||= JSON.parse(read_token)&.fetch("access_token", nil)
  end

  def read_token
    File.read(token_path)
  end

  def token_path
    access_token_path&.to_s || raise("No access token path configured!")
  end

  def token_expired?
    # the token expiration time should be in the *future*
    token_expiration_time.nil? || 
      token_expiration_time < (DateTime.now.to_i + TOKEN_EXPIRATION_TIME)
  end

  def token_expiration_time
    # cache the token expiration time; it won't change
    @token_expiration_time ||= decoded_token&.fetch("exp", nil)
  end

  def decoded_token
    @decoded_token ||= JWT.decode(access_token, nil, false).first
  end

  def new_token
    @access_token = store_token(new_access_token)
  end

  def store_token(token)
    @token_expiration_time = nil # reset cached values
    @decoded_token = nil
    IO.write(token_path, token)
    token
  end

  def new_access_token
    parse_token(request_token_response)
  end

  def parse_token(response)
    JSON.parse(response.body)&.fetch("access_token", nil)
  end

  def request_token_response
    RestClient.post(
      endpoint,
      credentials_hash,
      headers: { 'Content-Type' => 'application/x-www-form-urlencoded' }
    )
  end

  def credentials_hash
    {
      grant_type:    "client_credentials",
      client_id:     client_id || raise("Client ID not configured!"),
      client_secret: client_secret || raise("Client secret not configured!")
    }
  end
end

那么,这是如何工作的?

假设客户端代码使用token,仅评估token方法将导致检索或刷新未过期的令牌(如果它存在),或者获取一个新的 (如果它不存在).

因此,在使用 @client 连接之前无需“检查”令牌。当 @client 连接使用令牌时,正确的事情就会发生。

不会更改的值会被缓存,以避免必须重做生成它们的逻辑。 eg:不需要重复解码JWT字符串。

当当前令牌时间到期时,token_expired? 将变为真,导致其调用者为 return nil,导致该调用者获取 new_token,然后将其存储。

这些小方法的最大优点是每个方法都可以独立测试,因为它们每个都有一个非常简单的目的。

祝你项目顺利!