持続可能なソフトウェアのコーディング(3)シャイコーディング

振り返り

これまでの内容を整理してみましょう。

– DRY原則で重複コードを減らしましょう。
– 重複コードがあると、ビジネスロジックが複数になりデータが分散します。
– バグを修正しても、依然として同じバグが存在する可能性があります。
– 重複コードを減らすために、クラスが直交性を持つように設計しましょう。
– 直交性を持つクラスを設計するために、前回デメテルの法則を紹介しました。
– デメテルの法則を利用すると、クラスの結合度を下げるメリットがあります。
– しかし、実際のコードではどのように作成すればいいのか半分だけを説明しました。
– 結論として、シャイコーディング(Shy Coding)がデメテルの法則に効果的であると述べました。


それでは、シャイコーディングについて調べてみましょう。

シャイコーディング

オブジェクト間のコラボレーション関係を表現するために使用する伝統的なメタファー(比喩)は、サーバー – クライアントです。次のような形で、OrderServiceとProductEntityは互いに結合しています。

サーバー – クライアントメタファーの観点から、OrderService、ProductEntityの2つのオブジェクトは、メソッドと呼ばれるパブリックインターフェイスを通じて互いに結合しています。ProductEntityがインターフェイスを提供しているサーバー(server)であり、サーバーのインターフェイスを使用するOrderServiceがクライアント(client)で、お互いのメソッドを呼び出して1つの大きな機能を提供するのがソフトウェアです。前述したデメテルの法則は、クライアントがサーバーのインターフェイスを使用する際に、オブジェクト間の結合度を下げるための方法をガイドします。近い親しいという言葉をもう一度思い出してみましょう。しかし、サーバークラスをどのように作成すればオブジェクト間の結合度を下げられるかについては、まだ言及していません。ここでシャイコード(Shy Code)が登場します。

Keep it DRY, Shy and Tell the Other guy
in The Pragmatic Programmers by Andy Hunt and Dave Thomas

オブジェクト指向の文章で「DRYに、Shyに、そして他人に話しかけるように維持しよう」というものがあります。おそらく一度は聞いたことがあるのではないでしょうか。上記の文章は、サーバークラスを設計する際に、確実に必要な情報のみ公開することをガイドしており、そのため、Shyという表現をしています。意図しない情報が公開されると、OrderServiceのクライアントクラスがメソッドの呼び出しを開始し、結果としてクラスを開発した開発者の意図とは関係なく、他のクラスと強く結合されてしまいます。したがって、デメテルの法則を満たすためにシャイコーディングをするようにガイドしています。

シャイコーディングのサンプル

上記のProductEntityとOrderServiceを用いて、シャイコーディングのサンプルを見てみましょう。ProductEntityクラスは、製品を意味するエンティティ(Entity)クラスで、OrderServiceクラスは注文ユースケース(Use Case)機能を含むサービス(Service)クラスです。多くの開発者は、ProductEntityのようなJPAエンティティクラスを作成する場合、次のように作成します。下のサンプルとコメントを見て、どのようにリファクタリングすればよいか、一緒に考えてみましょう。

// シャイコーディングを妨害するLombokアノテーションです。
@Data
public class ProductEntity{
    @Id
    private Long productId;
    private String name;
    private SaleStatus saleStatus
    private Long stockCount;

    public ProductEntity(){
    }
}

@Service
public class OrderService {

    private ProductRepository productRepository;

    // 製品IDで注文すると、注文番号オブジェクトをリターンするメソッドです。
    public OrderNumber order(Long productId){

        // デメテルの法則「同じクラス内で宣言されたオブジェクトのメソッドは呼び出すことができる」をよく守っています。

        ProductEntity productEntity = productRepository.findById(productId);

        OrderNumber orderNumber;

        // デメテルの法則「オブジェクトO 自分のメソッドは呼び出せる」をよく守っています。
        if (this.isPurchaseable(productEntity)){
            productEntity.setStockCount(productEntity.getStockCount() - 1);
            orderNumber = ..
        } else {
            orderNumber = ..
        }
    }

    public boolean isPurchaseable(ProductEntity productEntity){
        return productEntity.getStockCount() > 0;
    }
}

 

OrderServiceクラスのorder()メソッドは、注文を処理します。このメソッドに基づいて、OrderServiceはproductRepositoryとproductEntityオブジェクトに結合されています。まず、クライアントであるOrderServiceオブジェクトのorderメソッドがどのようにデメテルの法則を遵守しているか確認してみましょう。

