在铁轨中放置给定逻辑的位置?

时间:2016-05-03 18:28:33

标签: ruby-on-rails ruby

我正在编写一个用户输入日期的应用程序,然后系统从外部API获取该周的历史天气数据(我假设星期三是整周的代表)。出于某些原因,我不想为每个日期进行实时调用 - 我想获取它一次并在现场持续存在。

在Spring中,我将大部分内容放入服务层。由于我是Rails的新手,我不知道在哪里放置某些逻辑,但这是我的建议:

WeatherController

def create
  transform date entered by user to Wednesday of the same week.
  Check if there is a already record for that date, if not, fetch the JSON from external API.
  Parse JSON to Ruby object, save.
  Return the weather data.

WeatherModel

  validate if the date is indeed Wednesday
  validate if entered date is unique

3 个答案:

答案 0 :(得分:1)

在rails中,我更喜欢创建PORO(计划旧的ruby对象)来处理我的应用程序中的大多数核心逻辑。通过这样做,我们可以使控制器变得简单,并且我们的模型没有与将数据保存到数据库无关的逻辑。如果你不从我们的模型中保留不必要的逻辑,它们将变得臃肿并且极难测试。

我最常用的两个PORO模式是actionsservices

actions通常直接与一个控制器动作相关联。

以你的例子来创建一个。我们将创建一个WeatherCreator类。我喜欢疯狂明确的名字。您问WeatherCreator什么?它当然会创建一个Weather记录!

# app/actions/weather_creator.rb
class WeatherCreator

 attr_reader :weather

 def initialize(args={})
   @date = args.fetch(:date)
   @weather = Weather.new
 end

 def create
   build_record
   @weather.save
 end

 private

 def build_record
   # All of your core logic goes here!
   # Plus you can delegate it out to various private methods in the class
   #
   # transform date entered by user to Wednesday of the same week.
   # Check if there is a already record for that date, if not, fetch the JSON from external API.
   # Parse JSON to Ruby object, save.
   #
   # Add necessary data to your model in @weather
 end

end

然后在我们的控制器中,我们可以使用action

# app/controllers/weather_controller.rb
class WeatherController < ApplicatonController

  def create
    creator = WeatherCreator.new(date: params[:date])
    if creator.create
      @weather = creator.weather
      render :new
    else
      flash[:success] = "Weather record created!"
      redirect_to some_path
    end
  end

end

现在你的控制器很简单。

这样做的好处是您的测试工作可以只关注action逻辑对象及其界面。

# spec/actions/weather_creator_spec.rb
require 'rails_helper'

RSpec.describe WeatherCreator do

  it "does cool things" do
    creator = WeatherCreator.new(date: Time.zone.now)
    creator.create
    expect(creator.weather).to # have cool things
  end

end
另一方面,

service个对象将存在于app/services/中。不同之处在于这些对象在应用程序的许多地方使用,但逻辑​​和测试实践的相同隔离适用。

根据您的应用程序,您可以为各种目的创建不同类型的POROS,因为一般service对象类别也会失控。

为了清楚起见,您可以使用不同的命名做法。因此,我们可以使用WeatherCreator课程,而不是将其称为WeatherCreatorActionAction::WeatherCreator。有些可以使用服务SomeLogicServiceService::SomeLogic

尽可能使用您喜好和风格的套房。干杯!

答案 1 :(得分:1)

通常,我不会将逻辑放在create动作中。即使您正在制作某些内容,您网站的用户也会要求您show天气。用户应该忘记您从哪里获取信息以及如何缓存信息。

选项1 - 使用Rails缓存

一种选择是在show动作中使用Rails caching。在该操作中,您将对API进行阻塞调用,然后Rails将返回值存储在缓存存储中(例如Redis)。

def show
  date = Date.parse params[:date]
  @info_to_show = Rails.cache.fetch(cache_key_for date) do
    WeatherAPIFetcher.fetch(date)
  end
end

private

def cache_key_for(date)
  "weather-cache-#{date + (3 - date.wday)}"
end

选项2:使用ActiveJobs进行非阻塞调用

上面的选项1将使您已经积累的数据访问有些尴尬(例如,对于统计数据,图表等)。此外,它在您等待API端点的响应时阻止服务器。如果这些都不是问题,您应该考虑选项1,因为它非常简单。如果您需要更多,以下是存储您在数据库中提取的数据的建议。

我建议存储数据的模型和检索数据的异步作业。请注意,您需要为WeatherFetcherJob设置ActiveJob

# migration file
create_table :weather_logs do |t|
  t.datetime :date

  # You may want to use an enumerized string field `status` instead of a boolean so that you can record 'not_fetched', 'success', 'error'.
  t.boolean :fetch_completed, default: false
  t.text :error_message
  t.text :error_backtrace

  # Whatever info you're saving

  t.timestamps
end
add_index :weather_logs, :date

# app/models/weather_log.rb

class WeatherLog
  # Return a log record immediately (non-blocking).
  def self.find_for_week(date_str)
    date = Date.parse(date_str)
    wednesday_representative = date + (3 - date.wday)
    record = find_or_create_by(date: wednesday_representative)
    WeatherFetcherJob.perform_later(record) unless record.fetch_completed
    record
  end
end

# app/jobs/weather_fetcher_job.rb

class WeatherFetcherJob < ActiveJob::Base
  def perform(weather_log_record)
    # Fetch from API
    # Update the weather_log_record with the information
    # Update the weather_log_record's fetch_completed to true
    # If there is an error - store it in the error fields.
  end
end

然后,在控制器中,您可以依赖API是否已完成以决定向用户显示的内容。这些是广泛的,你必须适应你的用例。

# app/controllers/weather_controller
def show
  @weather_log = WeatherLog.find_for_week(params[:date])
  @show_spinner = true unless @weather_log.fetch_completed
end

def poll
  @weather_log = WeatherLog.find(params[:id])
  render json: @weather_log.fetch_completed
end

# app/javascripts/poll.js.coffee

$(document).ready ->
  poll = -> 
    $.get($('#spinner-element').data('poll-url'), (fetch_in_progress) ->
      if fetch_in_progress
        setTimeout(poll, 2000)
      else
        window.location = $('#spinner-element').data('redirect-to')
    )
  $('#spinner-element').each -> poll()

# app/views/weather_controller.rb
...
<% if @show_spinner %>
    <%= content_tag :div, 'Loading...', id: 'spinner-element', data: { poll_url: poll_weather_path(@weather_log), redirect_to: weather_path(@weather_log) } %>
<% end %>
...

答案 2 :(得分:0)

我会给你一些有趣的方式来实现简单有趣的方式。你可以像书签逻辑一样:

例如:

书签的工作原理如何?用户向书签添加URL,服务器保存该书签的数据,当另一个用户尝试将相同的URL添加到书签时,服务器不会将URL保存到书签,因为它是重复的书签。它只是,服务器发现书签并分配给该用户。并再次为所有尝试添加相同网址的用户添加书签。

<强>天气:

在您的情况下,您所需要的只是:如果用户请求该城市的天气并且您没有该数据,则从api获取将其提供给用户并将其保存到DB。如果另一个人会请求同一个城市,现在只需从DB回复而不是第三方API。您需要的只是在请求数据时更新数据。