Design Patterns, and full fledged Pattern Languages, were originally introduced by C. Alexander as ways to reason about the architecture of building and cities, [al79]. More recently, Design Patterns have become deservedly popular in software development. However, not all design issues are so hard as to really need the power of these semi-formal approaches. When one is addressing simple issues, using very powerful approaches may be overkill, a bit like the "Big Design Up Front" fallacy [bd00] so prevalent in software development. Python tends to make many issues simpler. This, in turn, sometimes lets you use simpler tools, e.g. an idiom in lieu of a Design Pattern. Simplicity is an important ingredient of quality. This paper argues for this perspective, and exemplifies it with five simple Python "solution elements" that are not Design Patterns but may, to some extent, be satisfactorily used instead of Design Patterns in various contexts.
"Design Patterns" (henceforth DPs) have been brewing for quite a while. From the point of view of most software practitioners, however, DPs burst suddenly upon the scene when E. Gamma, R. Helm, R. Johnson and J. Vlissides (henceforth the "Gang of Four", or Gof4 — other works also call them the GoF, or GOF) published their book "Design Patterns" [go95]. Overnight, and deservedly, this book made DPs popular in the software development community.
The Patterns community has produced a lot of other fascinating publications connected with software development, particularly on Pattern Languages (integrated, coordinated systems of Patterns). These works deal with every conceivable area of software development, including organizational structure, analysis, and development processes, as well as design. A complete collection of all fundamental books and articles on these themes would overflow the average developer's available shelf space. Still, any developer should at least be familiar with the original Gof4's DP book, M. Fowler's "Analysis Patterns" [fo97] and "Refactoring" [fo99], and Vlissides' "Hatching Patterns" [vl98]. These are all very practical, highly usable books, directly oriented to software development. I think it's also a good idea to read some wider-view, "philosophical reflection" works in the Pattern field. My personal "dark horse" suggestion for the latter category is N. Salingaros' paper "The Structure of Pattern Languages" [sa99], which focuses on Alexander's original work on Pattern Languages for building and city architecture, [al77].
But I come neither to praise Design Patterns, nor, most particularly, to bury them: just to offer a small alternate perspective of their use, and non-use, when one is developing Python programs. A more traditional approach, centered on implementing Gof4's DPs rather than looking for alternatives, is competently explored in [sa98].
I focus on contrast (or, actually, just nuance!) from the Gof4's DP book. Indeed, by its superb quality and for being in the right place at the right time, the Gof4's book is the best known work on Design Patterns among the software community. The Gof4 do make the point, right in the first chapter, that "the choice of programming language is important because it influences one's point of view". The Gof4 consider this influence strong enough that language choice shapes what is or isn't "worthy" of being framed as a DP, or even feasible to so frame. Many of the book's patterns are of course C++ specific, at least to the extent of making most sense in a C++-like context (access specifiers, compile-time type checking, etc).
More to the point, however, a DP is, conceptually, a somewhat "heavy" construct. This, of course, applies in spades to a complete Pattern Language. But even just to write down a DP requires a semi-formal approach. You must identify relevant object and classes, at the right levels of abstraction. Then, you must single out and articulate intent, motivation, applicability, structure, participants, collaboration, consequences, implementation. Most particularly, you must research, find, and document Known Uses. Known Uses are not an optional element of a DP: at the very least, by the Gof4's relatively benign criterion, it's not a Pattern unless you can identify at least two usage examples from different domains. Writing down the DP implies a substantial investment of effort, so you need the "known uses" to provide some assurance that the whole conceptual edifice is indeed worthy of that effort, having proven itself in the field. This is just fine and dandy when this considerable investment of time and energy pays back by helping us control and conquer complexity, which is often the case. But what if some of the complexity goes away, or at least is considerably diminished, by the programming language we're using?
Python's forte is simplicity. Again and again, problems that are difficult in other languages scale down to "pretty easy" in the light of Python's bright Sun. The amount and caliber of intellectual weaponry that it makes sense to bring to bear on a problem depends on the problem's difficulty level. For a simpler problem, an informal approach may make more sense, while a harder problem might profit from more structured and formalized procedures. One plus of making problems simpler is that, the simpler a problem, the more simplicity you can deploy in solving it. Maybe you can get away with an idiom (language specific usage), or even just "good common practice", where another language would require you to unearth and apply a full-fledged design pattern. At times, it's a substantial simplicity gain to avoid forcing an aspect of the solution into the object oriented mold, so pervasive, and indeed so often appropriate, in software development Design Patterns. One aspect of Python's simplicity is, indeed, that you only do OO when you want to.
A popular, often implemented Gof4 DP is Singleton
. One
can't help but wonder why: besides the catchy name, what does
Singleton
offer that makes it so appealing? The DP's stated
Intent is to "ensure a class only has one instance" (and provide a
global point of access to it). But why would we want to ensure
that? Aren't we entering in the middle of the action, where a specific
approach has already been chosen to solve some other actual
underlying problem, just as in Bentley's classic "How do I use the system
sort from within my program" question (in the very first essay of his
outstanding "Programming Pearls" [be00])?
I think we are. Coming to the Motivation, in fact, we find that "It's important for some classes to have exactly one instance. Although there can be many printers in a system, there should be only one printer spooler". And so, maybe, there should be, depending on the system that we're modeling. But this still doesn't tell us why that "one printer spooler" in the system we're modeling should necessarily, or optimally, correspond to "exactly one instance" of some specific class in the software we write to model that system.
There appears to be a lot of unspoken subtext in this intent and
motivation. It is taken for granted, I surmise, that instance identity
must, or at least should, be in direct correspondence with an "entity in
the real world": more precisely, with a conceptual entity in the world
view that our software system is modeling. Some schools of OO design take
this as an axiom, but the Gof4 argue directly against this earlier in the
book. In section 1.6 in the Gof4 book, we read: "object oriented designs
often end up with classes that have no counterpart in the real world ...
abstractions that emerge during design are key to making a design
flexible". Right on! I happen to agree very closely with this opinion.
Yet, in identifying Singleton
's intent and motivation, the
need for "counterparts", which section 1.6 in the book denies, seems to be
an unspoken assumption.
Object identity is a frail reed on which to rely. A. Korzybski, engineer extraordinaire, had choice words on the subject of "the is of identity" (a catchy yet precise phrase he credits to mathematician W. S. Jevons) in [ko33]. Less catchily, but just as precisely, G. Santayana had remarked in [sa23] that "Whenever I use the word 'is', except in sheer tautology, I deeply misuse it". L. von Wittgenstein, another contemporary engineer of notice, had similarly observed in [wi22] that "to say of two things that they are identical is nonsense, and to say of one thing that it is identical with itself is to say nothing at all". (Why do we, collectively, keep forgetting the key insights of our grandparents' generation? Could it be that people who do not know history are doomed to repeat it? Korzybski's best known quote seems to be directly on the issue of "counterparts in the real world" for objects in an OO design: "the map is not the territory"!)
Lest I be accused of escaping into philosophy, although I am
quoting mostly engineers, let's look more closely at that "instance"
thing, of which Singleton
wants to ensure a certain class has
only one. In Python, an instance has an identity: we know because we can
print id(instance)
and see that identity displayed as an
integer right in front of our eyes. An instance has state: assuming the
instance belongs to a "classic class" (an assumption that we'll have to
critically revisit later, of course), the instance's state, sticking for a
moment to its "direct", per-instance part, is entirely held in
instance.__dict__
. An instance has behavior, coded in its
methods, which, in most cases, come (some have argued, should always come)
as the methods of instance.__class__
— some of them, of
course, could in turn be inherited, but that doesn't affect the point.
That's it — that's all an instance has: identity, state,
behavior.
Of course, an instance's "state" in a wider sense may partly come
from elsewhere. instance.__class__.__dict__
is a popular
place in which to keep some state, specifically that part which is
shared by all instances of the class. Then, there are global variables,
in the dictionary of the class's module, and, potentially, even more
indirect repositories of state. However, as we're discussing
Singleton
, we need not dwell on all of these potential
depots of state, as they're obviously held in common by all instances of
a class, and possibly by other entities yet. If there is any motivation
for ensuring that a class is only ever instantiated once, the motivation
cannot lie in aspects in which other instances of the same class, if
they existed, would be identical and indistinguishable. The
hypothetical motivation we're seeking has to rest, if anywhere, then in
those aspects that might distinguish, i.e., differentiate, a
multiplicity of instances, were such multiplicity allowed.
By the same token, the hypothetical advantage of Singleton
cannot be about behavior, i.e., code. Normally, all instances of a class
share behavior. We can arrange for deviations from this rule, by binding
freshly created bound methods as part of per instance state, but it's
quite a moot point whether we ever should. Again, therefore,
behavior-wise it cannot matter much whether we constrain a class to have
just one instance: even if the class has several instances, they share
behavior, as long as we conventionally agree to eschew the dubious
practice of creating and binding new per instance bound methods, at least
regarding instances of that particular class. Let's not forget that
Python gains much of its ease and simplicity by substituting clear,
sensible conventions in place of the hard, strictly-enforced
rules which many other languages try to impose on programmers.
So, it boils down to identity, and per-instance state. Why would we
want to ensure uniqueness of identity? If it's for the purpose of
testing equality with is
, why not define
__eq__
instead? That gives us at least as much control.
If it's for the purpose of using the instance as a dictionary key,
without forcing the instance to be immutable, we can get there just as
well by defining __hash__
: the "immutability" only needs to
refer to equality comparison results being unchangeable, and
hash(instance)
being similarly fixed. We don't need to get
metaphysical about this, but, should we want to, it's easy to argue that
"immutability" is defined by the context: if we ensure that the code of
built-in type dict
, the only relevant "observer", can never
notice any mutation, then who's to say any mutation has in fact
occurred...? So, naah, we don't really care about identity, not deeply
at all. So, is it all about per instance state...?
I think it must be. When we say there's only one printer spooler, all we really care about is that there be only one "printer spooler state": just one set of queues, jobs in the queues, currently set options, and so forth. Now this is concrete and juicy enough to sink our teeth into. Don't we need to ensure there is only one instance of the class, so that there will only be one "occurrence" of the relevant state?
And the answer is, no, of course we don't. Not in any language, or
object model, actually. We just have to ensure that all instances,
whatever number thereof might be in existence, share state.
This can be accomplished in any language, typically by delegation in
some form. Once we do that, we satisfy the real application need that
may indeed arise: uniqueness of state. Ensuring that the "number of
instances" is identically equal to 1
is one approach, but
it's far from being the only viable one.
So, we need to weigh the actual advantages and disadvantages of the
Singleton
approach versus delegation based alternatives. In
the alternatives, we allow client code to instantiate the target class
freely, but we must arrange for all instances thus created to delegate
their state to a single agent. You could look at these alternatives as
Structural patterns, or even, by a bit of a stretch, Behavioral ones,
while Singleton
is a Creational pattern. For example, it's
easy to envision the connection, almost antisymmetric, between
shared-state multiple instances and the Design Pattern
Flyweight
. Some of the Applicability issues are very close:
state is made extrinsic, the application must not depend on object
identity.
Before we proceed further, we really need a catchy name. Naming
is important, and the Patterns community recognizes this in
earnest. Singleton
thrives, "out there" in the real world of
software development, in part because its name is so catchy. A
good name makes something easier to recall, recognize, discuss. What name
would well represent some sort-of-Flyweight objects, numerous as to
identity, but deeper down all the same, indistinguishable, because they
all share state? Well, what about Borg? After all, the several
instances, despite their distinct identities, are really all One, because
they have no distinct state. Identity is irrelevant, resistance is
futile, prepare to be assimilated...! Yep, it works (and I am indebted to
D. Ascher for suggesting this name in discussion on the Python Cookbook
site, [ma01a]). Some might object to naming software
artifacts by inspiration from popular television shows, but surely no such
objection will come from a programming community which centers on a
language whose name honors Monty Python. Indeed, we might prefer to
rename Singleton
to Highlander
, since "There Can
Be Only One"...
So, how hard is it to implement Borg? Not very, in any language
or object model I know of: at worst, one has to explicitly code some
delegation boilerplate, but often it's not even as bad as that. For
example, in Consequences n. 5 of Singleton
, the Gof4 claim
Singleton
is "More flexible than class operations", i.e., use
of "static" member functions in C++, for example because those can't be
overridable (virtual, in C++ terms). True, but so what? Who'd ever want
to use static methods? All we need is for the instance methods,
which can perfectly well be virtual ones in C++, to only use "static"
— i.e., per-class, rather than per-instance — member
data. Prepend keyword static
to all data
members, leave the methods per-instance, virtual if you need or want
them that way, and bingo, instant C++ Borg
. This was
discussed as the Monostate
Pattern in [wh96].
But, of course, it is even easier in Python (isn't it always?-):
class Borg:
_shared_state = {}
def __init__(self):
self.__dict__ = self._shared_state
That's it: just derive your application class from Borg
,
mixin-like. Remember, of course, to invoke Borg.__init__(self)
,
right at the start of your own __init__
if any, like for any
other Python inheritance. Once you do this, your class is a
Borg
: all instances of your class share state. Your class
may in turn override Borg
's _shared_state
class
attribute. It is exactly in order to allow this "data override" that
Borg.__init__
accesses the attribute through
self
, not directly by qualifying Borg
, and the
attribute's name has one leading underscore, not two. This data-member
overriding, or lack thereof, determines whether your class also shares
state with other subclasses of Borg
: you can easily arrange
this in different ways, but, of course, resistance is futile, so don't
even bother trying to arrange that.
Let's take a small step back to look at this tiny snippet of code with
"beginners' eyes". Our design intent is for all instances to share state.
Our Python knowledge tells us that each instance's state lives in the
instance's __dict__
, ignoring, without loss of generality,
other state that is already shared "by nature", and delaying for the
moment the issue of Python 2.2's non-classic classes. Therefore, we
explicitly express this design intent by ensuring that the
__dict__
is the same dictionary object for each and every
instance, as elementarily done by our assignment. This is totally
consequential, direct, even trivially obvious. I shamefully confess it
took me a while of fiddling with __getattr__
,
__setattr__
and __delattr__
, before the obvious
solution at last jumped out at me. We can be so conditioned to complexity
and cleverness, that it becomes hard to find the simplicity and
obviousness "hiding" right in front of our nose! Fortunately, Python
helps a lot in the quest for simplicity and clarity. Indeed, I think this
obvious, elementary use of self.__dict__
re-binding, to
express an important design intent in a direct, elementary way, validates
Guido's then-controversial choice, back in the pre-historical times when
he was designing Python 1.5.2, to allow this re-binding. Pity that
non-classic classes, in Python 2.2, lose this ability; we'll see later
that this is not fatal, but it does make us work harder.
Back to what Borg
is giving us... Client code can just
instantiate your Borg
derived class, just as it might
instantiate any other class. Borg
is not a Creational
pattern: as already mentioned, it verges more on the Structural, although
you could make a case about it having Behavioral aspects. This means that
Borg
carries no Creational constraints. To put it another
way, Borg
does not require Creational Collaborations from
client code. Like any other Python class meant to be inherited,
Borg
does of course require from subclasses the elementary
Collaboration of calling the superclass's __init__
.
Borg
is a simple idea: it does not conflate
different and unrelated concerns, nor does it attempt to solve other
possible problems, not directly related to "state sharing" (or "limiting
number of instances"). If you want to have "controlled access to
instances", for example, which is listed as benefit number 1 for the
Gof4's Singleton
Pattern, you have to deal with it in some
other way. Just as you would for any other class for which you deem
controlled access desirable, whatever the number of its instances and the
kind of state these instances hold. Controlled access is clearly an
orthogonal consideration, independent from "limiting number of instances",
or "state sharing".
Ability to subclass class Singleton
is very important in
the Singleton
DP: it's the second condition in
Applicability, and the key issue in Consequences n.3 and n.5. However,
the issue of choosing which of the Singleton
's subclasses
is actually instantiated looms large. This issue takes up two thirds of
the Implementation section, leading to a rich, complex solution, a
registry of singletons. Even this rich solution still doesn't meet many
typical application needs. What if two separate subsystems each need to
refine Singleton
by subclassing? Instances of the two
separate subclasses can't both exist, or else two separate "instances of
Singleton
" would exist, each as a sub-object (base object)
of one of the subclasses. Borg
has no problem with this,
of course: as many instances as needed exist, all sharing state, and
therefore, in particular, any subclass of Borg
may be
independently further subclassed as needed. Often, the independent
subclasses are each providing different behavior tweaks or additions
with different mixins. State is still shared, but of course each
independent subclass may easily avoid accidental interference with
another, in the usual Python way, i.e. by naming with two leading
underscores those attributes and methods that are only needed for a
given class's internal operation.
One claimed advantage of Singleton
, that Borg
may not appear to match, is Consequence n.4, "Permits a variable number of
instances". When you're refactoring your code, you can of course easily
de-Borg
ize any given application level class, but there's no
easy third way: it's all or nothing — either per instance state is
shared, or it's kept by each instance on its own. On the other hand, a
separate PolyBorg
class isn't any harder to envisage,
than the PolySingleton
class the Gof4 may have had in mind
when writing about this Consequence. Suppose, for example, that, in a
given use-case of PolySingleton
, exactly 4 instances may
exist, and each call to PolySingleton::Instance
chooses which
of the 4 instances to yield in round robin fashion, something
like:
PolySingleton* PolySingleton::_instances[4];
int PolySingleton::_next=-1;
PolySingleton* PolySingleton::Instance() {
++_next;
if(_next>=4) _next=0;
if(_instances[_next]==0) {
_instances[_next] = new PolySingleton;
}
return _instances[_next];
}
I'm not too sure this variation makes much
sense, but perhaps there are cases in which it does, e.g. for load
balancing. More often, I suspect a class with a limited number of
instances would require some kind of selector argument for instance
selection. It typically does matter which one of the separate
instances you get. However, such an extra argument would make
PolySingleton
not interface compatible with
Singleton
any more. But anyway, if this version of
PolySingleton
meets requirements, so does the following
version of PolyBorg
:class PolyBorg:
_shared_states = [{} for i in range(4)]
_next = -1
def __init__(self):
self.__class__._next += 1
if self.__class__._next>=4: self.__class__._next = 0
self.__dict__ = self._shared_states[self.__class__._next]
We
do of course have to be explicit and use self.__class__._next
,
rather than self._next
. This is mandatory when we rebind
it: otherwise, it would uselessly become per instance, while it's
crucial that it stay per-class. For uniformity, we then obviously
choose to use the explicit form throughout.
Borg
, and its variation PolyBorg
, are the
first two of our five easy non-Patterns. They are as easy as pie,
mind you. Don't let the amount of discussion fool you into believing
there's anything deep or difficult about them. The discussion is mostly
addressing the complexity and hidden depths of Singleton
(and
PolySingleton
). Look at the Python code again: it's so much
terser, clearer, and simpler than the discussion! Four lines of code for
Borg
, seven for PolyBorg
, all clear and open and
understandable at an elementary level. Python makes it easy to unveil the
simplicity that, without it, masquerades as complexity.
So, what makes Borg
and PolyBorg
non-patterns? Why, if nothing else, they miss the prime requisite:
Known Uses! That's right, folks, these are scary, dangerous,
field-unproven, experimental ideas! Does this scare you off from the
effort of studying and understanding them so you can apply them to
everyday problems...? What effort? what study? they are clear at first
sight to any Pythonista worth his or her salt. Indeed, they're so simple
they have no doubt been independently reinvented over and over, as is
often the case in Python. It's so simple to just do it, that the
effort of combing the literature and published sources looking for
Patterns to extract is sometimes hard to cost-justify. Sure, Python's
readability and clarity reduce the cost of such a literature-search
effort, but they cannot reduce it to the point of making it lower than the
effort of writing four short lines of code, or thereabouts. So,
Borg
and PolyBorg
are not Patterns (with the
uppercase-P:-) because they're too simple and elementary to justify a
Pattern's necessary "infrastructure" investment, particularly the research
effort needed to find Known Uses. Do not forget that real Design Patterns
do really need that infrastructure, and most particularly that
research into Known Uses. Let's say Borg
and
PolyBorg
are idioms, then, or, at best, lowercase-p patterns.
I first wrote up Borg
in [ma01a] as a
Recipe, and that may be as good a name as any for this category of simple
Pythonic ideas.
Going back to the requisite of "multiple instances, but in limited
number", we might typically want each instance-request to have as an
argument a desired-instance identifier, say a string. If an instance
corresponding to the given identifier already exists, that instance must
be returned; otherwise, a new instance must be created and returned. This
is getting pretty far away from the "just one instance" idea, and yet it
is a reasonably frequent application need. Think, for example, of
opening files, or other URLs: we may well want to ensure that state is
shared, no matter how many times an URL is opened. Also, requirements
akin to these are what the "registry of singletons" in the Gof4's
Singleton
DP Implementation section strongly suggests (at
least, to me). Could Borg
be stretched to accommodate this
need — an extensible registry of hive-minds, tagged by identifiers,
with the right one available on demand...?
Well, yes, it's not all that different from PolyBorg
after
all, and of course Python's dictionaries make "the Registry" a
snap:
class RegisBorg:
_shared_states = {}
def __init__(self, ident):
try: self.__dict__ = self._shared_states[ident]
except KeyError: self.__dict__ = self._shared_states[ident] = {}
It is a snap, but one must nevertheless question if it's the
appropriate snap, or if we couldn't have snapped even more simply
and fruitfully. After all, the amended specs sound more and more like a
Creational request, and yet we're still using a solution that's rather
Structural. Such a "category mismatch" should rightfully makes us a
little bit uneasy. Aren't we over-stretching Borg
?
Are we abandoning the straight and narrow, but fruitful, path of
simplicity, for a seductive but ultimately fraught one of cleverness and
deviousness...? After all, the "intuitive" solution to the stated
requirements, the one that comes to mind at once, would be an application
of a Pattern (or pattern, or idiom, or recipe...) "Factory with a
Registry", or RegisFact
for
short:
class Whatever: pass
_instances = {}
def RegisFact(ident):
try: return _instances[ident]
except KeyError:
_instances[ident] = Whatever()
return _instances[ident]
RegisFact
uses a Creational idea to implement something that
feels very much like a Creational requirement. No "category mismatch",
then, and surely, no cleverness, no deviousness. Let's note, in passing,
that the Creational idea is a Factory, but a trivially simple one —
just a function, how un-OO! — not one of the powerful Creational
patterns, such as Abstract Factory and Factory Method. This is another
non-Pattern, deliberately choosing simplicity over power.
However, RegisFact
does not really achieve all that extra
simplicity over RegisBorg
, and it does have substantially
limited functionality. Abstract, semi-philosophical guidelines such as
"no category mismatch" are often useful as rules of thumb. Lighthouses
help us find our way in the fog. But it's even better when there is no
fog around, and we can just find our way by carefully examining our
surroundings. Python's already-mentioned Sun is one good way to help fog
disperse. Translation from fancy metaphors back down to Earth again:
writing down some actual Python code, rather than reasoning in the
abstract, we can more easily examine concrete perspectives of different
possible solutions.
RegisBorg
still has the same key useful aspect as the
other Borg
variations. We can subclass at will, to tweak
behavior or add per-subclass state, while keeping the essential defining
characteristic: all instances (here, all instances corresponding to a
given ident
) share state. Because of this,
RegisBorg
, exactly as above coded, is already useful,
although class RegisBorg
itself does just about nothing. You
could say the non-pattern is factored as follows: class
RegisBorg
handles the registry and state-sharing behavior,
while subclasses provide application-specific parts of behavior and
state.
On the other hand, RegisFact
, as coded, is not all that
useful. Factory functions do not let client-code easily use inheritance.
class Whatever
is thus hard-coded with a certain behavior: as
we wrote it, no behavior at all. Therefore, that's the behavior (or lack
thereof) that actually obtains from the point of view of
RegisFact
's clients. Better than nothing: we can still use
the Whatever
instances, that factory function
RegisFact
yields, as passive containers of arbitrary
attributes. Maybe we've even found the one case where adding per-instance
behavior is justifiable!-). Still, RegisFact
is far from
being fully satisfactory, because of these limitations.
For most potential uses, RegisFact
as written is too
simple. Yes, there can be such a thing as an artifact that
is too simple, not rich and complex enough, for the goals it aims to
accomplish. RegisFact
really wants to be quite a
bit richer and more complex than it is, to deploy its full potential.
Factories' specialty is their potential ability to return objects of
different classes, according to specifics that the Factory can
encapsulate. RegisFact
could be extended to hold a
registry of classes. It could further hold a set of rules (the Strategy
Design Pattern might be very appropriate here!) to select the right
class to instantiate for any given requested ident
—
parsing the ident
string, for example, and selecting
accordingly. But it doesn't do any of that, as written... it just
sort of sits there! So, RegisFact
as written is "easy",
yes, but... too easy — neither fish nor fowl, neither as
intrinsically simple as RegisBorg
, nor as rich and
sophisticated as the full-fledged Design Pattern that
RegisFact
might one day become, RegisFact
as
it stands basically serves the purpose of convincing us that a thorough
study of Design Patterns and Pattern Languages is anything but a waste
of effort. Simplicity sometimes can, for a short while (until we
examine it more closely, in bright light, and check if it sparkles, or
starts getting soggy and melting) be somewhat illusory in its intuitive
appeal!
For completeness, and to reach the number of five non-patterns and
justify the neat title, let's see how Borg
interacts with
the "non-classic" (a.k.a. "new-style") classes introduced in Python 2.2.
The "instant user appeal" of the new-style classes is, first and
foremost, that such classes let you subclass built-in types. However,
new-style classes come with a whole panoplia of new possibilities and
constraints. From the perspective of sharing state, in particular, the
key difference from a classic class is that a new-style class doesn't
keep all per instance data in a single dictionary. The class may
inherit from a built-in type, which may keep some per-instance state
wherever it pleases; also, a class may define or inherit a
__slots__
attribute, in which case per-instance state lives
in the slots rather than in a dictionary. Moreover, even for new-style
classes whose instances do keep state in __dict__
, the
__dict__
attribute itself may not be assigned (re-bound).
A new-style class, therefore, cannot just inherit a mix-in like
Borg
and have all per instance state become automatically
shared, as a classic class can. Rather, with new-style classes, we are
back to the "status quo" as in most other languages: to share state, we
must rely on Delegation.
Note that Delegation is not a Design Pattern, as the Gof4 explain well in their book: it is just too fundamental, too basilar to good object-oriented design. Delegation is not a DP for much the same reason such things as integer addition, while loops, or subroutines aren't DPs: they are, rather, some of the fundamental building blocks out of which all designs, and their inherent patterns (or Patterns), are built. The Gof4 list Delegation among the fundamental principles, right after Polymorphism, Mixin classes, "Program to an interface, not an implementation" (Python translation: don't type-test!-), and "Favor object composition over class inheritance"; and just before Generics and the principles of "Designing for change". All in Chapter 1, of course, before they start their Design Patterns catalog.
In Python, we're blessed with a particularly flexible and easy to code
form of automatic Delegation. Special methods __getattr__
and friends are strategic choke-points, from which we can easily control
and divert (e.g., delegate) any attribute and method access (and binding,
re-binding, unbinding). In pre-2.2 Python, we used such automatic
Delegation, for example, to "inherit" (so to speak) from built-in types,
as shown in [ma01b]. We couldn't actually inherit,
but we almost didn't notice, except where some uncouth piece of framework
or client code type-tested, and thus broke the wonderful, smooth
polymorphism. To quote the Gof4 again, "Delegation ... shows ... you can
always replace inheritance with object composition" — unless, of
course, somebody's busy coding deuced type-tests. In Python 2.2, we don't
need automatic Delegation to pseudo-subclass built-in types, as we can
subclass them in earnest. However, automatic delegation is anything but
obsolete. Old non-Patterns don't really ever die, they just fade away
into somewhat more obscure corners of language use.
Automatic Delegation still plays a precious role in Python 2.2. Consider, for example:
class DeleBorg:
_delegate = None
def __getattr__(self,name):
return getattr(self._delegate,name)
def __delattr__(self,name):
return delattr(self._delegate,name)
def __setattr__(self,name,value):
return setattr(self._delegate,name,value)
As coded, class DeleBorg
is a classic class, but it might
equally well be made into a new-style class, by having it inherit from
object
, since the triad of methods __getattr__
,
__delattr__
, __setattr__
would still work.
All substantial behavior, as well as all state, comes from the
self._delegate
object, since methods are accessed through
__getattr__
, just like any other attribute.
While the "data override" (of attribute _shared_state
)
was optional for subclasses of Borg
, we do need an
analogous "data override" (of the _delegate
attribute) to
make subclasses of DeleBorg
useful:
class Borg22(DeleBorg):
_delegate = object_to_be_wrapped
Again, therefore, DeleBorg
's methods access
self._delegate
, not DeleBorg._delegate
,
so as to enable the "data override" by subclasses. For the same reason,
we name the overridable attribute with one leading underscore, not two.
This is even more important for DeleBorg
than it was for
Borg
, since the "data override" plays such a central role
here, while previously it was just a nice option we wanted to
preserve.
However, not all is perfectly rosy here, alas.
DeleBorg
is not quite as neat as Borg
itself:
it's not such a direct expression of design intent. Rather than being
able to share state directly, we share it indirectly, by taking control of
the behavioral aspects of accessing, binding, re-binding, and unbinding
elements of the state. Thus, we have over twice the boilerplate code
(albeit still in a modest amount), and a small but non-null overhead, an
extra call for any operation. Further, instances of subclasses of
DeleBorg
do not satisfy isinstance
with the type
or class of the _delegate
attribute, while instances of
subclasses of Borg
, thanks to multiple inheritance, did.
Besides the bother of type-tests, this means, for example, that client
code becomes constrained with respect to extracting and applying unbound
methods from this class, or type.
These are disturbances at the margin, rather than crucial defects, but
still they show that DeleBorg
isn't quite as seamless, nor
quite as big a win, as Borg
used to be, pre-2.2.
Borg
still lives, therefore, even in Python 2.2: if the class
we want to Borg
ize is a classic one, using Borg
itself still has advantages over using DeleBorg
.
Design Patterns, and Pattern Languages, are very useful conceptual tools: they can help you think effectively about design, as well as providing immediately useful ways to frame specific design problems and their solutions. However, not all design ideas are Design Patterns, nor should they all be.
Some design ideas and approaches are too fundamental, basilar, pervasive, to be classified as Design Patterns. Others are too simple, elementary, intuitive, to be worth classifying as DPs. Such a classification is at least a semi-formal endeavor, requiring a definite amount of work (particularly to research and document Known Uses, an indispensable step). The work should be undertaken only when there is enough "substance" in the prospective DP to pay back the effort expended, with interest. Moreover, which design ideas it's feasible to classify as DPs, and which ideas are worth thus classifying, does depend on the programming language meant to be used to implement the design. Design is not an abstract, in-a-vacuum activity: rather, it is a concrete bridge between analysis and intended implementation, with a lot of "feedback" between the various phases.
These theses aren't all that controversial: indeed, they are asserted and argued in the very first chapter of the "Design Patterns" book! However, it appears that many readers of that excellent work skim its beginning lightly, eager to jump into the "meat" of the DP catalog that makes up most of the book. This is surely understandable, as the catalog is so rapidly useful to help with real-life problems. The start of the book, in contrast, may look like abstract, generic introductory and philosophical material, not immediately usable. However, such readers are shortchanging themselves, by not acquiring the meta-tools needed for critical analysis of specific design needs in term of DPs, and vice versa. Critical analysis of DPs and design needs is not an optional issue: without it, you cannot spot what patterns are anti-patterns, in terms of your actual design needs — including what programming languages you intend to target with your design.
In this paper, I single out one Design Pattern, the popular
Singleton
, for critical examination. I survey its
applicability, both in general terms, and, more specifically, with
regards to two subtly different languages, Python 2.1 and 2.2. I
propose and examine alternative design ideas (not full fledged Patterns)
addressing Forces very similar to the ones Singleton
deals
with, in very different ways (Structural, or even Behavioral, as opposed
to Creational ones). The alternative ideas are quite simple, which is
most often a very good thing. However, I also show one case in which
excessive (misplaced) simplicity makes a design idea not very useful for
our purposes: we do want to make our designs as simple as possible...
but, no simpler than that!
[al77]C. Alexander, et al, "A Pattern Language: Towns, Buildings, Construction", Oxford University Press 1977
[al79]C. Alexander, "The Timeless Way of Building", Oxford University Press 1979
[bd00]"Big Design Up Front", multi-author WikiWiki, http://xp.c2.com/BigDesignUpFront.html
[be00]J. Bentley, "Programming Pearls", Second Edition, Addison-Wesley 2000
[fo97]M. Fowler, "Analysis Patterns: Reusable Object Models", Addison-Wesley 1997
[fo99]M. Fowler, "Refactoring: Improving the Design of Existing Code", Addison-Wesley Longman 1999
[go95]E. Gamma, R. Helm, R. Johnson, J. Vlissides, "Design Patterns, Elements of Reusable Object-Oriented Software", Addison-Wesley 1995
[ko33]A. Korzybski, "Science and Sanity: An Introduction to Non-Aristotelian Systems and General Semantics", first published 1933; reprint [Science Press] 1961
[ma01a]A. Martelli, "Singleton? We don't need no stinkin' singleton: the Borg design non-pattern", in "Python Cookbook", http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/66531
[ma01b]A. Martelli, "Automatic delegation as an alternative to inheritance", in "Python Cookbook", http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/52295
[sa23]G. Santayana, "Skepticism and Animal Faith: Introduction to a System of Philosophy", first published 1923; reprint [Dover] 1955
[sa98]V. Savikko, "Design Patterns in Python", in "Proceedings of the 6th International Python Conference", http://www.python.org/workshops/1997-10/proceedings/savikko.html
[sa99]N. Salingaros, "The Structure of Pattern Languages", in "arq -- Architectural Research Quarterly" volume 4 (2000), http://www.math.utsa.edu/sphere/salingar/StructurePattern.html
[vl98]J. Vlissides, "Pattern Hatching: Design Patterns Applied", Addison-Wesley 1998
[wh96]R. G. White, "Advantages and disadvantages of unique representation patterns", C++ Report 8-8 pp 28-25, Sep 1996
[wi22]L. von Wittgenstein, "Logische-Philosophische Abhandlung", Annalen der Naturphilosophie 1922; bilingual edition (German/English) as "Tractatus Logico-Philosophicus" [Routledge] 1924