==========
Interfaces
==========
``oop-ext`` introduces the concept of interfaces, common in other languages.
An interface is a class which defines methods and attributes, defining a specific behavior,
so implementations can declare that they work with an specific interface without worrying about
implementations details.
Interfaces are declared by subclassing :class:`oop_ext.interface.Interface`:
.. code-block:: python
from oop_ext.interface import Interface
class IDataSaver(Interface):
"""
Interface for classes capable of saving a dict containing
builtin types into persistent storage.
"""
def save(self, data: dict[Any, Any]) -> None:
"""Saves the given list of strings in persistent storage."""
(By convention, interfaces start with the letter ``I``).
We can write a function which gets some data and saves it to persistent storage, without hard coding
it to any specific implementation:
.. code-block:: python
def run_simulation(params: SimulationParameters, saver: IDataSaver) -> None:
data = calculate(params)
saver.save(data)
``run_simulation`` computes some simulation data, and uses a generic ``saver`` to persist it
somewhere.
We can now have multiple implementations of ``IDataSaver``, for example:
.. code-block:: python
from oop_ext.interface import ImplementsInterface
@ImplementsInterface(IDataSaver)
class JSONSaver:
def __init__(self, path: Path) -> None:
self.path = path
def save(self, data: dict[Any, Any]) -> None:
with self.path.open("w", encoding="UTF-8") as f:
json.dump(f, data)
And use it like this:
.. code-block:: python
run_simulation(params, JSONSaver(Path("out.json")))
What about duck typing?
-----------------------
In Python declaring interfaces is not really necessary due to *duck typing*, however interfaces
bring to the table **runtime validation**.
If later on we add a new method to our ``IDataSaver`` interface, we will get errors at during
*import time* about implementations which don't implement the new method, making it easy to spot
the problems early. Interfaces also verify parameters names and default values, making
it easy to keep implementations and interfaces in sync.
.. note::
.. versionchanged:: 2.0.0
Interfaces do not check type annotations at all.
It was supported initially, but in practice
this feature has shown up to be an impediment to adopting type annotations incrementally, as it
discourages adding type annotations to improve existing interfaces, or annotating
existing implementations without having to update the interface (and all other implementations
by consequence).
It was decided to let the static type checker correctly deal with matching type annotations, as
it can do so more accurately than ``oop-ext`` did before.
Type Checking
-------------
.. versionadded:: 1.1.0
The interfaces implementation has been implemented many years ago, before type checking in Python
became a thing.
The static type checking approach is to use `Protocols `__,
which has the same benefits and flexibility of interfaces, but without the runtime cost. At ESSS
however migrating the entire code base, which makes extensive use of interfaces, is a lengthy process
so we need an intermediate solution to fill the gaps.
To bridge the gap between the runtime-based approach of interfaces, and the static
type checking provided by static type checkers, one just needs to subclass from both
`Interface` and ``TypeCheckingSupport``:
.. code-block:: python
from oop_ext.interface import Interface, TypeCheckingSupport
class IDataSaver(Interface, TypeCheckingSupport):
"""
Interface for classes capable of saving a dict containing
builtin types into persistent storage.
"""
def save(self, data: dict[Any, Any]) -> None:
"""Saves the given list of strings in persistent storage."""
The ``TypeCheckingSupport`` class hides from the user the details necessary to make type checkers
understand ``Interface`` subclasses.
Note that subclassing from ``TypeCheckingSupport`` has zero runtime cost, existing only
for the benefits of the type checkers.
.. note::
Due to how ``Protocol`` works in Python, every ``Interface`` subclass **also** needs to subclass
``TypeCheckingSupport``.
Proxies
-------
Given an interface and an object that implements an interface, you can call :func:`GetProxy `
to obtain a *proxy object* which only contains methods and attributes defined in the interface.
For example, using the ``JSONSaver`` from the previous example:
.. code-block:: python
def run_simulation(params, saver):
data = calculate(params)
proxy = GetProxy(IDataSaver, saver)
proxy.save(data)
The ``proxy`` object contains a stub implementation which contains only methods and attributes in ``IDataSaver``. This
prevents mistakes like accessing a method that is defined in ``JSONSaver``, but is not part of ``IDataSaver``.
Legacy Proxies
^^^^^^^^^^^^^^
With type annotations however, this is redundant: the type checker will prevent access to any method not declared in
``IDataSaver``:
.. code-block:: python
def run_simulation(params: SimulationParameters, saver: IDataSaver) -> None:
data = calculate(params)
saver.save(data)
However when adding type annotations to legacy code, one will encounter this construct:
.. code-block:: python
def run_simulation(params, saver):
data = calculate(params)
proxy = IDataSaver(saver)
proxy.save(data)
Here "creating an instance" of the interface, passing an implementation of that interface, returns the stub
implementation. This API was implemented like this for historic reasons, mainly because it would trick IDEs into
providing code completion for ``proxy`` as if a ``IDataSaver`` instance.
When adding type annotations, prefer to convert that to :func:`GetProxy `,
which is friendlier to type checkers:
.. code-block:: python
def run_simulation(params: SimulationParameters, saver: IDataSaver) -> None:
data = calculate(params)
proxy = GetProxy(IDataSaver, saver)
proxy.save(data)
Or even better, if you don't require runtime checking, let the type checker do its job:
.. code-block:: python
def run_simulation(params: SimulationParameters, saver: IDataSaver) -> None:
data = calculate(params)
saver.save(data)
.. note::
As of ``mypy 0.812``, there's `a bug `__ that prevents
:func:`GetProxy ` from being properly type annotated. Hopefully this will be improved in the future.