[Subversion] / ObjectRoles / README.txt  

Diff of /ObjectRoles/README.txt

Parent Directory | Revision Log

version 2343, Wed Jul 11 19:30:49 2007 UTC version 2344, Thu Jul 12 17:17:30 2007 UTC
Line 6 
Line 6 
 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
 class.  Attribute and method names for all sorts of different operations get  class.  Attribute and method names for all sorts of different operations get
 jammed into a single namespace -- even when using mixin classes.  shoved into a single namespace -- even when using mixin classes.
   
 Separating concerns into different objects, however, makes it easier to write  Separating concerns into different objects, however, makes it easier to write
 reusable and separately-testable components.  The ObjectRoles package  reusable and separately-testable components.  The ObjectRoles package
Line 64 
Line 64 
 the example below under `Threading Concerns`_.  the example below under `Threading Concerns`_.
   
 In summary, the ObjectRoles library provides you with a basic form of AOP,  In summary, the ObjectRoles library provides you with a basic form of AOP,
 that lets you attach (or "introduce" in AspectJ terminology) additional  that lets you attach (or "introduce", in AspectJ terminology) additional
 attributes and methods to an object, using a private namespace.  (If you also  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  want to do AspectJ-style "advice", the PEAK-Rules package can be used to do
 combination with ObjectRoles.)  "before", "after", and "around" advice in combination with ObjectRoles.)
   
   
   .. contents:: **Table of Contents**
   
   
 Basic API  Basic API
Line 84 
Line 87 
     >>> Persistence.exists_for(anotherThing)      >>> Persistence.exists_for(anotherThing)
     False      False
   
 Until you ask for it::  Until you refer to it directly, e.g.::
   
     >>> Persistence(aThing) is Persistence(anotherThing)      >>> Persistence(aThing) is Persistence(anotherThing)
     False      False
   
 At which point it will::  At which point it will of course exist::
   
     >>> Persistence.exists_for(anotherThing)      >>> Persistence.exists_for(anotherThing)
     True      True
   
 Maintaining its state, linked to its subject::  And maintain its state, linked to its subject::
   
     >>> Persistence(anotherThing) is Persistence(anotherThing)      >>> Persistence(anotherThing) is Persistence(anotherThing)
     True      True
