1
\$\begingroup\$

I'm experimenting with ways to reuse code throughout a Rails web app. I'm interested in feedback concerning one such approach!

Context

A user's request may be scoped to a physical location so that the view shows location-relevant information. In most cases, the location is stored in a cookie. It's possible for location to be overridden via query parameters (?where= or ?location=). When location is not in a cookie or in the query string, depending on where the code executes, I'll want to provide some other location.

Here's what I came up with:

class LocationResolver
  def initialize(locations)
    @locations = locations
  end

  def call(strategy)
    [*strategy].map { |strat| strat.(@locations) }.find(&:itself)
  end

  class QueryStrategy
    def initialize(params)
      @params = params
    end

    def call(locations)
      locations.find { |x| x.slug == slug }
    end

    private

    def slug
      @slug ||= @params.values_at(:where, :location).find(&:itself)
    end
  end

  class CookieStrategy
    def initialize(cookies)
      @cookies = cookies
    end

    def call(locations)
      content.presence && locations.find { |x| x.id == content["id"] }
    end

    private

    def content
      @content ||= JSON.parse(@cookies.fetch(:location, "{}"))
    end
  end

Given a simple Location implementation:

Location = Struct.new(:id, :slug) do
  def self.all
    [
      new(1, "one"),
      new(2, "two")
    ]
  end

  def self.anywhere
    new(nil, "anywhere")
  end
end

Usage inside a controller might look like this:

class PropertiesController << ApplicationController
  def effective_location
    @effective_location ||=
      LocationResolver.new(Location.all).([
        LocationResolver::QueryStrategy.new(request.query_parameters),
        LocationResolver::CookieStrategy.new(request.cookies),
        -> (_) { Location.anywhere }
      ])
  end
end

Dislikes

  • Feels complex

Likes

  • A strategy can be reused.
  • Strategies can be reordered or included/excluded depending on your needs.
  • Strategies can receive only context they require to do their job.
  • Strategies can be created without modifying the resolver
  • Use of .call allows defining a strategy at runtime.
  • Easy to stub and test.

What do you think?

  1. What do you think conceptually?
  2. What other approaches do you prefer?
  3. What in particular do you like or dislike about this approach?
  4. Any glaring issues?
  5. Are Resolver and Strategy suitable names?

Thank you so much! :)

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.