问题
I am confused with how to write decent code when using a lot of asynchronous code.
In the following code snippet I log in to get the authentication cookie and use that cookie for the next request to get a list of projects name (as an example):
def self.populateProjectsTable(projects_controller)
payload = {email: "email", password: "pass"}
HTTP.post("http://example.com/login", {payload: payload}) do |response|
authCookie = response.headers['Set-Cookie']
HTTP.get("http://example.com/projects.json", {cookie: authCookie}) do |response|
projects = JSON.parse(response.body.to_str)
projects_controller.projects = projects
projects_controller.reloadData
end
end
end
While this will work the code feels dirty. Not really following the single responsibility principle. I would like to extract this in a few methods:
def self.populateProjectsTable(projects_controller)
@taskList = TaskList.new
@taskList.doLogin
projects = @taskList.getProjects
projects_controller.projects = projects
projects_controller.reloadData
end
def doLogin
payload = {email: "email", password: "pass"}
HTTP.post("http://example.com/login", {payload: payload}) do |response|
@authCookie = response.headers['Set-Cookie']
end
end
def getProjects
HTTP.get("http://example.com/projects.json", {cookie: @authCookie}) do |response|
projects = JSON.parse(response.body.to_str)
end
end
This obviously does not work. The getProjects
method is called before doLogin
is finished and the projects are only known in the scope of the block, not giving back the data to the populateProjectsTable
method.
How does one program such applications without the nesting shown in the first example?
回答1:
You're not going to totally get away from the nesting. Taking Alan's answer and massaging it a bit, this is what I've come up with. It involves passing a block through a couple of methods.
def self.populateProjectsTable(projects_controller)
@taskList = TaskList.new
@taskList.loginAndGetProjects do |projects|
projects_controller.projects = projects
projects_controller.reloadData
end
end
def loginAndGetProjects(&block)
payload = {email: "email", password: "pass"}
HTTP.post("http://example.com/login", {payload: payload}) do |response|
@authCookie = response.headers['Set-Cookie']
getProjects(&block)
end
end
def getProjects(&block)
HTTP.get("http://example.com/projects.json", {cookie: @authCookie}) do |response|
projects = JSON.parse(response.body.to_str)
block.call(projects)
end
end
回答2:
I've had a similar problem trying to wrap methods that themselves took blocks. I wanted the new wrapper methods to still be able to take blocks. Here's what I did in ParseModel:
# with block:
# ParseModel::Cloud.callFunction("myFunction", {"myParam" => "myValue"}) do |result, error|
# # do something...
# end
# without block:
# ParseModel::Cloud.callFunction("myFunction", {"myParam" => "myValue"})
module ParseModel
class Cloud
def self.callFunction(function, params={}, &block)
return PFCloud.callFunction(function, withParameters:params) unless block_given?
PFCloud.callFunctionInBackground(function, withParameters:params, block:lambda do |result, error|
block.call(result, error)
end)
end
end
end
Applying this concept to your problem, you could rewrite your methods to take blocks themselves. Here's a bit of a refactor that I think might be helpful:
def self.populateProjectsTable(projects_controller)
@taskList = TaskList.new
@taskList.doLogin do |login_response|
authCookie = login_response.headers['Set-Cookie']
@taskList.getProjects(authCookie) do |projects_response|
projects = JSON.parse(projects_response.body.to_str)
projects_controller.projects = projects
projects_controller.reloadData
end
end
end
def doLogin(&block)
payload = {email: "email", password: "pass"}
HTTP.post("http://example.com/login", {payload: payload}) do |response|
block.call(response)
end
end
def getProjects(cookie, &block)
HTTP.get("http://example.com/projects.json", {cookie: cookie}) do |response|
block.call(response)
end
end
I don't think you're totally out of the woods regarding SRP, but this should be a good start.
回答3:
+1 for Jamon's answer.
I might suggest using a class to manage your session and splitting out the API into a module if you like SRP. This is especially helpful as you add additional API calls. Here I queue up requests that will be satisfied once login is completed. Later you can add handling for timeouts, etc.
module ProjectApi
def get_projects(&block)
with_session do
HTTP.get("http://example.com/projects.json", {cookie: @auth_cookie}) do |response|
projects = JSON.parse(response.body.to_str)
block.call(projects)
end
end
end
end
class MySession
include ProjectApi
def initialize(login, password)
@login = login
@password = password
@state = nil
@requests = []
end
def active?
@state == :active
end
def with_session(&block)
@requests << &block
active? ? handle_requests : login(true)
end
private
def login(do_handle_requests = false)
payload = {login: @login, password: @password}
@state = nil
HTTP.post("http://example.com/login", {payload: payload}) do |response|
@state = :active
@auth_cookie = response.headers['Set-Cookie']}
handle_requests if do_handle_requests
end
end
def handle_requests
while request = @requests.shift do
request.call
end if active?
end
end
def self.populateProjectsTable(projects_controller)
@session ||= MySession.new('mylogin', 'mypassword')
@session.get_projects do |projects|
projects_controller.projects = projects
projects_controller.reloadData
end
end
来源:https://stackoverflow.com/questions/13167304/rubymotion-async-programming-with-bubblewrap