web-dev-qa-db-ja.com

サブ機能で複雑さを隠す

コードのスタイルについて話し合っているところ、「好みの問題」のように聞こえてきました。私はそうでないと強く信じているので、私はあなたの意見を得て、賛否両論から学ぶためにこれを書いています。

問題は、コードの束をメソッドに置き換えることで複雑さを隠すことです。この議論は再利用可能な関数とは無関係であり、私は一般的な機能を除外するためではなく、単純なコード編成について議論しようとしています。

したがって、この例をルーズJavaで取り上げます。

public void purchase(Customer customer, Set<Item> items, Warehouse warehouse){
     int credit = customer.getCredit();    
     int orderPrice;

     for(Item item: items){
         orderPrice+=items.getPrice();
     }

     if(orderPrice + customer.getPendingPayments() > customer.getCredit()) {   
           warehouse.reserveStock(items);
           transportCenter.allocateTransport();
           customer.bill(items, orderPrice);
     }
}

これとは対照的に:

public boolean isCreditOk(Customer customer, Set<Item> items){
     int credit = customer.getCredit();    
     int orderPrice;

     for(Item item: items){
         orderPrice+=items.getPrice();
     }
     return  orderPrice + customer.getPendingPayments() > customer.getCredit();
}

public void purchase(Customer customer, Set<Item> items, Warehouse warehouse){
     if(isCreditOk(customer, items)){
           warehouse.reserveStock(items);
           transportCenter.allocateTransport();
           customer.bill(items, orderPrice);
     }
}

全体の議論は次のとおりです:isCreditOkメソッドを作成したでしょうか、それともインラインなら残しましたか?どちらを行うのですか?それは、一般的なケースでは、以下に依存しますか?

  • 抽出する関数の長さ
  • CreditOkがいくつのサブ関数とサブサブ関数を持っているか

繰り返しますが、isCreditOkは頻繁に使用されるため、これは行われませんが、コードを読みやすくするためです(私の意見では)。

コメントを頂けますか?ありがとう!

注:この質問が関連していることがわかりました: プライベート関数/メソッドが多すぎるなどの問題はありますか?

16
Miquel

少なくとも人間が読みやすいようにするには、より小さな関数に分割することが最善の方法です。

  • より小さな関数を抽出することで、コードのブロックを命名します。これにより読みやすさが向上します。
  • 読者は、各ステップの詳細に煩わされることなく、その機能が何をするかについてのマクロな考えを持つことができます。詳細を確認したい場合は、サブ機能を確認してください。これはボブおじさんのクリーンコードの新聞のメタファーです。

一般的に、複雑なifステートメントは独自のメソッドに抽出する必要があります。これらの厄介なifsは、テスト対象を説明する意味のある名前で置き換える必要があります。これにより、読者が膨大なコード行を解析する必要がなくなります。

しかし、それに加えて、そこで行われるリファクタリングがもっとあります。 purchase ou isCreditOkメソッドを読んでいる場合、合計値を計算しているforループを見たくありません。それは読みにくいです。その時点で目を止め、そのブロックを精神的に解析して、何が行われているのかを理解する必要があります。 forループは、getOrderPricegetTotalPriceなどの別のメソッドに抽出する必要があります。

それよりも良いのは、アイテムセットを独自のクラスOrderItemsに抽出することです。クラスには、独自のアイテムの合計を返すgetTotalのようなメソッドがあります。これが単一責任の原則であり、OrderItemsクラスは独自のアイテムを処理するクラスです。これに加えて、データ構造(この場合はSet)がカプセル化され、いつでも(たとえば、リストに変更する必要がある場合など)いつでも他のデータ構造に自由に変更できます。

5
Rafael Piccolo

以下は、Robert C. Martin(「クリーンなコード」の人)が行うことです。

http://blog.objectmentor.com/articles/2009/09/11/one-thing-extract-till-you-drop

だから私は彼があなたのisCreditOk関数をこのようにさらに分割すると思います

public int calculateOrderPrice(Set<Item> items)
{
     int orderPrice=0;
     for(Item item: items){
         orderPrice+=items.getPrice();
     }
     return orderPrice;
}

public boolean isCreditOk(Customer customer, Set<Item> items){
     int credit = customer.getCredit();    
     int orderPrice = calculateOrderPrice(items);
     int totalPayments = orderPrice + customer.getPendingPayments();
     return totalPayments > credit;
}

実際、これは良いことだと思います。再利用のためだけでなく、抽象化のためにも関数を使用することです。ここでの重要な要素は、関数の適切な命名です。そのため、小さなビルディングブロックとして使用できます。その後、コードの可読性が大幅に向上します。

もちろん、小さな関数にリファクタリングするときにどれくらい遠くに行くべきかについてはさまざまな意見があります。正直なところ、私はまだボブマーティンのようにコーディングしていません。しかし、ここ数年、小さな関数を作成することに焦点を当てるほど、多かれ少なかれすぐに見返りが得られるという経験が増えました-コードが読みやすくなり、コメントが少なくなり、バグが少なくなり、進化しやすくなりました(つまり、 、既存のコードに新機能を導入する方が簡単になります)。

20
Doc Brown

2番目の例はより読みやすくなっています(ただし、ネストを減らしてifisCreditOkに変更し、後者をisPurchaseAllowedに移動した可能性がありますが、Customerを反転します)。最初のものはコードを隠しますintent、実装を公開しているため、明確ですwhatコードは明確ですが、明確ではありませんwhyこれらのことを行います。

