書くまでにすっげぇ時間かかった。。。以前課題に挙げていた、「アプリケーション層とドメイン層の間にビジネス層を設ける」をお試しで実践してみる♪

52 min read

<障害発生日>
2021年9月26日頃...かな...その2(;´∀`)

<ざっくりとした環境情報>
OS:Windows10
言語:Java (jbr-11(JetBrains Runtime version 11.0.10))

忘れもしない2021年10月1日。Twitter上での友人(かがみは、の、つもり(;´∀`))『こー1@趣味のアプリエンジニア:@kouichi_highcom』さんとのTwitterでのやり取りで「ビジネス層」というアイデアを頂戴する

はい、みなさまお久しぶりです!かがみんです♪♪♪

以前、OOPに対する手厳しいご批判があることを知り、「OOPがオワコンってことは、DDDもオワコン!?」などと悶々と悩んでいたのですが、とあるTwitterでのやり取りで「OOPに対する手厳しいご批判のうちのいくつかに対する解決策」と思われるアイデアを戴いたので、それを試しに実践してみました(゚∀゚)!!

ちなみに、再掲するとOOPに対するこんなご批判(※またの名を『ご指導、ご鞭撻』)が、本記事発生の元ネタとなっております。

OOPへの批判その1:ポリモーフィズムは概念が狭すぎて柔軟では無い。

※元ネタ:

http://www.oocities.org/tablizer/oopbad.htm

<以下一部引用>

IF statements, case-blocks, and data query expressions are usually more flexible in dealing with changing dispatching (rule triggering) criteria; even multiple aspects can participate in an IF expression. It is very tricky and messy to achieve the same with polymorphism. Changing the IF criteria does not require moving the IF block.
Polymorphism is just too narrow a concept to be flexible. It generally requires a single, clean, global, and stable taxonomy/division-criteria to be effective, and those are hard to find except perhaps in nature (geometry, physics, etc.).

IFステートメント、ケースブロック、およびデータクエリ式は、通常、ディスパッチ(ルールトリガー)基準の変更をより柔軟に処理できます。複数の側面でさえIF式に参加できます。多形性で同じことを達成することは非常にトリッキーで厄介です。 IF基準を変更する場合、IFブロックを移動する必要はありません。
多形性は、概念が狭すぎて柔軟ではありません。それは一般に、効果的であるために単一の、クリーンで、グローバルで、安定した分類/分割基準を必要とします、そしてそれらはおそらく自然(幾何学、物理学など)を除いて見つけるのが難しいです。
(※上記はGoogle翻訳を使用)

<補足:具体例>
⇒『野菜クラス』の中に「おもちゃのジャガイモ」をぶっこんでみる。

a)普通のじゃがいも

abstract class Oyasai{
    public abstract String getName();
}

class Potato extends Oyasai{

    private String name = "じゃがいも";
    public String getName(){
        return name;
    }
}

b)おもちゃのじゃがいも

abstract class Oyasai{
    public abstract String getName();
}

//↓↓↓これですよ!これ!!↓↓↓
abstract class FakeOyasai extends Oyasai{
    public abstract String getName();
}

class PlasticPotato extends FakeOyasai{

    private String name = "じゃがいも(※プラスチック)";
    public String getName(){
        return name;
    }
}

説明ヘタ? これわかります(^_^;?
これ、カガミが書いた自費出版本『読むだけでわかる!?オブジェクト指向デザインパターンの基礎の基礎』という本の中のサンプルコードなんすけど、全体で355Stepにしかならない、たったこれだけのコードでも、ちょっと『ギャグのつもりで』変化球をやらかすと、新たな抽象が必要になってしまうんです。。

なので、
OOP批判者の方々どころか、OOP実践者でさえ、『継承』に対して否定的なのがなんとなく私にもわかるんです。これじゃ実際の業務アプリケーションならきっと『継承地獄』でしょうね。。。

あ、
もちろん、今回のサンプルコードは同じ『多形性(Polymorphism)』であってもインターフェース使いますからね(;^_^A

とりあえず『継承(extends)』はやめときましょう。。。

OOPへの批判その1への回答:アプリケーション層とドメイン層の間にビジネス層(腐敗防止層)を設ける!

さて、この問題に対する私とこー1さんとのTwitterでのやり取りはこんな感じ✨

※元ネタ

https://twitter.com/KagaminPower003/status/1443702185439621127

<以下一部転載>









<補足:「ビジネス層」に対するかがみのイメージ>


(※図は「DOMAIN DRIVEN DESIGN QUICKLY 日本語版」で使用されているものを一部修正したもの)

あ、ちなみに「アプリケーション層とドメイン層の間にビジネス層を設ける」というアドバイス自体はこー1さんのアイデアですが、本記事掲載内容の全責任はすべて私かがみんとなりますので、何卒宜しくお願い致します。

ちなみに、現時点でのかがみんの技術スペックはざっくり言うと下記のような感じとなります。。。

<本記事執筆者のスペック(2021年10月現在)>
①Javaの実務経験は約11ヶ月くらい。ただし、統合テストおよび保守フェーズでの開発。

②実務でDDDの設計経験なし。

③いちおう、「Gofの23のデザインパターン」および「SOLID原則」の基礎は理解済み(の、つもり^^;)。

④業務系アプリケーション開発の経験は15年以上。

⑤上記15年の経歴のうちもっとも長いのはSQLを使用したExcelVBAによるツール開発。

⑥とある中堅企業様の専属SEの経験あり。そこのお客様の基幹系システムのほぼ全域の保守を約3年半やっていた。ただしメインの言語はVB.NET(一部C#)。

⑦その他の補足として、BIツール開発やETLツール開発の実装経験もあり、いわゆるエンタープライズシステムに関するざっくり全体的なシステムイメージはあるにはある。ただしそれらってもはやモダンではないですよね。。。(;´∀`)

⑧昨年2020年12月2日に増田亨さんの「現場で役立つシステム設計の原則」の電子書籍を購入し、以来、DDDではない現場で働きながら朝活などでドメイン駆動設計について勉強中。エヴァンスさんの「ドメイン駆動設計」、ファウラーさんの「リファクタリング」や「エンタープライズアプリケーションアーキテクチャー」、「アナリシスパターン」などなどを拝読中。。。

まぁ、こんな感じで本記事掲載内容は、どこにでもいるふつうのシニア業務系アプリケーションエンジニアの独自見解のご意見となります。。。(;´∀`)

それではようやく!サンプルコードのご紹介♪

またいつものよーに前置き長っげえ。。。
とそれはさておき、今回は下記2パターンのサンプルコードをご用意しております♪

①1つの区分値に、2つの概念が混在している。

※元ネタ:

https://twitter.com/masuda220/status/1444904150987247617
(*私の「心の師匠」増田亨さんのツィートから。ちなみにわたしは氏の「ツィートと返信」を毎日数回チェックしております。。。)

<補足:「社員勤務表の選択肢」に対するかがみのイメージ>
a)社内・社外の場所区分

b)稼働・非稼働という状態区分

c)上記「a)」と「b)」が混在した「勤務状況サブステータス区分(※カガミの造語)」

そして「終日出勤」「祝祭日」などの「勤務区分」の内容を加味した範囲まででの、
区分値の全体イメージはこんな感じであると解釈いたしました。

d)「勤務状況」と「勤務状況サブステータス区分」と「勤務区分」の関係

では上記を踏まえて、今回のサンプルコードではこんな感じのプログラムを実装しますた(゚∀゚)!!

※仕様:
「各勤務状況の名称」でお問合せすると、対応する「区分値」などを回答してくれる。

<出力イメージ>
a)テレワーク

X:>社員勤務表.user.domainExpert.Pure テレワーク
『勤務状況』がテレワークの場合、該当する『勤務区分』は
  ●フル出勤
  ●午前休
  ●午後休
です。
『勤務状況』がテレワークの場合、該当する『勤務状況サブステータス区分』は
  ●社外
です。
『勤務状況サブステータス区分』が社外の場合、
  ⇒テレワークに該当する働き方を行い、一日のうちなんらかの仕事をした状態を指します。
です。

プロセスは終了コード 0 で終了しました

b)出社

X:>社員勤務表.user.domainExpert.Pure 出社
『勤務状況』が出社の場合、該当する『勤務区分』は
  ●フル出勤
  ●午前休
  ●午後休
です。
『勤務状況』が出社の場合、該当する『勤務状況サブステータス区分』は
  ●社内
です。
『勤務状況サブステータス区分』が社内の場合、
  ⇒一日のうちなんらかの仕事をした状態を指します。ただし、テレワークは含みません。
です。

プロセスは終了コード 0 で終了しました

c)非出社

X:>社員勤務表.user.domainExpert.Pure 非出社
『勤務状況』が非出社の場合、該当する『勤務区分』は
  ●週休日
  ●休暇
  ●欠勤
