[Subversion] / ObjectRoles / README.txt  

View of /ObjectRoles/README.txt

Parent Directory | Revision Log
Revision: 2343 - (download)
Wed Jul 11 19:30:49 2007 UTC (16 years, 9 months ago) by pje
File size: 7207 byte(s)
Created ObjectRoles package to replace the 'Aspect' class from PEAK-Rules.
======================================
Separating Concerns Using Object Roles
======================================

In any sufficiently-sized application or framework, it's common to end up
lumping a lot of different concerns into the same class.  For example, you
may have business logic, persistence code, and UI all jammed into a single
class.  Attribute and method names for all sorts of different operations get
jammed into a single namespace -- even when using mixin classes.

Separating concerns into different objects, however, makes it easier to write
reusable and separately-testable components.  The ObjectRoles package
(``peak.util.roles``) lets you manage concerns using ``Role`` classes.

``Role`` classes are like dynamic mixins, but with their own private attribute
and method namespaces.  A concern implemented using roles can be added at
runtime to any object that either has a writable ``__dict__`` attribute, or
is weak-referenceable.

``Role`` classes are also like adapters, but rather than creating a new
instance each time you ask for one, an existing instance is returned if
possible.  In this way, roles can keep track of ongoing state.  For example,
a ``Persistence`` role might keep track of whether its subject has been saved
to disk yet::

    >>> from peak.util.roles import Role

    >>> class Persistence(Role):
    ...     saved = True
    ...     def changed(self):
    ...         self.saved = False
    ...     def save_if_needed(self):
    ...         if not self.saved:
    ...             print "saving"
    ...             self.saved = True

    >>> class Thing: pass
    >>> aThing = Thing()

    >>> Persistence(aThing).saved
    True
    >>> Persistence(aThing).changed()
    >>> Persistence(aThing).saved
    False
    >>> Persistence(aThing).save_if_needed()
    saving
    >>> Persistence(aThing).save_if_needed() # no action taken

This makes it easy for us to, for example, write a loop that saves a bunch of
objects, because we don't need to concern ourselves with initializing the
state of the persistence role.  A class doesn't need to inherit from a special
base in order to be able to have this state tracked, and it doesn't need to
know *how* to initialize it, either.

Of course, in the case of persistence, a class does need to know *when* to call
the persistence methods, to indicate changedness and to request saving.
However, a library providing such a role can also provide decorators and other
tools to make this easier, while still remaining largely independent of the
objects involved.

Indeed, the ObjectRoles library was actually created to make it easier to
implement functionality using function or method decorators.  For example, one
can create a ``@synchronized`` decorator that safely locks an object -- see
the example below under `Threading Concerns`_.

In summary, the ObjectRoles library provides you with a basic form of AOP,
that lets you attach (or "introduce" in AspectJ terminology) additional
attributes and methods to an object, using a private namespace.  (If you also
want to do AspectJ-style "advice", the PEAK-Rules package can be used in
combination with ObjectRoles.)


Basic API
---------

If you need to, you can query for the existence of a Role::

    >>> Persistence.exists_for(aThing)
    True

And by default, it won't exist::

    >>> anotherThing = Thing()
    >>> Persistence.exists_for(anotherThing)
    False

Until you ask for it::

    >>> Persistence(aThing) is Persistence(anotherThing)
    False

At which point it will::

    >>> Persistence.exists_for(anotherThing)
    True

Maintaining its state, linked to its subject::

    >>> Persistence(anotherThing) is Persistence(anotherThing)
    True

Until/unless you delete it (or its subject is garbage collected)::

    >>> Persistence.delete_from(anotherThing)
    >>> Persistence.exists_for(anotherThing)
    False


Role Keys and Storage
---------------------


# XXX non-empty keys
# XXX non-dict'ed but weakrefable objects


Class Roles
-----------


Threading Concerns
------------------

Role lookup and creation is thread-safe (i.e. race-condition free), so long as
the role key contains no objects with ``__hash__`` or ``__equals__`` methods
written in Python (as opposed to C).  So, unkeyed roles, or roles whose keys
consist only of instances of built-in types (recursively, in the case of
tuples) or types that inherit their ``__hash__`` or ``__equals__`` methods from
built-in types, can be initialized in a thread-safe manner.

This does *not* mean, however, that two or more role instances can't be created
for the same subject at the same time.  Code in a role class' ``__new__`` or
``__init__`` methods **must not** assume that it will in fact be the sole
instance attached to its subject, if you wish the code to be thread-safe.

This is because the Role access machinery allows multiple threads to *create*
a role instance at the same time, but only one of those objects will *win* the
race, and no thread can know in advance whether it will win.  Thus, if you wish
your Role instances to do something to their subject at initialization time,
you must either give up on your role being thread-safe, or use some other
locking mechanism.

Of course, role initialization is only one small part of the thread-safety
puzzle.  Unless your role exists only to compute some immutable metadata about
its subject, the rest of your role's methods need to be thread-safe also.

One way to do that, is to use a ``@synchronized`` decorator, combined with a
``Locking`` role::

    >>> class Locking(Role):
    ...     def __init__(self, subject):
    ...         from threading import RLock
    ...         self.lock = RLock()
    ...     def acquire(self):
    ...         print "acquiring"
    ...         self.lock.acquire()
    ...     def release(self):
    ...         self.lock.release()
    ...         print "released"

    >>> def synchronized(func):
    ...     def wrapper(self, *__args,**__kw):
    ...         Locking(self).acquire()
    ...         try:
    ...             func(self, *__args,**__kw)
    ...         finally:
    ...             Locking(self).release()
    ... 
    ...     from peak.util.decorators import rewrap
    ...     return rewrap(func, wrapper)

    >>> class AnotherThing:
    ...     def ping(self):
    ...         print "ping"
    ...     ping = synchronized(ping)

    >>> AnotherThing().ping()
    acquiring
    ping
    released

If the ``Locking`` role were not thread-safe to acquire, this decorator would
not be able to do its job correctly, because two threads accessing an object
that didn't *have* the role yet, could end up locking two different locks, and
proceeding to run the "synchronized" method at the same time!

In general, thread-safety is harder than it looks.  But at least you don't have
to worry about this one small part of correctly implementing it.

Of course, synchronized methods will be slower than normal methods, which is
why ObjectRoles doesn't do anything besides that one small part of the thread-
safety puzzle, to avoid penalizing non-threaded code.  The PEAK motto is
STASCTAP: Simple Things Are Simple, Complex Things Are Possible.



# XXX aspects_for() GF


cvs-admin@eby-sarna.com

Powered by ViewCVS 1.0-dev

ViewCVS and CVS Help