RubyMotion async programming with BubbleWrap

狂风中的少年 提交于 2019-12-22 11:24:58

问题


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

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!