クリーンアーキテクチャにおけるデータ変換

2021年6月11日金曜日

Architecture

クリーンアーキテクチャの設計思想に基づき実装を始めると気付く点があります。それは依存関係を整理するために多数のデータ変換が発生することです。本記事は、このデータ変換の必要性と負担軽減方法について記載します。


アイキャッチ

変更容易性と冗長性

Clean Architectureは、円の外側から中心に向かってのみ依存関係が存在するように依存関係が整理されたアーキテクチャです。
このことによって、同心円の外側の修正が内側に影響しない設計となっています。

この設計は変更容易性を高める一方で、依存関係の整理のための実装が発生します。
これは将来の修正のしやすさに対してトレードオフの関係にあります。

そして、最も実装量が増えるであろう処理が「データ変換」となります。
なぜデータ変換が発生するのか以降の章で見ていきます。

データ変換の冗長性

Clean Architectureをクラス図レベルで表現したものが下記になります。

このクラス図の内、実装において煩雑になる箇所は依存関係を同心円の中心に向けるために調整されている下記の箇所かと思います。

ControllerInput Boundaryとの関係に注目します。
Input BoundaryはインターフェースであるためUse Case Interactorによって実装されます。
そしてInput Boundaryの引数となるDataはInput Dataによって定義されます。
この時のInput DataInput Boundaryの実装は下記のようになります。

// InputData, InputBoundary - Start
class InputData{
    public enum InputArgA{A, B}
    public enum InputArgB{A, B}

    public InputArgA input_a;
    public InputArgB input_b;
}
interface InputBoundary{
    public void setConfigUsecase(InputData data);
}
// InputData, InputBoundary - End

上記プログラムでは、インターフェースであるInputBoundaryInputBoundaryの引数となるInputDataを定義しています。
このInput Boundaryを呼び出すためのControllerは下記のようになります。

// Controller - Start
class Controller {
    // Viewで設定される設定値A
    public enum SetA{A, B}
    // Viewで設定される設定値B
    public enum SetB{A, B}

    private InputBoundary input;
    public Controller(InputBoundary instance){
        this.input = instance;
    }

    public void SetManyConfig(SetA set_a, SetB set_b){
        input.setConfigUsecase(this.transformer(set_a, set_b));
    }

    private InputData transformer(SetA set_a, SetB set_b){
        InputData data = new InputData();
        switch(set_a){
            case A:
                data.input_a = InputData.InputArgA.A;
            case B:
                data.input_a = InputData.InputArgA.B;
        }
        switch(set_b){
            case A:
                data.input_b = InputData.InputArgB.A;
            case B:
                data.input_b = InputData.InputArgB.B;
        }
        System.out.println("Controllerのデータ構造からInputDataのデータ構造に変換します");
        return data;
    }
}
// Controller - End

注目すべきは下記の関数です。
private InputData transformer(SetA set_a, SetB set_b)

この関数は、Controllerに入力されたデータから、InputBoundaryが要求するデータ構造に変換している関数です。
ViewからControllerに渡されたデータをInputBoundaryに変換しています。

次に、Use case InteractorData Access Interfaceを見ていきます。
これらの実装は下記のようになります。

// Usecase Interacter - Start
class SetConfigUsecase implements InputBoundary{
    DataAccessInterface dataAccess;
    public SetConfigUsecase(DataAccessInterface instance){
        this.dataAccess = instance;
    }
    public void setConfigUsecase(InputData data){
        dataAccess.saveConfig(this.transformer(data));
    }
    private Entity transformer(InputData data){
        Entity entity = new Entity();
        switch(data.input_a){
            case A:
                entity.entity_a = Entity.EntityA.A;
            case B:
                entity.entity_a = Entity.EntityA.A;
        }
        switch(data.input_a){
            case A:
                entity.entity_b = Entity.EntityB.A;
            case B:
                entity.entity_b = Entity.EntityB.B;
        }
        System.out.println("InputDataのデータ構造からEntityのデータ構造に変換します");
        return entity;
    }
}
// Usecase Interacter - End

EntityInputDataのデータ構造に依存できないため
Entityが望むデータ構造に変換する必要があります。
このため再度上記のようなデータ変換処理private Entity transformer(InputData data)が現れます。

このためUse case InteractorEntityが指定するデータ構造に変換しDomain処理をEntityに依頼することとなります。

これらの変換はクリーンアーキテクチャにおいて依存関係を管理するために避けては通れません。
そしてサンプルのコードでは、2つのデータを変換しているのみですが
大規模なアプリケーションを開発している際にはこのデータの量は爆発的に増えていきます。
stringやintなど一般的な基本型であれば変換の必要性はありませんが
一定数独自定義したデータ型は発生すると考えられます。
この冗長とも言える実装は、依存関係を整理することとのトレードオフになります。

トレードオフと言いつつも可能であればその実装量を減らしたいのが正直なところです。
そのため以降にその対応策を記載します。

対応策①

int, string等基本的なデータ型のみを利用する

