Sometimes the problem you’re solving is as simple as “I don’t have the interface that I want.” Two of the patterns in GoF Design Patterns solve this problem. Adapter takes one type and produces an interface to some other type. Façade creates an interface to a set of classes, providing a more comfortable way to deal with a library or bundle of resources.
When you’ve got this, and you need that, Adapter solves the problem. The only requirement is to produce a that, and there are a number of ways to accomplish this adaptation:
# adapter.py
# Variations on the Adapter pattern.
from typing import Any, override
class WhatIHave:
def g(self) -> None:
print("WhatIHave.g()")
def h(self) -> None:
print("WhatIHave.h()")
class WhatIWant:
def f(self) -> None: ...
class ProxyAdapter(WhatIWant):
def __init__(self, what_i_have: WhatIHave) -> None:
self.what_i_have = what_i_have
@override
def f(self) -> None:
# Implement behavior using
# methods in WhatIHave:
self.what_i_have.g()
self.what_i_have.h()
class WhatIUse:
def op(self, what_i_want: WhatIWant, /) -> None:
what_i_want.f()
# Approach 2: build adapter use into op():
class WhatIUse2(WhatIUse):
@override
def op(self, what_i_have: Any) -> None:
ProxyAdapter(what_i_have).f()
# Approach 3: build adapter into WhatIHave:
class WhatIHave2(WhatIHave, WhatIWant):
@override
def f(self) -> None:
self.g()
self.h()
# Approach 4: use an inner class:
class WhatIHave3(WhatIHave):
class InnerAdapter(WhatIWant):
def __init__(self, outer: WhatIHave3) -> None:
self.outer = outer
@override
def f(self) -> None:
self.outer.g()
self.outer.h()
def what_i_want(self) -> WhatIWant:
return WhatIHave3.InnerAdapter(self)
what_i_use = WhatIUse()
what_i_have = WhatIHave()
adapt = ProxyAdapter(what_i_have)
what_i_use2 = WhatIUse2()
what_i_have2 = WhatIHave2()
what_i_have3 = WhatIHave3()
what_i_use.op(adapt) # Approach 1: separate adapter
#: WhatIHave.g()
#: WhatIHave.h()
what_i_use2.op(what_i_have) # Approach 2: adapting op()
#: WhatIHave.g()
#: WhatIHave.h()
what_i_use.op(what_i_have2) # Approach 3: adapter built in
#: WhatIHave.g()
#: WhatIHave.h()
what_i_use.op(what_i_have3.what_i_want()) # Approach 4
#: WhatIHave.g()
#: WhatIHave.h()The output is deliberately monotonous. Four different
structures produce one behavior: every route ends at the same
two methods on a WhatIHave. The approaches differ
only in where the adaptation lives, a separate object, the call
site, the adaptee’s own class, or an inner class the adaptee
hands out. When the output cannot tell the approaches apart, the
choice among them is purely one of packaging, and the next
section argues Python lets you skip most of the packaging
too.
This takes liberty with the term “Proxy,” because GoF Design Patterns asserts that a Proxy must have an identical interface with the object for which it is a surrogate.
Two details in the listing repay attention. The
/ in WhatIUse.op() makes its parameter
positional-only, and it is load-bearing:
WhatIUse2.op() renames the parameter to
what_i_have, and renaming a keyword-callable
parameter in an override breaks substitutability, so the checker
rejects it without the /. The rename is the smaller
half of that story. WhatIUse2.op() also changes the
parameter’s type. Its base accepts a
WhatIWant, and it accepts a WhatIHave.
Annotate both precisely and a checker rejects the override
outright, reporting invalid-method-override,
because narrowing what a method accepts breaks substitutability.
That is why this one parameter stays Any while the
rest of the listing names real types. The Any is
not laziness. It is what allows an override that cannot
substitute for its base to compile. Approach 2 is a different
operation wearing an inherited name. Code holding a
WhatIUse cannot safely be handed a
WhatIUse2, and that is the price of building the
adapter into the operation. Second, the approaches split into
two families GoF Design Patterns names.
ProxyAdapter is an object adapter: it
holds the adaptee and can wrap any instance handed to it at
runtime. WhatIHave2 is a class adapter: it
inherits from the adaptee, which fixes the adapted class at
definition time and exposes the adaptee’s entire surface,
g() and h() included, to every client
of the adapter. Composition keeps the two interfaces separate;
inheritance merges them.
The four variations above are Java habits. Python is
dynamically typed. WhatIUse.op() only calls
f(), so it accepts any object that has an
f(). You do not need a shared base class or a
declared interface, only the method. The common adapter need is
“forward most calls unchanged, and add or change a few.”
__getattr__() does the forwarding, so the adapter
is tiny:
# getattr_adapter.py
from typing import Any
class WhatIHave:
def g(self) -> str: return "g"
def h(self) -> str: return "h"
class Adapter:
def __init__(self, adaptee: WhatIHave) -> None:
self._adaptee = adaptee
def f(self) -> str: # The new interface
return self._adaptee.g() + self._adaptee.h()
def __getattr__(self, name: str) -> Any: # Forwards the rest
return getattr(self._adaptee, name)
a = Adapter(WhatIHave())
print(a.f()) # Adapted method
#: gh
print(a.g()) # Forwarded to the adaptee unchanged
#: g__getattr__() runs only for attributes Python
does not find normally, so f() uses the adapter’s
own version while everything else falls through to the adaptee.
This is the idiomatic Python adapter: a thin wrapper, not a
hierarchy. You have already seen one in earnest:
PairCoord in Rethinking
Objects adapts a Pair to the Coord
protocol, an adapter as a frozen dataclass with two properties,
built precisely because the handed-to-you type did not fit. The
forwarding has the limit noted in Surrogate: special methods
bypass __getattr__(), so an adapter that must
support adapter[key] or len(adapter)
defines those dunders itself, as exercise 1 does with
__getitem__().
Testing verifies both halves of that behavior. The new
f() combines the adaptee’s methods, and calls to
methods it doesn’t override forward through to the wrapped
object:
# test_adapter.py
from getattr_adapter import Adapter, WhatIHave
def test_new_interface_combines_methods() -> None:
assert Adapter(WhatIHave()).f() == "gh"
def test_getattr_forwards_existing_methods_unchanged() -> None:
a = Adapter(WhatIHave())
assert a.g() == "g"
assert a.h() == "h"
def test_forwarding_targets_the_wrapped_object() -> None:
have = WhatIHave()
a = Adapter(have)
assert a.g.__self__ is have # __getattr__ delegates to adapteeIf something is ugly, hide it inside an object.
That is what Façade accomplishes. If you have a confusing collection of classes and interactions that the client programmer doesn’t really need to see, then you can create an interface that is useful for the client programmer and that only presents what’s necessary.
A Façade often takes the form of a Singleton Abstract Factory. You can easily get this effect by creating a class containing static factory methods:
# facade.py
class A:
def __init__(self, x: object) -> None: pass
class B:
def __init__(self, x: object) -> None: pass
class C:
def __init__(self, x: object) -> None: pass
# Other classes that aren't exposed by the
# facade go here ...
class Facade:
@staticmethod
def make_a(x: object) -> A:
return A(x)
@staticmethod
def make_b(x: object) -> B:
return B(x)
@staticmethod
def make_c(x: object) -> C:
return C(x)
# The client programmer gets the objects
# by calling the static methods:
a = Facade.make_a(1)
b = Facade.make_b(1)
c = Facade.make_c(1.0)The cleaner Python façade is a module. A module
already presents a curated set of names over whatever tangle of
classes lives behind it, and, as Singleton
notes, it loads once, and every importer shares the same module.
At module level, put the friendly functions and the few classes
to expose. Keep the messy internals private (using a leading
underscore, by convention), and the import is the
façade. A Facade class full of static methods only
reproduces, with more ceremony, what a module already gives
you.
This chapter completes a family of wrappers that share one
structure, a front object forwarding to something behind it,
often through the same few lines of __getattr__().
What separates them is intent, the distinction The Pattern Concept said
remains when structures match. A Proxy keeps the wrapped
object’s interface and controls access to it. A Decorator
keeps the interface and layers on behavior. An Adapter
changes the interface into the one you need. A Façade
fronts a whole tangle of objects rather than one, narrowing many
interfaces down to a comfortable few. When you cannot decide
what to call your wrapper, ask what would break if you removed
it: access control, added behavior, interface fit, or
simplicity.
PairsAdapter that wraps a list of
(key, value) tuples, following the shape of
getattr_adapter.py. Give it a dictionary-style
__getitem__() that finds a value by key, and
forward every other attribute to the wrapped list with
__getattr__(). Confirm adapter["name"]
finds a value while adapter.append(...) still
reaches the underlying list.