です。
『勤務状況』が非出社の場合、該当する『勤務状況サブステータス区分』は
  ●非稼働
です。
『勤務状況サブステータス区分』が非稼働の場合、
  ⇒終日仕事をしていない状態を指します。土日祝日も含みます。
です。

プロセスは終了コード 0 で終了しました

それでは、要点などかいつまんで、
サンプルコードをご紹介致します♪

Ⅰ:パッケージ構成

Ⅱ:ソースコード

①ドメイン層

①-a)社内・社外の場所区分

package 社員勤務表.domain.model;

public enum 場所区分 {
    社外,
    社内;

    public final String 詳細説明(){
        if(this.name() == 場所区分.社外.name()){ return "テレワークに該当する働き方を行い、一日のうちなんらかの仕事をした状態を指します。";}
        if(this.name() == 場所区分.社内.name()){ return "一日のうちなんらかの仕事をした状態を指します。ただし、テレワークは含みません。";}
        else{ return "その値は場所区分にはございません。";}
    }
}

①-b)稼働・非稼働という状態区分

package 社員勤務表.domain.model;

public enum 状態区分 {
    稼働,
    非稼働;

    public final String 補足説明(){
        if(this.name()==状態区分.稼働.name())  { return "一日のうちなんらかの仕事をした状態を指します。テレワークも含みます";}
        if(this.name()==状態区分.非稼働.name()){ return "終日仕事をしていない状態を指します。土日祝日も含みます。";}
        else{ return "その値は状態区分にはございません。";}
    }
}

①-c)テレワークなどを含む勤務状況

package 社員勤務表.domain.model;

public enum 勤務状況 {
    テレワーク,
    出社,
    非出社
}

①-d)午前休、フル出勤などを表す勤務区分

package 社員勤務表.domain.model;

public enum 勤務区分 {
    フル出勤,
    午前休,
    午後休,
    休暇,
    週休日,
    欠勤
}

②ビジネス層

②-a)ドメイン層の「勤務区分」をラップする勤務区分アダプター

package 社員勤務表.business.adapter;

import 社員勤務表.domain.model.勤務区分;

public class 勤務区分アダプター {
    private 勤務区分 my勤務区分;

    public 勤務区分アダプター(勤務区分 my勤務区分){
        this.my勤務区分 = my勤務区分;
    }

    public final String name(){
        return my勤務区分.name();
    }
}

②-b)「勤務区分アダプター」をコレクションに格納した「勤務区分問合せサービス」

package 社員勤務表.business.service;

import 社員勤務表.business.adapter.勤務区分アダプター;
import 社員勤務表.domain.model.勤務区分;
import java.util.List;
import java.util.ArrayList;
import java.util.Collections;

public class 勤務区分問合せサービス {

    private String my勤務状況 = "";
    private List<勤務区分アダプター> my勤務区分list = new ArrayList<>();

    public 勤務区分問合せサービス(String my勤務状況){
        勤務状況判定サービス my勤務状況判定 = new 勤務状況判定サービス(my勤務状況);

        if(my勤務状況判定.is非該当()){ System.out.println("その値は勤務状況には存在しません"); }
        else{ this.my勤務状況 = my勤務状況; }
    }
    public List<勤務区分アダプター> 勤務区分List(){
        勤務状況判定サービス my勤務状況判定 = new 勤務状況判定サービス(my勤務状況);

        if(my勤務状況判定.isテレワーク()) {return テレワーク設定(); }
        if(my勤務状況判定.is出社())      {return 出社設定(); }
        if(my勤務状況判定.is非出社())     {return 非出社設定(); }
        else{ my勤務区分list.clear(); }

        return  Collections.unmodifiableList(my勤務区分list);
    }
    private List<勤務区分アダプター> テレワーク設定(){
        my勤務区分list.add(new 勤務区分アダプター(勤務区分.フル出勤));
        my勤務区分list.add(new 勤務区分アダプター(勤務区分.午前休));
        my勤務区分list.add(new 勤務区分アダプター(勤務区分.午後休));

        return  Collections.unmodifiableList(my勤務区分list);
    }
    private List<勤務区分アダプター> 出社設定(){
        my勤務区分list.add(new 勤務区分アダプター(勤務区分.フル出勤));
        my勤務区分list.add(new 勤務区分アダプター(勤務区分.午前休));
        my勤務区分list.add(new 勤務区分アダプター(勤務区分.午後休));

        return  Collections.unmodifiableList(my勤務区分list);
    }
    private List<勤務区分アダプター> 非出社設定(){
        my勤務区分list.add(new 勤務区分アダプター(勤務区分.週休日));
        my勤務区分list.add(new 勤務区分アダプター(勤務区分.休暇));
        my勤務区分list.add(new 勤務区分アダプター(勤務区分.欠勤));

        return  Collections.unmodifiableList(my勤務区分list);
    }
}

②-c)「状態区分」と「場所区分」が混在した「勤務状況サブステータス区分アダプター」
はい、今回はここが最も重要です!!

package 社員勤務表.business.adapter;

import 社員勤務表.business.tool.状態区分判定;
import 社員勤務表.business.tool.場所区分判定;
import 社員勤務表.domain.model.状態区分;
import 社員勤務表.domain.model.場所区分;

public class 勤務状況サブステータス区分アダプター {
    private 状態区分 my状態区分;
    private 場所区分 my場所区分;

    //「状態区分」をぶっこむとき用の「コンストラクタ」
    public 勤務状況サブステータス区分アダプター(状態区分 my状態区分){
        this.my状態区分 = my状態区分;
    }

    //「場所区分」をぶっこむとき用の「コンストラクタ」
    public 勤務状況サブステータス区分アダプター(場所区分 my場所区分){
        this.my場所区分 = my場所区分;
    }

    public final String name(){
        if( new 状態区分判定(my状態区分).is有効() ){ return my状態区分.name(); }
        if( new 場所区分判定(my場所区分).is有効() ){ return my場所区分.name();}
        else{ return "該当する値がございません"; }
    }

    public final String 補足説明(){
        if( new 状態区分判定(my状態区分).is有効() ){ return my状態区分.補足説明(); }
        //※例えば、「場所区分」はメソッド名が異なっていたりしていてもOK
        if( new 場所区分判定(my場所区分).is有効() ){ return my場所区分.詳細説明(); }
        else{ return "該当する値がございません"; }
    }
}

⇒わかりにくいですかね(^^;
ドメイン層の「状態区分」と「場所区分」について、それぞれ別々のコンストラクタを用意することでそのどちらも1つのアダプターでラッピングしております。

②-d)「勤務状況サブステータス区分アダプター」をコレクションに格納した「勤務状況サブステータス区分問合せサービス」

package 社員勤務表.business.service;

import 社員勤務表.business.adapter.勤務状況サブステータス区分アダプター;
import 社員勤務表.domain.model.状態区分;
import 社員勤務表.domain.model.場所区分;
import java.util.List;
import java.util.ArrayList;
import java.util.Collections;

public class 勤務状況サブステータス区分問合せサービス {

    private String my勤務状況 = "";
    private List<勤務状況サブステータス区分アダプター> my勤務状況サブステータス区分list
            = new ArrayList<>();

    public 勤務状況サブステータス区分問合せサービス(String my勤務状況){
        勤務状況判定サービス my勤務状況判定 = new 勤務状況判定サービス(my勤務状況);

        if(my勤務状況判定.is非該当()){ System.out.println("その値は勤務状況には存在しません"); }
        else{ this.my勤務状況 = my勤務状況; }
    }
    public List<勤務状況サブステータス区分アダプター> 勤務状況サブステータス区分List(){
        勤務状況判定サービス my勤務状況判定 = new 勤務状況判定サービス(my勤務状況);

        if(my勤務状況判定.isテレワーク()) { return テレワーク設定(); }
        if(my勤務状況判定.is出社())      { return 出社設定(); }
        if(my勤務状況判定.is非出社())     { return 非出社設定(); }
        else{ my勤務状況サブステータス区分list.clear(); }

        return  Collections.unmodifiableList(my勤務状況サブステータス区分list);
    }
    private List<勤務状況サブステータス区分アダプター> テレワーク設定(){
        my勤務状況サブステータス区分list.add(new 勤務状況サブステータス区分アダプター(場所区分.社外));

        return Collections.unmodifiableList(my勤務状況サブステータス区分list);
    }
    private List<勤務状況サブステータス区分アダプター> 出社設定() {
        my勤務状況サブステータス区分list.add(new 勤務状況サブステータス区分アダプター(場所区分.社内));

        return Collections.unmodifiableList(my勤務状況サブステータス区分list);
    }
    private List<勤務状況サブステータス区分アダプター> 非出社設定() {
        my勤務状況サブステータス区分list.add(new 勤務状況サブステータス区分アダプター(状態区分.非稼働));

        return Collections.unmodifiableList(my勤務状況サブステータス区分list);
    }
}

