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