RAKUS Developers Blog | ラクス エンジニアブログ

株式会社ラクスのITエンジニアによる技術ブログです。

オブジェクト指向を学ぶためのオブジェクト指向エクササイズ

こんにちは。west-cです。

オブジェクト指向を意識した開発を行うようになってからしばらく経ちました。
当初に比べると手続き的な考え方からいくらかは脱却できたかと思いますが、オブジェクト指向的な設計手法やコーディング方法が完璧に身に付いたと言える自信はまだありません。

そこで今回は、オブジェクト指向的な考え方を鍛えることができると言われている「オブジェクト指向エクササイズ」を試してみることにしました。

オブジェクト指向とは

オブジェクト指向とは、システムで扱う事柄をオブジェクトとして捉える技法と言えます。

オブジェクト指向が普及する以前に用いられていた手続き型の設計の場合、システムの機能全体の手順を徐々に分解して小さな部品を作り上げていくような、いわゆるトップダウン的なアプローチとなります。
手続き型での開発手法の場合、改修時の影響範囲が広くなると一般に言われています。

一方、オブジェクト指向では部品(オブジェクト)を中心に考えます。
まず、システムの実現を担う個々の部品を定義し、その部品自身に振る舞いをもたせます。
そして、定義した部品同士を組み合わせることでシステムを実現します。
手続き型とは正反対の、ボトムアップのアプローチを取ることが特徴的です。
個々の部品の独立性が高いため、改修時の影響範囲も限られ、コードの保守性や再利用性が高まることが期待できます。

オブジェクト指向エクササイズとは

オブジェクト指向エクササイズとは、書籍『ThoughtWorksアンソロジー』で紹介されているオブジェクト指向設計を理解し実際に使えるようになるためのエクササイズです。

オブジェクト指向エクササイズの具体的な方法は、以下に定められた9つのルールを適用してコードを書く、というものになります。

  1. 1つのメソッドにつきインデントは1段階までにすること
  2. else句を使用しないこと
  3. すべてのプリミティブ型と文字列型をラップすること
  4. 1行につきドットは1つまでにすること
  5. 名前を省略しないこと
  6. すべてのエンティティを小さくすること
  7. 1つのクラスにつきインスタンス変数は2つまでにすること
  8. ファーストクラスコレクションを使用すること
  9. Getter、Setter、プロパティを使用しないこと

オブジェクト指向エクササイズをやってみた

お題には「チケット料金モデリング」を利用し、映画のチケットとその料金を決定するコードを、オブジェクト指向エクササイズの9つのルールを守った上で書いてみました。なお使用言語はJavaです。

作成したコードは以下に置いてあります。

github.com

以下、ルールのうちいくつかをコードを交えて紹介します。

・すべてのプリミティブ型と文字列型をラップすること

いわゆるValue Objectの考え方です。

今回の場合、チケット料金の決定には上映日(平日か・土日祝か・映画の日か)や上映時間(レイトショーかどうか)が必要になります。
そこで、LocalDateLocalTimeをそのまま利用するのではなく、これらをラップしたScreenTimeオブジェクトを作成しました。
これにより上映日時にまつわる必要な振る舞いをScreenTimeに閉じ込めることができ、「映画の日かどうか」や「レイトショーかどうか」といった業務固有の振る舞いもオブジェクト内で表現することができました。

public class ScreenTime {

    private final LocalDate date;
    private final LocalTime time;

    // 略

    /**
     * 映画の日かどうか.
     */
    public boolean isCinemaDay() {
        return date.getDayOfMonth() == 1;
    }

    /**
     * レイトショーかどうか.
     */
    public boolean isLateShow() {
        return time.getHour() >= 20;
    }
}

・ファーストクラスコレクションを使用すること

ファーストクラスコレクションとは、Javaでいうjava.util.Listのようなプリミティブなコレクション型をラップしたクラスのことを指します。
ファーストクラスコレクションを作成しシステムに必要な振る舞いに限定したメソッドを提供することで、このコレクションの意図を明確にすることができます。