⇒はい、ここで注目すべきは
new 勤務状況サブステータス区分アダプター(状態区分.非稼働)」と「new 勤務状況サブステータス区分アダプター(場所区分.社外)」いずれも「List<勤務状況サブステータス区分アダプター>」にぶっ込めることで、アプリケーション層側は1つのドメインオブジェクトである<かのように>操作が可能なわけです(^^♪


    private static void 勤務状況サブステータス区分問合せ(String my勤務状況){

        //問合せサービスのインスタンス化
        勤務状況サブステータス区分問合せサービス my勤務状況サブステータス区分問合せ
                = new 勤務状況サブステータス区分問合せサービス(my勤務状況);
	
	//コレクションの作成
        my勤務状況サブステータス区分list
                = my勤務状況サブステータス区分問合せ.勤務状況サブステータス区分List();

	//コレクションの展開(メッセージやcsv、htmlを生成するなど)
        System.out.println("『勤務状況』が" + my勤務状況 + "の場合、該当する『勤務状況サブステータス区分』は");
        for (勤務状況サブステータス区分アダプター my勤務状況サブステータス区分 :
                my勤務状況サブステータス区分list) {
            System.out.println("  ●" + my勤務状況サブステータス区分.name());
        }
        System.out.println("です。");
    }

最後に、上記ビジネス層のソースを見てて気になった方もおられたと思いますが、問合せサービスへの問合せに対する判定サービスのコードを掲載します(^^

②-e)「勤務区分問合せサービス」、「勤務状況サブステータス区分」どちらでも使用する「勤務状況判定サービス」

package 社員勤務表.business.service;

import 社員勤務表.domain.model.勤務状況;

public class 勤務状況判定サービス {

    private final String my勤務状況;

    public 勤務状況判定サービス(String my勤務状況){
        this.my勤務状況 = my勤務状況;
    }

    public Boolean is非該当(){
        try {
            勤務状況.valueOf(my勤務状況);
            return false;
        }
        catch (IllegalArgumentException e) {
            return true;
        }
    }

    public Boolean isテレワーク(){
        if(my勤務状況.equals(勤務状況.テレワーク.name())) { return true; }
        else{ return false; }
    }

    public Boolean is出社(){
        if(my勤務状況.equals(勤務状況.出社.name())) { return true; }
        else{ return false; }
    }

    public Boolean is非出社(){
        if(my勤務状況.equals(勤務状況.非出社.name())) { return true; }
        else{ return false; }
    }
}

ちなみに、ドメイン層へまったく影響を与えずに、上記2つの問合せサービスのみに「テレワーク」を「勤務状況」の区分値から外したい場合は下記のように修正します。

package 社員勤務表.business.service;

import 社員勤務表.domain.model.勤務状況;

public class 勤務状況判定サービス {

    private final String my勤務状況;

    public 勤務状況判定サービス(String my勤務状況){
        this.my勤務状況 = my勤務状況;
    }

    public Boolean is非該当(){
        try {
            勤務状況.valueOf(my勤務状況);
            
            //!!!緊急事態宣言長期休止によりテレワーク休止!!!
            if(this.isテレワーク()){ return true; }
            
            return false;
        }
        catch (IllegalArgumentException e) {
            return true;
        }
    }

    public Boolean isテレワーク(){
        if(my勤務状況.equals(勤務状況.テレワーク.name())) { return true; }
        else{ return false; }
    }

    public Boolean is出社(){
        if(my勤務状況.equals(勤務状況.出社.name())) { return true; }
        else{ return false; }
    }

    public Boolean is非出社(){
        if(my勤務状況.equals(勤務状況.非出社.name())) { return true; }
        else{ return false; }
    }


}

<出力イメージ>
a)テレワーク(※休止中)

X:>社員勤務表.user.domainExpert.Pure テレワーク
『勤務状況』がテレワークの場合、該当する『勤務区分』は
その値は勤務状況には存在しません
です。
『勤務状況』がテレワークの場合、該当する『勤務状況サブステータス区分』は
その値は勤務状況には存在しません
です。
その値は勤務状況には存在しません
です。

プロセスは終了コード 0 で終了しました

まぁちょっと変ですけど(^-^;
けどこれが人が読むものなら、たぶん伝わりますよね。。。

おまけ)
リファクタリング後に新たに追加した「ビジネス層内部」のみで使用するtoolパッケージのソースコード
②-f)-1. 「状態区分判定」

package 社員勤務表.business.tool;

import 社員勤務表.domain.model.状態区分;

public class 状態区分判定 {
    private 状態区分 my状態区分;

    public 状態区分判定(状態区分 my状態区分){ this.my状態区分 = my状態区分; }

    public Boolean is有効(){
        return my状態区分 != null;
    }

}

②-f)-2. 「場所区分判定」

package 社員勤務表.business.tool;

import 社員勤務表.domain.model.場所区分;

public class 場所区分判定 {

    private 場所区分 my場所区分;

    public 場所区分判定(場所区分 my場所区分){ this.my場所区分 = my場所区分; }

    public Boolean is有効(){
        return my場所区分 != null;
    }
}

②1つの区分値で使用したいメソッドに、パラメーター数が異なるケースが存在している。

※元ネタ:

https://zenn.dev/kagaminpower003/articles/633e947e35c217

はい、あっしのZenn記事ですね(^_^;
これ、さらなる元ネタはと言うと、前掲と同じくあっしの自費出版本のサンプルコードの
応用になります。

<「メソッドのパラメーター数が異なるケース」のカガミの想定イメージ>
①まず、あっしの自費出版で「Strategyパターン」を説明するためにこぉんな漫画を作りまして、

②んで、上記を「Strategyパターン」にするとこんな感じ。

class OujiSama{

    private String name   = "名無し";
    private int    weight =  -1;
    private int    height =  -1;
    private int    age    =  -1;

    public OujiSama(String name,int weight,int height,int age){
        this.name = name;
        this.weight = weight;
        this.height = height;
        this.age = age;
    }
    
    public String name(){
        return name;
    }

    public int height(){
        return height;
    }

    public int weight(){
        return weight;
    }

    public int age(){
        return age;
    }
    
}

//比較インターフェース
interface ComparatorStrategy{
    public String compare(OujiSama o1 , OujiSama o2);
}

//体重比較クラス
class WeightComparator implements ComparatorStrategy{
    public String compare(OujiSama o1 , OujiSama o2){
        //痩せてる人が好き♪
        if(o1.weight() < o2.weight()){
            return o1.name();
        }else if(o1.weight() == o2.weight()){
            return "両方とも";
        }else{
            return o2.name();
        }
    }
}

//身長比較クラス
class HeightComparator implements ComparatorStrategy{
    public String compare(OujiSama o1,OujiSama o2){
        //背が高い人が好き♪
        if(o1.height() > o2.height()){
            return o1.name();
        }else if(o1.height() == o2.height()){
            return "両方とも";
        }else{
            return o2.name();
        }
    }
}

class Happy{
    public static void main(String args[]) {
        OujiSama Ouji1 = new OujiSama("じぃじ",68,180,70);
        OujiSama Ouji2 = new OujiSama("ぱぁぱ",71,172,41);
        OujiSama Ouji3 = new OujiSama("チェホンマン",160,218,40);

        ComparatorStrategy WeightOtokoChk = new WeightComparator();
        ComparatorStrategy HeightOtokoChk = new HeightComparator();

        String type = args[0];
        String result = "";
        //If文スッキリ(⋈◍>◡<◍)。✧♡
        if(type.equals("痩せてる")){ result = WeightOtokoChk.compare(Ouji1, Ouji2); }
        if(type.equals("背が高い")){ result = HeightOtokoChk.compare(Ouji1, Ouji3); }

        System.out.println(result + "大好き♪");

    }
}

③けど、例えば「オトコどもの背の高さを較べたいの~♪」って時だけ「一度にまとめて3人較べて欲しいの♪」ってなパターンだと、同じ区分値内のIF文なのにインターフェースが2つ、必要になっちゃいますよね。。。業務アプリケーションで言えば、同じ区分内に「課税売上計算」と「非課税売上計算」がごっちゃ、みたいな(;´∀`)?

んでカガミは以前の記事でこぉんな感じの実装を書いたのですよね。。。

class OujiSama{

    private String name   = "名無し";
    private int    weight =  -1;
    private int    height =  -1;
    private int    age    =  -1;

    public OujiSama(String name,int weight,int height,int age){
        this.name = name;
        this.weight = weight;
        this.height = height;
        this.age = age;
    }
    
    public String name(){
        return name;
    }

    public int height(){
        return height;
    }

    public int weight(){
        return weight;
    }

    public int age(){
        return age;
    }
    
}

//比較サービス
class ComparatorService { 

    public String weight(OujiSama o1,OujiSama o2){ 
        return new WeightComparator().doubleCompare(o1,o2);
    }    
    //「背の高い殿方」較べるときは一度に3人♪
    public String height(OujiSama o1,OujiSama o2,OujiSama o3){
        return new HeightComparator().tripleCompare(o1,o2,o3); 
    }
}
//体重比較クラス
class WeightComparator {
    public String doubleCompare(OujiSama o1 , OujiSama o2){
        //痩せてる人が好き♪
        if(o1.weight() < o2.weight()){
            return o1.name();
        }else if(o1.weight() == o2.weight()){
            return "両方とも";
        }else{
            return o2.name();
        }
    }
}

//身長比較クラス ※ここがOO的には変化球(;^ω^)※
class HeightComparator {
    public String tripleCompare(OujiSama o1,OujiSama o2,OujiSama o3){
        //背が高い人が好き♪
        return compareRecursion(compareRecursion(o1,o2),o3).name();
    }
    private OujiSama compareRecursion(OujiSama o1,OujiSama o2){
        //背が高い人が好き♪
        if(o1.height() > o2.height()){
            return o1;
        }else if(o1.height() == o2.height()){
            return null;
        }else{
            return o2;
        }
    }
}

class Happy{
    public static void main(String args[]) {        
        OujiSama Ouji1 = new OujiSama("じぃじ",68,180,70);
        OujiSama Ouji2 = new OujiSama("ぱぁぱ",71,172,41);
        OujiSama Ouji3 = new OujiSama("チェホンマン",160,218,40);

        ComparatorService OtokoChk = new ComparatorService();

        String type = args[0];
        String result = "";
        //If文スッキリ(⋈◍>◡<◍)。✧♡
        if(type.equals("痩せてる")){ result = OtokoChk.weight(Ouji1, Ouji2); }
        if(type.equals("背が高い")){ result = OtokoChk.height(Ouji1, Ouji2, Ouji3); }

        System.out.println(result + "大好き♪");

    }
}

ググってみるとやっぱチェホンマンめちゃくちゃデカいな。。。(´∀`)
んで、今回はこれの発展系で、ちゃんとDDDっぽくレイヤー分けして、値オブジェクトやらインターフェースやらをきちんと書いて実装してみました。

Ⅰ:パッケージ構成

⇒お気づきかもしれませんが、今回「ビジネス層」に「アダプター」は1つもございませ~ん♪
これについてはのちのちご説明いたします。

Ⅱ:ソースコード

①ドメイン層

①-a)ドメイン製品「王子様」

