[ZPatterns] Demeter, Normal Forms, Caching and kickTriggers (was Re: New
ZPatterns release)
Phillip J. Eby
[email protected]
Sun, 27 May 2001 10:23:24 -0500
At 02:22 PM 5/27/01 +0100, Steve Alexander wrote:
>Phillip J. Eby wrote:
>
>> Aha. You're violating both database normalization rules and the Law of
>> Demeter here. Student has no business knowing the instructor name (LoD
>> violation)
>
>I'm confused about this then.
>
>Surely the LoD says that should ask a student for the attribute
>instructor_name rather than asking the student for its instructor, and
>then ask that instructor for its name. Or, does that only apply when the
>Student is responsible for its Instructor?
LoD is context-sensitive. That is, it's perfectly alright for a method of
Student to say the moral equivalent of:
print self.instructor.name()
Because a method is allowed to call methods of: 1) self, 2) attributes of
self, 3) arguments passed intot the method, and 4) objects created by the
method.
However, I did mis-speak in saying that the problem was an LoD violation.
Properly, it's an encapsulation violation.
One of the things that bothers me about the model, though, is that I don't
understand how "instructor name" can be a property of a student (as a
person). Being someone's instructor is a *role* (implying relationship
object) that occurs in some context (e.g. a "course"), which implies
another object. So when I see "student", I think:
Person - Student role - Course - Instructor role - Person
With "Course" having many student roles and one instructor role. "Course"
would list its students and instructor. These are one-hop items, so it's
correct for course to retrieve them from those members. The specialists
would be Students, Instructors, and Courses.
It may be that you are dealing with some other kind of model than I'm
thinking - for example, something like a gym with personal trainers. That
is, there really is no notion of "course", and you have:
Person - Student role - Instructor role - Person
In which case only Students and Instructors are specialists, and it is
proper for an Instructor to list its students, or a student to display its
Instructor. What isn't proper (IMHO) is for the student to store data for
its instructor.
>I agree here. However, I need to display a page that looks like this:
>
> student name instructor name other details about student
> student name instructor name other details about student
> student name instructor name other details about student
> student name instructor name other details about student
> etc.
>
>If I store instructor_name in the student's ZCatalog, I can get this
>display with just one ZCatalog query.
>This is an implementation-level optimisation, and is documented as such
>in the application.
Hm. This is the sort of thing I'd normally use an RDBMS for, because of
ZCatalogs' lacking "join" capability. Note, however, that the way you're
implementing this is exposed at application-level code, hampering
reusability. (Yes, there is a way to fix this, see below...)
>The alternative normalized way involves an extra query for each
>student's instructor to dereference the id to the instructor's name, or
>the creation of a look-up dict {instructor_id:instructor_name}.
Note that Racks cache retrievals within a transaction, so each instructor's
name displayed in that list is only going to be looked up once.
>Names don't change very often, but lookups happen extremely often. The
>application uses CatalogBrains as a fast cache of the information needed
>for views. This complexity is contained within the Instructors and
>Students specialists, and is not visible to the rest of the application.
It seems to me that there is a straightforward way to solve your problem,
then. Add a trigger to your instructor object, which upon a change of
instructor name, calls a method on the Students specialist to recatalog the
students. Voila. Now the individual triggering disappears. (Note that
you can do this recataloging several other ways as well: you can flag the
instructor as "dirty", you can add it to a list of instructors to be
recataloged, you can walk through the instructors' students and recatalog
them as part of transaction commit, etc.)
>Invalidating caches is something I'd like to use Triggers for. An object
>knows how to invalidate things that cache it when it changes. This is
>declared in the object's SkinScript. Sometimes, the data that is cached
>will be made up of information from the object, and from objects that
>are associated with it. When one of those associated objects changes,
>the caches need to be invalidated.
The objects which do the changing are the ones that should be triggered,
though. To do otherwise messes up your portability. See, if your app were
based on SQL, there'd be no need to "kick triggers", because your query
could simply join to the data. The fact that your ZODB-based version has
manual trigger kicking therefore means you have implementation exposed
where it shouldn't be. Put the trigger on the object that's doing the
actual changing! That doesn't expose this implementation-level detail to
application-level code.
By the way, I'm assuming that the "caching" you refer to is non-persistent.
That is, you're not actually storing an object's data in another object
(ZCatalog index aside).
>If I understand you correctly, you're saying that I shouldn't use
>Triggers to do this invalidation. Either that, or my applications
>shouldn't cache data like that.
Neither. Just that the triggers are on the wrong object.
>> Thing is, in every case I can think of, either a computed value is based on
>> other attributes of the same object, or on attributes of another object.
>> Basing attribute values on the values of another object is a design issue,
>> and if they're attributes of the same object, then the existing trigger
>> mechanism should suffice.
>
>
>I think there are some implications of your statement "basing attribute
>values on the values of another object is a design issue" that are
>crystal clear to you, but rather muddy for me. Can you spell this out a
>little more?
Hm. I guess what I'm trying to say is that typically attribute values
based on those of other objects, typically are things that "want to be"
domain-level methods, rather than attributes at all. Sort of like Coad's
"rankFor"/"rateMe" pattern. Instructor name is really *not* a student
attribute - per LOD it should be a method if it needs to be exposed by
students, e.g. getInstructorName(). It's okay for instructor to be an
attribute, but not the name. This is something that's clear-cut at the
domain-level design stage - implementation is utterly irrelevant.
Now, if you get down to cataloging something and you have to have this
data, it's certainly an acceptable hack to throw it in as sort of a
"private attribute" as you have done. That's an implementation detail, and
hideable. But if you need the instructor name from application code which
is not part of a method of "student", there should be a
getInstructorName()-type method on student. (If you need it within methods
of student, it's within LoD to do self.instructor.name.)
>> If you can give me an example of a situation you'd use this in that doesn't
>> violate good design principles, I'll reconsider. Right now it looks to me
>> like adding a procedural "goto" into what is otherwise a very declarative
>> framework. The only procedural item ZPatterns really has right now is
>> sub-transaction commit, and I was never too keen on that part. Its only
>> saving grace is that it's stateless - i.e. you can subtransaction commit as
>> many times as you want and it has no effect except to expose the effect of
>> changes made so far.
>
>You can call kickTriggers() as many times as you want, and it has much
>the same effect as changing a property on the object that the triggers
>aren't interested in.
Sorry, didn't mean to imply that kickTriggers() wasn't stateless in that
way. The problem with kickTriggers() is that it moves implementation
knowledge up into the application - otherwise how would you know when to
call it?
Which isn't to say that subtransaction commit doesn't have a similar issue.
But if you need a subtransaction commit, usually you need it regardless of
implementation/backend. That is, if you needed it for a ZCatalog backend,
you would need it for an LDAP or SQL backend, too, and in the same place in
the application code.