Borg Pattern Class usable from other classes?
Alex Martelli
aleax at aleax.it
Sat Oct 13 04:53:26 EDT 2001
Paul Winkler wrote:
> On Fri, 12 Oct 2001 10:27:23 +0200, Alex Martelli <aleax at aleax.it> wrote:
>>Highlander for the DP, and Borg for the non-pattern which I
>>posted and D. Ascher named, seem much more appropriate names,
>>as the connection to science fiction movies/TV series is sharp
>>and pleasingly anti-parallel.
>
> AFAIKT, Borg is the same as Monostate which seems to have been around
> for a couple years:
>
> http://www.ddj.com/articles/1998/9809/9809a/9809as2.htm
>
> At least, the Consequences are identical:
> """
> * Multiple local instantiations but always with only a single state.
> * Persistent object state in the absence of any instantiations.
> * Familiar syntax. Does not require an accessor function as with
> Singleton objects.
> """
>
> Borg is an OK name but Monostate is a little more obvious to those of
> us who don't know much about Star Trek (yes, we exist).
Borg and Monostate (attempt to) solve the same forces. However, they do so
in different languages. *Language-appropriateness* of patterns is often
ignored, even though the Gang of 4 mention it quite well right in Chapter 1,
exemplifying with Visitor, which would be pretty silly in a language with
multiple dispatch. Similarly, "just keep all data static" (which I had
first met, without a catchy name, in the context of C++) is OK for C++ or
Java, but not for Python, where you may want to allow, for example:
theobj.acount += 1
This would be bad practice in languages bereft of "property idioms" such as
C++ of Java, but it's fine where properties are indeed idiomatic, such as
Delphi or Python: the class author can and should ensure that accesses to
properties "from the outside" go through appropriate method calls
implicitly if need be. It would be anti-idiomatic to force client code
through such contortions as
theobj.setAcount(theobj.getAcount() + 1)
in such languages (even though people who don't fully grasp a language do
try to "program Java in Delphi" and so on, of course, good idiomatic
language use has substantial advantages).
The "keep it static" (Monostate) pattern in Python would give:
class AClass:
acount = 0
and would work fine as long as you're just accessing x.acount from client
code (AClass.acount would implicitly be used, just as in a similar
situation in C++ or Java), but not for rebinding (or deletion). You'd have
to impose supplementary, non-Pythonic restriction on client code, or start
messing with __setattr__ &c (in Python 2.2, you could use less-messy
specific per-attribute getters/setters, but if you have more attributes
it's still rather a lot of boilerplate).
> BTW, are there any "gotchas" with Borg/Monostate in a multi-threaded
> context? Singleton requires extra care, leading to the "Double-checked
> Locking" pattern which apparently has been found not to work in
> Java. Any such snags for Borg? I can't think of any, but I've only
> really toyed with threads.
If two Borg instances are created in separate threads, they still access
the same state-data, so you get the joys of dealing with whatever
synchronization issues. There's no implicit per-object or per-method
locking in Python anyway (unless you make it yourself with a metaclass, I
guess, but I've never tried doing that). No extra care wrt direct
accessing shared state, but no special help for that, either.
Although it may be a bit heavyweight for many uses, there is a Pythonic
pattern based on Queue for multi-thread access to shared state, which I
haven't seen published as such but I think could be called GateKeeper or
Guardian. A guardian-thread is sole accessor of a complex of shared state
(which could but need not be a Borg). All access to the guardian thread is
by posting to a specific Queue tuples of (ResponseQueue, boundmethod, args,
kwds) followed (at once or a bit later) by waiting on the ResponseQueue.
The Guardian thread just loops taking request pairs from its Queue, running
the boundmethod, posting the result to the ResponseQueue. Yep, it's
overkill in many cases, but it does lend itself to neat encapsulation.
Say the client code only "sees" object guardianProxy which wraps
sharedObject (which could but need not be a Borg). Then client code just
calls, e.g.:
myresult = guardianProxy.someMethod(someThing,anOther)
and the only issues for client code are with "granularity of locks" -- i.e.
if client code tries
mytotal = guardianProxy.oneThing()+guardianProxy.anOther()
anything may happen between the two method calls although each is running
atomically (needs some explicit locking if that is a problem!).
GuardianProxy's __getattr__ delegates to sharedObject's, but, when it gets
a callable, builds on the fly another callable wrapping it, and returns the
wrapper. The wrapper callable does the queue-minuet on behalf of its
caller:
class QueueWrapper:
def __init__(self, realthing, requestQueue):
self.realthing = realthing
self.requestQueue = requestQueue
def __call__(self, *args, **kwds):
responseQueue = Queue()
self.requestQueue.put((
responseQueue,self.realthing,args,kwds))
return responseQueue.get()
The overkill aspects come e.g. in the overgenerous creation of Queue
objects, but it's not hard to remedy by adding some usage conventions
regarding what reuse (or lack thereof) is made of the QueueWrapper
callables. It's just a bit harder to extend this to self.realthing raising
an exception in the guardian thread to be propagated this back to the
client thread: the responseQueue would have to receive exception-indicators
and re-raise appropriately.
But more often, I think non-hypergeneralized Queue usages that in some
sense specialcase this ambitious general scheme are typical and
appropriate. But the whole issue of non-Patterns vs Patterns is a rather
large subject -- we can hardly do it justice here.
Alex
More information about the Python-list
mailing list