【DDD練習】「JR 新幹線 料金ルールを実装してみよう」にチャレンジ(その1)
前置き
『現場で役立つシステム設計の原則 〜変更を楽で安全にするオブジェクト指向の実践技法』の著者の増田さんが、
ワークショップで使用する「JR 新幹線 料金ルールを実装してみよう」というサンプルコードをGitHubで公開されておりました。
明日 12/14 の「現場でDDD ! 2nd」のワークショップで使う、サンプルコードです。
— 増田 亨. (@masuda220) 2019年12月13日
新幹線の運賃と特急料金の計算ルールを、値オブジェクトで表現してみよう、という内容。
イベントに参加されない方も、ぜひ、チャレンジしてみてください。https://t.co/LlLKA5h7Bt
#genbadeDDD
今回、この課題に実装例を見ないでチャレンジしてみたいと思います。
※19/12/23に公開しましたが、バグがあったので修正して再度公開しました。
実装・前半戦開始
JR新幹線の料金の要件は以下のようになっています。
料金 = 運賃 + 特急料金
特急料金(super express surcharge)
- 指定席(ひかり)
- のぞみ割り増し (additional charge)
- 自由席特急料金 (free seat)
- 子供料金 (child)
自分はここから、ルールには以下3つの「区分」があると読み取りました。
自由席割引の要件から、「指定・自由」の「席種」、
のぞみ料金は割り増し、という要件から「ひかり・のぞみ」の「特急料金種」、
子供料金は割引き、という要件から「大人・子供」の「料金」区分を抽出しました。
料金
料金区分(FareType) |
---|
大人 |
子供 |
特急料金
特急料金種(SuperExpressSurchargeType) | 席種(SeatType) |
---|---|
ひかり | 指定 |
のぞみ | 自由 |
料金区分(Fare)の実装
- 料金 = 運賃 + 特急料金
の計算式から 「Fare(料金)」インターフェイス、「運賃(BasicFare)」クラス、「特急料金(SuperExpressSurcharge)」インターフェイスを実装しました。
「Fare」を実装した「大人料金(AdultFare)」「子供料金(ChildFare)」の具象クラスを実装しました。
「子供料金(ChildFare)」は、「運賃」と「特急料金」を割引するロジックが実装されています。
public class BasicFare { public readonly int value; public BasicFare(int value) { this.value = value; } }
public interface SuperExpressSurcharge { public int value { get; } }
public interface Fare { public BasicFare basicFare { get; } public SuperExpressSurcharge superExpressSurcharge { get; } int value(); }
public class AdultFare : Fare { public BasicFare basicFare { get; } public SuperExpressSurcharge superExpressSurcharge { get; } public AdultFare(BasicFare basicFare, SuperExpressSurcharge superExpressSurcharge) { this.basicFare = basicFare; this.superExpressSurcharge = superExpressSurcharge; } public int value() { return basicFare.value + superExpressSurcharge.value; } }
public class ChildFare : Fare { public BasicFare basicFare { get; } public SuperExpressSurcharge superExpressSurcharge { get; } private const double discountRate = 0.5; private const double roundDownNumber = 0.1; public ChildFare(BasicFare basicFare, SuperExpressSurcharge superExpressSurcharge) { this.basicFare = basicFare; this.superExpressSurcharge = superExpressSurcharge; } public int value() { return calculateChildFare(basicFare.value) + calculateChildFare(superExpressSurcharge.value); } public int calculateChildFare(int value) { return (int)Math.Floor((value * discountRate) * roundDownNumber) * 10; } }
席種(SeatType)の実装
続いて「席種(SeatType)」の実装です。
「席種(SeatType)」インターフェイスから「指定席(ReservedSeatCharge)」クラスと、「自由席(FreeSeatCharge)」クラスを実装。
「自由席」の料金は「ひかり」の料金から自由席割引額を引いた額になるので、
「自由席割引(FreeSeatDiscount)」クラスを実装。
「自由席(FreeSeat)」クラスは、「ひかり料金(HikariCharge)」クラスと「自由席割引(FreeSeatDiscount)」クラスを内包したクラスとして表しました。
「指定席」は「ひかり」の料金そのままなので、「ひかり料金(HikariCharge)」を内包しただけのクラスとなります。
class HikariCharge { public readonly int value; public HikariCharge(int value) { this.value = value; } }
class FreeSeatDiscount { public int value; public FreeSeatDiscount(int value) { this.value = value; } }
public interface SeatCharge { public int value { get; } }
public class ReservedSeatCharge :SeatCharge { private readonly HikariCharge hikariCharge; public ReservedSeatCharge(HikariCharge hikariCharge) { this.hikariCharge = hikariCharge; } public int value => hikariCharge.value; }
public class FreeSeatCharge : SeatCharge { private readonly HikariCharge hikariCharge; private readonly FreeSeatDiscount freeSeatDiscount; public FreeSeatCharge(HikariCharge hikariCharge, FreeSeatDiscount freeSeatDiscount) { this.hikariCharge = hikariCharge; this.freeSeatDiscount = freeSeatDiscount; } public int value => this.hikariCharge.value - freeSeatDiscount.value; }
特急料金種(SuperExpressSurchargeType)の実装
続いて、特急料金を表すSuperExpressSurcharge
インターフェイスから、「ひかり(Hikari)」クラス、「のぞみ(Nozomi)」クラスを実装しました。
それぞれ、「席種(SeatCharge)」を内包させています。
「席種(SeatCharge)」が「ひかりの料金(HikariCharge)」を内包しているためです。
「のぞみ」の料金は、以下の要件から「ひかり」の「料金」に割増するモノなので、「のぞみ(Nozomi)」は「席種(SeatCharge)」クラスと、「のぞみ割増料金(NozomiAddtionalCharge)」クラスを内包したクラスとしました。
のぞみ割り増し (additional charge)
ひかりの特急料金に、以下の金額を加算する。
- 新大阪まで 320 円
- 姫路まで 530 円
public class Hikari : SuperExpressSurcharge { public int value => seatCharge.value; private readonly SeatCharge seatCharge; public Hikari(SeatCharge seat) { this.seatCharge = seat; } }
public class NozomiAdditionalCharge { public readonly int value; public NozomiAdditionalCharge(int value) { this.value = value; } }
public class Nozomi : SuperExpressSurcharge { private readonly SeatCharge seatCharge; private readonly NozomiAdditionalCharge nozomiAdditionalCharge; public Nozomi(SeatCharge seatCharge, NozomiAdditionalCharge nozomiAdditionalCharge) { this.seatCharge = seatCharge; this.nozomiAdditionalCharge = nozomiAdditionalCharge; } public int value => seatCharge.value + nozomiAdditionalCharge.value; }
「区分オブジェクト」を使いさらに分かりやすく
上記の例では計算方法ごとに関数を別にしていますが、実際にUIから「大人 or 子供」「のぞみ or ひかり」「指定席 or 自由席」といった条件から各クラスのインスタンスを作成するとなった場合、
ここまでの実装だけだとIf文を使って分岐することになってしまいそうです。
そこで各クラスの生成を「区分オブジェクト」にやらせることでコードをもっと分かりやすくしたいと思います。
public class SuperExpressSurchargeType { private readonly SeatCharge seatCharge; private readonly NozomiAdditionalCharge nozomiAdditionalCharge; public SuperExpressSurchargeType(SeatCharge seatCharge, NozomiAdditionalCharge nozomiAdditionalCharge) { this.seatCharge = seatCharge; this.nozomiAdditionalCharge = nozomiAdditionalCharge; } public Hikari Hikari() { return new Hikari(seatCharge); } public Nozomi Nozomi() { return new Nozomi(seatCharge, nozomiAdditionalCharge); } public SuperExpressSurcharge valueOf(string name) { var method = typeof(SuperExpressSurchargeType).GetMethod(name); return method.Invoke(this, null) as SuperExpressSurcharge; } }
class SeatType { private readonly HikariCharge hikariCharge; private readonly FreeSeatDiscount freeSeatDiscount; public SeatType(HikariCharge hikariCharge, FreeSeatDiscount freeSeatDiscount) { this.hikariCharge = hikariCharge; this.freeSeatDiscount = freeSeatDiscount; } public ReservedSeat Reserved() { return new ReservedSeat(hikariCharge); } public FreeSeat Free() { return new FreeSeat(new ReservedSeat(hikariCharge), freeSeatDiscount); } public Seat valueOf(string name) { var method = typeof(SeatType).GetMethod(name); return method.Invoke(this, null) as Seat; } }
public class FareType { private readonly BasicFare basicFare; private readonly SuperExpressSurcharge superExpressSurcharge; public FareType(BasicFare basicFare, SuperExpressSurcharge superExpressSurcharge) { this.basicFare = basicFare; this.superExpressSurcharge = superExpressSurcharge; } public AdultFare Adult() { return new AdultFare(basicFare, superExpressSurcharge); } public ChildFare Child() { return new ChildFare(basicFare, superExpressSurcharge); } public Fare valueOf(string name) { var method = typeof(FareType).GetMethod(name); return method.Invoke(this, null) as Fare; } }
ポイントはvalueOf
メソッドです。
各区分オブジェクトクラスに、インスタンスを生成する関数を実装。
.GetMethod
により文字列で対象のクラスを指定し、インスタンスを生成している箇所です。
上記の手法を用いることで、IF文を使って分岐する必要がなくなります。
Fare
、SuperExpressSurcharge
、SeatType
を抽象化した実装もここで効果が出ていると感じます。
区分オブジェクト導入後の使用例
料金を計算するサービスクラス「FareSystemService」です。
各料金は出発地・目的地ごとに異なるため、これらはDBなどで定義されていると仮定し、「FaresRepository」というリポジトリークラスを実装。
出発地(Departure)、目的地(Destination)ごとの運賃、特急料金等を保持した「FareByrRoute」オブジェクトを、「FaresRepository」から取得する形にしました。
※自由席割引額(FreeSeatDiscount)もマスターテーブルのような場所で定義するのがよさそうですが、例では省略してハードコーディングしてしまっています。
public class FareSystemService { private readonly IFaresRepository _faresRepository; public FareSystemService(IFaresRepository faresRepository) { _faresRepository = faresRepository; } public int calculateFare(Departure departure, Destination destination, string superExpressName, string seatName, string fareName) { FareByRoute fareByRoute = _faresRepository.GetFareByRoute(departure, destination); SeatChargeType seatType = new SeatChargeType(fareByRoute.hikariCharge, new FreeSeatDiscount(530)); SeatCharge seat = seatType.valueOf(seatName); SuperExpressSurchargeType superExpressType = new SuperExpressSurchargeType(seat, fareByRoute.nozomiAdditionalCharge); SuperExpressSurcharge superExpressSurcharge = superExpressType.valueOf(superExpressName); FareType fareType = new FareType(fareByRoute.basicFare, superExpressSurcharge); Fare fare = fareType.valueOf(fareName); return fare.value(); }
public interface IFaresRepository { public FareByRoute GetFareByRoute(Departure departure, Destination destination); }
public class Departure { public readonly string value; public Departure(string value) { this.value = value; } }
public class Destination { public readonly string value; public Destination(string value) { this.value = value; } }
public class FareByRoute { public readonly Departure departure; public readonly Destination destination; public readonly BasicFare basicFare; public readonly HikariCharge hikariCharge; public readonly NozomiAdditionalCharge nozomiAdditionalCharge; public FareByRoute(Departure departure, Destination destination, BasicFare basicFare, HikariCharge hikariCharge, NozomiAdditionalCharge nozomiAdditionalCharge) { this.departure = departure; this.destination = destination; this.basicFare = basicFare; this.hikariCharge = hikariCharge; this.nozomiAdditionalCharge = nozomiAdditionalCharge; } }
まとめ
前半戦の実装は以上です。 残りは後日「その2」で公開できればと思います。
残る要件は
- 割引 (discount)
- 季節(season)による特急指定席料金の変動
です。 今回の練習の結果は以下GitHubリポジトリに公開しています。