調べ物した結果

現役SEが仕事と直接関係ないことを調べた結果とか感想とか

読書感想文。WEB+DB PRESS 特集 モデリングから実装まで・・・を

やったこと

・掲題。
・特集3を読んでみて、感想など。
・僕はJavaのコードだと頭に入ってこないからC#に書き起こしながら読んでみた。
・DDDよくわからないオジサンなので読んでみた。
・詳しいことは本を読んだほうが良い。僕の解釈。

サンプルコード ドメイン貧血症をC#で実装してみる。

lombokはそういえば職場の同僚がいっていたような。簡単にできることもあるだろうけど、
簡単にSetterうめこめる、ある意味恐ろしものや。
Java特有のあれこれは見逃してる可能性が高いけど、ひとまずコーディング。

冒頭の貧血大発生のクラス。

    public class InterviewV1
    {
        public string interviewId { get; set; }
        public string screeningId { get; set; }
        public DateTime screeningDate;
        public int interviewNumber { get; set; }
        public ScreeningStepResult screeningStepResult;
        public long recruiterId;
    }

    public class ScreeningV1
    {
        public string screeningId { get; set; }
        public DateTime applyDate { get; set; }
        public ScreeningStatusV1 status { get; set; }
        public string applicantEmailAddress { get; set; }
    }

いちいちprivateで宣言してgettersetterつくるの面倒なのでまとめた。本誌の意図は変えてないと思う。
それでも変数名はかなり気を使っているように見えるので、このレベルならまだなんとかなるんじゃないかとも思う。
idはidだろうし、numberならそのままnumberってついてそう。
同じ意味のオブジェクトも変数名は統一されてるし。
これはこれできちんと意味を持って設計しているひとコードのように読み取れる。

ScreeningStatusV1 、ScreeningStepResult が何者かわからなくてエラーになっているが、いったん無視する。
アプリケーション部分も書いていく。


// デフォルトコンストラクタ で インスタンス 作成 ScreeningV 1 screening = new ScreeningV 1();
// ID は UUID を 使用 screening. setScreeningId( UUID. randomUUID(). toString());
// 面談 からの 場合 は ステータス「 未 応募」 で 登録 screening. setStatus( ScreeningStatus. NotApplied);

WEB+DB PRESS Vol.113 (Kindle の位置No.3628-3631). 株式会社技術評論社. Kindle 版.

このへんで、ScreeningStatusがEnumなんだろう。とあたりをつける。とりあえずEnumで定義しておく。

    public enum ScreeningStatusV1
    {
        NotApplied = 0,
    }

うーん。たぶん、boolで持っちゃってるケースのが多いと思うけどなぁ。隠しきれない設計への思いを感じる。

        public void apply(applicantEmailAddress)
        {
            if (isEmpty(applicantEmailAddress) || isInvalidFormatEmailAddress(applicantEmailAddress))
            {
                throw new ApplicationException("メールアドレスが正しくありません。");
            }
        }

こーゆーのよくある。よくあるんだよー。つらい。
同じような判定ロジックがそこら中に散らばっている。よく見る。非常によく見る。

そんなこんなでかけた。

   public class ScreeningApplicationServiceV1
    {
        private ScreeningDao screeningDao;
        private InterviewDao interviewDao;

        public void startFromPreInterview(string applicantEmailAddress)
        {
            if (isEmpty(applicantEmailAddress) || isInvalidFormatEmailAddress(applicantEmailAddress))
            {
                throw new ApplicationException("メールアドレスが正しくありません。");
            }

            ScreeningV1 screening = new ScreeningV1()
            {
                // https://www.sejuku.net/blog/61879
                screeningId = "UUID",
                status = ScreeningStatusV1.NotApplied,
                applyDate = null,
                applicantEmailAddress = applicantEmailAddress,
            };

            screeningDao.insert(screening);
        }

        private bool isEmpty(string value)
        {
            return string.IsNullOrEmpty(value);
        }
        private bool isInvalidFormatEmailAddress(string email)
        {
            if(email ==null)
            {
                return false;
            }

            string emailRegex = "CONST_EMAIL_REGEX";
            // たぶんこーゆーことのはず。
            // CONST_ EMAIL_ REGEX は 適切 な 正規表現 が 記述 さ れ て いる と する 
            // String emailRegex = CONST_ EMAIL_ REGEX; return !Pattern. compile( emailRegex) .matcher( email). matches();
            // WEB + DB PRESS Vol.113(Kindle の位置No.3637 - 3639).株式会社技術評論社.Kindle 版.
            return !Regex.IsMatch(email, emailRegex);
        }
        public void apply(string applicantEmailAddress)
        {
            if (isEmpty(applicantEmailAddress) || isInvalidFormatEmailAddress(applicantEmailAddress))
            {
                throw new ApplicationException("メールアドレスが正しくありません。");
            }

            ScreeningV1 screening = new ScreeningV1()
            {
                // https://www.sejuku.net/blog/61879
                screeningId = "UUID",
                status = ScreeningStatusV1.Interview,
                applyDate = System.DateTime.Now,
                applicantEmailAddress = applicantEmailAddress,
            };
            screeningDao.insert(applicantEmailAddress);
        }

        public void addNextInterview(string screeningId, DateTime? interviewDate)
        {
            ScreeningV1 screening = screeningDao.findScreeningById(screeningId);
            if (screening.status != ScreeningStatusV1.Interview)
            {
                throw new ApplicationException("不正な操作です");
            }

            List<InterviewV1> interviews = interviewDao.findByScreeningId(screeningId);
            InterviewV1 interview = new InterviewV1()
            {
                interviewId = "UUID",
                screeningId = screeningId,
                interviewNumber = interviews.Count(),
                screeningDate = interviewDate,
            };

            interviewDao.insert(interview);
        }
    }