– OrderServiceオブジェクト内部で生成されたProductRepositoryオブジェクトのfindByIdメソッドを使用している点
– Order ServiceオブジェクトのメソッドであるisPurchasable()を使用している点
– isPurchasable()もまた引数ProductEntityオブジェクトのgetStockCount()メソッドを使用している点


一見すると、大きな問題もなくデメテルの法則がよく適用されているように見えますが、上記のコードもメンテナンスにおいて脆弱性があります。ProductEntityクラスが、開発者が意図せず非常に多くの情報を公開しているからです。ProductEntityクラスのすべてのメンバー変数に対して、getter/setterメソッドが開いています。そのため、自然に注文を処理するためisPurchaseable()というメソッドをOrderServiceに宣言します。そしてメソッド内部では、製品(ProductEntity)の在庫量(stockCount)をgetStockCount()メソッドで確認します。つまり、OrderServiceクラスは、ProductEntityの属性であるstockCount情報を知っておく必要があります。そうすると、次のような疑問が生じることでしょう。

Q.注文の行為と製品の在庫量は互いに密接な関係であるか?
Q-1.密接な場合、なぜ2つの情報は1つのクラスにまとめることができないのか?
Q-2.密接でない場合、なぜOrderServiceのisPurchaseable()メソッドがProductEntityのstockCountを知る必要があるのか?


注文機能と製品の在庫情報は非常に密接な関係ですが、互いに結合されてはいけないようです。次章で検討してみましょう。

シャイコーディングとメンテナンス、および結合度

企画者のA君が開発者のB君のところに急いでやってきました。
A君:これは違うよ。どうして注文を在庫量で確認するんですか?
在庫量(ProductEntity.stockCount)ではなく、1日生産量(capabilityCount)に合わせて受注し、1日置いてから処理してください。
B君:確認します。

開発者のB君は、ProductEntityにcapabilityCount属性を追加し、OrderServiceのorder()メソッドから分析を開始します。そして、最終的にisPurchaseable()メソッドの存在を確認して修理します。もし、開発者B君が適当にコード分析をしてisPurchaseable()メソッドの存在まで把握できなかった場合はどうなるでしょうか。そこで、次のようにコーディングしてはいかがでしょうか。

// @Dataの代わりに修正した部分

@Getter
public class ProductEntity{
    @Id
    private Long productId;
    private String name;
    private SaleStatus saleStatus
    private Long stockCount;

    // 修正箇所:新規に追加された属性。一日生産量
    private Long capabilityCount;

    public ProductEntity(){
    }

    // 修正箇所:企画者の要求事項は複雑ですが、疑似コードなので簡単に整理します。
    public boolean isPurchaseable(){
        if (capabilityCount > 1)
            return true;
        return false;
    }

    public void deductCapabilityCount(){
        this.capabilityCount--;
    }
}

以前のコードと比較して3つの部分を修正しました。Lombok@Dataアノテーションの代わりに@Getterを使用し、一日の生産量(capabilityCount)属性とOrderServiceで宣言されたisPurchaseable()メソッドをProductEntity内部に移動させました。OrderServiceのorder()はProductEntityのisPurchaseable()を呼び出すだけで、注文可能かどうか分かるようになります。OrderServiceクラスはProductEntity内部にどのような情報があるか知る必要はなく、さらにどのように実装されたかも知る必要はありません。メソッドの名前が非常に明確なことから、属性とその内容を扱うメソッドがProductEntity内部において、しっかりと結合されています。そして、他のクラスから購入可能かどうかを確認するには、ProductEntityのisPurchaseable()メソッドを呼び出すだけでよく、重複することなく使用することができます。

私たちが何気なく使っているLombok@Dataアノテーションが、デメテルの法則のシャイコードに勝っています。参考までに、@Data = @ToString + @EqualsAndHashCode + @Getter + @Setter + @RequiredArgsConstructorとなります。getter、setterのおかげで、私たちはProductEntityにshyなメソッドを作成する必要がありません。なぜなら必要な場合には、OrderServiceからProductEntityのgetter setterを利用して、データを取得したり操作したりするからです。少なくとも@Dataを@Getterに変えれば、ProductEntityクラスは多くがshyになります。適切なところで妥協して便利なLombokの機能を使用したい場合なら、@Getterだけを宣言すれば可能です。

変更されたコードを見て、次の質問について考えてみましょう。

– DRY原則についてよく守られていますか?
– Order ServiceとProduct Entityは直交性を持っていますか?
– ProductEntityはデメテルの法則のシャイコードと言えますか?


答えはイエスです。
次回はこの言葉について、もう一度考えてみましょう。

Keep it DRY, keep it shy and tell the other guy

TOAST Meetup 編集部

TOASTの技術ナレッジやお得なイベント情報を発信していきます
pagetop