Uniform Access Principle definition
Firstly, some background. The Uniform Access Principle was coined by the creator of the Eiffel language, and states:All services offered by a module should be available through a uniform notation, which does not betray whether they are implemented through storage or through computation.I'm not 100% certain on the exact reasons why this was considered the best idea, but from reading some sources it seemed that the benefits primarily are derived from the abstraction of the source of the data from callers - i.e if you call getFoo(), you don't need to know whether it's stored directly or computed, which also means that the provider of the data can change that source without having to change all callers.
On hiding the data source
The main criticism of the UAP seems to be that hiding the cost of computing some data can be problematic - e.g. there is a big difference between returning a member, and computing it via RPC to somewhere that has to do a long calculation before the value is known. This is certainly a problem, and in particular is even moreso in a more network-based client-server world, but in itself seems to be mostly solved by simply not hiding expensive calculations in get() methods. That is, getCommentCount()#int that requires a database index look-up is more likely to be queryCommentCount(Callback<int>)#void - callers know this will take a while, must provide an asynchronous solution, and everyone is happy.On hiding the dependencies
The more fundamental problem I see is making a calculated value appear like one that is a normal member, independent to the other members of the containing object. Firstly, if the calculation is only to look up the value from an external source (i.e. it's not stored locally) then that's no issue, and is mostly solved by fixing the querying as mentioned above - e.g having some way to convert an UnresolvedFoo into a ResolvedFoo, from which the required members can be retrieved locally.No, the main problem is from calculations of 'members' which depend on other members of the object. The best example of this is a collection's size, but also things like displayName (= firstName + ' ' + lastName) or BMI (= mass/(height * height)). Having the value of 'members' depend on the values of others embeds within it a scoping issue - if the value of one of the dependencies changes, so does the value of the calculated member, and this should be made clear to callers of the API.
On commutativity
To express this more clearly, there's a concept of commutativity of an API - that is, the order in which you call methods does not matter. Or, to put it another way, the methods are as independent/orthogonal as possible, which is good for maintainability, but also is theorised to improve scalability.Obviously, there are some things which this can't hold, but ideally they're as easy to spot as possible. For example, for a mutable member, setFoo() and getFoo() do not commute, but this is clear from the fact they operate on the same state. Calling setFoo() resets the foo scope, and everything that was using the value (displaying it on a screen, used as input for a calculation, ...) is now invalid, and needs to acquire the new value.
So as long as our members are kept separate, commutativity is clear. With dependencies between a local member and a calculated member however, these become very complex - how should a caller know that calling setFirstName() no longer commutes with getDisplayName()? This knowledge is required in order to have the correct value when needed (e.g. to reset the display name scope when the first name is changed), but it is exactly this dependency that the UAP is hiding!
No comments:
Post a Comment