package オトコチェック.domain.model.item.otoko;

import オトコチェック.domain.model.item.message.回答;
import オトコチェック.domain.model.item.otoko.parts.*;

public class 王子様 {

    private お名前 myお名前;
    private 身体 my身体;

    public 王子様(String お名前, int 身長, int 体重, int 年齢){
        this.myお名前 = new お名前(お名前);
        this.my身体 = new 身体(身長,体重,年齢);
    }

    public Boolean is非該当(){
        if(myお名前.is非該当()){ return true; }
        if(my身体.is非該当()){ return true; }
        else{ return false; }
    }

    public 回答 非該当箇所(){
        String my非該当箇所 = "";

        if(myお名前.is非該当()){
            my非該当箇所 = myお名前.getClass().getSimpleName() + ":" + myお名前.value() ;
        }

        if(my身体.is非該当()){
            my非該当箇所 = my身体.getClass().getSimpleName() + ":" + my身体.非該当箇所() ;
        }

        if( my非該当箇所.isEmpty() ){ my非該当箇所 = "誤った設定は1つもございません。"; }

        return new 回答( my非該当箇所 );
    }

    public String お名前(){
        return myお名前.value();
    }

    public Integer 身長(){
        return my身体.身長();
    }

    public Integer 体重(){
        return my身体.体重();
    }

    public Integer 年齢(){
        return my身体.年齢();
    }
}

①-a)-1.「王子様」のドメイン部品「お名前」※以下他類似部品の掲載割愛

package オトコチェック.domain.model.item.otoko.parts;

import オトコチェック.domain.tool.非該当判断;

public class お名前 {
    private String お名前 = "(名無しの権兵衛)";
    private static final int 許容最大文字数 = 30;
    private static final int 許容最小文字数 = 0;

    public お名前(String お名前){
        this.お名前 = お名前;
    }

    public Boolean is非該当(){
        非該当判断 my非該当判断 = new 非該当判断(許容最大文字数, 許容最小文字数, お名前.length());
        return my非該当判断.is非該当();
    }

    public final String value(){
        return this.お名前;
    }
}

①-a)-2.「王子様」のドメイン複合部品「身体」

package オトコチェック.domain.model.item.otoko.parts;

import オトコチェック.domain.model.item.message.回答;

public class 身体 {
    private 身長 my身長;
    private 体重 my体重;
    private 年齢 my年齢;

    public 身体(int 身長, int 体重, int 年齢){
        this.my身長 = new 身長(身長);
        this.my体重 = new 体重(体重);
        this.my年齢 = new 年齢(年齢);
    }

    public Boolean is非該当(){
        if(my身長.is非該当()){ return true; }
        if(my体重.is非該当()){ return true; }
        if(my年齢.is非該当()){ return true; }
        else{ return false; }
    }

    public 回答 非該当箇所(){
        String my非該当箇所 = "";

        if(my身長.is非該当()){
            my非該当箇所 = my身長.getClass().getSimpleName() + ":" + my身長.value() ;
        }

        if(my体重.is非該当()){
            my非該当箇所 = my体重.getClass().getSimpleName() + ":" + my体重.value() ;
        }

        if(my年齢.is非該当()){
            my非該当箇所 = my年齢.getClass().getSimpleName() + ":" + my年齢.value() ;
        }

        if( my非該当箇所.isEmpty() ){ my非該当箇所 = "誤った設定は1つもございません。"; }

        return new 回答( my非該当箇所 );
    }

    public Integer 身長(){
        return my身長.value();
    }

    public Integer 体重(){
        return my体重.value();
    }

    public Integer 年齢(){
        return my年齢.value();
    }
}

①-b)ドメイン製品「回答」

package オトコチェック.domain.model.item.message;

import オトコチェック.domain.tool.非該当判断;

public record 回答( String 回答 ) {
    private static final int 許容最大文字数 = 100;
    private static final int 許容最小文字数 = 1;

    public Boolean is非該当(){
        非該当判断 my非該当判断 = new 非該当判断(許容最大文字数, 許容最小文字数, 回答.length());
        return my非該当判断.is非該当();
    }
}

⇒はい、今回この製品だけ「レコードクラス」にしてみました。
理由は、
これ作ってる最中に突然レコードについて思い出して、
書いてみたくなったからただそれだけです。

あ、名前「回答メッセージ」にした方がより分かりやすかったかも。。Orz

確かにドメイン駆動設計って、適材適所でないとプレミアムかも。
見直すたんびに新たなアイデアが思い浮かんできて、
リファクタリングが止まりませんなぁ。。。

①-AB)ドメイン層内で横断的に使用する道具「非該当判断」

package オトコチェック.domain.tool;

public class 非該当判断 {
    private Integer _max;
    private Integer _minimum;
    private Integer _value;

    public 非該当判断(Integer _max, Integer _minimum, Integer _value){
        this._max = _max;
        this._minimum = _minimum;
        this._value = _value;
    }

    public Boolean is非該当(){

        if( this._value >= _max)    { return true; }
        if( this._value < _minimum) { return true; }
        else { return false; }

    }
}

⇒なんというか、カガミ的には、こういった横断的な部品は、「サブシステム内」かつ「同一レイヤー内」までで抑えておきたいような。。。
理由としては、

  1. 対象レンジが狭ければ、開発者がすばやく使える・追加開発できる

  2. やたら範囲を広げると、のちのち「共通化のワナ」にハマる可能性がある。
    ⇒これは、「ミノ駆動@MinoDriven」さんの下記動画から学びました。。。
    参考)

https://twitter.com/minodriven/status/1127539251761909760

※ミノ駆動さん、無断転載ごめんなさい。。。

①-c)ドメイン製品「オトコ二人比較区分」

package オトコチェック.domain.model.compare.otoko;

import オトコチェック.domain.model.compare.otoko.parts.HeightComparator;
import オトコチェック.domain.model.compare.otoko.parts.WeightComparator;
import オトコチェック.domain.model.item.message.回答;
import オトコチェック.domain.model.item.otoko.王子様;

public enum オトコ二人比較区分 {
     身長( new HeightComparator())
    ,体重( new WeightComparator());

     private ComparatorStrategy myComparatorStrategy;

     オトコ二人比較区分(ComparatorStrategy myComparatorStrategy){
         this.myComparatorStrategy = myComparatorStrategy;
     }

     public 回答 compare(王子様 o1, 王子様 o2 ){
         return myComparatorStrategy.compare( o1, o2 );
     }
}

