在 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
,然后将其存储。
这些小方法的最大优点是每个方法都可以独立测试,因为它们每个都有一个非常简单的目的。
祝你项目顺利!
我有一个 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
,然后将其存储。
这些小方法的最大优点是每个方法都可以独立测试,因为它们每个都有一个非常简单的目的。
祝你项目顺利!