2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

PythonコードをDDDっぽく改良する

Posted at

はじめに

この記事では、プログラム設計に焦点をあてて、よりよい設計をするために私が気を付けている点などについてまとめようとおもいます。

今回 Python のコードを取り上げますが、これは私が実際業務でおこなったコード改善活動で書いたコードをベースにしています。

コードの概要

このコードはログの解析をおこなっていまして、処理としては、

1. 対象となるログファイルを読み込む
2. 集計する
3. データベースに結果を書き込む

というシンプルな内容です。
とあるログファイルを解析し、ユーザーのログイン状況を集計する内容になっています。

今回のコード改良にあたり、まずざっくりとした設計図を書くところからはじめました。

pythonddd.png

設計をするときには、ドメインを抽出するところからはじめます。

そしてそれらドメインを呼び出すアプリケーション、あるいはインフラストラクチャに役割を分割しながら考えていきました。

そもそもなぜ改良したかったか

もともとこのコードはバッチとして稼働していましたが、その性質上アプリというよりは "処理" という側面が強く、コードも処理の羅列とよぶべきものでした。

そのため、コードが読みにくかったり、似たようなほかのバッチを追加するために再利用するのが難しかったりと、いろいろ問題がでてきたため、再設計する運びとなりました。

コードの内容

ドメイン

pythonddd_domain.png

実装の詳細にまでは立ち入りませんが、だいたいどんな設計をしたか、ということを述べようとおもいます。

このバッチ処理はログファイルを読み込んでユーザーのログイン状況を解析するため、Login というクラスを作りました。

また、TenantGroup はユーザーが属する組織を示していまして、それらもドメインとして作りました。

以下に実際のコードをいくつか載せます。

login.py
#!/usr/bin/env python3
# -*= coding: utf-8 -*-
import re

class Login:
    LOGIN_MSG = "Logged in successfully\."

    LOG_FMT      = r"( ip:)([^ ]+)( domain:)([^ ]+)"
    LOG_FMT      += r"( tid:)([^ ]+)( gid:)([^ ]+)"
    LOG_FMT      += r" [^ ]+\] " + LOGIN_MSG

    COLUMN_TENANT_ID = 6
    COLUMN_GROUP_ID = 8

    def __init__(self, line, tenant_id):

        self.pattern = re.compile(Login.LOG_FMT)
        self.match = self.pattern.search(line)

        if (int(tenant_id) != int(self.match.group(Login.COLUMN_TENANT_ID))):
            return None

        self.group_id = int(self.match.group(Login.COLUMN_GROUP_ID))

    def get_group_id(self):
        return self.group_id

    def output(self):
        print(self.match)
        print(self.match.groups())
        print(self.tenant_id)
        print(self.group_id)

ドメインの中には、処理対象に関するコアな情報を記載しています。

処理対象となるログファイルの構成や、読み込みたい文字列など、処理の中核となる情報をドメインに閉じ込めるようにします。

このとき、これらドメインは、アプリケーションが呼び出すだけなく、ファイルやDBへのアクセスをおこなうリポジトリも呼び出しますから、"使われ方" を気にしながら作っていきます。

ある程度作ったら、Python インタフェースからクラスを読み込んでインスタンス化してみたりして、使い方をイメージしながら作りこんでいきます。

tenant.py
#!/usr/bin/env python3
# -*= coding: utf-8 -*-

class Tenant:

    def __init__(self, tenant_id, groups):
        self.tenant_id = tenant_id
        self.groups = groups

    def get_tenant_id(self):
        return self.tenant_id

    def get_groups(self):
        return self.groups

ドメインとなるクラスを考えるときには、なにをクラスとして切り出すのか、それぞれのメンバ変数にはなにがあるのか、といったことに悩むことが多々あります。

最初に思い描いた設計の通りに進まないことが多いので、手を動かしながら着地点を模索します。

アプリケーション

pythonddd_app.png

アプリケーションでは、ドメインを呼び出し、処理の順番を制御します。
今回は **Service という名前のアプリケーションクラスを3つ作成し、それらを統合するクラスとして Controller をつくっています。

3つのアプリケーションクラスの役割は以下の通りです。

  • ConfigService : 設定ファイルを読み込んで、処理対象となるテナントを取得する
  • LogService : メインとなるログファイルの分析をおこなう
  • ResultService : 処理結果をデータベースに格納する
config_service.py
#!/usr/bin/env python3
# -*= coding: utf-8 -*-

