OpenWork Engineer Blog

OpenWork を運営するエンジニアによるテックブログです。

ロギングで読みづらいコードをDomain Probeパターンで整理する

f:id:seiya_hamada:20191219162745j:plain:w450
Photo by Jason Abdilla on Unsplash

ビジネスロジック vs ロギング

こんにちは。ウェブエンジニアの濱田です。普段の業務ではスマホアプリ用のAPIを開発する一方で、古くなったコードベースの改善にも取り組んでいます。

早速ですがビジネスロジックのロギング処理は言うまでもなく重要なことですよね。エラーを検知するためのログ、分析用のデータを収集するログなど、弊社でもさまざまなログを取っています。 一方でビジネスロジック中にログのためのコードが混在することで「コードが読みづらくなった」ことのある方は多いのではないでしょうか。

本記事ではDomain Probeパターンという設計手法を用いて、ビジネスロジックからロギングの責務を切り離し、見通しを良くする方法を紹介したいと思います。

もともとはコード設計等で著名なMartin Fowler氏のサイトで紹介されていて知った手法です。文中ではこちらを「元記事」と呼びます。 https://martinfowler.com/articles/domain-oriented-observability.html

ロギングにまみれたコード

架空のコード例として新しいブログ記事を作成するBlogクラスを考えてみます。 タイトルと本文から記事を作成するシンプルなものです。コードはJavaScriptですが使用する言語は特に問いません。

class Blog {
    createPost(title, body) {
        this.create(title, body);
    }
}

この処理はとても大切な部分なのでエラーを検知したいです。そして分析用にイベントのログを保存する要件もあります。さらに処理にかかる時間が気になるのでパフォーマンス計測用のログも仕込んでしまいましょう。

class Blog {
    constructor(errorLogger, analyticsLogger, perfLogger) {
        this.errorLogger = errorLogger;
        this.analyticsLogger = analyticsLogger;
        this.perfLogger = perfLogger;
    }

    // ログに必要なため引数にtraceIdを追加
    createPost(title, body, traceId) {
        try {
            // パフォーマンス計測
            this.perfLogger.log(time, 'createPost-start')
            this.create(title, body);
            this.perfLogger.log(time, 'createPost-end')
        } catch (e) {
            // エラーログ
            this.errorLogger.log('エラーが起きました', e);
            return;
        }

        // 分析用ログ
        data = {
            traceId,
            titleLength: title.length,
            bodyLength: body.length,
        };
        this.analyticsLogger.log(data);
    }
}

というわけで各種ロギング処理を仕込んでみました。いくつかLoggerオブジェクトへの依存が増えたのに加え、ログに必要なデータも引数に渡すようになりました。 ロギングだらけになったこのコードを見て、Blogクラスの本来の責務を読み取ることができるでしょうか。

ロギングの責務を負うことのデメリット

ビジネスロジックに加えてロギングの責務まで一つのクラスが持つといくつかの問題が生じます。

1. 可読性が落ちる

ログに必要なデータを作るコードなどが増えることで、ビジネスロジックがわかりづらくなります。

2. 依存が増える

上記の例ではログの種類だけロガーへの依存を増やしてしまいました。ログに必要な値をメソッドの引数に渡す必要もありました。

3. テストが複雑になる

依存が増えることで、Blogインスタンスの作成にかかる準備がテストコード中に多くなります。例えば各Loggerオブジェクトをテストダブル化するコードですね。 また責務が増えることで、必要なテストと条件の組み合わせが増大します。

こういった煩雑さに対し、ビジネスロジック中のロギングを別クラスの責務に切り分ける設計を元記事ではDomain Probeパターンと呼んでいます。

Domain Probeパターンの適用

Domain Probeパターンを使用する方法は単純で、ログの具体的な処理(どのようなデータか、どこへ送るか)を責務とするクラスをつくりビジネスロジッククラスからそのクラスに依存させるようにするだけです。

(※元記事ではそういったクラスを「〜Instrumentation(計測器)クラス」と名付けていますが、非英語ネイティブには馴染みの薄そうな言葉なので個人的には「〜Recorderクラス」と名付けることが多いです。以下コード例でもその命名を使用します。)

// Domain Probe役のクラス
class BlogRecorder {
    constructor(errorLogger, analyticsLogger, perfLogger, logContext) {
        this.errorLogger = errorLogger;
        this.analyticsLogger = analyticsLogger;
        this.perfLogger = perfLogger;

        // ログに必要なデータを集めたオブジェクト
        this.logContext = logContext;
    }

    errorOccurred(e) {
        this.errorLogger.log('エラーが起きました', e);
    }

    startCreating() {
        this.perfLogger.log(time, 'createPost-start');
    }

    finishedCreating() {
        this.perfLogger.log(time, 'createPost-end');
    }

    succeeded(title, body) {
        data = {
            traceId: this.logContext.traceId,
            titleLength: title.length,
            bodyLength: body.length,
        };
        this.analyticsLogger.log(data);
    }
}

// Domain Probeパターンを適用したビジネスロジッククラス
class Blog {
    constructor(blogRecorder) {
        this.blogRecorder = blogRecorder;
    }

    createPost(title, body) {
        try {
            this.blogRecorder.startCreating();
            this.create(title, body);
            this.blogRecorder.finishedCreating();
        } catch (e) {
            this.blogRecorder.errorOccurred(e);
            return;
        }

        this.blogRecorder.succeeded(title, body);
    }
}

Recorderクラスのメソッドは「いつログを取るのか」というビジネスロジック上の文脈に沿う命名にし、具体的なロギングの方法には触れないのがポイントです。 シンプルなコード例ですが適用前に比べデメリットを改善することができました。

可読性が上がった

ロギングをビジネスロジック上の文脈で記述できることで流れを読み取りやすくなりました。

依存が減った

複数のロガークラスへの依存やログに必要な引数のバケツリレーを減らすことができました。

テストがしやすくなった

Blogクラスのユニットテストで「ログの内容が意図したものか」といった観点のテストを行う必要はなくなりました。代わりにBlogRecorderクラスのテストダブルをつくり、条件に応じたメソッドが実行されるかを確認することができます。また、依存が減ったことでテストコードも簡潔なものになるはずです。

ロギングは誰の責務か

冒頭で述べたように、ビジネスロジックのロギングはとても大切な要件です。 しかし、一口にログを取ると言ってもその責務は複数に分割することができます。

  • いつログを取るのか
    • ビジネスロジック中のどのような文脈でログを取るべきか
  • ログデータはどのような内容か
    • エラーメッセージの文言
    • ログデータの整形
  • ログをどこに送るか
    • サーバー上のログファイル
    • TreasureDataなどのデータストアサービス
    • RDB

このうち後者2つを抽象化することでビジネスロジックを変更に強いものにする、それがDomain Probeパターンのコアとなる考え方だと思います。エラーメッセージの内容を変更しようとも、データストアサービスを乗り換えようとも、どこにどのようなログを取るのかに関係なく、ビジネスロジックはログを取るという責務を遂行し続けます。

最後に

個人的にクリーンアーキテクチャやドメイン駆動設計を勉強していたとき、「ログを取るのはどのレイヤーなのか」という点で悩んだことがありました。

しかしこのDomain Probeパターンを知ったことで、ロギングもRepositoryパターンと同様に扱えることに気づきました。 データの取得、永続化を抽象化するRepositoryパターンに対し、Domain Probeパターンはロギングの方法を抽象化しているわけですね。

Domain Probeパターン自体はとてもシンプルなので既存コードのリファクタにも適用しやすいと思います。ロギングのコードにお悩みの方は試してみてはいかがでしょうか。