88d586f038
Change-Id: I5a30a62f8d62e2c97c30e795317e95c207e196b9 Closes-Bug: #1946166
291 lines
9.9 KiB
ReStructuredText
291 lines
9.9 KiB
ReStructuredText
..
|
|
Copyright 2015 OpenStack Foundation
|
|
All Rights Reserved.
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
not use this file except in compliance with the License. You may obtain
|
|
a copy of the License at
|
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
License for the specific language governing permissions and limitations
|
|
under the License.
|
|
|
|
============
|
|
Domain model
|
|
============
|
|
|
|
The main goal of a domain model is refactoring the logic around
|
|
object manipulation by splitting it to independent layers. Each
|
|
subsequent layer wraps the previous one creating an "onion" structure,
|
|
thus realizing a design pattern called "Decorator." The main feature
|
|
of domain model is to use a composition instead of inheritance or
|
|
basic decoration while building an architecture. This provides
|
|
flexibility and transparency of an internal organization for a developer,
|
|
because they do not know what layers are used and works with a domain
|
|
model object as with a common object.
|
|
|
|
Inner architecture
|
|
~~~~~~~~~~~~~~~~~~
|
|
|
|
Each layer defines its own operations' implementation through a
|
|
special ``proxy`` class. At first, operations are performed on the
|
|
upper layer, then they successively pass the control to the underlying
|
|
layers.
|
|
|
|
The nesting of layers can be specified explicitly using a programmer
|
|
interface Gateway or implicitly using ``helper`` classes. Nesting
|
|
may also depend on various conditions, skipping or adding additional
|
|
layers during domain object creation.
|
|
|
|
Proxies
|
|
~~~~~~~
|
|
|
|
The layer behavior is described in special ``proxy`` classes
|
|
that must provide exactly the same interface as the original class
|
|
does. In addition, each ``proxy`` class has a field ``base``
|
|
indicating a lower layer object that is an instance of another
|
|
``proxy`` or ``original`` class.
|
|
|
|
To access the rest of the fields, you can use special ``proxy``
|
|
properties or universal methods ``set_property`` and ``get_property``.
|
|
|
|
In addition, the ``proxy`` class must have an ``__init__`` format
|
|
method::
|
|
|
|
def __init__(self, base, helper_class=None, helper_kwargs=None, **kwargs)
|
|
|
|
where ``base`` corresponds to the underlying object layer,
|
|
``proxy_class`` and ``proxy_kwargs`` are optional and are used to
|
|
create a ``helper`` class.
|
|
Thus, to access a ``meth1`` method from the underlying layer, it is
|
|
enough to call it on the ``base`` object::
|
|
|
|
def meth1(*args, **kwargs):
|
|
…
|
|
self.base.meth1(*args, **kwargs)
|
|
…
|
|
|
|
To get access to the domain object field, it is recommended to use
|
|
properties that are created by an auxiliary function::
|
|
|
|
def _create_property_proxy(attr):
|
|
def get_attr(self):
|
|
return getattr(self.base, attr)
|
|
|
|
def set_attr(self, value):
|
|
return setattr(self.base, attr, value)
|
|
|
|
def del_attr(self):
|
|
return delattr(self.base, attr)
|
|
|
|
return property(get_attr, set_attr, del_attr)
|
|
|
|
So, the reference to the underlying layer field ``prop1`` looks like::
|
|
|
|
class Proxy(object):
|
|
…
|
|
prop1 = _create_property_proxy('prop1')
|
|
…
|
|
|
|
If the number of layers is big, it is reasonable to create a common
|
|
parent ``proxy`` class that provides further control transfer. This
|
|
facilitates the writing of specific layers if they do not provide a
|
|
particular implementation of some operation.
|
|
|
|
Gateway
|
|
~~~~~~~
|
|
|
|
``gateway`` is a mechanism to explicitly specify a composition of
|
|
the domain model layers. It defines an interface to retrieve the
|
|
domain model object based on the ``proxy`` classes described above.
|
|
|
|
Example of the gateway implementation
|
|
-------------------------------------
|
|
|
|
This example defines three classes:
|
|
|
|
* ``Base`` is the main class that sets an interface for all the
|
|
``proxy`` classes.
|
|
* ``LoggerProxy`` class implements additional logic associated with
|
|
the logging of messages from the ``print_msg`` method.
|
|
* ``ValidatorProxy`` class implements an optional check that helps to
|
|
determine whether all the parameters in the ``sum_numbers`` method
|
|
are positive.
|
|
|
|
::
|
|
|
|
class Base(object):
|
|
""Base class in domain model."""
|
|
msg = "Hello Domain"
|
|
|
|
def print_msg(self):
|
|
print(self.msg)
|
|
|
|
def sum_numbers(self, *args):
|
|
return sum(args)
|
|
|
|
class LoggerProxy(object):
|
|
""""Class extends functionality by writing message to log."""
|
|
def __init__(self, base, logg):
|
|
self.base = base
|
|
self.logg = logg
|
|
|
|
# Proxy to provide implicit access to inner layer.
|
|
msg = _create_property_proxy('msg')
|
|
|
|
def print_msg(self):
|
|
# Write message to log and then pass the control to inner layer.
|
|
self.logg.write("Message %s has been written to the log") % self.msg
|
|
self.base.print_msg()
|
|
|
|
def sum_numbers(self, *args):
|
|
# Nothing to do here. Just pass the control to the next layer.
|
|
return self.base.sum_numbers(*args)
|
|
|
|
class ValidatorProxy(object):
|
|
"""Class validates that input parameters are correct."""
|
|
def __init__(self, base):
|
|
self.base = base
|
|
|
|
msg = _create_property_proxy('msg')
|
|
|
|
def print_msg(self):
|
|
# There are no checks.
|
|
self.base.print_msg()
|
|
|
|
def sum_numbers(self, *args):
|
|
# Validate input numbers and pass them further.
|
|
for arg in args:
|
|
if arg <= 0:
|
|
return "Only positive numbers are supported."
|
|
return self.base.sum_numbers(*args)
|
|
|
|
Thus, the ``gateway`` method for the above example may look like:
|
|
|
|
::
|
|
|
|
def gateway(logg, only_positive=True):
|
|
base = Base()
|
|
logger = LoggerProxy(base, logg)
|
|
if only_positive:
|
|
return ValidatorProxy(logger)
|
|
return logger
|
|
|
|
domain_object = gateway(sys.stdout, only_positive=True)
|
|
|
|
It is important to consider that the order of the layers matters.
|
|
And even if layers are logically independent from each other,
|
|
rearranging them in different order may lead to another result.
|
|
|
|
Helpers
|
|
~~~~~~~
|
|
|
|
``Helper`` objects are used for an implicit nesting assignment that
|
|
is based on a specification described in an auxiliary method (similar
|
|
to ``gateway``). This approach may be helpful when using a *simple
|
|
factory* for generating objects. Such a way is more flexible as it
|
|
allows specifying the wrappers dynamically.
|
|
|
|
The ``helper`` class is unique for all the ``proxy`` classes and it
|
|
has the following form:
|
|
|
|
::
|
|
|
|
class Helper(object):
|
|
def __init__(self, proxy_class=None, proxy_kwargs=None):
|
|
self.proxy_class = proxy_class
|
|
self.proxy_kwargs = proxy_kwargs or {}
|
|
|
|
def proxy(self, obj):
|
|
"""Wrap an object."""
|
|
if obj is None or self.proxy_class is None:
|
|
return obj
|
|
return self.proxy_class(obj, **self.proxy_kwargs)
|
|
|
|
def unproxy(self, obj):
|
|
"""Return object from inner layer."""
|
|
if obj is None or self.proxy_class is None:
|
|
return obj
|
|
return obj.base
|
|
|
|
Example of a simple factory implementation
|
|
------------------------------------------
|
|
|
|
Here is a code of a *simple factory* for generating objects from the
|
|
previous example. It specifies a ``BaseFactory`` class with a
|
|
``generate`` method and related ``proxy`` classes:
|
|
|
|
::
|
|
|
|
class BaseFactory(object):
|
|
"""Simple factory to generate an object."""
|
|
def generate(self):
|
|
return Base()
|
|
|
|
class LoggerFactory(object):
|
|
"""Proxy class to add logging functionality."""
|
|
def __init__(self, base, logg, proxy_class=None, proxy_kwargs=None):
|
|
self.helper = Helper(proxy_class, proxy_kwargs)
|
|
self.base = base
|
|
self.logg = logg
|
|
|
|
def generate(self):
|
|
return self.helper.proxy(self.base.generate())
|
|
|
|
class ValidatorFactory(object):
|
|
"""Proxy class to add validation."""
|
|
def __init__(self, base, only_positive=True, proxy_class=None, proxy_kwargs=None):
|
|
self.helper = Helper(proxy_class, proxy_kwargs)
|
|
self.base = base
|
|
self.only_positive = only_positive
|
|
|
|
def generate(self):
|
|
if self.only_positive:
|
|
# Wrap in ValidatorProxy if required.
|
|
return self.helper.proxy(self.base.generate())
|
|
return self.base.generate()
|
|
|
|
Further, ``BaseFactory`` and related ``proxy`` classes are combined
|
|
together:
|
|
|
|
::
|
|
|
|
def create_factory(logg, only_positive=True):
|
|
base_factory = BaseFactory()
|
|
logger_factory = LoggerFactory(base_factory, logg,
|
|
proxy_class=LoggerProxy,
|
|
proxy_kwargs=dict(logg=logg))
|
|
validator_factory = ValidatorFactory(logger_factory, only_positive,
|
|
proxy_class = ValidatorProxy)
|
|
return validator_factory
|
|
|
|
Ultimately, to generate a domain object, you create and run a factory
|
|
method ``generate`` which implicitly creates a composite object. This
|
|
method is based on specifications that are set forth in the ``proxy``
|
|
class.
|
|
|
|
::
|
|
|
|
factory = create_factory(sys.stdout, only_positive=False)
|
|
domain_object = factory.generate()
|
|
|
|
Why do you need a domain if you can use decorators?
|
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
|
|
In the above examples, to implement the planned logic, it is quite
|
|
possible to use standard Python language techniques such as
|
|
decorators. However, to implement more complicated operations, the
|
|
domain model is reasonable and justified.
|
|
|
|
In general, the domain is useful when:
|
|
|
|
* there are more than three layers. In such case, the domain model
|
|
usage facilitates the understanding and supporting of the code;
|
|
* wrapping must be implemented depending on some conditions,
|
|
including dynamic wrapping;
|
|
* there is a requirement to wrap objects implicitly by helpers.
|