Commit 377cc728 authored by Maciej Wiatrzyk's avatar Maciej Wiatrzyk
Browse files

redesign introduction part of tutorial

and rename it to quickstart
parent 95ef8fc0
......@@ -90,7 +90,7 @@ User's Guide
:maxdepth: 3
installation
tutorial
quickstart
api
changelog
license
......@@ -10,35 +10,25 @@
.. See LICENSE.txt for details.
.. ----------------------------------------------------------------------------
Tutorial
========
Quickstart
==========
Introduction
------------
When you develop an application in accordance to SOLID principles, then most
likely you start by writing use cases at first, supported by dozens of
abstractions instead of final implementations. But that abstractions have to
be implemented at some point in time. And most likely, you will have to
implement at least 2 implementations for each: one for testing/development
purposes, and one for production use. When number of use cases grows, then
number of abstractions needed grows as well. That leads to more and more
complications in configuration part of application being developed. Finally,
you find yourself in a place, where some kind of tool is needed to manage
that part of application. And PyDio is one of such tools.
In this quickstart guide, we are going to write a simple TODO application
that allows:
Begin writing a TODO application
--------------------------------
* creating items,
* listing items,
* marking items as completed,
* deleting items
Let's assume we are writing a "todo" application that allows:
Application's business logic
----------------------------
* creating todos,
* listing todos,
* marking todos as completed,
* deleting todos.
Since this is a todo application, it will work on some todo items. Single
todo item can be defined like this:
First, we need a data class to represent our todo items. Let's then start by
creating a **TodoItem** entity:
.. testcode::
......@@ -52,27 +42,42 @@ todo item can be defined like this:
description: str
done: bool = False
This is a data class - a first business object in our application.
Now let's write first two use cases: one for creating items, and the other
for listing. Here's a final implementation - with all needed interfaces:
Now we need some kind of storage where our todo items will be stored. We will
do this formally, by designing interface. Of course we don't need it (it's a
Python), but interfaces are pretty useful with annotations. Here's our TODO
item storage interface:
.. testcode::
import abc
import collections
from typing import Iterable
from typing import Iterable, Optional
class ITodoWriter(abc.ABC):
class ITodoItemStorage(abc.ABC):
@abc.abstractmethod
def save(self, item: TodoItem):
pass
class AddTodo:
@abc.abstractmethod
def get(self, item_uuid: uuid.UUID) -> Optional[TodoItem]:
pass
def __init__(self, todo_registry: ITodoWriter):
self._todo_registry = todo_registry
@abc.abstractmethod
def delete(self, item_uuid: uuid.UUID):
pass
@abc.abstractmethod
def list(self) -> Iterable[TodoItem]:
pass
Finally, let's write our use case classes:
.. testcode::
class CreateTodo:
def __init__(self, todo_storage: ITodoItemStorage):
self._todo_storage = todo_storage
def invoke(self, title, description):
item = TodoItem()
......@@ -81,21 +86,15 @@ for listing. Here's a final implementation - with all needed interfaces:
item.title = title
item.description = description
item.done = False
self._todo_registry.save(item)
class ITodoReader(abc.ABC):
@abc.abstractmethod
def list(self) -> Iterable[TodoItem]:
pass
self._todo_storage.save(item)
class ListTodos:
def __init__(self, todo_registry: ITodoReader):
self._todo_registry = todo_registry
def __init__(self, todo_storage: ITodoItemStorage):
self._todo_storage = todo_storage
def invoke(self):
for item in self._todo_registry.list():
for item in self._todo_storage.list():
yield { # we don't want to expose our entity
'uuid': item.uuid,
'created': item.created,
......@@ -104,65 +103,95 @@ for listing. Here's a final implementation - with all needed interfaces:
'done': item.done
}
So far, we were writing that classes with a help of unit tests, so our
**ITodoWriter** and **ITodoReader** interfaces were in fact mocked. But now we
want to use that classes inside our application:
class CompleteTodo:
.. testcode::
def __init__(self, todo_storage: ITodoItemStorage):
self._todo_storage = todo_storage
class TodoApp:
def invoke(self, item_uuid: uuid.UUID):
item = self._todo_storage.get(item_uuid)
if item is None:
raise ValueError("invalid item uuid: {}".format(item_uuid))
item.done = True
self._todo_storage.save(item)
def create(self, title, description):
todo_writer = None # here we need an implementation
handler = AddTodo(todo_writer)
handler.invoke(title, description)
class DeleteTodo:
def list(self):
todo_reader = None # here we need an implementation
handler = ListTodos(todo_reader)
return [x for x in handler.invoke()]
def __init__(self, todo_storage: ITodoItemStorage):
self._todo_storage = todo_storage
No we cannot use mocks any longer - we need a real implementation. Since we
are still doing development, we still don't have to use any SQL databases -
just a simple in-memory store will do. And since both use cases operate on
the same storage, we can make a single class that inherits from both
interfaces:
def invoke(self, item_uuid: uuid.UUID):
self._todo_storage.delete(item_uuid)
And that's entire business logic of our simple TODO application. But so far,
we were only using a suite of unit tests, with **ITodoItemStorage** interface
mocked. Now, let's put some life into our application.
Application's API
-----------------
To make our business logic running we cannot use mocks any longer - now we
need a real implementation of **ITodoItemStorage** interface. Since we are
still doing development of our application, we still don't have to use any
SQL databases - just a simple in-memory store will do. Here's a very basic
implementation:
.. testcode::
class InMemoryTodoRegistry(ITodoWriter, ITodoReader):
class InMemoryTodoRegistry(ITodoItemStorage):
def __init__(self):
self._todos = []
self._todos = {}
def save(self, item):
self._todos.append(item)
self._todos[item.uuid] = item
def delete(self, item_uuid):
del self._todos[item_uuid]
def get(self, item_uuid):
return self._todos.get(item_uuid)
def list(self):
yield from self._todos
for item in self._todos.values():
yield item
Now we can use it in our application:
Now we can use it in our application. It will be represented by
**TodoApplication** class, with all use cases exposed as methods:
.. testcode::
class TodoApp:
from typing import List
class TodoApplication:
def __init__(self):
self._todo_registry = InMemoryTodoRegistry()
self._todo_storage = InMemoryTodoRegistry()
def create(self, title, description):
handler = AddTodo(self._todo_registry)
handler.invoke(title, description)
def create(self, title: str, description: str):
CreateTodo(self._todo_storage).invoke(title, description)
def list(self):
handler = ListTodos(self._todo_registry)
return [x for x in handler.invoke()]
def complete(self, item_uuid: uuid.UUID):
CompleteTodo(self._todo_storage).invoke(item_uuid)
def list(self) -> List[dict]:
return [x for x in ListTodos(self._todo_storage).invoke()]
def delete(self, item_uuid: uuid.UUID):
DeleteTodo(self._todo_storage).invoke(item_uuid)
And here's how it works:
.. doctest::
>>> app = TodoApp()
>>> app = TodoApplication()
>>> app.create('shopping', 'buy some milk')
>>> app.list()
>>> items = app.list()
>>> items
[{'uuid': ..., 'created': ..., 'title': 'shopping', 'description': 'buy some milk', 'done': False}]
>>> app.complete(items[0]['uuid'])
>>> app.list()
[{'uuid': ..., 'created': ..., 'title': 'shopping', 'description': 'buy some milk', 'done': True}]
>>> app.delete(items[0]['uuid'])
>>> app.list()
[]
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment