クリーンアーキテクチャの設計思想に基づき実装を始めると気付く点があります。それは依存関係を整理するために多数のデータ変換が発生することです。本記事は、このデータ変換の必要性と負担軽減方法について記載します。
変更容易性と冗長性
Clean Architectureは、円の外側から中心に向かってのみ依存関係が存在するように依存関係が整理されたアーキテクチャです。
このことによって、同心円の外側の修正が内側に影響しない設計となっています。
この設計は変更容易性を高める一方で、依存関係の整理のための実装が発生します。
これは将来の修正のしやすさに対してトレードオフの関係にあります。
そして、最も実装量が増えるであろう処理が「データ変換」となります。
なぜデータ変換が発生するのか以降の章で見ていきます。
データ変換の冗長性
Clean Architectureをクラス図レベルで表現したものが下記になります。
このクラス図の内、実装において煩雑になる箇所は依存関係を同心円の中心に向けるために調整されている下記の箇所かと思います。
Controller
とInput Boundary
との関係に注目します。
Input Boundary
はインターフェースであるためUse Case Interactor
によって実装されます。
そしてInput Boundary
の引数となるDataはInput Data
によって定義されます。
この時のInput Data
とInput 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
上記プログラムでは、インターフェースであるInputBoundary
とInputBoundary
の引数となる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 Interactor
とData 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
Entity
はInputData
のデータ構造に依存できないため
Entity
が望むデータ構造に変換する必要があります。
このため再度上記のようなデータ変換処理private Entity transformer(InputData data)
が現れます。
このためUse case Interactor
はEntity
が指定するデータ構造に変換しDomain処理をEntity
に依頼することとなります。
これらの変換はクリーンアーキテクチャにおいて依存関係を管理するために避けては通れません。
そしてサンプルのコードでは、2つのデータを変換しているのみですが
大規模なアプリケーションを開発している際にはこのデータの量は爆発的に増えていきます。
stringやintなど一般的な基本型であれば変換の必要性はありませんが
一定数独自定義したデータ型は発生すると考えられます。
この冗長とも言える実装は、依存関係を整理することとのトレードオフになります。
トレードオフと言いつつも可能であればその実装量を減らしたいのが正直なところです。
そのため以降にその対応策を記載します。
対応策①
int, string等基本的なデータ型のみを利用する
int
, string
等基本的なデータ型のみを利用する。
一つの対応策に上記が挙げられるかと思います。
この対応策は、InputBoundary
、Entity
への入力データ構造を全てデータ型で統一してしまうことによって
データ変換そのものの必要性を減らしていきます。
しかしながらデメリットはenum
のようなデータをstring
やint
で扱う必要が発生することです。
このことによって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
0 件のコメント :
コメントを投稿