Line 106 
Line 109 
     False      False
   
   
 Role Keys and Storage  Role Keys and Instances
 ---------------------  -----------------------
   
   Roles are stored either in their subject's ``__dict__``, or if it does not have
   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.
   
   By default, the dictionary key is a one-element tuple containing the role
   class, so that there is exactly one role instance per subject::
   
       >>> aThing.__dict__
       {<class 'Persistence'>: <Persistence object at...>}
   
   But in some cases, you may wish to have more than one instance of a given role
   class for a subject.  (For example, PEAK-Rules uses roles to represent indexes
   on different expressions contained within rules.)  For this purpose, you can
   redefine your Role's ``__init__`` method to accept additional arguments besides
   its subject.  The additional arguments become part of the key that instances
   are stored under, such that more than one role instance can exist for a given
   object::
   
       >>> from UserDict import UserDict
       >>> class Index(Role, UserDict):
       ...     def __init__(self, subject, expression):
       ...         self.expression = expression
       ...         self.data = {}
   
       >>> something = Thing()
       >>> Index(something, "x>y")["a"] = "b"
       >>> dir(something)
       ['__doc__', '__module__', (<class 'Index'>, 'x>y')]
   
       >>> "a" in Index(something, "z<22")
       False
   
       >>> dir(something)
       ['__doc__', '__module__', (<class 'Index'>, 'x>y'), (<class 'Index'>, 'z<22')]
   
       >>> Index.exists_for(something, 'x>y')
       True
   
       >>> Index.exists_for(anotherThing, 'q==42')
       False
   
   By default, a role class' key is either the class by itself, or a tuple
   containing the class, followed by any arguments that appeared in the
   constructor call after the role's subject.  However, you can redefine the
   ``role_key()`` classmethod in your subclass, and change it to do something
   different.  For example, you could make different role classes generate
   overlapping keys, or you could use attributes of the arguments to generate the
   key.  You could even generate a string key, to cause the role to be attached
   as an attribute!::
   
       >>> class Leech(Role):
       ...     def role_key(cls):
       ...         return "__leech__"
       ...     role_key = classmethod(role_key)
   
       >>> something = Thing()
   
       >>> Leech(something) is something.__leech__
       True
   
   The ``role_key`` method only receives the arguments that appear *after* the
   subject in the constructor call.  So, in the case above, it receives no
   arguments.  Had we called it with additional arguments, we'd have gotten an
   error::
   
       >>> Leech(something, 42)
       Traceback (most recent call last):
         ...
       TypeError: role_key() takes exactly 1 argument (2 given)
   
   Naturally, your ``role_key()`` and ``__init__()`` (and/or ``__new__()``)
   methods should also agree on how many arguments there can be, and what they
   mean!
   
   In general, you should include your role class (or some role class) as part of
   your key, so as to make collisions with other people's role classes impossible.
   Keys should also be designed for thread-safety, where applicable.  (See
   the section below on `Threading Concerns`_ for more details.)
   
   
   Role Storage and Garbage Collection
   -----------------------------------
   
 # XXX non-empty keys  By the way, the approach above of using an string as a role key won't always
 # XXX non-dict'ed but weakrefable objects  make the role into an attribute of the subject!  If an object doesn't have a
   ``__dict__``, or that ``__dict__`` isn't writable (as in the case of new-style
   classes), then the role is stored in a weakly-keyed dictionary, maintained
   elsewhere::
   
       >>> class NoDict(object):
       ...     __slots__ = '__weakref__'
   
       >>> dictless = NoDict()
   
       >>> Leech(dictless)
       <Leech object at ...>
   
       >>> dictless.__leech__
       Traceback (most recent call last):
         ...
       AttributeError: 'NoDict' object has no attribute '__leech__'
   
   Of course, if an object doesn't have a dictionary *and* isn't weak-
   referenceable, there's simply no way to store a role for it::
   
       >>> ob = object()
       >>> Leech(ob)
       Traceback (most recent call last):
         ...
       TypeError: cannot create weak reference to 'object' object
   
   However, there is a ``roledict_for()`` function in the ``peak.util.roles``
   module that you can extend using PEAK-Rules advice.  Once you add a method to
   support a type that otherwise can't be used with roles, you should be able to
   use any and all kinds of role objects with that type.  (Assuming, of course,
   that you can implement a suitable storage mechanism!)
   
   Finally, a few words regarding garbage collection.  If you don't want to create
   a reference cycle, don't store a reference to your subject in your role.  Even
   though the ``__init__`` and ``__new__`` messages get the subject passed in, you
   are not under any obligation to *store* the subject, and often won't need to.
   Usually, the code that is accessing the role knows what subject is in use, and
   can pass the subject to the role's methods if needed.  It's rare that the
   role really needs to keep a reference to the subject past the ``__new__()`` and
   ``__init__()`` calls.
   
   Role instances will usually be garbage collected at the same time as their
   subject, unless there is some other reference to them.  If they keep a
   reference to their subject, their garbage collection may be delayed until
   Python's cycle collector is run.  But if they don't keep a reference, they will
   usually be deleted as soon as the subject is::
   
       >>> def deleting(r):
       ...     print "deleting", r
   
       >>> from weakref import ref
   
       >>> r = ref(Leech(something), deleting)
       >>> del something
       deleting <weakref at ...; dead>
   
   (Roles that are stored outside the instance dictionary of their subject,
   however, may take slightly longer, as Python processes weak reference
   callbacks.)
   
   It is also *not* recommended that you have ``__del__`` methods on your role
   objects, especially if you keep a reference to your subject.  In such a case,
   garbage collection may become impossible, and both the role and its subject
   would "leak" (i.e., take up memory forever without being recoverable).
   
   
 Class Roles  Class Roles
 -----------  -----------
   
   Sometimes, it's useful to attach roles to classes instead of instances.  You
   could use normal ``Role`` classes, of course, as they work just fine with both
   classic classes and new-style types -- even built-ins::
   
       >>> Persistence.exists_for(int)
       False
   
       >>> Persistence(int) is Persistence(int)
       True
   
       >>> Persistence.exists_for(int)
       True
   
       >>> class X: pass
   
       >>> Persistence.exists_for(X)
       False
   
       >>> Persistence(X) is Persistence(X)
       True
   
       >>> Persistence.exists_for(X)
       True
   
   But, sometimes you have roles that are specifically intended for adding
   metadata to classes -- perhaps by way of class or method decorators.  In such
   a case, you need a way to access the role *before* its subject even exists!
   
   The ``ClassRole`` base class provides a mechanism for this.  It adds an extra
   classmethod, ``for_enclosing_class()``, that you can use to access the role
   for the class that is currently being defined in the scope that invoked the
   caller.  For example, suppose we want to have a method decorator that adds
   the method to some class-level registry::
   
       >>> from peak.util.roles import ClassRole
   
       >>> class SpecialMethodRegistry(ClassRole):
       ...     def __init__(self, subject):
       ...         self.special_methods = {}
       ...         super(SpecialMethodRegistry, self).__init__(subject)
   
       >>> def specialmethod(func):
       ...     smr = SpecialMethodRegistry.for_enclosing_class()
       ...     smr.special_methods[func.__name__] = func
       ...     return func
   
       >>> class Demo:
       ...     def dummy(self, foo):
       ...         pass
       ...     dummy = specialmethod(dummy)
   
       >>> SpecialMethodRegistry(Demo).special_methods
       {'dummy': <function dummy at ...>}
   
       >>> class Demo2(object):
       ...     def dummy(self, foo):
       ...         pass
       ...     dummy = specialmethod(dummy)
   
       >>> SpecialMethodRegistry(Demo2).special_methods
       {'dummy': <function dummy at ...>}
   
   You can of course use the usual role API for class roles::
   
       >>> SpecialMethodRegistry.exists_for(int)
       False
   
       >>> SpecialMethodRegistry(int).special_methods['x'] = 123
   
       >>> SpecialMethodRegistry.exists_for(int)
       True
   
   Except that you cannot explicitly delete them, they must be garbage collected
   naturally::
   
       >>> SpecialMethodRegistry.delete_from(Demo)
       Traceback (most recent call last):
         ...
       TypeError: ClassRoles cannot be deleted
   
   
   Delayed Initialization
   ~~~~~~~~~~~~~~~~~~~~~~
   
   When a class role is initialized, the class may not exist yet.  In this case,
   ``None`` is passed as the first argument to the ``__new__`` and ``__init__``
   methods.  You must be able to handle this case correctly, if your role will
   be accessed inside a class definition with ``for_enclosing_class()``.
   
   You can, however, define a ``created_for()`` instance method that will be
   called as soon as the actual class is available.  It is also called by the
   default ``__init__`` method, if the role is initially created for a class that
   already exists.  Either way, the ``created_for()`` method should be called at
   most once for any given role instance.  For example::
   
       >>> class SpecialMethodRegistry(ClassRole):
       ...     def __init__(self, subject):
       ...         print "init called for", subject
       ...         self.special_methods = {}
       ...         super(SpecialMethodRegistry, self).__init__(subject)
       ...
       ...     def created_for(self, cls):
       ...         print "created for", cls.__name__
   
       >>> class Demo:
       ...     def dummy(self, foo):
       ...         pass
       ...     dummy = specialmethod(dummy)
       init called for None
       created for Demo
   
   Above, ``__init__`` was called with ``None`` since the type didn't exist yet.
   However, accessing the role for an existing type (that doesn't have the role
   yet) will call ``__init__`` with the type, and the default implementation of
   ``ClassRole.__init__`` will also call ``created_for()`` for us, when it sees
   the subject is not ``None``::
   
       >>> SpecialMethodRegistry(float)
       init called for <type 'float'>
       created for float
       <SpecialMethodRegistry object at ...>
   
       >>> SpecialMethodRegistry(float)    # created_for doesn't get called again
       <SpecialMethodRegistry object at ...>
   
   One of the most useful features of having this ``created_for()`` method is
   that it allows you to set up class-level metadata that involves inherited
   settings from base classes.  In ``created_for()``, you have access to the
   class' ``__bases__`` and or ``__mro__``, and you can just ask for an instance
   of the same role for those base classes, then incorporate their data into your
   own instance as appropriate.  You are guaranteed that any such roles you access
   will already be initialized, including having their ``created_for()`` method
   called.
   
   Since this works recursively, and because class roles can be attached even to
   built-in types like ``object``, the work of creating a correct class metadata
   registry is immensely simplified, compared to having to special case such base
   classes, check for bases where no metadata was added or defined, etc.
   
   Instead, classes that didn't define any metadata will just have a role instance
   containing whatever was setup by your role's ``__init__()`` method, plus
   whatever additional data was added by its ``created_for()`` method.
   
   Thus, metadata accumulation using class roles can actually be simpler than
   doing the same things with metaclasses, since metaclasses can't be
   retroactively added to existing classes.  Of course, class roles can't entirely
   replace metaclasses or base class mixins, but for the things they *can* do,
   they are much easier to implement correctly.
   
   
   Keys, Decoration, and ``for_enclosing_class()``
   ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
   
   Class roles can have role keys, just like regular roles, and they're
   implemented in the same way.  And, you can pass the extra arguments as
   positional arguments to ``for_enclosing_class()``.  For example::
   
       >>> class Index(ClassRole):
       ...     def __init__(self, subject, expr):
       ...         self.expr = expr
       ...         self.funcs = []
       ...         super(Index, self).__init__(subject)
   
       >>> def indexedmethod(expr):
       ...     def decorate(func):
       ...         Index.for_enclosing_class(expr).funcs.append(func)
       ...         return func
       ...     return decorate
   
       >>> class Demo:
       ...     def dummy(self, foo):
       ...         pass
       ...     dummy = indexedmethod("x*y")(dummy)
   
       >>> Index(Demo, "x*y").funcs
       [<function dummy at ...>]
   
       >>> Index(Demo, "y+z").funcs
       []
   
   Note, by the way, that you do not need to use a function decorator to add
   metadata to a class.  You just need to be calling ``for_enclosing_class()``
   in a function called directly from the class body::
   
       >>> def special_methods(**kw):
       ...     smr = SpecialMethodRegistry.for_enclosing_class()
       ...     smr.special_methods.update(kw)
   
       >>> class Demo:
       ...     special_methods(x=23, y=55)
       init called for None
       created for Demo
   
       >>> SpecialMethodRegistry(Demo).special_methods
       {'y': 55, 'x': 23}
   
   By default, the ``for_enclosing_class()`` method assumes is it being called by
   a function that is being called directly from the class suite, such as a
   method decorator, or a standalone function call as shown above.  But if you
   make a call from somewhere else, such as outside a class statement, you will
   get an error::
   
       >>> special_methods(z=42)
       Traceback (most recent call last):
         ...
       SyntaxError: Class decorators may only be used inside a class statement
   
   Similarly, if you have a function that calls ``for_enclosing_class()``, but
   then you call that function from another function, it will still fail::
   
       >>> def sm(**kw):
       ...     special_methods(**kw)
   
       >>> class Demo:
       ...     sm(x=23, y=55)
       Traceback (most recent call last):
         ...
       SyntaxError: Class decorators may only be used inside a class statement
   
   This is because ``for_enclosing_class()`` assumes the class is being defined
   two stack levels above its frame.  You can change this assumption, however,
   by using the ``level`` keyword argument::
   
       >>> def special_methods(level=2, **kw):
       ...     smr = SpecialMethodRegistry.for_enclosing_class(level=level)
       ...     smr.special_methods.update(kw)
   
       >>> def sm(**kw):
       ...     special_methods(level=3, **kw)
   
       >>> class Demo:
       ...     sm(x=23)
       ...     special_methods(y=55)
       init called for None
       created for Demo
   
       >>> SpecialMethodRegistry(Demo).special_methods
       {'y': 55, 'x': 23}
   
   (Alternately, you can pass a specific Python frame object via the ``frame``
   keyword argument to ``for_enclosing_class()``.)
   
   
 Threading Concerns  Threading Concerns
 ------------------  ------------------