チケットの種類の決定には年齢・学生かどうかに加えて、各種会員か・障がい者かなども関係します。
今回は、「各種会員か」「障がい者か」といった割引要素をDiscountというEnumで定義することにしました。

public enum Discount {
    CINEMA_CITIZEN,
    HANDICAPPED,
    MICARD
}

1人で複数の割引要素を持つことも考えられるためファーストクラスコレクションDiscountListを作成し、チケット種類の決定にはこのモデルを利用するようにしました。
今回は各種要素が含まれているかどうか程度でしか利用していないため恩恵は少ないですが、外部からの意図せぬコレクション操作を防ぐことができるという点は保守性の向上にも繋がると思います。

public class DiscountList {

    private final List<Discount> discounts;

    public DiscountList(Discount... discounts) {
        this.discounts = Arrays.asList(discounts);
    }

    /**
     * シネマシティズン会員かどうか.
     */
    public boolean isCinemaCitizen() {
        return contains(Discount.CINEMA_CITIZEN);
    }

    /**
     * 障がい者かどうか.
     */
    public boolean isHandicapped() {
        return contains(Discount.HANDICAPPED);
    }

    /**
     * エムアイカード会員かどうか.
     */
    public boolean isMicard() {
        return contains(Discount.MICARD);
    }

    private boolean contains(Discount type) {
        return discounts.contains(type);
    }
}

・Getter、Setter、プロパティを使用しないこと

今回、観客を表すAudienceモデルにAgeという名前で年齢を持たせることにしました。
このAgeを利用して「70才以上か」「幼児か」といった判定を行いたいのですが、AudienceにはAgeを取得するようなgetterメソッドは作成せず、Audience経由で年齢にまつわる必要な振る舞いを提供するようにしました。

前述の「すべてのプリミティブ型と文字列型をラップすること」「ファーストクラスコレクションを使用すること」のルールとセットで、Tell, Don’t Ask の原則に沿ったコードとするために押さえておきたい考え方だと思いました。

public class Audience {

    private final Age age;
    private final StudentType studentType;

    public Audience(Age age, StudentType studentType) {
        this.age = age;
        this.studentType = studentType;
    }

    /**
     * 指定した年齢以上かどうか.
     */
    public boolean isYearsAndOver(int other) {
        return age.isYearsAndOver(other);
    }

    /**
     * 幼児かどうか.
     */
    public boolean isInfant() {
        return age.isInfant();
    }

    // 略

}

・else句を使用しないこと

上映日時(ScreenTime)からどの日時・時間帯の料金を適用すべきかを決定するために、料金のタイプを表すPriceTypeというEnumを定義しました。
ScreenTimeをもとにPriceTypeを適用するファクトリメソッドは分岐が多くなってしまいましたが、else句を利用せず、かつなるべく可読性が高くなるよう早期returnやprivateメソッドへの抽出を行うようにしました。

ただ、Enumで定義している定数自体が多くなってしまったこともあり、工夫したところでやはり可読性が低いことには変わりない点は課題に感じています。
改善の余地ありと感じている部分です。

public enum PriceType {

    WEEKDAY,
    WEEKDAY_LATE,
    WEEKEND_AND_HOLIDAY,
    WEEKEND_AND_HOLIDAY_LATE,
    CINEMA_DAY_ON_WEEKDAY,
    CINEMA_DAY_ON_WEEKDAY_LATE,
    CINEMA_DAY_ON_WEEKEND_AND_HOLIDAY,
    CINEMA_DAY_ON_WEEKEND_AND_HOLIDAY_LATE;

    public static PriceType of(ScreenTime screenTime) {
        if (screenTime.isCinemaDay()) {
            return priceTypeOfCinemaDay(screenTime);
        }
        if (screenTime.isWeekDay()) {
            return priceTypeOfWeekDay(screenTime);
        }
        return priceTypeOfWeekend(screenTime);
    }

