├── .gitignore ├── README.md ├── bin └── todo ├── image └── architecture.png ├── setup.py ├── test ├── __init__.py ├── adapter │ ├── __init__.py │ ├── repo │ │ ├── __init__.py │ │ └── test_task.py │ └── test_user.py ├── conftest.py ├── domain_model │ ├── __init__.py │ └── test_task.py └── read_model │ ├── __init__.py │ └── test_task.py └── todolist ├── __init__.py ├── adapter ├── __init__.py ├── eventbus.py ├── redis │ ├── __init__.py │ └── task.py ├── repo │ ├── __init__.py │ └── task │ │ ├── __init__.py │ │ ├── memory.py │ │ └── redis.py └── user.py ├── app ├── __init__.py ├── cli.py ├── config.py └── read_model │ ├── __init__.py │ └── updater.py ├── domain_model ├── __init__.py ├── task.py └── user.py ├── port ├── __init__.py └── eventbus.py ├── read_model ├── __init__.py └── task.py └── testing.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *,cover 46 | .hypothesis/ 47 | 48 | # Translations 49 | *.mo 50 | *.pot 51 | 52 | # Django stuff: 53 | *.log 54 | local_settings.py 55 | 56 | # Flask stuff: 57 | instance/ 58 | .webassets-cache 59 | 60 | # Scrapy stuff: 61 | .scrapy 62 | 63 | # Sphinx documentation 64 | docs/_build/ 65 | 66 | # PyBuilder 67 | target/ 68 | 69 | # IPython Notebook 70 | .ipynb_checkpoints 71 | 72 | # pyenv 73 | .python-version 74 | 75 | # celery beat schedule file 76 | celerybeat-schedule 77 | 78 | # dotenv 79 | .env 80 | 81 | # virtualenv 82 | venv/ 83 | ENV/ 84 | 85 | # Spyder project settings 86 | .spyderproject 87 | 88 | # Rope project settings 89 | .ropeproject 90 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Todo List 2 | 3 | DDD, CQRS and Hexagonal Architecture example using inject package. 4 | 5 | Presentation slide in PyCon JP 2017: [Python におけるドメイン駆動設計(戦術面)の勘どころ](https://www.slideshare.net/secret/6DnooTLkqmnXsz) 6 | 7 | # Architecture 8 | 9 | ![architecture](image/architecture.png?raw=true "Architecture") 10 | 11 | # Requirements 12 | 13 | - click 14 | - enum34 15 | - inject 16 | - gxredis 17 | - pytest 18 | 19 | # Setup 20 | 21 | ``` 22 | $ git clone https://github.com/ledmonster/ddd-python-inject 23 | $ cd todolist 24 | $ python setup.py develop 25 | ``` 26 | 27 | Also, you need to run redis. 28 | 29 | # Usage 30 | 31 | ``` bash 32 | $ ./bin/todo add --name foo 33 | #1: foo 34 | 35 | $ ./bin/todo add --name bar 36 | #2: bar 37 | 38 | $ ./bin/todo add --name baz 39 | #3: baz 40 | 41 | $ ./bin/todo list 42 | [ ] #1: foo 43 | [ ] #3: baz 44 | [ ] #2: bar 45 | 46 | $ ./bin/todo done 2 47 | [x] #2: bar 48 | 49 | $ ./bin/todo list 50 | [ ] #1: foo 51 | [ ] #3: baz 52 | [x] #2: bar 53 | ``` 54 | -------------------------------------------------------------------------------- /bin/todo: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | from todolist.app.cli import main 4 | 5 | 6 | if __name__ == "__main__": 7 | main() 8 | -------------------------------------------------------------------------------- /image/architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ledmonster/ddd-python-inject/4d54bc957aac032dca707108aad8f18c82ed81db/image/architecture.png -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from setuptools import setup, find_packages 3 | 4 | 5 | setup( 6 | name="todolist", 7 | version="0.1", 8 | packages=find_packages(), 9 | install_requires=[ 10 | "click", 11 | "enum34", 12 | "inject", 13 | "gxredis", 14 | "pytest", 15 | ], 16 | license="MIT", 17 | scripts=["bin/todo"], 18 | ) 19 | -------------------------------------------------------------------------------- /test/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ledmonster/ddd-python-inject/4d54bc957aac032dca707108aad8f18c82ed81db/test/__init__.py -------------------------------------------------------------------------------- /test/adapter/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ledmonster/ddd-python-inject/4d54bc957aac032dca707108aad8f18c82ed81db/test/adapter/__init__.py -------------------------------------------------------------------------------- /test/adapter/repo/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ledmonster/ddd-python-inject/4d54bc957aac032dca707108aad8f18c82ed81db/test/adapter/repo/__init__.py -------------------------------------------------------------------------------- /test/adapter/repo/test_task.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import inject 3 | import pytest 4 | 5 | from todolist import testing 6 | from todolist.adapter.repo.task import ( 7 | TaskMemoryRepository, TaskRedisRepository, 8 | ) 9 | from todolist.domain_model.task import Task, TaskRepository 10 | 11 | 12 | class TestTaskRepository(object): 13 | u""" TaskRepository のテスト """ 14 | 15 | @pytest.fixture( 16 | params=[TaskMemoryRepository, TaskRedisRepository]) 17 | def repo(self, request): 18 | repo = request.param() 19 | testing.overwrite_binding(TaskRepository, lambda: repo) 20 | yield repo 21 | repo._clear() 22 | 23 | def test_generate_id(self, repo): 24 | task_id = repo.generate_id() 25 | assert isinstance(task_id, int) 26 | 27 | next_id = repo.generate_id() 28 | assert next_id == task_id + 1 29 | 30 | def test_create(self, repo): 31 | task = Task.create(1, u"タスク1") 32 | assert isinstance(task, Task) 33 | 34 | stored = repo.get(task.task_id) 35 | assert stored.task_id == task.task_id 36 | assert stored.name == task.name 37 | assert stored.status == task.status 38 | 39 | def test_rename(self, repo): 40 | task = Task.create(1, u"タスク1") 41 | assert isinstance(task, Task) 42 | 43 | task.rename(u"タスク名変更") 44 | 45 | stored = repo.get(task.task_id) 46 | assert stored.task_id == task.task_id 47 | assert stored.name == u"タスク名変更" 48 | -------------------------------------------------------------------------------- /test/adapter/test_user.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from todolist.adapter.user import SimpleUserService 3 | from todolist.domain_model.user import User 4 | 5 | 6 | def test_simple_user_service(): 7 | service = SimpleUserService() 8 | user = service.get_current_user() 9 | assert isinstance(user, User) 10 | -------------------------------------------------------------------------------- /test/conftest.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import inject 3 | 4 | from todolist import testing 5 | 6 | 7 | def pytest_runtest_setup(item): 8 | # dependency injection 9 | inject.clear_and_configure(testing.config) 10 | -------------------------------------------------------------------------------- /test/domain_model/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ledmonster/ddd-python-inject/4d54bc957aac032dca707108aad8f18c82ed81db/test/domain_model/__init__.py -------------------------------------------------------------------------------- /test/domain_model/test_task.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from todolist.domain_model.task import Task, TaskStatus 3 | 4 | 5 | def test_create(): 6 | task = Task.create(1, u'PyConJP の資料を作る') 7 | assert isinstance(task, Task) 8 | assert task.name == u'PyConJP の資料を作る' 9 | assert task.status == TaskStatus.todo 10 | 11 | 12 | def test_rename(): 13 | task = Task.create(1, u'PyConJP の資料を作る') 14 | task.rename(u'PyConJP のスライドを作る') 15 | assert task.name == u'PyConJP のスライドを作る' 16 | 17 | 18 | def test_done(): 19 | task = Task.create(1, u'PyConJP の資料を作る') 20 | task.done() 21 | assert task.status == TaskStatus.done 22 | -------------------------------------------------------------------------------- /test/read_model/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ledmonster/ddd-python-inject/4d54bc957aac032dca707108aad8f18c82ed81db/test/read_model/__init__.py -------------------------------------------------------------------------------- /test/read_model/test_task.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import time 3 | 4 | import inject 5 | import pytest 6 | import redis 7 | 8 | from todolist import testing 9 | from todolist.app.read_model.updater import register_readmodel_updater 10 | from todolist.adapter.repo.task import TaskRedisRepository 11 | from todolist.domain_model.task import Task, TaskRepository 12 | from todolist.read_model.task import TaskQuery 13 | 14 | 15 | class TestTaskQuery(object): 16 | u""" TaskQuery のテスト """ 17 | 18 | def setup(self): 19 | register_readmodel_updater() 20 | 21 | def teardown(self): 22 | redis_client = inject.instance(redis.StrictRedis) 23 | redis_client.flushdb() 24 | 25 | @pytest.fixture 26 | def repo(self): 27 | repo = TaskRedisRepository() 28 | testing.overwrite_binding(TaskRepository, lambda: repo) 29 | return repo 30 | 31 | @pytest.fixture 32 | def query(self): 33 | return TaskQuery() 34 | 35 | def test_find_by_user_id(self, repo, query): 36 | for i in range(10): 37 | task = Task.create(1, "task{}".format(i)) 38 | 39 | task_list = query.find_by_user_id(user_id=1) 40 | assert len(task_list) == 10 41 | 42 | def test_two_users(self, repo, query): 43 | for i in range(10): 44 | task = Task.create(1, "task{}".format(i)) 45 | for i in range(20): 46 | task = Task.create(2, "task{}".format(i)) 47 | 48 | task_list = query.find_all() 49 | assert len(task_list) == 30 50 | task_list = query.find_by_user_id(user_id=1) 51 | assert len(task_list) == 10 52 | task_list = query.find_by_user_id(user_id=2) 53 | assert len(task_list) == 20 54 | 55 | def test_find_todo_and_done_by_user_id(self, repo, query): 56 | tasks = [] 57 | for i in range(10): 58 | tasks.append(Task.create(1, "task{}".format(i))) 59 | tasks[0].done() 60 | 61 | # redis が更新されるのを待つ 62 | for i in range(10): 63 | todo_list = query.find_todo_by_user_id(user_id=1) 64 | if len(todo_list) == 9: 65 | break 66 | time.sleep(0.01) 67 | else: 68 | assert False 69 | 70 | task_list = query.find_by_user_id(user_id=1) 71 | assert len(task_list) == 10 72 | done_list = query.find_done_by_user_id(user_id=1) 73 | assert len(done_list) == 1 74 | -------------------------------------------------------------------------------- /todolist/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ledmonster/ddd-python-inject/4d54bc957aac032dca707108aad8f18c82ed81db/todolist/__init__.py -------------------------------------------------------------------------------- /todolist/adapter/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ledmonster/ddd-python-inject/4d54bc957aac032dca707108aad8f18c82ed81db/todolist/adapter/__init__.py -------------------------------------------------------------------------------- /todolist/adapter/eventbus.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import logging 3 | 4 | from todolist.port.eventbus import EventBus 5 | 6 | 7 | logger = logging.getLogger(__name__) 8 | 9 | 10 | class SimpleEventBus(EventBus): 11 | u""" シンプルなドメインイベントの通知クラス """ 12 | 13 | def __init__(self): 14 | self._listeners = [] 15 | 16 | def publish(self, event): 17 | u""" ドメインイベントを通知する """ 18 | logger.info(event) 19 | for listener in self._listeners: 20 | listener(event) 21 | 22 | def register_listener(self, listener): 23 | u""" ドメインイベントを購読する """ 24 | assert callable(listener) 25 | self._listeners.append(listener) 26 | -------------------------------------------------------------------------------- /todolist/adapter/redis/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ledmonster/ddd-python-inject/4d54bc957aac032dca707108aad8f18c82ed81db/todolist/adapter/redis/__init__.py -------------------------------------------------------------------------------- /todolist/adapter/redis/task.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import absolute_import 3 | 4 | from gxredis import RedisDao, RedisString, RedisHash, RedisSet 5 | 6 | 7 | class TaskDao(RedisDao): 8 | u""" Task 用の DAO """ 9 | # task_id を管理する 10 | counter = RedisString("todolist:task:counter") 11 | 12 | # タスクの一覧(ハッシュ) 13 | tasks = RedisHash("todolist:task:tasks") 14 | 15 | # ユーザのタスクの task_id 一覧 16 | user_tasks = RedisSet("todolist:task:{user_id}:tasks") 17 | 18 | # ユーザの todo タスクの task_id 一覧 19 | todo = RedisSet("todolist:task:{user_id}:todo") 20 | 21 | # ユーザの done タスクの task_id 一覧 22 | done = RedisSet("todolist:task:{user_id}:done") 23 | -------------------------------------------------------------------------------- /todolist/adapter/repo/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ledmonster/ddd-python-inject/4d54bc957aac032dca707108aad8f18c82ed81db/todolist/adapter/repo/__init__.py -------------------------------------------------------------------------------- /todolist/adapter/repo/task/__init__.py: -------------------------------------------------------------------------------- 1 | from .memory import TaskMemoryRepository 2 | from .redis import TaskRedisRepository 3 | -------------------------------------------------------------------------------- /todolist/adapter/repo/task/memory.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from todolist.domain_model.task import Task, TaskRepository 3 | 4 | 5 | class TaskMemoryRepository(TaskRepository): 6 | u""" TaskRepository のメモリ実装 """ 7 | 8 | def __init__(self): 9 | self._last_id = 0 10 | self._tasks = {} 11 | 12 | def generate_id(self): 13 | u""" タスクIDを生成する 14 | 15 | :rtype: int 16 | """ 17 | self._last_id += 1 18 | return self._last_id 19 | 20 | def get(self, task_id): 21 | u""" タスクを取得する 22 | 23 | :type task_id: int 24 | :rtype: (Task|None) 25 | """ 26 | assert isinstance(task_id, int) 27 | return self._tasks.get(task_id) 28 | 29 | def save(self, task): 30 | u""" タスクを保存する 31 | 32 | :type task: Task 33 | """ 34 | assert isinstance(task, Task) 35 | self._tasks[task.task_id] = task 36 | 37 | def _clear(self): 38 | u""" 全データを削除(テスト用) """ 39 | self._tasks = {} 40 | -------------------------------------------------------------------------------- /todolist/adapter/repo/task/redis.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import absolute_import 3 | 4 | import inject 5 | import json 6 | import redis 7 | 8 | from todolist.adapter.redis.task import TaskDao 9 | from todolist.domain_model.task import Task, TaskStatus, TaskRepository 10 | 11 | 12 | class TaskRedisRepository(TaskRepository): 13 | u""" TaskRepository のRedis実装 """ 14 | 15 | _redis_client = inject.attr(redis.StrictRedis) 16 | 17 | def __init__(self): 18 | self._dao = TaskDao(self._redis_client) 19 | 20 | def generate_id(self): 21 | u""" タスクIDを生成する 22 | 23 | :rtype: int 24 | """ 25 | return self._dao.counter.incr() 26 | 27 | def get(self, task_id): 28 | u""" タスクを取得する 29 | 30 | :type task_id: int 31 | :rtype: (Task|None) 32 | """ 33 | assert isinstance(task_id, int) 34 | json_str = self._dao.tasks.hget(task_id) 35 | if json_str is None: 36 | return None 37 | value = json.loads(json_str.decode('utf-8')) 38 | return self._from_dict(value) 39 | 40 | def save(self, task): 41 | u""" タスクを保存する 42 | 43 | :type task: Task 44 | """ 45 | assert isinstance(task, Task) 46 | json_str = json.dumps(self._to_dict(task), ensure_ascii=False) 47 | self._dao.tasks.hset(task.task_id, json_str) 48 | 49 | def _from_dict(self, value): 50 | return Task( 51 | task_id=value['task_id'], 52 | user_id=value.get('user_id', 1), 53 | name=value['name'], 54 | status=TaskStatus(value['status'])) 55 | 56 | def _to_dict(self, task): 57 | return { 58 | "task_id": task.task_id, 59 | "user_id": task.user_id, 60 | "name": task.name, 61 | "status": task.status.value, 62 | } 63 | 64 | def _clear(self): 65 | u""" 全データを削除(テスト用) """ 66 | self._redis_client.flushdb() 67 | -------------------------------------------------------------------------------- /todolist/adapter/user.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from todolist.domain_model.user import User, UserService 3 | 4 | 5 | class SimpleUserService(UserService): 6 | 7 | def get_current_user(self): 8 | u""" 現在ログイン中のユーザを返す 9 | 10 | :rtype: User 11 | """ 12 | # 常に junya を返す 13 | return User(1, "junya") 14 | -------------------------------------------------------------------------------- /todolist/app/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ledmonster/ddd-python-inject/4d54bc957aac032dca707108aad8f18c82ed81db/todolist/app/__init__.py -------------------------------------------------------------------------------- /todolist/app/cli.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | u""" CLI インタフェース """ 3 | import logging 4 | 5 | import click 6 | import inject 7 | 8 | from .config import create_config 9 | from .read_model.updater import register_readmodel_updater 10 | from todolist.domain_model.task import Task, TaskStatus, TaskRepository 11 | from todolist.domain_model.user import UserService 12 | from todolist.read_model.task import TaskQuery 13 | 14 | 15 | logging.basicConfig(level=logging.INFO) 16 | 17 | 18 | @click.group() 19 | def main(): 20 | params = { 21 | "redis_host": "localhost", 22 | "redis_port": "6379", 23 | "redis_db": "2", 24 | } 25 | inject.configure(create_config(params)) 26 | register_readmodel_updater() 27 | 28 | 29 | @main.command() 30 | @click.option("-s", "--status", type=click.Choice(['all', 'todo', 'done']), 31 | default='all') 32 | def list(status): 33 | query = inject.instance(TaskQuery) 34 | user = inject.instance(UserService).get_current_user() 35 | if status == 'todo': 36 | tasks = query.find_todo_by_user_id(user.user_id) 37 | elif status == 'done': 38 | tasks = query.find_done_by_user_id(user.user_id) 39 | else: 40 | tasks = query.find_by_user_id(user.user_id) 41 | for task in tasks: 42 | if task.status is TaskStatus.todo: 43 | click.echo(u"[ ] #{}: {}".format(task.task_id, task.name)) 44 | else: 45 | click.echo(u"[x] #{}: {}".format(task.task_id, task.name)) 46 | 47 | 48 | @main.command() 49 | @click.option("--name", type=unicode, help="task name", prompt="task name") 50 | def add(name): 51 | repo = inject.instance(TaskRepository) 52 | user = inject.instance(UserService).get_current_user() 53 | task = Task.create(user.user_id, name) 54 | repo.save(task) 55 | click.echo(u"#{}: {}".format(task.task_id, task.name)) 56 | 57 | 58 | @main.command() 59 | @click.argument("task_id", type=int) 60 | def done(task_id): 61 | repo = inject.instance(TaskRepository) 62 | user = inject.instance(UserService).get_current_user() 63 | task = repo.get(task_id) 64 | if task is None: 65 | click.echo("task not found: #{}".format(task_id)) 66 | elif task.user_id != user.user_id: 67 | click.echo("task not found: #{}".format(task_id)) 68 | else: 69 | if task.status is TaskStatus.todo: 70 | task.done() 71 | repo.save(task) 72 | click.echo(u"[x] #{}: {}".format(task.task_id, task.name)) 73 | -------------------------------------------------------------------------------- /todolist/app/config.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | u""" Dependency Injection 用の設定関数を提供するモジュール """ 3 | import inject 4 | import redis 5 | 6 | from todolist.adapter.eventbus import SimpleEventBus 7 | from todolist.adapter.repo.task import TaskRedisRepository 8 | from todolist.adapter.user import SimpleUserService 9 | from todolist.domain_model.task import TaskRepository 10 | from todolist.domain_model.user import UserService 11 | from todolist.port.eventbus import EventBus 12 | 13 | 14 | def create_config(params): 15 | u""" inject 用の config を生成する 16 | 17 | :param dict params: パラメータの辞書 18 | :return function: inject 用の config 関数 19 | """ 20 | def config(binder): 21 | binder.bind(redis.StrictRedis, redis.StrictRedis( 22 | params['redis_host'], params['redis_port'], params['redis_db'])) 23 | binder.bind_to_constructor(EventBus, SimpleEventBus) 24 | binder.bind_to_constructor(TaskRepository, TaskRedisRepository) 25 | binder.bind_to_constructor(UserService, SimpleUserService) 26 | return config 27 | -------------------------------------------------------------------------------- /todolist/app/read_model/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ledmonster/ddd-python-inject/4d54bc957aac032dca707108aad8f18c82ed81db/todolist/app/read_model/__init__.py -------------------------------------------------------------------------------- /todolist/app/read_model/updater.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import inject 3 | import redis 4 | 5 | from todolist.adapter.redis.task import TaskDao 6 | from todolist.domain_model.task import TaskCreated, TaskDone 7 | from todolist.port.eventbus import EventBus 8 | 9 | 10 | def register_readmodel_updater(): 11 | eventbus = inject.instance(EventBus) 12 | eventbus.register_listener(update_readmodel) 13 | 14 | 15 | def update_readmodel(event): 16 | u""" ドメインイベントを入力として ReadModel を更新する """ 17 | redis_client = inject.instance(redis.StrictRedis) 18 | dao = TaskDao(redis_client) 19 | if isinstance(event, TaskCreated): 20 | dao.user_tasks(user_id=event.user_id).sadd(event.task_id) 21 | dao.todo(user_id=event.user_id).sadd(event.task_id) 22 | elif isinstance(event, TaskDone): 23 | dao.done(user_id=event.user_id).sadd(event.task_id) 24 | dao.todo(user_id=event.user_id).srem(event.task_id) 25 | -------------------------------------------------------------------------------- /todolist/domain_model/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ledmonster/ddd-python-inject/4d54bc957aac032dca707108aad8f18c82ed81db/todolist/domain_model/__init__.py -------------------------------------------------------------------------------- /todolist/domain_model/task.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from abc import ABCMeta, abstractmethod 3 | 4 | import inject 5 | from enum import Enum 6 | 7 | from todolist.port.eventbus import EventBus 8 | 9 | 10 | class TaskStatus(Enum): 11 | u""" タスクステータス """ 12 | todo = "todo" 13 | done = "done" 14 | 15 | 16 | class TaskCreated(object): 17 | u""" タスク作成イベント """ 18 | def __init__(self, task_id, user_id, name): 19 | self.task_id = task_id 20 | self.user_id = user_id 21 | self.name = name 22 | 23 | def __repr__(self): 24 | return "<{}: task_id={}>".format( 25 | self.__class__.__name__, self.task_id) 26 | 27 | 28 | class TaskDone(object): 29 | u""" タスク完了イベント """ 30 | def __init__(self, task_id, user_id): 31 | self.task_id = task_id 32 | self.user_id = user_id 33 | 34 | def __repr__(self): 35 | return "<{}: task_id={}>".format( 36 | self.__class__.__name__, self.task_id) 37 | 38 | 39 | class TaskRenamed(object): 40 | u""" タスク名変更イベント """ 41 | def __init__(self, task_id, user_id, name): 42 | self.task_id = task_id 43 | self.user_id = user_id 44 | self.name = name 45 | 46 | def __repr__(self): 47 | return "<{}: task_id={}>".format( 48 | self.__class__.__name__, self.task_id) 49 | 50 | 51 | class TaskRepository(object): 52 | 53 | __metaclass__ = ABCMeta 54 | 55 | @abstractmethod 56 | def generate_id(self): 57 | u""" タスクIDを生成する 58 | 59 | :rtype: int 60 | """ 61 | 62 | @abstractmethod 63 | def get(self, task_id): 64 | u""" タスクを取得する 65 | 66 | :type task_id: int 67 | :rtype: (Task|None) 68 | """ 69 | 70 | @abstractmethod 71 | def save(self, task): 72 | u""" タスクを保存する 73 | 74 | :type task: Task 75 | """ 76 | 77 | 78 | class Task(object): 79 | u""" タスク 80 | 81 | :param int task_id: タスクID 82 | :param int user_id: ユーザID 83 | :param (str|unicode) name: タスク名 84 | :param TaskStatus status: ステータス 85 | """ 86 | _eventbus = inject.attr(EventBus) 87 | _repo = inject.attr(TaskRepository) 88 | 89 | def __init__(self, task_id, user_id, name, status): 90 | u""" コンストラクタ 91 | 92 | :param int task_id: タスクID 93 | :param int user_id: ユーザID 94 | :param (str|unicode) name: タスク名 95 | :param TaskStatus status: ステータス 96 | 97 | 新規のタスクを作成する際には create メソッドを利用すること 98 | """ 99 | assert isinstance(task_id, int) 100 | assert isinstance(user_id, int) 101 | assert isinstance(name, basestring) 102 | assert isinstance(status, TaskStatus) 103 | self._task_id = task_id 104 | self._user_id = user_id 105 | self._name = name 106 | self._status = status 107 | 108 | def __repr__(self): 109 | return "<{}: task_id={}>".format( 110 | self.__class__.__name__, self.task_id) 111 | 112 | @classmethod 113 | def create(cls, user_id, name): 114 | u""" タスクを生成する 115 | 116 | :type user_id: int 117 | :type name: str|unicode 118 | :rtype: Task 119 | """ 120 | repo = inject.instance(TaskRepository) 121 | eventbus = inject.instance(EventBus) 122 | 123 | # タスクを生成して保存する 124 | task_id = repo.generate_id() 125 | task = cls(task_id, user_id, name, TaskStatus.todo) 126 | repo.save(task) 127 | 128 | # タスク生成イベントを通知する 129 | event = TaskCreated(task_id, user_id, name) 130 | eventbus.publish(event) 131 | return task 132 | 133 | @property 134 | def task_id(self): 135 | return self._task_id 136 | 137 | @property 138 | def user_id(self): 139 | return self._user_id 140 | 141 | @property 142 | def name(self): 143 | return self._name 144 | 145 | @property 146 | def status(self): 147 | return self._status 148 | 149 | def rename(self, name): 150 | u""" タスク名を変更する """ 151 | self._name = name 152 | self._repo.save(self) 153 | 154 | # 名前の変更を通知する 155 | event = TaskRenamed(self.task_id, self._user_id, self.name) 156 | self._eventbus.publish(event) 157 | 158 | def done(self): 159 | u""" タスクを終了させる """ 160 | self._status = TaskStatus.done 161 | self._repo.save(self) 162 | 163 | # タスクの終了を通知する 164 | event = TaskDone(self.task_id, self.user_id) 165 | self._eventbus.publish(event) 166 | -------------------------------------------------------------------------------- /todolist/domain_model/user.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from abc import ABCMeta, abstractmethod 3 | 4 | 5 | class User(object): 6 | u""" ユーザを表現した Value Object 7 | 8 | :param int user_id: ユーザID 9 | :param str name: ユーザ名 10 | 11 | コンテキスト内でライフサイクルを管理しないオブジェクトは、 12 | 利用する属性を最低限に絞った上で Entity ではなく Value Object として 13 | 定義するとメンテナンスしやすい。 14 | """ 15 | 16 | def __init__(self, user_id, name): 17 | assert isinstance(user_id, int) 18 | assert isinstance(name, str) 19 | self.__user_id = user_id 20 | self.__name = name 21 | 22 | @property 23 | def user_id(self): 24 | return self.__user_id 25 | 26 | @property 27 | def name(self): 28 | return self.__name 29 | 30 | 31 | class UserService(object): 32 | u""" ユーザに関連した機能を提供するサービス """ 33 | __metaclass__ = ABCMeta 34 | 35 | @abstractmethod 36 | def get_current_user(self): 37 | u""" 現在ログイン中のユーザを返す 38 | 39 | :rtype: User 40 | """ 41 | -------------------------------------------------------------------------------- /todolist/port/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ledmonster/ddd-python-inject/4d54bc957aac032dca707108aad8f18c82ed81db/todolist/port/__init__.py -------------------------------------------------------------------------------- /todolist/port/eventbus.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from abc import ABCMeta, abstractmethod 3 | 4 | 5 | class EventBus(object): 6 | u""" ドメインイベントの通知インタフェース """ 7 | 8 | __metaclass__ = ABCMeta 9 | 10 | @abstractmethod 11 | def publish(self, event): 12 | u""" ドメインイベントを通知する """ 13 | -------------------------------------------------------------------------------- /todolist/read_model/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ledmonster/ddd-python-inject/4d54bc957aac032dca707108aad8f18c82ed81db/todolist/read_model/__init__.py -------------------------------------------------------------------------------- /todolist/read_model/task.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | u""" 読み出し用のモデル (DTO: Data Transfer Object) """ 3 | import inject 4 | import json 5 | import redis 6 | 7 | from todolist.adapter.redis.task import TaskDao 8 | from todolist.domain_model.task import TaskStatus 9 | 10 | 11 | class TaskDto(object): 12 | u""" 読み出し用のタスク """ 13 | 14 | def __init__(self, task_id, user_id, name, status): 15 | self.task_id = task_id 16 | self.user_id = user_id 17 | self.name = name 18 | self.status = status 19 | 20 | 21 | class TaskQuery(object): 22 | u""" タスク用の Query クラス """ 23 | 24 | _redis_client = inject.attr(redis.StrictRedis) 25 | 26 | def __init__(self): 27 | self._dao = TaskDao(self._redis_client) 28 | 29 | def find_all(self): 30 | u""" 全ユーザの全タスク一覧を取得する 31 | 32 | :rtype: list[TaskDto] 33 | """ 34 | tasks = [] 35 | json_strs = self._dao.tasks.hgetall() 36 | values = [json.loads(x.decode('utf-8')) for x in json_strs.values()] 37 | return [self._from_dict(x) for x in values] 38 | 39 | def find_by_user_id(self, user_id): 40 | u""" ユーザのタスク一覧を取得する 41 | 42 | :type user_id: int 43 | :rtype: list[TaskDto] 44 | """ 45 | assert isinstance(user_id, int) 46 | keys = self._dao.user_tasks(user_id=user_id).smembers() 47 | tasks = [] 48 | if not keys: 49 | return [] 50 | json_strs = self._dao.tasks.hmget(*keys) 51 | values = [json.loads(x.decode('utf-8')) for x in json_strs] 52 | return [self._from_dict(x) for x in values] 53 | 54 | def find_todo_by_user_id(self, user_id): 55 | u""" ユーザの未完了タスク一覧を取得する 56 | 57 | :type user_id: int 58 | :rtype: list[TaskDto] 59 | """ 60 | assert isinstance(user_id, int) 61 | keys = self._dao.todo(user_id=user_id).smembers() 62 | tasks = [] 63 | if not keys: 64 | return [] 65 | json_strs = self._dao.tasks.hmget(*keys) 66 | values = [json.loads(x.decode('utf-8')) for x in json_strs] 67 | return [self._from_dict(x) for x in values] 68 | 69 | def find_done_by_user_id(self, user_id): 70 | u""" ユーザの完了タスク一覧を取得する 71 | 72 | :type user_id: int 73 | :rtype: list[TaskDto] 74 | """ 75 | assert isinstance(user_id, int) 76 | keys = self._dao.done(user_id=user_id).smembers() 77 | tasks = [] 78 | if not keys: 79 | return [] 80 | json_strs = self._dao.tasks.hmget(*keys) 81 | values = [json.loads(x.decode('utf-8')) for x in json_strs] 82 | return [self._from_dict(x) for x in values] 83 | 84 | def _from_dict(self, value): 85 | return TaskDto( 86 | task_id=value['task_id'], 87 | user_id=value['user_id'], 88 | name=value['name'], 89 | status=TaskStatus(value['status'])) 90 | -------------------------------------------------------------------------------- /todolist/testing.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import inject 3 | import redis 4 | 5 | from todolist.adapter.eventbus import SimpleEventBus 6 | from todolist.adapter.repo.task import TaskMemoryRepository 7 | from todolist.adapter.user import SimpleUserService 8 | from todolist.domain_model.task import TaskRepository 9 | from todolist.domain_model.user import UserService 10 | from todolist.port.eventbus import EventBus 11 | 12 | 13 | def config(binder): 14 | binder.bind(redis.StrictRedis, redis.StrictRedis("localhost", 6379, 14)) 15 | binder.bind_to_constructor(EventBus, SimpleEventBus) 16 | binder.bind_to_constructor(TaskRepository, TaskMemoryRepository) 17 | binder.bind_to_constructor(UserService, SimpleUserService) 18 | 19 | 20 | def overwrite_binding(key, constructor): 21 | injector = inject.get_injector() 22 | injector._bindings[key] = constructor 23 | --------------------------------------------------------------------------------