【DDD練習】「JR 新幹線 料金ルールを実装してみよう」にチャレンジ(その2)


※ 個人blogに投稿した記事(投稿日:2020/1/21)をQiitaに移行しました

関連記事

前置き

前回からの続きです。ようやく着手できました・・・

嬉しかったこと

『現場で役立つシステム設計の原則 〜変更を楽で安全にするオブジェクト指向の実践技法』の著者の増田さんから、リアクションいただけました。

今回実装する要件

往復割引 (round trip discount)

片道の営業キロが601km以上あれば、「ゆき」と「かえり」の運賃がそれぞれ1割引になります。

東京からの営業キロ

  • 新大阪 553km
  • 姫路 644km

例:東京-姫路の往復料金

片道の割引額 10,010円 × 90% = 9,009円 → 10円未満の端数切り捨て 9,000円

割引後の往復運賃 : 9,000円 × 2 = 18,000円

2,020円の割引になる

まずは「営業キロ(RailWayDistance)」を値オブジェクトとして実装。
「601km以上の場合に割引になる」要件をisDiscountDistance関数で表現しました。

public class RailWayDistance
    {
        public readonly int value;

        public RailWayDistance(int value)
        {
            this.value = value;
        }

        public bool isDiscountDistance()
        {
            return this.value >= 601;
        }

    }

往復割引の要件から、運賃(BasicFare)は割引が存在することが判明しました。
割引後の運賃を表現するRoundTripDiscountedOnewayBasicFareクラスを実装。
元になる運賃(BasicFare)と、割引のロジックを実装したクラスです。

public class RoundTripDiscountedOnewayBasicFare : BasicFareWithTripType
    {
        private readonly BasicFare basicFare;
        private const double discountRate = 0.9;
        private const double roundDownNumber = 0.1;

        public RoundTripDiscountedOnewayBasicFare(BasicFare basicFare)
        {
            this.basicFare = basicFare;
        }

        public int value =>  (int) Math.Floor((this.basicFare.value* discountRate) * roundDownNumber) * 10;

    }

割引が適用されない運賃は「片道運賃(OnewayBasicFare)」としました。

BasicFareを内包するだけのクラスです。

public class OnewayBasicFare : BasicFareWithTripType
    {
         public readonly BasicFare basicFare;

        public OnewayBasicFare(BasicFare basicFare)
        {
            this.basicFare = basicFare;
        }

        public int value => basicFare.value;
    }

実装クラスの説明が先になりましたが、運賃と特急料金から料金を計算するFareクラスから両者を扱うために、BasicFareWithTripTypeインターフェイスを定義しました。

public interface BasicFareWithTripType
    {
        int value { get; }
    }

そして、文字列「OnewayTrip(片道)/ RoundTrip(往復)」の判定と、往復だった場合は営業キロが割引適用かを判定し、割引適用後の運賃クラスを生成する「運賃種別(BasicFareType)」クラスを実装しました。

public class BasicFareType
    {
        private readonly BasicFare basicFare;
        private readonly RailWayDistance railWayDistance;

        public BasicFareType(BasicFare basicFare, RailWayDistance railWayDistance)
        {
            this.basicFare = basicFare;
            this.railWayDistance = railWayDistance;
        }

        public BasicFareWithTripType Oneway()
        {
            return new OnewayBasicFare(basicFare);
        }

        public BasicFareWithTripType RoundTrip()
        {
            return createBasicFareForRoundTrip();
        }

        private BasicFareWithTripType createBasicFareForRoundTrip()
        {
            if (this.railWayDistance.isDiscountDistance())
            {
                return new RoundTripDiscountedOnewayBasicFare(this.basicFare);
            }
            return new OnewayBasicFare(this.basicFare);
        }

        public BasicFareWithTripType valueOf(string name)
        {
            var method = typeof(BasicFareType).GetMethod(name);
            return method.Invoke(this, null) as BasicFareWithTripType;
        }
    }

ここまで往復時の運賃割引のロジックは完成ですが、さらに「片道料金」「往復料金」を表現するクラスを実装します。

型はTripとし、「片道OnewayTrip」「往復RoundTrip」クラスとして表現しました。
「往復」は「行き(departingFare)」と「帰り(returningFare)」の金額を合計した額を返します。
(※Fareの参照渡しになってしまっているのが若干モヤモヤしますが)

public interface Trip
    {
        public int value();

    }
public class OnewayTrip : Trip
    {
        private Fare fare;

        public OnewayTrip(Fare fare)
        {
            this.fare = fare;
        }

        public int value() => fare.value();
    }
public class RoundTrip : Trip
    {
        private Fare departingFare;
        private Fare returningFare;

        public RoundTrip(Fare fare)
        {
            this.departingFare = fare;
            this.returningFare = fare;
        }

        public int value() => this.departingFare.value() + this.returningFare.value();

    }

割引運賃と同じく、区分オブジェクトでTripTypeクラスを表現しました。

public class TripType
    {
        private readonly Fare fare;

        public TripType(Fare fare)
        {
            this.fare = fare;
        }

        public OnewayTrip Oneway()
        {
            return new OnewayTrip(fare);
        }

        public RoundTrip RoundTrip()
        {
            return new RoundTrip(fare);
        }

        public Trip valueOf(string name)
        {
            var method = typeof(TripType).GetMethod(name);
            return method.Invoke(this, null) as Trip;
        }

    }

ここまで実装し、最終的にServiceクラスでの前回からの差分は以下のようになりました。
運賃BasicFareクラスを参照していたFareインターフェイスおよびFareの実装クラスは、BasicFareWithTripTypeを参照するように修正が必要となりました。

今のところ、ほぼ区分オブジェクトだけで要件を実現できているような形です。
以下のスクショはデバッグでブレークしたところですが、このように保持しているクラスの型で要件をそのまま表現できているかと思います。

見ていて思ったのですが、value(金額)はyen(円)という値オブジェクトで表現してもよいかもですね。

前回からの差分コミット

感想

思ったより時間かかっています・・・第5回くらいまで必要になりそう・・・
牛歩で最後まで頑張りたいと思います。