10
Serg Rogovtsev

この決定は、2つの(矛盾する可能性のある)原則/指針に依存しています: 単一の責任の原則 および あなたはそれを必要としないでしょう 。 SRPごとに、ここではメソッドの境界ではなく、クラスの境界について考える必要がありますが、この質問のために、メソッドについて話しましょう。 SRPから得られる大きな利点は、クラス/メソッドのそれぞれに変更する理由が1つしかないことです。

将来、倉庫を追加するため、注文に必要な在庫がある倉庫をオーダーディスパッチャーオブジェクトに問い合わせる必要があるとします。実装として巨大なメソッドを使用している場合、そのメソッドは成長し始めます。すでにテスト済みの機能に不必要かつ危険なほどに近いものを変更するだけでなく、この機能の一部をメソッドの周りで無関係なコードに流し込むことが一般的になります。モノリシック方式は、一部の商品に税金を適用する必要があり、注文の総重量に基づいて注文に対しても送料を請求する必要があると判断した場合、メンテナンスの手間になります。この動作を(最初に)変更する単一の理由で小さなメソッドに分割すると、変更がコードの小さな単位で行われ、不可視の副作用が誤って導入されることがはるかに困難になるため、将来の変更がはるかに簡単かつ安全になります。

このコインのもう1つの側面は、この柔軟性(YAGNI)が不要になる可能性があるという事実であり、概念実証のための迅速でダーティなソリューションを急上昇させることは、まさにあなたが本当に必要とするものかもしれません。 「ソフトウェアの出荷」は機能であり、可能な最も重要な機能を主張する人もいます。あなたが上向きに見えないときに次の原則に何時間も費やすことは、代わりに出荷することができる場合には価値がありません。

ここでの現実は、それは完全にあなた次第(または多分あなたの上司/技術リーダー)の判断の呼び出しであるということです。最新のIDEを使用すると、複数のクラスやメソッドを簡単にナビゲートできます。名前空間とディレクトリ構造の優れたプラクティスにより、すべてがどこにあるかを簡単に追跡できます。 1つのクラスの1つのメソッドですべての機能を確認したい場合は、オブジェクト指向の設計を望まない手続き型プログラマーである可能性があります。それは問題ありませんが、慎重に決定する必要があります。すべてのOOガイドラインは乱用され、極端に実行される可能性があり、さまざまな「ルール」を効果的に適用するタイミングを学習することは、学ぶのが難しいスキルです。あなたがする必要があるのは、質問を続け、経験を振り返ることです。

5
Chris Bye

関数を分割することは良い考えだと思います。しかし、注意する必要があります。関数を分割するために単に関数を分割する場合、少し混乱することになります。ボブおじさんのものを見たとき、彼はしばしば考えずに分裂しているようです。関数の分解には注意とスキルが必要であり、軽く行うべきではありません。

public boolean isCreditOk(Customer customer, Set<Item> items){
     int credit = customer.getCredit();    
     int orderPrice;

     for(Item item: items){
         orderPrice+=items.getPrice();
     }
     return  orderPrice + customer.getPendingPayments() > customer.getCredit();
}

これは一例に過ぎないと思いますが、どこが間違っていたと思うかを指摘します。 2つの異なるタスクを組み合わせ、注文の合計を計算し、クレジットを確認しました。この関数は次のように書くのがよいでしょう。

public boolean isCreditOk(Customer customer, int amount)
{
    return amount <= customer.getCredit() - customer.getPendingPayments();
}

これは、この関数がCustomerのメソッドであるとよいことも示唆しています。アイテムのリストを作成するのではなく、単に金額を渡すことができれば、この関数をテストする方がはるかに簡単になります。

原則として再利用できる機能を作ることを目指しています。私は必ずしもそれらを再利用することを期待していませんが、関数が再利用可能であるときに、便利な分解ができたことを教えてくれます。

4
Winston Ewert

私は2番目の例を好みます。私の推論では、これがコード内でユーザーのクレジットをチェックする必要がある唯一の場所であるとしても、現在は、常にそうであるとは限りません。このコードを抽象化したので、必要な状況に遭遇した場合、コードは再利用可能になります。

ただし、他のすべてのように、これはあまりにも遠すぎる可能性があります。個人的には、@ Doc BrownというRobert Martinのサイトの最後の例は少し行き過ぎだと思います。

3
GSto

機能を再利用する予定がない場合でも、機能を分割しています。また、関数サイズを画面サイズよりも小さく保つようにしています。

これには2つの利点があります。最初に、スクロールせずに関数が何をするかを把握でき、コメントを最小限に抑えることができます。

3
K..

IsCreditOKは独自のメソッドではなく、Customerのプロパティであると思います。私はそれがよりクリーンなアプローチだと思います...

2
Forty-Two

getAvailableCredit()メソッドを持つようにCustomerクラスを変更します。

理想的には、これを「Working」コード行として表示したいと思います。

if (order.getTotal() <= customer.getAvailableCredit() ) order.Ship();

特に、アイテムのセットをカプセル化し、それ自体を合計し、関連する税金、送料などを追加するPurchaseOrderクラスが欲しいです。

また、Customerが所有するCreditAccountクラスを作成して、Credit関数をCustomerクラスから分離することもできるでしょう。

1
Chris Cudmore