Line 129 
Line 520 
 built-in types, can be initialized in a thread-safe manner.  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  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  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  ``__init__`` methods **must not** assume that it will in fact be the only role
 instance attached to its subject, if you wish the code to be thread-safe.  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*  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  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  race to become "the" role instance, and no thread can know in advance whether
 your Role instances to do something to their subject at initialization time,  it will win.  Thus, if you wish your Role instances to do something *to* their
 you must either give up on your role being thread-safe, or use some other  constructor arguments at initialization time, you must either give up on your
 locking mechanism.  role being thread-safe, or use some other locking mechanism.
   
 Of course, role initialization is only one small part of the thread-safety  Of course, role initialization is only one small part of the overall thread-
 puzzle.  Unless your role exists only to compute some immutable metadata about  safety puzzle.  Unless your role exists only to compute some immutable metadata
 its subject, the rest of your role's methods need to be thread-safe also.  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  One way to do that, is to use a ``@synchronized`` decorator, combined with a
 ``Locking`` role::  ``Locking`` role::
Line 179 
Line 570 
     ping      ping
     released      released
   
 If the ``Locking`` role were not thread-safe to acquire, this decorator would  If the ``Locking()`` role constructor were not thread-safe, this decorator would
 not be able to do its job correctly, because two threads accessing an object  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  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!  proceeding to run the supposedly-"synchronized" method at the same time!
   
 In general, thread-safety is harder than it looks.  But at least you don't have  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.  to worry about this one tiny part of correctly implementing it.
   
 Of course, synchronized methods will be slower than normal methods, which is  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-  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  safety puzzle, to avoid penalizing non-threaded code.  As the PEAK motto says,
 STASCTAP: Simple Things Are Simple, Complex Things Are Possible.  STASCTAP! (Simple Things Are Simple, Complex Things Are Possible.)
   
   
   Mailing List
   ------------
   
 # XXX aspects_for() GF  Questions, discussion, and bug reports for this software should be directed to
   the PEAK mailing list; see http://www.eby-sarna.com/mailman/listinfo/PEAK/
   for details.
   


Generate output suitable for use with a patch program
Legend:
Removed from v.2343  
changed lines
  Added in v.2344

cvs-admin@eby-sarna.com

Powered by ViewCVS 1.0-dev

ViewCVS and CVS Help