Separating Concerns Using Object Roles |
Separating Concerns Using Object Roles |
====================================== |
====================================== |
|
|
|
(NEW in version 0.6: the``Registry`` base class, and the |
|
``ClassRole.for_frame()`` classmethod.) |
|
|
In any sufficiently-sized application or framework, it's common to end up |
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 |
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 |
may have business logic, persistence code, and UI all jammed into a single |
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 |