⇒なんか、サンプルコードがこんななのに対して慙愧の念が堪えないのですが、
けど僕らだって普段ヤンジャンとかでスリーサイズがどーとかなぁんも感ぜずに立ち読みしてますもんねぇ。。。←てか買えよ!!

①-c)-1.「オトコ二人比較区分」のドメイン部品「HeightComparator」

package オトコチェック.domain.model.compare.otoko.parts;

import オトコチェック.domain.model.compare.otoko.ComparatorStrategy;
import オトコチェック.domain.model.item.message.回答;
import オトコチェック.domain.model.item.otoko.王子様;

//身長比較クラス
public class HeightComparator implements ComparatorStrategy {
    public 回答 compare(王子様 o1, 王子様 o2) {
        //背が高い人が好き♪
        if (o1.身長() > o2.身長()) { return new 回答(o1.お名前()); }
        if (o1.身長() < o2.身長()) { return new 回答(o2.お名前()); }
        else { return new 回答("両方とも"); }
    }
}

①-c)-2.「オトコ二人比較区分」のドメイン部品「WeightComparator」

package オトコチェック.domain.model.compare.otoko.parts;

import オトコチェック.domain.model.compare.otoko.ComparatorStrategy;
import オトコチェック.domain.model.item.message.回答;
import オトコチェック.domain.model.item.otoko.王子様;

//体重比較クラス
public class WeightComparator implements ComparatorStrategy {
    public 回答 compare(王子様 o1 , 王子様 o2){
        //痩せてる人が好き♪
        if (o1.体重() > o2.体重()) { return new 回答(o2.お名前()); }
        if (o1.体重() < o2.体重()) { return new 回答(o1.お名前()); }
        else { return new 回答("両方とも"); }
    }
}

①-d)ドメインインターフェース「ComparatorStrategy」

package オトコチェック.domain.model.compare.otoko;

import オトコチェック.domain.model.item.otoko.王子様;
import オトコチェック.domain.model.item.message.回答;

//比較インターフェース
public interface ComparatorStrategy {
    回答 compare(王子様 o1 , 王子様 o2);
}

①-e)ドメイン製品「オトコ三人比較区分」

package オトコチェック.domain.model.compare.otoko;

import オトコチェック.domain.model.compare.otoko.parts.HeightThreeComparator;
import オトコチェック.domain.model.item.message.回答;
import オトコチェック.domain.model.item.otoko.王子様;

public enum オトコ三人比較区分 {
     身長( new HeightThreeComparator());

     private ThreeComparatorStrategy myThreeComparatorStrategy;

     オトコ三人比較区分(ThreeComparatorStrategy myThreeComparatorStrategy){
         this.myThreeComparatorStrategy = myThreeComparatorStrategy;
     }

     public 回答 threeCompare( 王子様 o1, 王子様 o2, 王子様 o3 ){
         return myThreeComparatorStrategy.threeCompare( o1, o2, o3 );
     }
}

①-e)-1.「オトコ三人比較区分」のドメイン部品「HeightThreeComparator」

package オトコチェック.domain.model.compare.otoko.parts;

import オトコチェック.domain.model.item.message.回答;
import オトコチェック.domain.model.item.otoko.王子様;
import オトコチェック.domain.model.compare.otoko.ThreeComparatorStrategy;

public class HeightThreeComparator implements ThreeComparatorStrategy {
    public 回答 threeCompare(王子様 o1, 王子様 o2, 王子様 o3) {
        //背が高い人が好き♪
        return new 回答(
                        compareRecursion(compareRecursion(o1, o2), o3)
                        .お名前()
                        );
    }

    private 王子様 compareRecursion(王子様 o1, 王子様 o2) {
        //背が高い人が好き♪
        if (o1.身長() > o2.身長()) { return o1; }
        if (o1.身長() < o2.身長()) { return o2; }
        else {
            String name = o1.お名前() + "♪" + o2.お名前();
            int height = o1.身長();
            int weight = o2.体重();
            int age = o2.年齢();
            return new 王子様(name, height, weight, age);
        }
    }
}

⇒あ、
「Recursion」とは「リクルージョン」と読みまして、
「再帰」を意味しております。。。

かがみも英語超苦手なんですけど(^-^;;

①-f)ドメインインターフェース「ThreeComparatorStrategy」

package オトコチェック.domain.model.compare.otoko;

import オトコチェック.domain.model.item.otoko.王子様;
import オトコチェック.domain.model.item.message.回答;

//三人比較インターフェース
public interface ThreeComparatorStrategy {
    回答 threeCompare(王子様 o1 , 王子様 o2 , 王子様 o3);
}

はい、「ドメイン層」についてはここまで!
今回長いな~💧

②ビジネス層

②-a)オトコ比較サービス

package オトコチェック.business.service;

import オトコチェック.domain.model.compare.otoko.オトコ二人比較区分;
import オトコチェック.domain.model.compare.otoko.オトコ三人比較区分;
import オトコチェック.domain.model.item.otoko.王子様;
import オトコチェック.domain.model.item.message.回答;

//比較サービス
public class オトコ比較サービス {

    private final int my人数;
    private final String my比較内容;

    public オトコ比較サービス( int 人数, String 比較内容 ){
        全人数比較可能判定サービス my比較可能判定 = new 全人数比較可能判定サービス(人数,比較内容);
        if(my比較可能判定.is全オトコ比較区分非該当()){ System.out.println("その値はオトコ比較サービスには存在しません"); }

        this.my人数 = 人数; this.my比較内容 = 比較内容;
    }

    public final 回答 二人比較(王子様 o1, 王子様 o2){
        二人比較可能判定サービス my比較可能判定 = new 二人比較可能判定サービス(my人数,my比較内容);
        if(my比較可能判定.is非該当()){ return new 回答(my比較可能判定.非該当箇所().回答()); }

        return オトコ二人比較区分.valueOf(my比較内容).compare(o1, o2);
    }

    public final 回答 三人比較(王子様 o1, 王子様 o2, 王子様 o3){
        三人比較可能判定サービス my比較可能判定 = new 三人比較可能判定サービス(my人数,my比較内容);
        if(my比較可能判定.is非該当()){ return new 回答(my比較可能判定.非該当箇所().回答()); }

        return オトコ三人比較区分.valueOf(my比較内容).threeCompare(o1,o2,o3);
    }
}

⇒上記のソースコードですが、元ネタ「オブジェクト指向に対する批判記事を読んで、...(以下略)」との違いにお気づきでしょうか!?
「急場をしのぐ」場合、確かに元ネタのような実装が妥当な場合もございますでしょうが、
その場合、現時点の仕様でも「身長 体重 年齢」と「パラメーターが2つ、3つ」で、
最大6つもの似たようなメソッドが必要になってしまいます。
これはこれで適宜必要に応じて「サービスの中身のドメイン部品を、しれっと置き換えできる」ので必要要件を満たしているとも言えなくもないのですが、今回はせっかくなのでサービス層はインターフェースで可変に対応できるように致しました。

