<html><head><meta http-equiv="Content-Type" content="text/html; charset=us-ascii"></head><body style="word-wrap: break-word; -webkit-nbsp-mode: space; line-break: after-white-space;" class="">Hi Tim<div class=""><br class=""></div><div class="">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. <a href="https://github.com/varvet/pundit" class="">https://github.com/varvet/pundit</a></div><div class=""><br class=""></div><div class="">So in your case you might have a <font face="Courier" class="">BookPolicy</font> with an <font face="Courier" class="">access?(user, book)</font> method, which might look like this:</div><div class=""><br class=""></div><div class=""><div class=""><font face="Courier" class="">class BookPolicy</font></div><div class=""><font face="Courier" class=""><span class="Apple-tab-span" style="white-space:pre"> </span>attr_reader :user, :book</font></div><div class=""><font face="Courier" class=""><br class=""></font></div><div class=""><font face="Courier" class=""><span class="Apple-tab-span" style="white-space:pre"> </span>def initialize(user, book)</font></div><div class=""><font face="Courier" class=""><span class="Apple-tab-span" style="white-space:pre"> </span>@user = user</font></div><div class=""><font face="Courier" class=""><span class="Apple-tab-span" style="white-space:pre"> </span>@book = book</font></div><div class=""><font face="Courier" class=""><span class="Apple-tab-span" style="white-space:pre"> </span>end</font></div><div class=""><font face="Courier" class=""><br class=""></font></div><div class=""><font face="Courier" class=""><span class="Apple-tab-span" style="white-space:pre"> </span>def access?</font></div><div class=""><font face="Courier" class=""><span class="Apple-tab-span" style="white-space:pre"> </span>user.has_subscription? || user.purchases.includes?(book)</font></div><div class=""><font face="Courier" class=""><span class="Apple-tab-span" style="white-space:pre"> </span>end</font></div><div class=""><font face="Courier" class="">end</font></div></div><div class=""><br class=""></div><div class="">Then wherever you need to check if a user can access a book, you just do <font face="Courier" class="">BookPolicy.new(user, book).access? </font>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).</div><div class=""><br class=""></div><div class="">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.</div><div class=""><br class=""></div><div class="">Not sure if this is helpful or not!</div><div class=""><br class=""></div><div class="">Ed</div><div class="">
<div><br class=""><blockquote type="cite" class=""><div class="">On 22 Jun 2021, at 15:43, Tim Cowlishaw <<a href="mailto:tim@timcowlishaw.co.uk" class="">tim@timcowlishaw.co.uk</a>> wrote:</div><br class="Apple-interchange-newline"><div class=""><div dir="ltr" class=""><div class="">Hi there folks! Hope you're all doing well.</div><div class=""><br class=""></div><div class="">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.</div><div class=""><br class=""></div><div class="">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.</div><div class=""><br class=""></div><div class="">An example:</div><div class=""><br class=""></div><div class="">I'm currently working on a project which is a digital library. It contains lots of Books:</div><div class=""><br class=""></div><div style="margin-left:40px" class=""><span style="font-family:monospace" class="">class Book < Struct.new(:id, :title, :author, :content); end</span></div><div class=""><br class=""></div><div class="">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:<br class=""></div><div class=""><span style="font-family:monospace" class=""><br class=""></span></div><div style="margin-left:40px" class=""><span style="font-family:monospace" class="">class User</span></div><div style="margin-left:40px" class=""><span style="font-family:monospace" class=""> def initialize(purchases=[])</span></div><div style="margin-left:40px" class=""><span style="font-family:monospace" class=""> @purchases = purchases</span></div><div style="margin-left:40px" class=""><span style="font-family:monospace" class=""> end</span></div><div style="margin-left:40px" class=""><span style="font-family:monospace" class=""> attr_reader :purchases</span></div><div style="margin-left:40px" class=""><span style="font-family:monospace" class=""><br class=""></span></div><div style="margin-left:40px" class=""><span style="font-family:monospace" class=""> def has_access_to?(book)</span></div><div style="margin-left:40px" class=""><span style="font-family:monospace" class=""> purchases.any? { |p| p.grants_access_to?(book) }<br class=""></span></div><div style="margin-left:40px" class=""><span style="font-family:monospace" class=""> end<br class=""></span></div><div style="margin-left:40px" class=""><span style="font-family:monospace" class="">end</span><br class=""></div><div style="margin-left:40px" class=""><br class=""></div><div style="margin-left:40px" class=""><span style="font-family:monospace" class="">class Purchase < Struct.new(:product)</span></div><div style="margin-left:40px" class=""><span style="font-family:monospace" class=""> def grants_access_to?(book)</span></div><div style="margin-left:40px" class=""><span style="font-family:monospace" class=""> product.grants_access_to?(book)<br class=""></span></div><div style="margin-left:40px" class=""><span style="font-family:monospace" class=""> end <br class=""></span></div><div style="margin-left:40px" class=""><span style="font-family:monospace" class="">end</span></div><div style="margin-left:40px" class=""><span style="font-family:monospace" class=""><br class=""></span></div><div style="margin-left:40px" class=""><span style="font-family:monospace" class="">class Product</span></div><div style="margin-left:40px" class=""><span style="font-family:monospace" class=""> def self.price</span></div><div style="margin-left:40px" class=""><span style="font-family:monospace" class=""> raise NotImplementedError<br class=""></span></div><div style="margin-left:40px" class=""><span style="font-family:monospace" class=""> end</span></div><div style="margin-left:40px" class=""><span style="font-family:monospace" class=""><br class=""></span></div><div style="margin-left:40px" class=""><span style="font-family:monospace" class=""> def <a href="http://self.name/" class="">self.name</a></span></div><div style="margin-left:40px" class=""><span style="font-family:monospace" class=""> raise NotImplementedError</span></div><div style="margin-left:40px" class=""><span style="font-family:monospace" class=""> end<br class=""></span></div><div style="margin-left:40px" class=""><span style="font-family:monospace" class=""><br class=""></span></div><div style="margin-left:40px" class=""><span style="font-family:monospace" class=""> def grants_access_to?(book)</span></div><div style="margin-left:40px" class=""><span style="font-family:monospace" class=""> raise NotImplementedError<br class=""></span></div><div style="margin-left:40px" class=""><span style="font-family:monospace" class=""> end<br class=""></span></div><div style="margin-left:40px" class=""><span style="font-family:monospace" class="">end</span></div><div style="margin-left:40px" class=""><span style="font-family:monospace" class=""><br class=""></span></div><div style="margin-left:40px" class=""><span style="font-family:monospace" class="">class OneOffPurchase < Purchase</span></div><div style="margin-left:40px" class=""><span style="font-family:monospace" class=""> def initialize(book)</span></div><div style="margin-left:40px" class=""><span style="font-family:monospace" class=""> @book = book</span></div><div style="margin-left:40px" class=""><span style="font-family:monospace" class=""> end</span></div><div style="margin-left:40px" class=""><span style="font-family:monospace" class=""> attr_reader :book</span></div><div style="margin-left:40px" class=""><span style="font-family:monospace" class=""><br class=""></span></div><div style="margin-left:40px" class=""><span style="font-family:monospace" class=""> def self.price</span></div><div style="margin-left:40px" class=""><span style="font-family:monospace" class=""> 500</span></div><div style="margin-left:40px" class=""><span style="font-family:monospace" class=""> end</span></div><div style="margin-left:40px" class=""><span style="font-family:monospace" class=""><br class=""></span></div><div style="margin-left:40px" class=""><span style="font-family:monospace" class=""> def <a href="http://self.name/" class="">self.name</a></span></div><div style="margin-left:40px" class=""><span style="font-family:monospace" class=""> "A single book"<br class=""></span></div><div style="margin-left:40px" class=""><span style="font-family:monospace" class=""> end</span></div><div style="margin-left:40px" class=""><span style="font-family:monospace" class=""><br class=""></span></div><div style="margin-left:40px" class=""><span style="font-family:monospace" class=""> def grants_access_to?(other_book)</span></div><div style="margin-left:40px" class=""><span style="font-family:monospace" class=""> <a href="http://book.id/" class="">book.id</a> == <a href="http://other_book.id/" class="">other_book.id</a><br class=""></span></div><div style="margin-left:40px" class=""><span style="font-family:monospace" class=""> end</span></div><div style="margin-left:40px" class=""><span style="font-family:monospace" class="">end</span></div><div style="margin-left:40px" class=""><span style="font-family:monospace" class=""><br class=""></span></div><div style="margin-left:40px" class=""><span style="font-family:monospace" class="">def Subscription < Purchase</span></div><div style="margin-left:40px" class=""><span style="font-family:monospace" class=""> def self.price</span></div><div style="margin-left:40px" class=""><span style="font-family:monospace" class=""> 500</span></div><div style="margin-left:40px" class=""><span style="font-family:monospace" class=""> end</span></div><div style="margin-left:40px" class=""><span style="font-family:monospace" class=""><br class=""></span></div><div style="margin-left:40px" class=""><span style="font-family:monospace" class=""> def <a href="http://self.name/" class="">self.name</a></span></div><div style="margin-left:40px" class=""><span style="font-family:monospace" class=""> "All inclusive special subscription"</span></div><div style="margin-left:40px" class=""><span style="font-family:monospace" class=""> end</span></div><div style="margin-left:40px" class=""><span style="font-family:monospace" class=""><br class=""></span></div><div style="margin-left:40px" class=""><span style="font-family:monospace" class=""> def grants_access_to?(book)</span></div><div style="margin-left:40px" class=""><span style="font-family:monospace" class=""> true<br class=""></span></div><div style="margin-left:40px" class=""><span style="font-family:monospace" class=""> end</span></div><div style="margin-left:40px" class="">end</div><div style="margin-left:40px" class=""><br class=""></div><div class="">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:</div><div class=""><br class=""></div><div style="margin-left:40px" class=""><span style="font-family:monospace" class="">class Product < Struct.new(:unique_key, :name, :price, :book)</span></div><div style="margin-left:40px" class=""><span style="font-family:monospace" class=""> def grants_accesss_to?(other_book)</span></div><div style="margin-left:40px" class=""><span style="font-family:monospace" class=""> if unique_key = :one_off && book.present?<br class=""></span></div><div style="margin-left:40px" class=""><span style="font-family:monospace" class=""> <a href="http://book.id/" class="">book.id</a> == <a href="http://other_book.id/" class="">other_book.id</a></span></div><div style="margin-left:40px" class=""><span style="font-family:monospace" class=""> elsif unique_key == :one_off</span></div><div style="margin-left:40px" class=""><span style="font-family:monospace" class=""> raise "oops! we're in a totally inconsistent state<br class=""></span></div><div style="margin-left:40px" class=""><span style="font-family:monospace" class=""> elsif unique_key == :subscription && book.present?<br class=""></span></div><div style="margin-left:40px" class=""><span style="font-family:monospace" class=""> raise "yep, this makes absolutely no sense either"</span></div><div style="margin-left:40px" class=""><span style="font-family:monospace" class=""> elsif unique_key == :subscription</span></div><div style="margin-left:40px" class=""><span style="font-family:monospace" class=""> true</span></div><div style="margin-left:40px" class=""><span style="font-family:monospace" class=""> else</span></div><div style="margin-left:40px" class=""><span style="font-family:monospace" class=""> raise "this isn't even a real product type. hopeless."<br class=""></span></div><div style="margin-left:40px" class=""><span style="font-family:monospace" class=""> end<br class=""></span></div><div style="margin-left:40px" class=""><span style="font-family:monospace" class="">end</span></div><div class=""><br class=""></div><div class="">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?</div><div class=""><br class=""></div><div class="">Therefore, anyone got any smart ideas about how to do this better? The requirements I'm looking to fulfil, in summary:</div><div class=""><br class=""></div><div class="">1) A product has a specific 'strategy' for granting access to a book for a user, expressed as ruby code</div><div class="">2) A product has a price and name that is editable as data through our web app's admin interface</div><div class=""><br class=""></div><div class="">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.<br class=""></div><div class=""><br class=""></div><div class="">Any thoughts gratefully received!</div><div class=""><br class=""></div><div class="">Cheers,</div><div class=""><br class=""></div><div class="">Tim</div><div class=""><br class=""></div></div>
_______________________________________________<br class="">Chat mailing list<br class=""><a href="mailto:Chat@lists.lrug.org" class="">Chat@lists.lrug.org</a><br class="">Archives: http://lists.lrug.org/pipermail/chat-lrug.org<br class="">Manage your subscription: http://lists.lrug.org/options.cgi/chat-lrug.org<br class="">List info: http://lists.lrug.org/listinfo.cgi/chat-lrug.org<br class=""></div></blockquote></div><br class=""></div></body></html>