====================================== 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