ってか、これこそ本来のオブジェクト指向の力ですもんね。
だって、「身長 体重 年齢 収入 職業 ルックス 性格 低燃費の度合い...」とまぁ、
そのままだとその気になれば指数関数的にメソッド数が増えそうですもんね(´∀`;)

そしてようやくこのパートの冒頭での回答になりますが、
ビジネス層で、コレクションなど配列のような要素を伴う複合部品を生成する必要がある場合以外は、おそらくアダプター部品を作らない方がシンプルになりより良い実装となるかと思います。

これは、「第一弾:勤務状況お問合せアプリケーション」で味を占めたカガミが、「第二弾:オトコチェックアプリケーション」でもアダプターを実装しようと四苦八苦してリファクタリングしまくった結果の持論となります。。。

ハイ、なのでパッケージ構成にその名残で空の「アダプター」パッケージが残ってございます。。。

②-b)二人比較可能判定サービス

package オトコチェック.business.service;

import オトコチェック.domain.model.compare.otoko.オトコ二人比較区分;
import オトコチェック.domain.model.item.message.回答;

public class 二人比較可能判定サービス {
    private final int my人数;
    private final String my比較内容;

    public 二人比較可能判定サービス( int 人数, String 比較内容 ){
        this.my人数 = 人数;
        this.my比較内容 = 比較内容;
    }

    public Boolean is非該当() {
        try {
              オトコ二人比較区分.valueOf(my比較内容);

              if (my人数 != 2) { return true; }

              return false;
             }
        catch (IllegalArgumentException e) {
              return true;
             }
    }

    public 回答 非該当箇所(){

        if(my人数 < 2){ return new 回答( "人数が足りません。設定した値:" + my人数 ); }
        if(my人数 > 2){ return new 回答( "人数が多すぎです。設定した値:" + my人数 ); }

        if(my人数 == 2 && is非該当()){ return new 回答( "比較内容が誤っております。設定した値:" + my比較内容 ); }

        else{ return new 回答("非該当箇所は見つかりませんでした。"); }
    }
}

②-c)三人比較可能判定サービス

package オトコチェック.business.service;

import オトコチェック.domain.model.compare.otoko.オトコ三人比較区分;
import オトコチェック.domain.model.item.message.回答;

public class 三人比較可能判定サービス {
    private final int my人数;
    private final String my比較内容;

    public 三人比較可能判定サービス( int 人数, String 比較内容 ){
        this.my人数 = 人数;
        this.my比較内容 = 比較内容;
    }

    public Boolean is非該当() {
        try {
              オトコ三人比較区分.valueOf(my比較内容);

              if (my人数 != 3) { return true; }

              return false;
            }
        catch (IllegalArgumentException e) {
              return true;
            }
    }

    public 回答 非該当箇所(){

        if(my人数 < 3){ return new 回答( "人数が足りません。設定した値:" + my人数 ); }
        if(my人数 > 3){ return new 回答( "人数が多すぎです。設定した値:" + my人数 ); }

        if(my人数 == 3 && is非該当()){ return new 回答( "比較内容が誤っております。設定した値:" + my比較内容 ); }

        else{ return new 回答("非該当箇所は見つかりませんでした。"); }
    }
}

②-d)全人数比較可能判定サービス

package オトコチェック.business.service;

import オトコチェック.domain.model.item.message.回答;

public class 全人数比較可能判定サービス {
    private final int my人数;
    private final String my比較内容;

    public 全人数比較可能判定サービス( int 人数, String 比較内容 ){
        this.my人数 = 人数;
        this.my比較内容 = 比較内容;
    }

    public Boolean isオトコ二人比較区分非該当() {
        return new 二人比較可能判定サービス(my人数,my比較内容).is非該当();
    }

    public Boolean isオトコ三人比較区分非該当() {
        return new 三人比較可能判定サービス(my人数,my比較内容).is非該当();
    }

    public Boolean is全オトコ比較区分非該当(){
        return isオトコ二人比較区分非該当() && isオトコ三人比較区分非該当();
    }

    public 回答 二人比較非該当箇所(){
        return new 二人比較可能判定サービス(my人数,my比較内容).非該当箇所();
    }

    public 回答 三人比較非該当箇所(){
        return new 三人比較可能判定サービス(my人数,my比較内容).非該当箇所();
    }

    public 回答 全オトコ比較非該当箇所(){
        final String newLine = System.lineSeparator();
        return new 回答(
                  二人比較非該当箇所().回答()
                + newLine
                + 三人比較非該当箇所().回答()
                );
    }
}

⇒なんか必要最低限に絞らないとのちのち神クラス化しそうだなオイ(´∀`)!
「二人ほにゃらら」や「三人ほにゃらら」でも賄ってる部分はサクった方が良いかもな。。。

③ユーザー層(※通常アプリケーション層!?)

③-a)「痩せている殿方」を2人同時に比較したいHappyちゃん(※現在6歳)

package オトコチェック.user.利用者;

import オトコチェック.business.service.オトコ比較サービス;
import オトコチェック.domain.model.item.otoko.王子様;

public class Happy {
    public static void main(String args[]) {
        王子様 候補1 = new 王子様("じぃじ",180,68,70);
        王子様 候補2 = new 王子様("ぱぁぱ",172,71,41);
        王子様 候補3 = new 王子様("チェホンマン",218,160,40);

        オトコ比較サービス my体重比較 = new オトコ比較サービス(2,"体重");
        オトコ比較サービス my身長比較 = new オトコ比較サービス(3,"身長");

        String type = args[0];

        //If文スッキリ(⋈◍>◡<◍)。✧♡
        if (type.equals("痩せてる")){ System.out.println( my体重比較.二人比較( 候補1, 候補2 ).回答() + "大好き♪" ); }
        if (type.equals("背が高い")){ System.out.println( my身長比較.三人比較( 候補1, 候補2, 候補3 ).回答() + "大好き♪" ); }
    }
}

<出力イメージ>

Y:>オトコチェック.user.利用者.Happy 痩せてる
じぃじ大好き♪

プロセスは終了コード 0 で終了しました

③-b)「背が高い殿方」を3人同時に比較したいDelightちゃん(※現在2歳)

package オトコチェック.user.利用者;

import オトコチェック.business.service.オトコ比較サービス;
import オトコチェック.domain.model.item.otoko.王子様;

public class Delight {
    public static void main(String args[]) {
//        王子様 候補1 = new 王子様("ぱぁぱ",172,71,41);
        王子様 候補2 = new 王子様("ぱぁぱ",172,71,41);
//        王子様 候補3 = new 王子様("ぱぁぱ",172,71,41);

        オトコ比較サービス my体重比較 = new オトコ比較サービス(2,"体重");
        オトコ比較サービス my身長比較 = new オトコ比較サービス(3,"身長");

        String type = args[0];

        //If文スッキリ(⋈◍>◡<◍)。✧♡
        if (type.equals("痩せてる")){ System.out.println( my体重比較.二人比較( 候補2, 候補2 ).回答() + "大好き♪" ); }
        if (type.equals("背が高い")){ System.out.println( my身長比較.三人比較( 候補2, 候補2, 候補2 ).回答() + "大好き♪" ); }
    }
}

<出力イメージ>

Y:>オトコチェック.user.利用者.Delight 背が高い
ぱぁぱ♪ぱぁぱ♪ぱぁぱ大好き♪

プロセスは終了コード 0 で終了しました

⇒。。。はい、この出力イメージはかんぺきかがみの夢であり、妄想です(^-^)。
ええ、もちろん現実のカガミ家でもあっしは娘たちにモテモテですょぉ。。。

まとめ:DDDに「ビジネス層」はプレミアムか!?

はい、2つのサンプルコードの解説も仕上がったところで、今回の記事のまとめというか、
諸注意に入りたいと思いま~す✨

①レイヤー分けは諸刃の剣でもある。

オーバーエンジニアリングの多くが、柔軟性の名の下に正当化されてきた。しかし、抽象化や間接化のためのレイヤを作りすぎてしまうと、邪魔になることの方が多い。扱う人に対して本当に力を与えるソフトウェアがどのような設計になっているか、調べてみるといい。通常はシンプルなものであることがわかるだろう。

Eric Evans. エリック・エヴァンスのドメイン駆動設計 (p.248). 翔泳社. Kindle 版

⇒このことに関しては、オブジェクト指向の重鎮、マーティン・ファウラー氏も「エンタープライズアプリケーションアーキテクチャー(通称PoEAA。以下この通称を使用)」という本で似たようなことを述べられております。

以下PoEAAに掲載の「レイヤーアーキテクチャー」のメリデメを引用いたします。

a)メリット

■他のレイヤをよく知らなくても、1つのレイヤを全体として考えることができる。たとえば、イーサネットの動作方法方法を知らなくても、TCPの上にあるFTPサービスを構築する方法を理解できる。

■同じ基本サービスの代替実装でレイヤを置き換えることができる。たとえば、FTPサービスは、イーサネット、PPP、またはケーブル会社が変わっても、その実行に支障はない。

■レイヤ間の依存を最小限にできる。たとえば、ケーブル会社がIPを動作させる物理的な伝送システムを変更しても、FTPサービスを修正する必要はない。

■レイヤは標準化に適している。TCPとIPは、レイヤの動作方法を定義しているので標準である。

■レイヤを構築すれば、多くの高水準のサービスがそのレイヤを使用できる。このため、TCP/IPはFTP、telnet、SSH、HTTPによって使用される。TCP/IPがなければ、高水準のプロトコルは独自の低水準のプロトコルを作成しなければいけない。

b)デメリット

■レイヤは一部の物事をカプセル化してしまう。その結果、連鎖的な変更が起こる場合がある。レイヤ化されたエンタープライズアプリケーションにおける典型的な例は、UI(ユーザインタフェース)上で表示すべきフィールドの追加である。このフィールドはデータベースに存在する必要があり、したがってUIからデータベースまでのすべてのレイヤにそのフィールドを追加しなければいけない。

■レイヤを追加するとパフォーマンスを損ねる場合がある。一般的には、レイヤごとに表示を変換する必要がある。しかし、基盤となる機能をカプセル化すると、そのようなパフォーマンスの損失を補う以上の効率性をもたらすことが多い。トランザクションを制御するレイヤは最適化することができ、その結果すべての機能を高速にすることができる。

マーチン・ファウラー エンタープライズアプリケーションアーキテクチャパターン (p.17-18). 翔泳社. Kindle 版.

※引用箇所の一部の改行は読みやすいようカガミによるもの

