Skip to content

Commit

Permalink
Add a background document on ZCA.
Browse files Browse the repository at this point in the history
  • Loading branch information
jamadden committed Oct 12, 2017
1 parent 65133ca commit e81fb10
Show file tree
Hide file tree
Showing 5 changed files with 269 additions and 1 deletion.
3 changes: 2 additions & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ python:
- pypy
- pypy3
script:
- sphinx-build -b doctest -d docs/_build/doctrees docs docs/_build/html
- coverage run -m zope.testrunner --test-path=src
after_success:
- coveralls
Expand All @@ -16,7 +17,7 @@ install:
- pip install -U pip
- pip install -U setuptools
- pip install -U coveralls coverage
- pip install -U -e ".[test]"
- pip install -U -e ".[test,docs]"

cache: pip

Expand Down
1 change: 1 addition & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ package works, see :doc:`using_zca`.

getting_started
first_theme
zca_background
using_zca
macros
viewlets
Expand Down
8 changes: 8 additions & 0 deletions docs/using_zca.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,14 @@

.. todo:: Write ZCA section

Background
==========

If you're not familiar with component architectures, and
``zope.component`` in particular, please take a few minutes to read
:doc:`zca_background`.


Theory of Operation
===================

Expand Down
257 changes: 257 additions & 0 deletions docs/zca_background.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,257 @@
====================================================
An Introduction to the Zope Component Architecture
====================================================

.. note:: This document is not intended to be a comprehensive guide to
zope.component or component systems in general. It will only
discuss concepts that are relevant to this package. For more
information, see the `zope.component documentation
<https://zopecomponent.readthedocs.io/en/latest/>`_ or `this
guide <http://muthukadan.net/docs/zca.html>`_.

This document will provide a brief introduction to some of the
concepts in a component architecture and an example specifically of
using ``zope.component``. If you're already familiar with these
things, you can skip it.

Definitions
===========

For our purposes, we will define a *component* as a unit of software
that provides some specific functionality. What that functionality is
and what it does is the component's *interface*; a single component
may provide multiple different functions and thus multiple interfaces.
(Interfaces can also be thought of as a way to represent a particular
concept.)

A *component registry* provides us with a way to find components that
provide specific interfaces. Sometimes there can me multiple
components that can all provide the same interface (different ways to
implement a specific function), so we can choose between them based on
a *name*.

Sometimes a component will need particular other objects in order to
fulfill its function. In this case the component registry stores a
*factory*, and when we ask it for the component, we provide the
objects we want to work with to the registry. The registry then finds
the correct factory, passes it the objects, and returns the resulting
component. This process is called using an *adapter* because the
factory *adapts* the objects we give it to produce the desired
component. (In Python, factories are often classes, and the returned
component is just an instance of the class.)

Certain types of factories will know how to adapt certain kinds of
objects, while other types of factories will know how to adapt others,
so the component registry will inspect the objects we give it to find
the most specific factory---the one that's the best fit for the
objects we give.

In Python, objects are components. Interfaces can either be
represented by classes, or more flexibly by the interface objects
created by ``zope.interface``. (These are more flexible than classes
because they can be added and removed from particular objects
independently of any code.)

Example
=======

For example, let's imagine we're working in the United Nations general
assembly, and we need to provide language translators for the various
diplomats.

We'll begin by defining an interface for someone who can speak a
language, plus some specific language examples:

.. doctest::

>>> from zope.interface import Interface
>>> class ILanguageSpeaker(Interface):
... """Someone who can speak a language"""
>>> class IGermanSpeaker(ILanguageSpeaker):
... """Someone who can speak German."""
>>> class ISpanishSpeaker(ILanguageSpeaker):
... """Someone who can speak Spanish"""
>>> class IFrenchSpeaker(ILanguageSpeaker):
... """Someone who can speak French."""


Now we'll create some diplomat classes and let them speak their native
language (because the class is a factory, this is called
*implementing* an interface; the resulting class instance is said to
*provide* the interface):

.. doctest::

>>> from zope.interface import implementer

>>> @implementer(IGermanSpeaker)
... class GermanDiplomat(object):
... """The German diplomat speaks German."""

>>> @implementer(ISpanishSpeaker)
... class SpanishDiplomat(object):
... """The Spanish diplomat speaks Spanish."""

>>> @implementer(IFrenchSpeaker)
... class FrenchDiplomat(object):
... """The French diplomat speaks French."""

>>> french_diplomat = FrenchDiplomat()
>>> spanish_diplomat = SpanishDiplomat()
>>> german_diplomat = GermanDiplomat()

None of the diplomats speak the same language and are thus unable to
communicate. Let's remedy that by providing a translator. We'll first
define an interface to represent someone that can translate:

.. doctest::

>>> class ILanguageTranslator(ILanguageSpeaker):
... """Someone who can translate between languages. """