import sys
sys.path.append('../')
from domain.tenant import Tenant
from repository.config_repository import ConfigRepository

class ConfigService:
    def handle(self):
        repo = ConfigRepository()
        tenants = repo.list()

        return tenants

設定ファイルを読み込む処理はリポジトリでおこなっているので、ここではリポジトリを呼び出すことでデータを取得しています。

log_service.py
#!/usr/bin/env python3
# -*= coding: utf-8 -*-

import sys
sys.path.append('../')
from domain.tenant import Tenant
from domain.group import Group
from repository.log_repository import LogRepository

class LogService:
    def handle(self, _date, tenants):

        count = 0
        repo = LogRepository(_date)

        for tenant in tenants:
            logins = repo.findByTenantId(tenant.get_tenant_id())

            for group in tenant.get_groups():
                count = logins.count(group.get_group_id())
                group.set_count(count)

        return tenants

LogService も、リポジトリを呼び出すことでログファイルを読み込む処理をおこなったり、ドメインに定義されたメソッドを呼び出したりして処理を実行します。

アプリケーションが担う責務は、ドメインやリポジトリで定義されたメソッドを正しい順番で呼び出して、全体を統括することを中心としたいので、アプリケーションにはできるだけ業務知識を書かないようにしています。

アプリケーションがデータの取得をリポジトリに依頼する際には、その結果がドメインとして返されるようにしています。

その後、アプリケーションでは受け取ったドメインを使って処理を行い、処理結果を格納する際にはまたリポジトリにそれを依頼します。

controller.py
#!/usr/bin/env python3
# -*= coding: utf-8 -*-

import sys
sys.path.append('../')
from domain.tenant import Tenant
from application.log_service import LogService
from application.config_service import ConfigService
from application.result_service import ResultService

class Controller:
    def exec(self, _date):

        c_srv = ConfigService()
        tenants = c_srv.handle()

        l_srv = LogService()
        results = l_srv.handle(_date, tenants)

        r_srv = ResultService()
        r_srv.handle(results)

        return True

リポジトリ

pythonddd_infra.png

リポジトリでは、ファイルからデータを読み込んだり、データベースを読み書きしたりすることを役目とします。

3つのクラスを作りまして、それぞれの役割は以下の通りです。

  • ConfigRepository : 今回の処理に利用する設定ファイルの読み込みをおこなう
  • LogRepository : ログファイルを読み込む
  • ResultRepository : 処理結果をデータベースに書き込む
log_repository.py
import sys
from pathlib import Path
import configparser
sys.path.append('../')
from domain.login import Login
from domain.logins import Logins

class LogRepository:

    def __init__(self, _date):
        self._date = _date
        self.root = Path().cwd().parent.parent
        self.log_path = self.root / 'storage' / 'logs'
        self.p = list(self.log_path.glob('*' + self._date + '*'))[0]

    def findByTenantId(self, tenant_id):

        logins = Logins(self._date, tenant_id)

        with self.p.open('r', encoding='utf-8') as f:
            for line in f:
                if re.search(Login.LOGIN_MSG, line):
                    login = Login(line, tenant_id)

                    if (login.uid is None): continue
                    logins.add_login(login)

        return logins

リポジトリでは、ファイルやデータベースの読み書きを行うので、書き込む場合にはアプリケーションから渡されたドメインを書き込み、読み込む場合には指定されたデータを読み込んで、ドメインとしてアプリケーションに渡します。

上記の例では読み込みを行っているので、指定されたデータをファイルの中から検索し、見つかったデータをドメインとしてパッケージしてからアプリケーションに返してあげます。

改良してみて

今回のコード改良では、ドメインとしてなにを切り出すかということが一番悩みましたが、改善後のほうが明らかに保守性が上昇しました。

DDDを取り入れて設計していくにあたって、精緻な全体像をつくってから開発にとりかかるというよりかは、設計して開発して、という小さいサイクルを繰り返すほうが作りやすいと感じました。

1. コアとなるドメインだけをざっくり作る
2. 1を呼び出して利用するアプリケーションやリポジトリのコアな部分を作る
3. ドメインに肉付けして拡張していく
  :

といった感じで私はつくっていきました。

つくっていくうちに、「あ、これはドメインクラスとして作ったほうがいいな」とか、「これはメンバ変数にしておけばいいな」といったことが見えてくるので、その都度設計図に手をいれながら、少しずつ拡張していくやり方が相性が良いとおもいました。

今回の記事は以上です。

2
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?