う~ん、特に「連鎖的な変更が起こる場合がある。」が怖いですね(^_^;;
特に原因がはっきりとはわからないようなトラブルが発生した場合。。。(;´∀`)

また長所として「レイヤ間の依存を最小限にできる」も大変納得できるところです。

例えば、サンプルコードの第一弾の「勤務状況」のEnumクラスがその他のプログラムでも使用されている場合、「勤務状況判定サービス」を書き変えるだけで、「テレワーク実施/テレワーク休止(または廃止)」が、「問合せサービス」のみの影響範囲内で自由に切り替えることが可能となります。。。

②エヴァンス氏はおそらく「ドメイン層内でのサービス」を推奨している。

ドメインを分析し、ドメインモデルを構成する主要なオブジェクトを定義しようとすると、ドメインのある側面は簡単にオブジェクトに対応づけられないことがわかります。
一般的にオブジェクトは属性を持ちます。
また、オブジェクト自身によって管理される内部的な状態を持ち、振る舞いを外部に公開すると考えられています。
ユビキタス言語を構築するとき、ユビキタス言語はドメインの主要な概念を取り込みます。
ユビキタス言語に含まれる名詞をオブジェクトに対応づけるのは簡単です。
動詞は適切な名詞と関連し、オブジェクトの振る舞いの一部になります。
しかし、ドメインにはどのオブジェクトにも属さないような振る舞い、つまり動詞がいくつかあります。
これらの振る舞いはドメインにとって重要です。
無視したり、よく考えずに何らかのエンティティやバリューオブジェクトに含めることはできません。
しかし、特定のオブジェクトにこの振る舞いを加えれば、そのオブジェクトは台無しになってしまうでしょう。
本来はどのオブジェクトにも属さない振る舞いを加えることになるからです。
にもかかわらず、オブジェクト指向言語を使うと、ドメインに属する振る舞いでも、何らかのオブジェクトに属さなければなりません。
どのオブジェクトにも属していない振る舞いは定義できません。
振る舞いは必ず何らかのオブジェクトに属しています。
このような振る舞いの多くは、複数のオブジェクトを横断して作用します。
ひょっとしたら複数のクラスに対しても作用するかもしれません。

例えば、ある銀行口座から他の口座へと送金する場合、送金機能は送金元の口座と送金先の口座のどちらにあるべきでしょうか。どちらにあっても間違っているように思えます。

ドメインにこのような振る舞いが見つかった場合、最もよい方法はこの振る舞いをサービスとして定義することです。

Abel Avram, Floyed Marinescu, 徳武 聡 Domain-Driven Design(ドメイン駆動設計) Quickly 日本語版 (p.36-37). InfoQ. Enterprise Software Development Series.

※引用太字箇所および一部の改行はカガミによるもの

はい、長い引用でごめんなさい(^ー^;

「Domain-Driven Design Quickly 日本語版」の、「サービス」に対する説明がかがみの中ではとても秀逸に思われ、ためにどーしても引用したく、けど簡潔にもしたくてあれこれ考えましたが上記長文引用となってしまいました。。。Orz

この場合、先の本家「エリック・エヴァンスのドメイン駆動設計 (p.248)」も加味して考えると、ひょっとしたらエヴァンス氏ご本人はサービスクラスの活用はあるかもしれないけど、レイヤーにまで昇格する必要は無いよ、とのお考えであったのかもしれません。。。

しかし、昨今の「ニューノーマル」な時代の波を踏まえると、まさかのちゃぶ台返しを想定して、いっそビジネス層といったようなレイヤーを新たに設けてしまっても良いのではないのかとも、そうかがみは感じてしまいます。

なんせ、本記事第一弾なんかは、まさに「コロナウィルス」なんていう想定外のアクターが突然現れたからこその「ぶっこみ区分」であるのかなぁと思うのです。。

③違和感を感じるかも知れないが、上記2つのプログラムも「がんばれば」おそらくサービスに頼らずともドメイン層内で実装可能。

あ、ちなみに話を変えて「区分値の意味論的な混在では無い」第二弾の実装は、頑張ればドメイン層内ですべて丸く収めることもとりあえずは可能かと思われます。。。

参考に、ががみ案のサンプルコードを掲載します。。。

補足)ドメイン製品「比較区分」

package オトコチェック.domain.model.compare.otoko;

import オトコチェック.domain.model.compare.otoko.parts.HeightComparator;
import オトコチェック.domain.model.compare.otoko.parts.HeightThreeComparator;
import オトコチェック.domain.model.compare.otoko.parts.WeightComparator;
import オトコチェック.domain.model.item.otoko.王子様;

public enum 比較区分 {
     三人の身長( new HeightThreeComparator())
    ,二人の身長( new HeightComparator())
    ,二人の体重( new WeightComparator());

     private ThreeComparatorStrategy myThreeComparatorStrategy;
     private ComparatorStrategy myComparatorStrategy;

     private 比較区分(ThreeComparatorStrategy myThreeComparatorStrategy){
         this.myThreeComparatorStrategy = myThreeComparatorStrategy;
     }

     private 比較区分(ComparatorStrategy myComparatorStrategy){
         this.myComparatorStrategy = myComparatorStrategy;
     }

     public String threeCompare( 王子様 o1, 王子様 o2, 王子様 o3 ){
         if(myThreeComparatorStrategy == null){ return  "設定した人数が異なっております"; }
         return myThreeComparatorStrategy.threeCompare( o1, o2, o3 );
     }

    public String Compare( 王子様 o1, 王子様 o2 ){
        if(myComparatorStrategy == null){ return  "設定した人数が異なっております"; }
         return myComparatorStrategy.compare( o1, o2 );
    }
}

<使用例>

package オトコチェック.user.利用者;

import オトコチェック.domain.model.item.otoko.王子様;
import オトコチェック.domain.model.compare.otoko.比較区分;

public class Beauty {
    public static void main(String args[]) {
        王子様 Ouji1 = new 王子様("じぃじ",180,68,70);
        王子様 Ouji2 = new 王子様("ぱぁぱ",172,71,41);
        王子様 Ouji3 = new 王子様("チェホンマン",218,160,40);

        比較区分 my二人体重比較 = 比較区分.valueOf(比較区分.二人の体重.name());
        比較区分 my三人身長比較 = 比較区分.valueOf(比較区分.三人の身長.name());

        String type = args[0];
        String winner = "";
        //If文スッキリ(⋈◍>◡<◍)。✧♡
        if(type.equals("痩せてる")){ winner = my二人体重比較.Compare( Ouji1, Ouji2 ); }
        if(type.equals("背が高い")){ winner = my三人身長比較.threeCompare( Ouji1, Ouji2, Ouji3 ); }

        System.out.println(winner + "大好き♪");
    }
}

⇒実装まではしておりませんが、これ以外にかがみが考えられるのは、「二人比較区分(enum)」と「三人比較区分(enum)」それぞれに実装可能なインターフェースを設けることです。
ただし、その場合そもそもメソッドのパラメーターが異なっているので、同時に比較する人数が増えるたびにそれぞれに本来不要なメソッドを追加していかなければなりません。

ビジネス層で実装するにしろ、ドメイン層で実装するにしろ、インターフェースを設けるのではなくアダプターおよびサービスを活用するメリットはここにあります。

これらのクラスの場合、メソッド名やメソッドパラメーターに対するタイプミスなどの自動的な型チェックは叶いませんが、
インターフェースのような上位概念では無いので、必要なメソッドなり区分値なりを持っている実装クラスそのものを、その必要な箇所だけに限定して着脱可能です。

もちろん、オブジェクト指向の実践者の方々が、コンストラクタのオーバーロードを実装することを好むのかどうかについては、気になるところですが。

そして最後の最後に、
「オブジェクト指向であること」に対するこだわりを捨ててしまえば、第一弾の実装さえも1つの区分値として収めてしまうことは可能であるかと思います。
ただし、その場合例えば、そのシステムが進化や発展をしていく際にめんどうな対応が必要となるかもしれません。

<実装イメージ>

package 社員勤務表.domain.model;

public enum 勤務状況サブステータス区分 {
    稼働,
    非稼働,
    社外,
    社内;

    public final String 補足説明(){
        if(this.name()==勤務状況サブステータス区分.稼働.name())  { return "一日のうちなんらかの仕事をした状態を指します。テレワークも含みます";}
        if(this.name()==勤務状況サブステータス区分.非稼働.name()){ return "終日仕事をしていない状態を指します。土日祝日も含みます。";}
        if(this.name() == 勤務状況サブステータス区分.社外.name()){ return "テレワークに該当する働き方を行い、一日のうちなんらかの仕事をした状態を指します。";}
        if(this.name() == 勤務状況サブステータス区分.社内.name()){ return "一日のうちなんらかの仕事をした状態を指します。ただし、テレワークは含みません。";}
        else{ return "その値は勤務状況サブステータス区分にはございません。";}
    }
}

⇒神!?

補足。いわゆる主に海外勢の超有名アーキテクトの方々が、すでにより良いアーキテクチャーを模索し、提案をしている。

あ、すっかり忘れてた(;^_^A
いわゆるDDDに留まらず!?ひろくアプリケーションアーキテクチャーについてのモダンな考え方がすでに世にたくさん広まっているようです。。。Orz

かがみも概要レベルをざっくり拝見しましが、やはり似たような発想・アイデアですかね。。。

私たちが考えたのとピッタリ一致してるわけでは無いんですけど(´∀`;)
まぁ自分たちが知らずに考えたアイデアで、ピタリ賞狙うなんて恐れ多いですよね。。。

参考)

