[LRUG] Objects that are neither classes nor records but something in between
Tim Cowlishaw
tim at timcowlishaw.co.uk
Tue Jun 22 07:43:28 PDT 2021
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
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.lrug.org/pipermail/chat-lrug.org/attachments/20210622/d6eccbc5/attachment.html>
More information about the Chat
mailing list