    private static PriceType priceTypeOfCinemaDay(ScreenTime screenTime) {
        if (!screenTime.isLateShow()) {
            return screenTime.isWeekDay() ? CINEMA_DAY_ON_WEEKDAY : CINEMA_DAY_ON_WEEKEND_AND_HOLIDAY;
        }
        return screenTime.isWeekDay() ? CINEMA_DAY_ON_WEEKDAY_LATE : CINEMA_DAY_ON_WEEKEND_AND_HOLIDAY_LATE;
    }

    private static PriceType priceTypeOfWeekDay(ScreenTime screenTime) {
        return screenTime.isLateShow() ? WEEKDAY_LATE : WEEKDAY;
    }

    private static PriceType priceTypeOfWeekend(ScreenTime screenTime) {
        return screenTime.isLateShow() ? WEEKEND_AND_HOLIDAY_LATE : WEEKEND_AND_HOLIDAY;
    }
}

・名前を省略しないこと

「名前を省略しないこと」とありますが、このルールの本来の趣旨は「省略したくなるような長い名前を付けないこと」と言えると思います。

『すべてのエンティティの名前には1つか2つの単語だけを使い、省略しないでください』とありますが、今回は厳守はできませんでした。
例えば、チケット種別「中・高生」を表すクラスではJuniorAndHighSchoolという名前を利用してしまっています。
もしかするとTeenagerなどでの言い換えが可能かもしれないですが、本当にイコールとして扱って良いのかの判別がつかなかったため、一旦このままとしました。

たかが命名されど命名ということで、実際の開発時にはより良い命名をチーム内で模索していく活動が必要なのだと感じました。

・1行につきドットは1つまでにすること

このルールでは「複数のドットを使っているコードは責務の配置を間違っているはず」ということに基づいていると言えると思います。

一方、このルールを文字通りに受け取ると、以下のようなStream処理もルール違反とみなされてしまいます。

    private List<TicketPlan> sortByPrice(PriceType priceType) {
        return plans.stream()
            .sorted(Comparator.comparing(plan -> plan.price(priceType)))
            .collect(Collectors.toList());
    }

今回はルールに従うため、Streamは利用せず以下のようにComparatorを別途作成したうえでソートを行うようにしました。
ただ、メソッド名等からその操作の意図が分かるようであれば、盲目的にルールに従うのではなくある程度許容するのもアリなのではと感じた部分です。

    private List<TicketPlan> sortByPrice(PriceType priceType) {
        Comparator<TicketPlan> ticketPriceComparator = TicketPriceComparatorFactory.create(priceType);
        List<TicketPlan> sortedList = new ArrayList<>(plans);
        sortedList.sort(ticketPriceComparator);
        return sortedList;
    }

おわりに

今回オブジェクト指向エクササイズを実践したことで、今まで以上にモデルの責務等について思考を巡らせることができました。
エクササイズ実践後の自身の考え方にも変化があったと思っており、たとえば実務のコードでプリミティブ型をメソッド間で引き回しているようなコードを見かけると違和感を覚えるようになり、よりよい概念でモデリングができるのではないかといった考えが生まれるようになりました。

一方で、「オブジェクト指向エクササイズのルールを守ったからといって必ずしもオブジェクト指向設計ができるとは限らない」ということも同時に感じました。
出来上がったモデルを見直してみるとぎこちなさや違和感を覚えるものも多くあり、自身のモデリング力の不足を痛感しています。
今回は自身の未知のドメイン領域ということで、余計にぎこちなさを感じるモデリングになってしまったと思います。
実務においては担当領域のドメイン知識習得に励むとともに、オブジェクト指向エクササイズのルールを頭の片隅に入れながらモデリング力の向上にも努めていきたいです。

皆さんもぜひ、オブジェクト指向エクササイズを試してみてはいかがでしょうか。

参考文献・参考資料


◆TECH PLAY
techplay.jp

◆connpass
rakus.connpass.com

Copyright © RAKUS Co., Ltd. All rights reserved.