https://qiita.com/little_hand_s/items/ebb4284afeea0e8cc752

https://little-hands.hatenablog.com/entry/2017/10/11/075634

https://little-hands.hatenablog.com/entry/2018/12/10/ddd-architecture

結びに

今回の記事の執筆のきっかけは、ちょうどほぼあと2ヶ月でドメイン駆動設計という存在を学び始めてから1年が経とうとしていた矢先に起こった出来事でした。

そして、ふり返れば
その間に

増田亨さんや
ゆーせんさん、
ミノ駆動さん、
イロフさん、
なりさんや
バトルプログラマーのTTPさん、
そしてもちろんこー1さん。

その他いろいろな方達からDDDについて沢山のことを教わりました。

さらにさらにインフラとなると

カルロスわらふじさん
マルチレイヤーさん。

マルチレイヤーさんにはRDBあるいはDBについて、
カルロスわらふじさんはもうDDDだろうがクラウドだろうが何でもかんでもww
ご教示いただきました~♪

また、DDDなど技術的な話以外に、
主にTwitter上で

おばみんさんや
くま父さん、
ダンディさん

などの方々に、いろいろな出来事が身の回りに起こるたびに
精神崩壊を起こしそうな(あるいは「起こしてしまった」)
かがみは大変助けられました。

特に、ダンディさんの「本日も人生楽しくでお願いします✌」には
とある精神崩壊寸前の時に本当に助けられました。

改めて、
皆々様に感謝です。

DDDに対して、僕はある「節目」を感じております。

ざっくりというとやはり自分には大勢の人たちと交わる活動と言うのものは
おそらく無理であるということ。

さらにもう一点追加すると
それでも、僕如きが積極的に活動しなくとも、
若い人たちを中心に

「ドメインを中心に据えたアプリケーション」

の実践がけっこう広まっているのだなぁと感じられたということ。

ネット上のDDDに関する記事を見ると、
かがみがOOのコードについて不慣れなためか
ワケワカランみたいな
実装コードも多く、
なんとなく「ホントにダイジョブなのか?DDD。」
などと感じていたときもあったけど、
この前のWebセミナーとか参加したとき

あ、きっと普通に広まるんだろうな!

と感じました(●^^●)

まぁあとは、
それらの私自身の問題や世間の技術動向などの状況などを鑑みて、
これからの自分のことについて、
自分自身が、自分自身で、

「何をしたいか」「どうあるべきか」

をよくよく考えなきゃならんですかね(^^)♪

まぁもともとかがみが書いている
「読むだけでわかる!?」シリーズは、

実は当初からオブジェクト指向だけの執筆に留まるつもりはなく、

「世間的にあまり存在しないけど、<あったらいいな>と思えるようなIT技術の
解説本を書いて、あわよくばそれがバズったりして、人に揉まれまくりのSIビジネスから離脱して物書きのエンジニアになる」

という、<ニッチ分野のスキマ産業>を狙った淡い夢であったわけなんです。。。

ので、
とりあえず、次考えてるのは、

DDDやOOPの実践でのサポート的なモノ、
例えば
「RDRA2.0作成用のツール」
とか

あるいは
「不具合なく実際に動かすことができるサンプルコードを使った<DDD版「読むだけでわかる!?」シリーズ>」
とか

あるいは当初の計画通り
「RDBの各種インデックスの仕組みについての「読むだけでわかる!?」」
「PC超初心者用のコンピューターの基礎の基礎が「読むだけでわかる!?」」
とか。

そんなあれこれを夢想しながら、
次の手を考え中です。

あ、最後の最後の最後に。
本記事に掲載したサンプルコードのGitHub上のURLです。

https://github.com/KagaminPower003/MySystemDesignLabo/tree/master/AD20211005_「社員勤務表」の選択肢

https://github.com/KagaminPower003/MySystemDesignLabo/tree/master/AD20211019_引数の数の異なる「オトコチェック」

はい、とりあえず今日は以上です。
それではまた~✨✨✨

※2021/11/10追記) Enum区分値に「インターフェース」を設けることで、「アダプター」が不要となるケースも見出せました。

自分の主張が二転三転して申し訳ないのですが(;^_^A
下記のコードでご納得下さるのであれば、「勤務状況サブステータス区分アダプター」は、「勤務状況サブステータス区分インターフェース」に置き換え可能でございます。。。

では上述で提案した「アダプター」を使用することの利点は何かといえば、
やはりドメインモデルを基に実装されたドメイン層のコードに、変化・変更がより頻繁に発生すると思われるビジネス層にあるインターフェースを記述しなければならないという事でしょうか。

これは、インターフェースはそれを必要とする「呼び出し側パッケージ」に配置することで、パッケージ間の疎結合を図る依存関係の逆転の原則によるものです。

package 社員勤務表.business.service;

public interface 勤務状況サブステータス区分 {
    String name();
    String 補足説明();
}
package 社員勤務表.domain.model;

import 社員勤務表.business.service.勤務状況サブステータス区分;

public enum 状態区分 implements 勤務状況サブステータス区分 {
    稼働,
    非稼働;

    public final String 補足説明(){
        if(this.name().equals(状態区分.稼働.name()))  { return "一日のうちなんらかの仕事をした状態を指します。テレワークも含みます";}
        if(this.name().equals(状態区分.非稼働.name())){ return "終日仕事をしていない状態を指します。土日祝日も含みます。";}
        else{ return "その値は状態区分にはございません。";}
    }
}
package 社員勤務表.domain.model;

import 社員勤務表.business.service.勤務状況サブステータス区分;

public enum 場所区分 implements 勤務状況サブステータス区分 {
    社外,
    社内;

    public final String 補足説明(){
        if(this.name().equals(場所区分.社外.name())){ return "テレワークに該当する働き方を行い、一日のうちなんらかの仕事をした状態を指します。";}
        if(this.name().equals(場所区分.社内.name())){ return "一日のうちなんらかの仕事をした状態を指します。ただし、テレワークは含みません。";}
        else{ return "その値は場所区分にはございません。";}
    }
}
package 社員勤務表.business.service;

import 社員勤務表.domain.model.状態区分;
import 社員勤務表.domain.model.場所区分;
import java.util.List;
import java.util.ArrayList;
import java.util.Collections;

public class 勤務状況サブステータス区分問合せサービス {

    private String my勤務状況 = "" ;
    private List<勤務状況サブステータス区分> my勤務状況サブステータス区分list
            = new ArrayList<>();

    public 勤務状況サブステータス区分問合せサービス(String my勤務状況){
        勤務状況判定サービス my勤務状況判定 = new 勤務状況判定サービス(my勤務状況);

        if(my勤務状況判定.is非該当()){ System.out.println("その値は勤務状況には存在しません"); }
        else{ this.my勤務状況 = my勤務状況; }
    }
    public List<勤務状況サブステータス区分> 勤務状況サブステータス区分List(){
        勤務状況判定サービス my勤務状況判定 = new 勤務状況判定サービス(my勤務状況);

        if(my勤務状況判定.isテレワーク()) { return テレワーク設定(); }
        if(my勤務状況判定.is出社())      { return 出社設定(); }
        if(my勤務状況判定.is非出社())     { return 非出社設定(); }
        else{ my勤務状況サブステータス区分list.clear(); }

        return  Collections.unmodifiableList(my勤務状況サブステータス区分list);
    }
    private List<勤務状況サブステータス区分> テレワーク設定(){
        my勤務状況サブステータス区分list.add(場所区分.社外);

        return Collections.unmodifiableList(my勤務状況サブステータス区分list);
    }
    private List<勤務状況サブステータス区分> 出社設定() {
        my勤務状況サブステータス区分list.add(場所区分.社内);

        return Collections.unmodifiableList(my勤務状況サブステータス区分list);
    }
    private List<勤務状況サブステータス区分> 非出社設定() {
        my勤務状況サブステータス区分list.add(状態区分.非稼働);

        return Collections.unmodifiableList(my勤務状況サブステータス区分list);
    }
}

<出力イメージ>

X:> 社員勤務表.user.domainExpert.Pure 出社
『勤務状況』が出社の場合、該当する『勤務区分』は
  ●フル出勤
  ●午前休
  ●午後休
です。
『勤務状況』が出社の場合、該当する『勤務状況サブステータス区分』は
  ●社内
です。
『勤務状況サブステータス区分』が社内の場合、
  ⇒一日のうちなんらかの仕事をした状態を指します。ただし、テレワークは含みません。
です。

<※変更後のパッケージ構成>

Discussion

ログインするとコメントできます