Separating Concerns Using Object Roles |
Separating Concerns Using Object Roles |
====================================== |
====================================== |
|
|
In any sufficiently-sized application or framework, it's common to end up |
This project has been superseded by the ``AddOns`` package; as it turns out, |
lumping a lot of different concerns into the same class. For example, you |
the term "add-on" is a much better way to describe what the package does. |
may have business logic, persistence code, and UI all jammed into a single |
This final release is just a stub that wraps the ``AddOns`` package to provide |
class. Attribute and method names for all sorts of different operations get |
a backward-compatible API. |
shoved into a single namespace -- even when using mixin classes. |
|
|
Please use the `AddOns`_ package in future, by performing the following renames |
Separating concerns into different objects, however, makes it easier to write |
in your code:: |
reusable and separately-testable components. The ObjectRoles package |
|
(``peak.util.roles``) lets you manage concerns using ``Role`` classes. |
Old Name New Name |
|
--------------- ---------------- |
``Role`` classes are like dynamic mixins, but with their own private attribute |
peak.util.roles peak.util.addons |
and method namespaces. A concern implemented using roles can be added at |
Role AddOn |
runtime to any object that either has a writable ``__dict__`` attribute, or |
ClassRole ClassAddOn |
is weak-referenceable. |
roledict_for addons_for |
|
role_key addon_key |
``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 |
And of course, you should change your ``install_requires`` to depend on |
possible. In this way, roles can keep track of ongoing state. For example, |
``AddOns`` instead of ``ObjectRoles``. |
a ``Persistence`` role might keep track of whether its subject has been saved |
|
to disk yet:: |
.. _AddOns: http://pypi.python.org/pypi/AddOns/ |
|
|
|
.. contents:: **Table of Contents** |
|
|
|
NOTE: The remainder of this document is here to keep testing in place for the |
|
old API. |
|
|
>>> from peak.util.roles import Role |
>>> from peak.util.roles import Role |
|
|
saving |
saving |
>>> Persistence(aThing).save_if_needed() # no action taken |
>>> 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 to do |
|
"before", "after", and "around" advice in combination with ObjectRoles.) |
|
|
|
|
|
.. contents:: **Table of Contents** |
|
|
|
|
|
Basic API |
Basic API |
one (or is a new-style class with a read-only ``__dict__``), they are stored in |
one (or is a new-style class with a read-only ``__dict__``), they are stored in |
a special dictionary linked to the subject via a weak reference. |
a special dictionary linked to the subject via a weak reference. |
|
|
By default, the dictionary key is a one-element tuple containing the role |
By default, the dictionary key is the role class, so there is exactly one role |
class, so that there is exactly one role instance per subject:: |
instance per subject:: |
|
|
>>> aThing.__dict__ |
>>> aThing.__dict__ |
{<class 'Persistence'>: <Persistence object at...>} |
{<class 'Persistence'>: <Persistence object at...>} |
are stored under, such that more than one role instance can exist for a given |
are stored under, such that more than one role instance can exist for a given |
object:: |
object:: |
|
|
>>> from UserDict import UserDict |
>>> class Index(Role, dict): |
>>> class Index(Role, UserDict): |
|
... def __init__(self, subject, expression): |
... def __init__(self, subject, expression): |
... self.expression = expression |
... self.expression = expression |
... self.data = {} |
|
|
|
>>> something = Thing() |
>>> something = Thing() |
>>> Index(something, "x>y")["a"] = "b" |
>>> Index(something, "x>y")["a"] = "b" |
>>> "a" in Index(something, "z<22") |
>>> "a" in Index(something, "z<22") |
False |
False |
|
|
|
>>> Index(something, "x>y") |
|
{'a': 'b'} |
|
|
|
>>> Index(something, "x>y").expression |
|
'x>y' |
|
|
>>> dir(something) |
>>> dir(something) |
['__doc__', '__module__', (<class 'Index'>, 'x>y'), (<class 'Index'>, 'z<22')] |
['__doc__', '__module__', (<class 'Index'>, 'x>y'), (<class 'Index'>, 'z<22')] |
|
|
... |
... |
AttributeError: 'NoDict' object has no attribute '__leech__' |
AttributeError: 'NoDict' object has no attribute '__leech__' |
|
|
Of course, if an object doesn't have a dictionary *and* isn't weak- |
Of course, if an object doesn't have a dictionary *and* isn't |
referenceable, there's simply no way to store a role for it:: |
weak-referenceable, there's simply no way to store a role for it:: |
|
|
>>> ob = object() |
>>> ob = object() |
>>> Leech(ob) |
>>> Leech(ob) |
>>> SpecialMethodRegistry(Demo).special_methods |
>>> SpecialMethodRegistry(Demo).special_methods |
{'y': 55, 'x': 23} |
{'y': 55, 'x': 23} |
|
|
(Alternately, you can pass a specific Python frame object via the ``frame`` |
Alternately, you can pass a specific Python frame object via the ``frame`` |
keyword argument to ``for_enclosing_class()``.) |
keyword argument to ``for_enclosing_class()``, or use the ``for_frame()`` |
|
classmethod instead. ``for_frame()`` takes a Python stack frame, followed by |
|
any extra positional arguments needed to create the key. |
|
|
|
|
|
Class Registries (NEW in version 0.6) |
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
|
|
|
For many of common class role use cases, you just want a dictionary-like object |
|
with "inheritance" for the values in base classes. The ``Registry`` base class |
|
provides this behavior, by subclassing ``ClassRole`` and the Python ``dict`` |
|
builtin type, to create a class role that's also a dictionary. It then |
|
overrides the ``created_for()`` method to automatically populate itself with |
|
any inherited values from base classes. |
|
|
|
Let's define a ``MethodGoodness`` registry that will store a "goodness" |
|
rating for methods:: |
|
|
|
>>> from peak.util.roles import Registry |
|
|
|
>>> class MethodGoodness(Registry): |
|
... """Dictionary of method goodness""" |
|
|
|
>>> def goodness(value): |
|
... def decorate(func): |
|
... MethodGoodness.for_enclosing_class()[func.__name__]=value |
|
... return func |
|
... return decorate |
|
|
|
>>> class Demo(object): |
|
... def aMethod(self, foo): |
|
... pass |
|
... aMethod = goodness(17)(aMethod) |
|
... def another_method(whinge, spam): |
|
... woohoo |
|
... another_method = goodness(-99)(another_method) |
|
|
|
>>> MethodGoodness(Demo) |
|
{'aMethod': 17, 'another_method': -99} |
|
|
|
So far, so good. Let's see what happens with a subclass:: |
|
|
|
>>> class Demo2(Demo): |
|
... def another_method(self, fixed): |
|
... pass |
|
... another_method = goodness(42)(another_method) |
|
|
|
>>> MethodGoodness(Demo2) |
|
{'another_method': 42, 'aMethod': 17} |
|
|
|
Values set in base class registries are automatically added to the current |
|
class' registry of the same type and key, if the current class doesn't have |
|
an entry defined. Python's new-style method resolution order is used to |
|
determine the precedence of inherited attributes. (For classic classes, a |
|
temporary new-style class is created that inherits from the classic class, in |
|
order to determine the resolution order, then discarded.) |
|
|
|
Once the class in question has been created, the registry gets an extra |
|
attribute, ``defined_in_class``, which is a dictionary listing the entries that |
|
were actually defined in the corresponding class, e.g.:: |
|
|
|
>>> MethodGoodness(Demo).defined_in_class |
|
{'aMethod': 17, 'another_method': -99} |
|
|
|
>>> MethodGoodness(Demo2).defined_in_class |
|
{'another_method': 42} |
|
|
|
As you can see, this second dictionary contains only the values registered in |
|
that class, and not any inherited values. |
|
|
|
Finally, note that ``Registry`` objects have one additional method that can |
|
be useful to call from a decorator: ``set(key, value)``. This method will |
|
raise an error if a different value already exists for the given key, and is |
|
useful for catching errors in class definitions, e.g.: |
|
|
|
>>> def goodness(value): |
|
... def decorate(func): |
|
... MethodGoodness.for_enclosing_class().set(func.__name__, value) |
|
... return func |
|
... return decorate |
|
|
|
>>> class Demo3(object): |
|
... def aMethod(self, foo): |
|
... pass |
|
... aMethod = goodness(17)(aMethod) |
|
... def aMethod(self, foo): |
|
... pass |
|
... aMethod = goodness(27)(aMethod) |
|
Traceback (most recent call last): |
|
... |
|
ValueError: MethodGoodness['aMethod'] already contains 17; can't set to 27 |
|
|
|
|
Threading Concerns |
Threading Concerns |