書いている最中にかってに手が直そうとしてつらかったが。よくある形だと思う。
さぁ、問題点を読んでみよう。


よく ある 伝統 的 な コード です。 第 1 章 で 紹介 し た よう に、
・採用 選考 クラス、 面接 クラス が ドメイン 知識 を 持っ て い ない
・新規 登録、 面接 追加 時 の ドメイン 知識 が 複数 クラス に 散らばっ て いる

WEB+DB PRESS Vol.113 (Kindle の位置No.3661-3663). 株式会社技術評論社. Kindle 版.

ここでいっているのは、
InterviewV1とか
ScreeningV1のことだと思う。

そして伝統的なコードであることは激しく同意をする。が、割と真面目な人がつくってるとも思うけど。
全体的に設計をしようという意思は感じられるし、こんなのばっかりだったらまだいいのになぁ。と思ってしまった。

本誌の通りにリファクタリングしてみる。

さぁ、次は1段磨かれたクラスの実装
とりあえずScreeningをバージョンアップさせる

    public class ScreeningV2
    {
        private string screeningId { get; set; }
        private DateTime? applyDate { get; set; }
        private ScreeningStatusV1 status { get; set; }
        private string applicantEmailAddress { get; set; }
        private List<InterviewV2> interviews { get; set; }

        private ScreeningV2()
        {
        }

        public static ScreeningV2 startFromPreInterview(string applicantEmailAddress)
        {
            if (isEmpty(applicantEmailAddress) || isInvalidFormatEmailAddress(applicantEmailAddress))
            {
                throw new ApplicationException("メールアドレスが正しくありません。");
            }

            ScreeningV2 screening = new ScreeningV2()
            {
                // https://www.sejuku.net/blog/61879
                screeningId = "UUID",
                interviews = new List<InterviewV2>(),
                applicantEmailAddress = applicantEmailAddress,

                status = ScreeningStatusV1.NotApplied,
                applyDate = null,
            };

            return screening;
        }
        public static ScreeningV2 apply(string applicantEmailAddress)
        {
            if (isEmpty(applicantEmailAddress) || isInvalidFormatEmailAddress(applicantEmailAddress))
            {
                throw new ApplicationException("メールアドレスが正しくありません。");
            }

            ScreeningV2 screening = new ScreeningV2()
            {
                // https://www.sejuku.net/blog/61879
                screeningId = "UUID",
                interviews = new List<InterviewV2>(),
                applicantEmailAddress = applicantEmailAddress,

                status = ScreeningStatusV1.Interview,
                applyDate = System.DateTime.Now,
            };
            return screening;
        }

        public void addNextInterview(DateTime? interviewDate)
        {
            if (status != ScreeningStatusV1.Interview)
            {
                throw new ApplicationException("不正な操作です");
            }

            InterviewV2 interview = new InterviewV2()
            {
                interviewId = "UUID",
                screeningId = screeningId,
                interviewNumber = interviews.Count(),
                screeningDate = interviewDate,
            };
            interviews.Add(interview);
        }

        private static bool isEmpty(string value)
        {
            return string.IsNullOrEmpty(value);
        }
        private static bool isInvalidFormatEmailAddress(string email)
        {
            if (email == null)
            {
                return false;
            }

            string emailRegex = "CONST_EMAIL_REGEX";
            // たぶんこーゆーことのはず。
            // CONST_ EMAIL_ REGEX は 適切 な 正規表現 が 記述 さ れ て いる と する 
            // String emailRegex = CONST_ EMAIL_ REGEX; return !Pattern. compile( emailRegex) .matcher( email). matches();
            // WEB + DB PRESS Vol.113(Kindle の位置No.3637 - 3639).株式会社技術評論社.Kindle 版.
            return !Regex.IsMatch(email, emailRegex);
        }
    }

