Implementing View Types in Python
Venkatesh-Prasad Ranganath
Posted on January 12, 2020
In object-oriented languages like Java, C#, or Kotlin, given a type T
, an associated view type TView
is used to expose a specific view (parts) of an object of type T
. This helps hide implementation details.
For example, in the following Kotlin example, Ledger
interface is used to provide access to a ledger while hiding the underlying implementation details, i.e., LedgerImpl
provides the functionalities of a ledger and it has a process
and container
members.
interface Ledger {
fun getValue(i: Int): Int?
}
class LedgerImpl: Ledger {
val container = HashMap<Int, Int>()
override fun getValue(i: Int) = container.get(i)
fun process() {
// processing
}
}
fun getLedger(): Ledger {
val c = LedgerImpl()
c.process()
return c as Ledger
}
Can we achieve the same in Python?
Yes, we can mimic the above code structure in Python as follows.
from abc import ABC
from collections import defaultdictclass Ledger(ABC):
def get_value(self, i: int) -> int:
passclass _LedgerImpl(Ledger):
def __init__(self):
self._container = defaultdict(int)
def get_value(self, i: int) -> int:
return self._container[i] def process(self) -> None:
...
def facade() -> Ledger:
l = _LedgerImpl()
l.process()
return l
While _container
is marked as private by convention (i.e., the name is prefixed with an underscore), callers of facade
can still access _container
in the returned value as Python does not enforce access restrictions at runtime. So, the implementation details are not truly hidden.
Can we do better?
(Accidentally) Yes, we can do better. We can use namedtuple
support in Python to realize the view type.
from abc import ABC
from collections import defaultdict
from typing import Callable, NamedTuple
class Ledger(NamedTuple):
get_value: Callable[[int], int]
class _LedgerImpl():
def __init__(self):
self._container = defaultdict(int)
def process(self) -> None:
...
def get_view(self) -> Ledger:
return Ledger(lambda x: self._container[x])
def facade() -> Ledger:
l = _LedgerImpl()
l.process()
return l.get_view()
With this implementation, unless we thread our way thru the lambda function created in get_view
, the implementation details stay truly hidden when compared to the previous Python implementation.
Also, this implementation pattern relies on composition instead of inheritance. While the earlier implementation pattern can be changed to use composition, it still does not truly hide implementation details.
When should we use this pattern?
This pattern is ideal to use when implementation details need to be truly hidden.
And, here’s my yardstick for when should implementation details be truly hidden.
If the client programs of a library/program will respect the access restrictions communicated via conventions, then this pattern is not helpful/required. This is most likely the case when modules within a library or program act as clients of other modules in a library or program. In these situation, simpler realizations of view types (e.g., the first python example) will suffice.
On the other hand, if client programs may take dependence on the implementation of a library/program (e.g., for performance reasons) when the current version of the library/program does not support the capabilities need by the client programs, then this pattern can be helpful to truly hide the implementation.
Note
I stumbled on this pattern during my coding sessions. Since I found it to be interesting and useful, I blogged about it. That said, as with all patterns, use them only when they are required.
(Originally posted here.)
Posted on January 12, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.