[LRUG] Objects that are neither classes nor records but something in between

Tim Cowlishaw tim at timcowlishaw.co.uk
Tue Jun 22 08:52:40 PDT 2021


Aha thanks Ed! This is super useful - I'm not actually using Rails this
time, but my way of approaching the problem was definitely informed by
Warden / Cancancan and their ilk -  I just don't think i'd followed that
thought through to it's logical conclusions!

You kinda hint at this too, but this kinda pushes the problem down into
that User#has_subscription? method - the user's purchased Products may or
may not contain a Product which happens to have 'subscription' behaviour,
but the solution kinda falls straight out of flipping the problem around
this way - I just need a #is_subscription? flag on products which governs
how they behave wrt authorization. nice and simple!

Thanks very much :-D

Cheers!

Tim

On Tue, 22 Jun 2021 at 17:10, Ed Jones <ed at error.agency> wrote:

> Hi Tim
>
> Are you using Rails for this stuff? Regardless of whether it's a Rails
> project or not, I like using Pundit for problems like this - it abstracts
> away the authorisation from the model quite nicely.
> https://github.com/varvet/pundit
>
> So in your case you might have a BookPolicy with an access?(user, book)
> method, which might look like this:
>
> class BookPolicy
> attr_reader :user, :book
>
> def initialize(user, book)
> @user = user
> @book = book
> end
>
> def access?
> user.has_subscription? || user.purchases.includes?(book)
> end
> end
>
> Then wherever you need to check if a user can access a book, you just do BookPolicy.new(user,
> book).access? and your logic to determine that is encapsulated away
> nicely in the policy class. (Or if you're doing it in Rails controllers,
> there are helper methods).
>
> This example is predicated on having some sort of way to determine if the
> user has a subscription or not; (assuming Rails) you could have a
> polymorphic association to different types of purchase to address this.
>
> Not sure if this is helpful or not!
>
> Ed
>
> On 22 Jun 2021, at 15:43, Tim Cowlishaw <tim at timcowlishaw.co.uk> wrote:
>
> Hi there folks! Hope you're all doing well.
>
> I've just run into a problem / conundrum / anti-pattern / code smell that
> seems to recur fairly frequently in projects I've worked on of late, and
> which i'm not really sure how to name in order to search for an existing
> solution, so i figured I'd post it here in case anyone has some sensible
> advice, or in the hope that it provokes some interesting discussion, at
> least.
>
> The problem basically arises when there's some 'object' in my system
> (using this word in the vaguest possible sense for now) which has both
> *data* (so fields that can be updated through an API or webapp, and which
> should probably be stored in a database row), but also has a one-to-one
> correspondence with a specific bit of behaviour, expressed as code.
>
> An example:
>
> I'm currently working on  a project which is a digital library. It
> contains lots of Books:
>
> class Book < Struct.new(:id, :title, :author, :content); end
>
> Users can buy access to these books either individually (all books are the
> same price), or by buying a subscription to the whole library. Forgetting
> the DB / ORM side of this for a sec, i'd model a toy implementation of what
> i want to do something like the following:
>
> class User
>   def initialize(purchases=[])
>     @purchases = purchases
>    end
>    attr_reader :purchases
>
>    def has_access_to?(book)
>      purchases.any? { |p| p.grants_access_to?(book) }
>    end
> end
>
> class Purchase < Struct.new(:product)
>   def grants_access_to?(book)
>     product.grants_access_to?(book)
>   end
> end
>
> class Product
>   def self.price
>    raise NotImplementedError
>   end
>
>   def self.name
>     raise NotImplementedError
>   end
>
>   def grants_access_to?(book)
>     raise NotImplementedError
>   end
> end
>
> class OneOffPurchase < Purchase
>   def initialize(book)
>     @book = book
>   end
>   attr_reader :book
>
>   def self.price
>     500
>   end
>
>   def self.name
>     "A single book"
>   end
>
>   def grants_access_to?(other_book)
>     book.id == other_book.id
>   end
> end
>
> def Subscription < Purchase
>   def self.price
>     500
>   end
>
>   def self.name
>     "All inclusive special subscription"
>   end
>
>   def grants_access_to?(book)
>     true
>   end
> end
>
> This all to me looks reasonably sensible so far, but i also want those
> product names and prices to be editable by site admins through our
> 'backoffice' webapp, so they need to be stored in the DB, and this is where
> I have trouble finding a solution i'm happy with. Naively I could have an
> ActiveRecord type object and a fairly simple dispatch mechanism to the code
> that works out the permissions, but this has some fairly obvious flaws:
>
> class Product < Struct.new(:unique_key, :name, :price, :book)
>   def grants_accesss_to?(other_book)
>      if unique_key = :one_off && book.present?
>        book.id == other_book.id
>    elsif unique_key == :one_off
>       raise "oops! we're in a totally inconsistent state
>     elsif unique_key == :subscription && book.present?
>        raise "yep, this makes absolutely no sense either"
>     elsif unique_key == :subscription
>       true
>    else
>      raise "this isn't even a real product type. hopeless."
>     end
> end
>
> aside from all the brittleness and smells that you can see above, this is
> also kinda useless in a bunch of respects - we can't add new product types
> without making a change to the codebase, we have to do a bunch of
> convoluted validation to avoid all the various possible inconsistent states
> we can get into above, and our codebase is tightly coupled to the value of
> that unique_key database field, which i'm very suspicious of?
>
> Therefore, anyone got any smart ideas about how to do this better? The
> requirements I'm looking to fulfil, in summary:
>
> 1) A  product has a specific 'strategy' for granting access to a book for
> a user, expressed as  ruby code
> 2) A product has a price and name that is editable as data through our web
> app's admin interface
>
> And, in general, i'd be interested to know -  is there a name for this
> type of (anti-)pattern - Types of things that refuse to sensibly sit in the
> domain of software classes or Database fields? Does any of this even make
> any sense? I'm not so sure myself anymore.
>
> Any thoughts gratefully received!
>
> Cheers,
>
> Tim
>
> _______________________________________________
> Chat mailing list
> Chat at lists.lrug.org
> Archives: http://lists.lrug.org/pipermail/chat-lrug.org
> Manage your subscription: http://lists.lrug.org/options.cgi/chat-lrug.org
> List info: http://lists.lrug.org/listinfo.cgi/chat-lrug.org
>
>
> _______________________________________________
> Chat mailing list
> Chat at lists.lrug.org
> Archives: http://lists.lrug.org/pipermail/chat-lrug.org
> Manage your subscription: http://lists.lrug.org/options.cgi/chat-lrug.org
> List info: http://lists.lrug.org/listinfo.cgi/chat-lrug.org
>
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.lrug.org/pipermail/chat-lrug.org/attachments/20210622/09f9a525/attachment-0001.html>


More information about the Chat mailing list