int, string等基本的なデータ型のみを利用する。
一つの対応策に上記が挙げられるかと思います。
この対応策は、InputBoundaryEntityへの入力データ構造を全てデータ型で統一してしまうことによって
データ変換そのものの必要性を減らしていきます。
しかしながらデメリットはenumのようなデータをstringintで扱う必要が発生することです。
このことによってstringの文字列のtypoや、intの数字に意味を持たせるため
些末な不具合の発生確率が上がります。

対応策②

Jsonを利用する

この対応策はサーバーとクライアントのデータ受け渡しに近い方法となります。
下図の赤い円であるApplication Business Rulesを含めた中心部ではJsonデータによってデータの受け渡します。

この対策によってControllerにデータが受け渡される時点で
Jsonとして受け取れば、以降の処理でデータの変換は不要となります。
しかしながらデメリットとしては、Controllerや、Use case Interactor
逐次Jsonのparse/composeが発生するため実行速度に影響がでる可能性があります

さいごに

これらの対応策はそれぞれの開発現場によって選択肢が変わってくると思います。
高い品質が求められる場合は、厳格にデータ変換することが最善ですし、
クリーンアーキテクチャを適用しつつもある程度手を抜ける場合は対応策①を、
さらに実行環境のHWがリッチである場合は対応策②を、といった具合になるかもしれません。
いずれにせよ、なぜその選択をしたのか設計段階で明確にすることが重要になるかと思います。

最後になりますが、記事の中で紹介しているサンプルコードの全てを下記に記載します。
雰囲気をつかんで頂くことを重視しているため1ファイルにすべてをまとめています。

import java.util.*;

// View - Start
public class Main {

    public static void main(String[] args) throws Exception {
        // Initialize
        DataAccessInterface dataAccess = new DataAccess();
        InputBoundary input = new SetConfigUsecase(dataAccess);
        Controller controller = new Controller(input);


        System.out.println("ViewからConfigの設定が要求されました");
        controller.SetManyConfig(Controller.SetA.A, Controller.SetB.B);
    }
}
// View - End

// Controller - Start
class Controller {
    // Viewで設定される設定値A
    public enum SetA{A, B}
    // Viewで設定される設定値B
    public enum SetB{A, B}

    private InputBoundary input;
    public Controller(InputBoundary instance){
        this.input = instance;
    }

    public void SetManyConfig(SetA set_a, SetB set_b){
        input.setConfigUsecase(this.transformer(set_a, set_b));
    }

    private InputData transformer(SetA set_a, SetB set_b){
        InputData data = new InputData();
        switch(set_a){
            case A:
                data.input_a = InputData.InputArgA.A;
            case B:
                data.input_a = InputData.InputArgA.B;
        }
        switch(set_b){
            case A:
                data.input_b = InputData.InputArgB.A;
            case B:
                data.input_b = InputData.InputArgB.B;
        }
        System.out.println("Controllerのデータ構造からInputDataのデータ構造に変換します");
        return data;
    }
}
// Controller - End

// InputData, InputBoundary - Start
class InputData{
    public enum InputArgA{A, B}
    public enum InputArgB{A, B}

    public InputArgA input_a;
    public InputArgB input_b;
}
interface InputBoundary{
    public void setConfigUsecase(InputData data);
}
// InputData, InputBoundary - End

// Usecase Interacter - Start
class SetConfigUsecase implements InputBoundary{
    DataAccessInterface dataAccess;
    public SetConfigUsecase(DataAccessInterface instance){
        this.dataAccess = instance;
    }
    public void setConfigUsecase(InputData data){
        dataAccess.saveConfig(this.transformer(data));
    }
    private Entity transformer(InputData data){
        Entity entity = new Entity();
        switch(data.input_a){
            case A:
                entity.entity_a = Entity.EntityA.A;
            case B:
                entity.entity_a = Entity.EntityA.A;
        }
        switch(data.input_a){
            case A:
                entity.entity_b = Entity.EntityB.A;
            case B:
                entity.entity_b = Entity.EntityB.B;
        }
        System.out.println("InputDataのデータ構造からEntityのデータ構造に変換します");
        return entity;
    }
}
// Usecase Interacter - End

// Entity - Start
class Entity{
    enum EntityA{
        A, B
    }
    enum EntityB{
        A, B
    }
    public EntityA entity_a;
    public EntityB entity_b;

    public int getData(){
        // Domainとしての処理
        return 0;
    }
}
// Entity - End

// DataAccessInterface - Start
interface DataAccessInterface{
    public void saveConfig(Entity entity);
}
// DataAccessInterface - End

// DataAccess - Start
class DataAccess implements DataAccessInterface{
    public void saveConfig(Entity entity){
        System.out.println("永続化などの処理実行");
    }
}
// DataAccess - End

AIで副業ならココから!

まずは無料会員登録

プロフィール

メーカーで研究開発を行う現役エンジニア
組み込み機器開発や機会学習モデル開発に従事しています

本ブログでは最新AI技術を中心にソースコード付きでご紹介します


Twitter

カテゴリ

このブログを検索

ブログ アーカイブ

TeDokology