I'm not a big fan of returning None and then having my
January 19, 2016 ยท View on GitHub
from abc import ABC, ABCMeta, abstractmethod from collections import namedtuple from itertools import count
PayloadFactory = namedtuple('PayloadFactory', [ 'good', 'created', 'queued', 'unchanged', 'requires_auth', 'permission_denied', 'not_found', 'invalid', 'error' ]) """ This factory produces DTOs for the next level up so we can communicate the intent of our return value without needlessly re-examining what the domain has already discovered.
In this example, I use namedtuples as the DTOs so instead of defining a class to produce them, I've simply used a namedtuple as the class. In a real application you could use a real class that runs serialization on the outputted data before spitting a DTO.
These could also be named ViewModels as they should only contain basic data structures ready to be inserted into some view. """
I'm not a big fan of returning None and then having my
callers try to guess at why the None was returned
sometimes it's a useful return value to have, but as
far as communicating failure I prefer exceptions
class FrobNotFound(Exception): pass
class CannotCreateFrob(Exception): pass
class InvalidFrobInput(Exception): pass
class FrobRepository(object): """ Emulate a database. In a real application, you could wrap a database connection or even a SQLAlchemy/Django ORM repository with a similar sort of interface so you're not dependent on SQLAlchemy/Django ORM directly. Joins get a little weird in that case though. Consider that an exercise left for the reader.
Or define this as an abstract class that you fulfill in multiple ways:
..code-block:: python
class FrobRepository(ABC):
...
class SQLAFrobRepository(FrobRepository):
def __init__(self, session, model):
self.session = session
self.model = model
def find_it(self, id):
frob = self.session.query(self.model).get(id)
if frob is None:
raise FrobNotFound(...)
return frob
class CachingFrobRepository(FrobRepository):
def __init__(self, cache, repository):
self._repository = repository
self._cache = cache
def find_it(id):
frob = self._cache.get('frob::{!s}'.format(id), None)
if frob is None:
frob = self._repository.find_it(id)
# set timeout for three minutes
self._cache.set('frob::{!s}'.format(id), frob, timeout=180)
return frob
"""
def __init__(self):
self._frobs = {}
self._next_id = count(1)
def find_it(self, id):
try:
return self._frobs[id]
except KeyError:
raise FrobNotFound("Couldn't find frob: " + str(id)) from None
def create_frob(self, **kwargs):
if 'name' not in kwargs:
raise InvalidFrobInput("Must have name to be valid frob")
id = next(self._next_id)
r = randrange(2, 7)
if not id % r:
raise CannotCreateFrob("Constraint violated: {} % {} == 0".format(id, r))
kwargs['id'] = id
self._frobs[id] = kwargs
return kwargs
@property
def available_frobs(self):
return list(self._frobs.keys())
class FrobService(object): """ This is basically an abstraction layer between data access and the whatever wants it. We can stick more specific, less broad rules here. In a multi tenant application, we might enforce things like a restriction on what frobs can be named by passing in a validator that the tenant defines:
def __init__(self, frob_repository, payload_factory, validator=None):
...
# validators should raise InvalidFrobInput
self._validator = validator or lambda data: None
def create_frob(self, data):
try:
self._validator(data)
frob = self._frob_repository.create_frob(**data)
except InvalidFrobInput as e:
return self._payload_factory.invalid({'error': e.args[0]}, {})
...
This layer also serves to translate information from the domain (the repository, etc)
into datastructures that the thing calling it can use via the payload_factory
dependency. In this example, it simply returns namedtuples but it could do things
like run a serializer.
"""
def __init__(self, frob_repository, payload_factory):
self._frob_repository = frob_repository
self._payload_factory = payload_factory
def find_it(self, id):
try:
frob = self._frob_repository.find_it(id)
return self._payload_factory.good(frob, {})
except FrobNotFound:
return self._payload_factory.not_found(
{'error': 'Could not find frob with id: ' + str(id)},
{'available_frobs': self._frob_repository.available_frobs}
)
def create_frob(self, data):
try:
frob = self._frob_repository.create_frob(**data)
except CannotCreateFrob as e:
return self._payload_factory.error({'error': e.args[0]}, {})
except InvalidFrobInput as e:
return self._payload_factory.invalid({'error': e.args[0]}, {'action': 'adjust input and try again'})
else:
return self._payload_factory.created(
{'frob': frob},
{'frob_id': frob['id']}
)
class CreateFrobAction(object): """ Represents an actual action an application would want to preform. This layer isn't strictly needed, but it does help to solidify a single business rule in the frame of the application. There's tons of ways that actions like this could be used:
.. code-block:: python
from flask import request
cfa = CreateFrobAction(...)
frob_view = View(...)
@frob.request('/create', methods=['POST'])
def create_frob():
return view.render(cfa(request.form), request)
# or...
class FrobView(View):
view = None
def dispatch_request(self, *args, **kwargs):
rv = super().dispatch_request(*args, **kwargs)
return self.view.render(rv, request)
class CreateFrobController(FrobView, MethodView):
view = View(...)
def __init__(self, frob_creator, form):
self._frob_creator = frob_creator
self._form = form
def get(self):
return HTTPPayload({'form': form}, {}, 200)
def post(self):
if self._form.validate_on_submit():
return self._frob_creator(**self._form.data)
else:
return HTTPPayload({'form': form}, {}, 400)
But in any case the top level caller serves as a translation
device between the outside world (web, terminal, files, etc)
and our actual application. The only real validation that should
happen here are things like:
* Ensure the required arguments are present
* Ensure the arguments are the correct type
But not things like "Does the domain consider this a valid input?"
because only the domain can ensure that.
"""
def __init__(self, frob_service):
self._frob_service = frob_service
def __call__(self, request):
return self._frob_service.create_frob(request.data)
class View(ABC): """ NOT strictly an HTTP view (template, json, etc). This could handle things like console coloring if this is a terminal application or converting to a specific format for transport, or it could be a HTTP thing.
The main method -- View::render -- receives both the output and the input so it can
properly represent the output if the requester had specific instructions
(content-type, signing key, no colors, etc).
"""
@abstractmethod
def render(self, payload, request):
return NotImplemented
class Renderer(ABC):
"""
Would be helper class for a View if needed. In the case of HTTP Views this
could carry a media type such as application/json and the renderer method
would call json.dumps. Or could carry platform specific encoding knowledge.
"""
@abstractmethod
def render(self, payload):
return NotImplemented