If we ask the component registry for a translator for the Spanish and German
diplomats, we won't find anyone yet (we use
:func:`zope.component.getMultiAdapter` to request an adapter for
multiple objects):

.. doctest::

>>> from zope import component
>>> component.getMultiAdapter((spanish_diplomat, german_diplomat), ILanguageTranslator)
Traceback (most recent call last):
...
zope.interface.interfaces.ComponentLookupError: ((<SpanishDiplomat...>, <GermanDiplomat...>),...


Let's create someone who can speak both languages:

.. doctest::

>>> @implementer(ISpanishSpeaker, IGermanSpeaker)
... class Steve(object):
... """Steve speaks two languages."""
...
... def __repr__(self):
... return "<Hi, I'm Steve>"
>>> steve = Steve()

Now lets hire that person and put them to work as a translator by
registering them in the component registry (notice that the object
that implements ``ILanguageTranslator`` is given the two people who
need the translating done, but Steve doesn't need them to do his
job---he can translate for any Spanish and German speakers---so we
just use a lambda function):

.. doctest::

>>> component.provideAdapter(lambda spanish_speaker, german_speaker: steve,
... provides=ILanguageTranslator,
... adapts=(ISpanishSpeaker, IGermanSpeaker))

We can now find someone who will translate for the diplomats:

.. doctest::

>>> component.getMultiAdapter((spanish_diplomat, german_diplomat), ILanguageTranslator)
<Hi, I'm Steve>

Extending Interfaces
--------------------

But what if the diplomats need to have a conversation about Security
Council matters, something that Steve isn't cleared for? We'll need an
interface to represent a translator with a security clearance:

.. doctest::

>>> class ISecureLanguageTranslator(ILanguageTranslator):
... """A secure translator."""

We'll imagine that computer translation skills are proceeding apace
and are good enough for this sort of thing, so we'll create a computer
that can speak all the languages. As a computer, it's considered
inherently secure:

.. doctest::

>>> @implementer(ISecureLanguageTranslator,
... ISpanishSpeaker,
... IGermanSpeaker,
... IFrenchSpeaker)
... class ComputerTranslator(object):
... def __init__(self, person_a, person_b):
... pass

(The computer might want to know exactly who it is translating
for---maybe to adapt to regional dialects---so we'll let it have
access to the diplomats.) Now we can install the computer to do some
secure translating:

.. doctest::

>>> component.provideAdapter(ComputerTranslator,
... provides=ISecureLanguageTranslator,
... adapts=(ISpanishSpeaker, IGermanSpeaker))
>>> component.provideAdapter(ComputerTranslator,
... provides=ISecureLanguageTranslator,
... adapts=(ISpanishSpeaker, IFrenchSpeaker))

The diplomats can now have a secure conversation:

.. doctest::

>>> component.getMultiAdapter((spanish_diplomat, german_diplomat), ISecureLanguageTranslator)
<ComputerTranslator ...>

Steve only speaks Spanish and German, but what if the Spanish and
French speakers want to have a (non-secure) conversation about their home town
football teams? Steve can't do it. Can anyone?

.. doctest::

>>> component.getMultiAdapter((spanish_diplomat, french_diplomat), ILanguageTranslator)
<ComputerTranslator ...

The computer can! Because ``ISecureLanguageTranslator`` extends
``ILanguageTranslator``, when we ask for the latter, the registry is
smart enough to know that a secure translator can just as well handle
non-secure communications.

Names
-----

Steve is a great German speaker, but his Spanish accent is a bit
rough, and the Spanish diplomat would prefer someone a bit easier to
understand, so we'll hire someone else.

.. doctest::

>>> @implementer(ISpanishSpeaker, IGermanSpeaker)
... class Joe(object):
... """Joe speaks two languages."""
...
... def __repr__(self):
... return "<Hi, I'm Joe>"
>>> joe = Joe()

This time, we'll register the translator so that the Spanish diplomat
can ask for the translator by name:

.. doctest::

>>> component.provideAdapter(lambda spanish_speaker, german_speaker: joe,
... name="Joe",
... provides=ILanguageTranslator,
... adapts=(ISpanishSpeaker, IGermanSpeaker))

>>> component.getMultiAdapter((spanish_diplomat, german_diplomat),
... ILanguageTranslator,
... name="Joe")
<Hi, I'm Joe>

Steve is still available by default:

.. doctest::

>>> component.getMultiAdapter((spanish_diplomat, german_diplomat), ILanguageTranslator)
<Hi, I'm Steve>
1 change: 1 addition & 0 deletions tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ deps =

[testenv:docs]
commands =
sphinx-build -a -v -b doctest -d docs/_build/doctrees docs docs/_build/html
sphinx-build -b html -d docs/_build/doctrees docs docs/_build/html
deps =
.[docs]

0 comments on commit e81fb10

Please sign in to comment.