ScreeningApplicationServiceV1から移動しただけ。といえばそうかもしれない。
Screeningの情報をApplication側のソースではなく、Screeningを見ればわかる。
というのがこの変更のもつ大きな意味だと解釈した。
addNextInterviewの実装は怪しげだが、たぶんこーゆーことをしたいんだと思う。

applicationの方は非常にシンプルになる

    public class ScreeningApplicationServiceV2
    {
        private ScreeningRepository screeningRepository;

        public void startFromPreInterview(string applicantEmailAddress)
        {
            ScreeningV2 screening = ScreeningV2.startFromPreInterview(applicantEmailAddress);
            screeningRepository.insert(screening);
        }
        public void apply(string applicantEmailAddress)
        {
            ScreeningV2 screening = ScreeningV2.apply(applicantEmailAddress);
            screeningRepository.insert(screening);
        }
        public void addNextInterview(string screeningId, DateTime? interviewDate)
        {
            ScreeningV2 screening = screeningRepository.findById(screeningId);
            screening.addNextInterview(interviewDate);
            screeningRepository.update(screening);
        }
    }

ふむ。グレイト。
読み進めていく。


if (this. status != ScreeningStatusV 2. Interview)
{ throw new ApplicationException(" 不正 な 操作 です"); }
// ❷ 面接 次数 は 1 から インクリメント さ れる
int nextInterviewNumber = this. interviews. size() + 1;
InterviewV 2 nextInterview = new InterviewV 2( interviewDate, nextInterviewNumber);
this. interviews. add( nextInterview);
}

WEB+DB PRESS Vol.113 (Kindle の位置No.3766-3771). 株式会社技術評論社. Kindle 版.

適当に実装したaddNextInterviewはおおよそあっていた様子。
ただ、interviewV2としてinterveiwもほうも拡張している。ので、追従させていく。
Setterが排除されていく形になるだろう。

interview2の実装(おそらく)

    public class InterviewV2
    {
        private string interviewId { get; set; }
        private string screeningId { get; set; }
        private DateTime? screeningDate;
        private int interviewNumber { get; set; }
        private ScreeningStepResult screeningStepResult;
        private long recruiterId;

        public InterviewV2(DateTime? interviewDate, int interviewNumber)
        {
            interviewId = "UUID";
            // あれ、これうけとってないけどどうするんだろ。
            screeningId = "";
            interviewDate = interviewDate;
            interviewNumber = interviewNumber;
        }
    }

変数名が決まってこないのはコンストラクタだからかなぁ。
このインスタンスを生成する。という操作がどういうことか。きちんと理解できていれば
引数もそれに合った名前になる気がする。
screeningIdも元々のロジックではセットされていたけど、変更後のロジックでは引数で受け取っていないので、
受け取る工夫がいるだろう。

と、このへんで力尽きてきたのでC#に書き直すのは終わり。

感想

DDD。よくわからん状態ではあったんだけど。
コードの上ではまぁなんとなくでいつもやっていることではあった。一安心。
とりあえず同じようになんだかわからない人は読んでみると謎の障壁は取り払われるかもしれない。

モデリングについては自分のやり方にかなり怪しい部分もあって、もう少し踏み込んで
色々やってみないとなーと思った。勝手にbranchきってあれこれやってみようかな。など。

あとは。


こうした 言葉 の 齟齬 は 会話 による 意思 疎通 を 阻害 し ます。
文化 を 理解 する そ ぶり も 見せ ない 技術者 に対して 辟易 し、 ドメイン エキスパート は 近い 将来、
細かい ニュアンス を 伝える こと を 諦め て しまう でしょ う。 待ち受ける のは ドメイン とは かけ離れ た ソフトウェア の 完成 です。

WEB+DB PRESS Vol.113 (Kindle の位置No.4360-4363). 株式会社技術評論社. Kindle 版.

ここがね。ずしりときてしまった。すでにかけ離れてしまっているし。
ロジックで話をしてしまうこともあるなぁ。
はたしてそれはドメイン貧血に陥っているからなのか、僕らが耳をふさいでいるからなのか・・・どっちもだろうなぁ。
そのうえで一番悲しい思いをしているのはエンドユーザなんだよなぁ。
便利に快適になるはずのツールで不便をかけて仕事を増やしてしまっている。勉強が足りない。