Skip to content

Instantly share code, notes, and snippets.

@stevenharman
Created December 11, 2011 01:41
Show Gist options
  • Select an option

  • Save stevenharman/1457584 to your computer and use it in GitHub Desktop.

Select an option

Save stevenharman/1457584 to your computer and use it in GitHub Desktop.
When wrapping AR behind an intentional interface, go explicit or generic/idiomatic?
# This is a record, as in a vinyl.
class Record < ActiveRecord::Base
belongs_to :user
belongs_to :album
# explicitly stating what is needed
def from_library_for_album(collector, album)
where(user_id: collector).where(album_id: album)
end
# or, if we go more generic/idiomatic
def from_library(collector, options={})
album = options.fetch(:album) { false }
status = options.fetch(:status) { :mint }
records = where(user_id: collector)
records = records.where(album_id: album) if album
records = records.where(status: status) if status
records
end
end
class Album < ActiveRecord::Base
has_many :records
belongs_to :artist
end
class User < ActiveRecord::Base
has_many :records
end
# I prefer to keep AR as a private interface used only w/in the Model.
class Library
def initialize(collector, fetch_records=Record)
@collector = collector
@fetch_records = fetch_records
end
def fetch_records_for_album(album)
fetch_records.from_library_for_album(collector, album)
# ... or ...
fetch_records.from_library(collector, album: album)
# and what about more params?
fetch_records.from_library_for_album_in_mint_condition(collector, album)
# ... or ...
fetch_records.from_library(collector, album: album, condition: :mint)
end
end
class LibrariesController < ApplicationController
def album
library = Library.new(current_user)
@album = Album.find(params[:id])
@records = library.fetch_records_for_album(@album)
end
end
@stevenharman
Copy link
Author

@leongersing
As always, I much appreciate the thoughtful feedback! Overall I agree that it feels like a lot of indirection for little benefit, and certainly a broken, if not entirely missing, abstraction.

Re: the Library - My intent was to use it to centralize the concept of exactly what Artists, Albums, and individual Records a given User (collector) has. That is, I wanted there to be single, and obvious way to query his/her library. The desire my just be a knee jerk, and inappropriate, reaction to seeing this kind of querying sprinkled, and repeated, all over a code base. Take for instance the point you raised about going directly to the Album instance to fetch it's records:

@album = Album.find(params[:id])
@records = @album.records

That isn't exactly what we're after; that would return ALL instances of that album, across all collectors. Instead, we're after something a bit more complicated - @album.records.collected_by(@current_user). If we roll with that, the domain concept of the actual records user has in his library is muddied; there is no one source of the truth. Does that make sense?

In that vein, you mentioned named_scopes may be a cleaner and more idiomatic way to attack this issue. You're probably right and in looking at my real domain (which looks like the following), I think adding some more named scopes, and judiciously combining them across models via Relation#merge would go along way.

UPDATE: I'm using a bit of my real domain to help illustrate, so Beer is a moral equivalent of Record from the example above.

class Beer < ActiveRecord::Base
  belongs_to :brew
  belongs_to :user

  validates :brew, presence: true
  validates :user, presence: true
  validates :status, inclusion: [:stocked, :drunk, :traded, :skunked]

  attr_accessible :batch, :bottled_on, :best_by

  scope :stocked, where(status: :stocked)
  # etc...

  def self.from_cellar(keeper, options={})
    brew = options.fetch(:brew) { false }

    beers = stocked.where(user_id: keeper)
    beers = beers.where(brew_id: brew) if brew
    beers
  end
end

The Beer.from_cellar could change to

@brew = Brew.find(params[:id])
@beers = @some_collector.beers.stocked.by_brew(@brew) # <- THIS, I hate.

# It can be wrapped into something like
module FetchBeers
  def stocked_beers(brew)
    beers.stocked.by_brew(brew)
  end
end

@some_collector.extend FetchBeers
@beers = @some_collector.stocked_beers(@brew)

Or something, I suppose. Maybe I need more ☕ or 🍺 before free-wheeling something like this. At any rate, we'll get into it more in a couple of weeks at Codemash! Thanks again, buddy!

@steveklabnik
Copy link

Yeah, the real problem is that AR is kinda broken by design. Yet so many things are written to work with it, going with a lesser-used ORM can be rough.

@stevenharman
Copy link
Author

@steveklabnik,
Agreed. For this particular app, I really considered going with a Data Mapper approach, but in the end the thought of all of the friction scared me into sticking with AR. Perhaps, in hindsight, the friction I'm feeling in my design w/AR is MORE than would have been felt by going with a different ORM.

Fucking software, it's hard, man.

@steveklabnik
Copy link

Fucking software, it's hard, man.

Word. Datamapper is okay, but the Ruby implementation tries to pretend it's AR too much, it has the same problems.

I don't have the time and energy to write a good ORM...

@laribee
Copy link

laribee commented Dec 20, 2011

I feel like the DCI pattern could help you here... similar to your last code posting. (As we just discussed offline, but for posterity.)

Let data objects be primarily data objects. AR does an OK job with that anyway. That's the D.

The C would be your use case (I like the term activity better, but anyway). C is for context.

I are your interactions that you extend onto AR's domain/data objects at runtime inside your context.

The DCI pattern puts contextual analysis over the domain modelling desire "single source of truth," so I understand it's a different motivation but there might be more value in that POV?

@hkarthik
Copy link

There's some great discussion here, wish I could make it to CodeMash this year to partake in the booze filled continuation.

I agree AR definitely leaves a lot to be desired when it comes to separation of concerns. It's true that if you follow the idiomatic rules, things can remain quite testable purely because of the powerful stubbing capabilities in Ruby and RSpec. You don't have to wrap everything in a testable abstraction like we were all used to doing with C#.

But there are definitely things that still feel weird with AR and don't work quite as well if you try to build any abstraction layers around it. I'd consider Sequel as a possible alternative to using an ORM, although its really raw by comparison.

Or just try using MongoDB and get out of relational mapping all together. For side projects, I think its probably the best way to go.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment