抽象化が多すぎてコードの拡張が困難


9

コードベースの抽象化が多すぎる(または少なくともそれを処理している)と感じている問題に直面しています。コードベースのほとんどのメソッドは、コードベースの最上位の親Aを取り込むように抽象化されていますが、この親の子Bには、これらのメソッドの一部のロジックに影響を与える新しい属性があります。問題は、入力がAに抽象化されているため、これらのメソッドでこれらの属性をチェックできないことです。もちろん、Aにはこの属性がありません。Bを異なる方法で処理する新しいメソッドを作成しようとすると、コードの重複のために呼び出されます。私の技術リーダーによる提案は、ブール型パラメーターを受け取る共有メソッドを作成することですが、これの問題は、これを「隠された制御フロー」と見なす人がいることです。 、また、この共有メソッドは、小さな属性に分割されたとしても、将来の属性を追加する必要がある場合、一度複雑になりすぎたり複雑になったりします。これはまた、カップリングを増やし、結束を減らし、チームの誰かが指摘した単一責任の原則に違反します。

基本的に、このコードベースの抽象化の多くはコードの重複を減らすのに役立ちますが、メソッドの拡張/変更が最高の抽象化を行うようになっている場合は難しくなります。このような状況ではどうすればよいですか?私は非難の中心にいますが、他の誰もが彼らが良いと思うことに同意することはできませんが、それは結局私を傷つけています。


10
「問題」を読み解くコードサンプルを追加すると、状況をより深く理解するのに役立ちます
Seabizkit

ここでは、2つのSOLID原則が壊れていると思います。単一の責任-動作を制御することになっている関数にブール値を渡すと、関数は単一の責任を持たなくなります。もう1つは、リスコフ置換の原理です。クラスAをパラメーターとして受け取る関数があるとします。Aの代わりにクラスBを渡すと、その関数の機能は壊れますか?
bobek

メソッドAはかなり長く、複数のことをするのではないかと思います。それは事実ですか?
Rad80

回答:


27

Bを異なる方法で処理する新しいメソッドを作成しようとすると、コードの重複のために呼び出されます。

すべてのコードの複製が同じように作成されるわけではありません。

たとえば、2つのパラメータを取り、それらを一緒に追加するメソッドがあるとしtotal()ます。と呼ばれる別の1つがあるとしadd()ます。それらの実装は完全に同一に見えます。それらを1つのメソッドにマージする必要がありますか?番号!!!

ドント・リピート・ユアセルフまたはDRY原則はコードを繰り返すことではありません。それは、決定やアイデアを広めることです。そのため、アイデアを変更した場合は、そのアイデアを広めるあらゆる場所で書き直す必要があります。ブレグ。それはひどい。しないでください。代わりに、DRYを使用して、1か所で意思決定行います。

DRY(Do n't Repeat Yourself)原則は次のように述べています。

すべての知識は、システム内で単一の明確で信頼できる表現を持つ必要があります。

wiki.c2.com-自分を繰り返さないでください

しかし、DRYは、どこか他の場所のコピーアンドペーストのように見える、同様の実装を探すコードをスキャンする習慣に破損する可能性があります。これがDRYの脳死型です。地獄、あなたは静的分析ツールでこれを行うことができます。コードの柔軟性を維持するというDRY のポイントを無視するので、役に立ちません。

合計要件が変わった場合、total実装を変更する必要があるかもしれません。これは、add実装を変更する必要があるという意味ではありません。誰かがそれらを一緒に1つの方法にスムーズ化した場合、私は今や少し不必要な苦痛を味わっています。

どれくらいの痛み?確かに、コードをコピーして、必要なときに新しいメソッドを作成することはできます。だから大したことない?マラキー!他に何もないなら、私にいい名前をつけてください!良い名前は手に入れるのが難しく、意味をいじるとうまく反応しません。意図を明確にする良い名前は、メソッドに正しい名前を付けた方が正直に修正しやすいバグをコピーしたリスクよりも重要です。

ですから、私のアドバイスは、同様のコードに対する反応がひどくてコードベースを結び目に縛られないようにすることです。メソッドが存在するという事実を無視して、代わりにwilly nillyをコピーして貼り付けてもよいと言っているのではありません。いいえ、各メソッドには、それに関する1つのアイデアをサポートするすてきな名前が必要です。その実装がたまたま他の優れたアイデアの実装と一致している場合、今、今日、誰が気にかけているのですか?

一方、sum()実装がと同じか、場合によっては異なるメソッドがある場合でもtotal()、合計要件が変わるたびに変更する必要がある場合sum()は、これらが2つの異なる名前で同じ考えである可能性が高くなります。マージされたコードはより柔軟になるだけでなく、使用するのに混乱が少なくなります。

ブール型パラメーターについては、はい、それは厄介なコードのにおいです。その制御フローは問題を引き起こすだけでなく、悪いことに、抽象化の悪い点を切り開いたことを示しています。抽象化は、物事をより単純にするためのものであり、複雑にするためのものではありません。メソッドにブール値を渡してその動作を制御することは、実際に呼び出すメソッドを決定する秘密の言語を作成するようなものです。わぁ!私にそれをしないでください。ポリモーフィズムを実行する正直なところがない限り、各メソッドに独自の名前を付けます。

今、あなたは抽象化に燃え尽きているようです。抽象化は上手くいけば素晴らしいことなので、それは残念です。何も考えずにたくさん使っています。ラックアンドピニオンシステムを理解しなくても車を運転するたび、OSの割り込みを考慮せずに印刷コマンドを使用するたび、および個々の剛毛について考えることなく歯を磨くたびに。

いいえ、あなたが直面しているように見える問題は悪い抽象化です。ニーズとは異なる目的を果たすために作成された抽象化。複雑なオブジェクトへのシンプルなインターフェイスが必要です。これにより、オブジェクトを理解する必要なく、ニーズを満たすことができます。

別のオブジェクトを使用するクライアントコードを作成すると、ニーズが何であり、そのオブジェクトから何が必要かがわかります。そうではありません。これが、クライアントコードがインターフェイスを所有する理由です。あなたがクライアントであるとき、あなたのニーズが何であるかをあなたに伝えることは何もありません。あなたはあなたのニーズが何であるかを示すインターフェースを出し、あなたに渡されるものは何でもそれらのニーズを満たすことを要求します。

それが抽象化です。クライアントとして、私はを話しているのかもわかりません。私はそれから何が必要かを知っています。それが意味する場合は、インターフェースを変更するために何かをまとめてから、私にそれを渡す必要があります。私は気にしません。必要なことをしてください。複雑にするのはやめてください。

それを使用する方法を理解するために抽象化の内部を調べる必要がある場合、抽象化は失敗しました。それがどのように機能するかを知る必要はありません。それが機能するだけです。良い名前を付けてください。もし私が中を見ても、見つけたものに驚かないでください。使い方を思い出すために中を見続けさせないでください。

抽象化がこのように機能すると主張する場合、その背後にあるレベルの数は重要ではありません。あなたが抽象化の背後を見ていなければ。あなたは抽象化がそれに適応しないあなたのニーズに準拠していると主張しています。これが機能するには、使いやすく、適切な名前を付け、リークしないことが必要です。

それがDependency Injectionを生み出した態度です(または、私のような古い学校の場合は、参照渡しのみ)。継承よりも優先構成と委任でうまく機能します。態度は多くの名前で行きます。私の好きなものは教えてください、尋ねないでください

一日中、原則的にあなたを溺死させることができました。そして、それはあなたの同僚がすでにそうであるように聞こえます。しかし、ここに問題があります。他のエンジニアリング分野とは異なり、このソフトウェアは100年も経っていません。我々はまだそれを理解しています。だから、威圧的な響きのある本をたくさん持っている人に、読みにくいコードを書くようにいじめさせないでください。それらに耳を傾けるが、彼らは理にかなっていると主張する。信仰については何も取らないでください。なぜすべてを最大の混乱にするのかわからないまま、こう言われたからといって何らかの方法でコーディングする人々。


私は心から同意します。DRYは、3ワードのキャッチフレーズであるDot Repeat Yourselfの3文字の頭字語です。これは、wikiの 14ページの記事です。14ページの記事を読んで理解せずにこれらの3つの文字を盲目的につぶやくだけなら、あなたトラブルに遭遇するでしょ。また、Once And Only Once(OAOO)と密接に関連しており、Single Point of Truth(SPOT)/ Single Source Of Truth(SSOT)とはより緩やかに関連しています。
イェルクWミッターク

「それらの実装は完全に同一に見えます。それらを1つのメソッドにマージする必要がありますか?いいえ!!!」–逆も当てはまります。2つのコードが異なるからといって、コードが重複していないわけではありません。OAOO wikiページの Ron Jeffriesによるすばらしい引用があります。「ベックがほぼ完全に異なるコードの2つのパッチを「複製」であると宣言し、それらが複製になるように変更してから、新しく挿入された複製を削除するのを見たことがあります。明らかにより良いもので」
イェルクWミッターク

もちろん@JörgWMittag。重要なのはアイデアです。別の見栄えのコードでアイデアを複製している場合は、まだドライに違反しています。
candied_orange

自分を繰り返さないことについての14ページの記事は、何度も繰り返される傾向があると想像しなければなりません。
チャックアダムス

7

私たちはみんなここを読んでいるといういつものことわざがあります

すべての問題は、抽象化の別のレイヤーを追加することで解決できます。

まあ、これは真実ではありません!あなたの例はそれを示しています。したがって、少し修正したステートメントを提案します(自由に再利用してください;-))。

すべての問題は、正しいレベルの抽象化を使用して解決できます。

あなたの場合には2つの異なる問題があります:

  • 過一般抽象レベルですべてのメソッドを追加することによって引き起こされます。
  • 全体像がわからない印象や失われた感覚につながる具体的な行動の断片化。Windowsイベントループのようなものです。

両方が関連しています:

  • すべての専門分野が異なる方法でメソッドを抽象化する場合、すべてが問題ありません。が特殊な方法でShape計算できることを理解するのに問題はありませんsurface()
  • 一般的な一般的な行動パターンがある操作を抽象化する場合、2つの選択肢があります。

    • また、すべての専門分野で共通の動作を繰り返すことになります。これは非常に冗長です。維持するのが難しく、特に共通部分がスペシャライゼーション全体で一貫していることを確認するために、
    • テンプレートメソッドパターンの一種のバリアントを 使用します。これにより、簡単に特殊化できる追加の抽象メソッドを使用して、共通の動作を考慮に入れることができます。冗長性は低くなりますが、追加の動作は極端に分割される傾向があります。多すぎるということは、それがおそらく抽象的すぎることを意味します。

さらに、このアプローチは、設計レベルで抽象的なカップリング効果をもたらす可能性があります。ある種の新しい特殊な動作を追加するたびに、それを抽象化し、抽象親を変更し、他のすべてのクラスを更新する必要があります。これは、希望する種類の変更の伝播ではありません。そして、それは実際には(少なくとも設計では)特殊化に依存しない抽象化の精神ではありません。

私はあなたのデザインを知りませんし、これ以上は助けられません。おそらくそれは本当に非常に複雑で抽象的な問題であり、これ以上の方法はありません。しかし、オッズは何ですか?過剰一般化の症状はここにあります。それをもう一度見て、一般化よりも構成を検討 する時がきたのではないでしょうか?


5

動作がパラメーターの型に切り替わるメソッドを見るときはいつでも、そのメソッドが実際にメソッドパラメーターに属しているかどうかをすぐに検討します。たとえば、次のようなメソッドの代わりに:

public void sort(List values) {
    if (values instanceof LinkedList) {
        // do efficient linked list sort
    } else { // ArrayList
        // do efficient array list sort
    }
}

私はこれを行います:

values.sort();

// ...

class ArrayList {
    public void sort() {
        // do efficient array list sort
    }
}

class LinkedList {
    public void sort() {
        // do efficient linked list sort
    }
}

私たちはそれをいつ使うべきかを知っている場所に振る舞います。実装のタイプや詳細を知る必要がない実際の抽象化を作成します。あなたの状況では、このメソッドを元のクラス(私が呼び出すO)からタイプに移動しA、タイプでオーバーライドする方が理にかなっている可能性がありますB。メソッドがdoIt何らかのオブジェクトで呼び出された場合は、に移動doItAて、の異なる動作でオーバーライドしますBdoIt最初に呼び出された場所からのデータビットがある場合、またはメソッドが十分な場所で使用されている場合は、元のメソッドを残して委任できます。

class O {
    int x;
    int y;

    public void doIt(A a) {
        a.doIt(this.x, this.y);
    }
}

ただし、もう少し深く潜ることができます。代わりにブール型パラメーターを使用するという提案を見て、同僚の考え方について何がわかるかを見てみましょう。彼の提案はすることです:

public void doIt(A a, boolean isTypeB) {
    if (isTypeB) {
        // do B stuff
    } else { 
        // do A stuff
    }
}

これは、instanceof最初の例で使用したものと非常によく似ていますが、そのチェックを外部化している点が異なります。つまり、次の2つの方法のいずれかで呼び出す必要があります。

o.doIt(a, a instanceof B);

または:

o.doIt(a, true); //or false

最初の方法では、呼び出しポイントはAそれがどのタイプであるかを知りません。したがって、ブール値をずっと下に渡す必要がありますか?これは、コードベース全体で本当に必要なパターンですか?考慮する必要がある3番目のタイプがある場合はどうなりますか?これがメソッドの呼び出し方法である場合は、それを型に移動し、システムに実装を多態的に選択させる必要があります。

2番目の方法では、呼び出しポイントでのタイプをすでに知っている必要がありaます。これは通常、そこでインスタンスを作成するか、そのタイプのインスタンスをパラメーターとして受け取ることを意味します。here Oを取るメソッドを作成するBと機能します。コンパイラーはどの方法を選択するかを知っています。このような変更を進めている場合、少なくとも実際にどこに向かっているのかがわかるまで、複製は間違った抽象化を作成するよりも優れています。もちろん、この時点で何を変更しても、実際には完了していないことをお勧めします。

との関係をさらに詳しく調べる必要がAありBます。一般に、継承よりも構成を優先する必要があると言われています。これは、すべての場合には当てはまりませんが、私たちは掘るたら、それは例驚くべき数で真である。B継承からA、私たちは信じていることを意味しBていますA。動作が少し異なることを除いて、とB同じようAに使用する必要があります。しかし、それらの違いは何ですか?違いにもっと具体的な名前を付けることはできますか?それはでBはありませんが、A本当にAありXますA'B'?そうした場合、コードはどのように見えるでしょうか?

A前述のようにメソッドをに移動した場合、Xintoのインスタンスを注入し、Aそのメソッドをに委任できますX

class A {
    X x;
    A(X x) {
        this.x = x;
    }

    public void doIt(int x, int y) {
        x.doIt(x, y);
    }
}

とを実装A'してB'、を取り除くことができますB。より暗黙的な概念に名前を付けてコードを改善し、コンパイル時ではなく実行時に自分でその動作を設定できるようにしました。A実際には、抽象度も低くなっています。拡張された継承関係の代わりに、委任されたオブジェクトのメソッドを呼び出します。そのオブジェクトは抽象的ですが、実装の違いにのみ焦点を当てています。

ただし、最後に確認する必要があります。同僚の提案に戻りましょう。すべての呼び出しサイトで自分のタイプを明示的に知ってAいる場合は、次のような呼び出しを行う必要があります。

B b = new B();
o.doIt(b, true);

以前に、またはのいずれかでAあるXを作成するときに想定しました。しかし、この仮定でさえ正しくないかもしれません。これは、この差の唯一の場所であるとの問題?もしそうなら、多分少し異なるアプローチを取ることができます。orのいずれかがまだありますが、には属していません。それだけを気にするので、それだけに渡してみましょう:A'B'ABXA'B'AO.doItO.doIt

class O {
    int x;
    int y;

    public void doIt(A a, X x) {
        x.doIt(a, x, y);
    }
}

これで、呼び出しサイトは次のようになります。

A a = new A();
o.doIt(a, new B'());

もう一度、B消え、抽象化はより焦点を絞ったに移りXます。しかし、今回は、A知識が少ないため、さらに単純になります。それは抽象的なものではありません。

コードベースでの重複を減らすことが重要ですが、重複が最初に発生する理由を考慮する必要があります。重複は、抜け出そうとしているより深い抽象化の兆候である可能性があります。


1
ここであなたが与えている「悪い」コードの例は、私がオブジェクト指向以外の言語でやろうとする傾向に似ていることに私は驚かされます。彼らが間違った教訓を学び、彼らをコーディングの方法としてOOの世界に持ち込んだのだろうか?
Baldrickk

1
@Baldrickk各パラダイムには、独自の考え方と、独自の利点と欠点があります。関数型のHaskellでは、パターンマッチングの方が適しています。そのような言語ではありますが、元の問題のいくつかの側面も不可能です。
cbojar

1
これが正解です。操作するタイプに基づいて実装を変更するメソッドは、そのタイプのメソッドである必要があります。
Roman Reiner

0

継承による抽象化はかなり醜くなります。典型的なファクトリーを持つ並列クラス階層。リファクタリングは頭痛の種になる可能性があります。そして、その後の開発、あなたがいる場所。

代替策が存在します:厳密な抽象化の拡張ポイント、および段階的なカスタマイズ。特定の都市のカスタマイズに基づいて、政府顧客のカスタマイズを1つ言ってください。

警告:残念ながら、これはすべて(またはほとんど)のクラスを拡張する場合に最適に機能します。あなたには選択肢がない、おそらく小さい。

この拡張性は、拡張可能なオブジェクトの基本クラスに拡張機能を持たせることで機能します。

void f(CreditorBO creditor) {
    creditor.as(AllowedCreditorBO.class).ifPresent(allowedCreditor -> ...);
}

内部的には、拡張クラスによる拡張オブジェクトへのオブジェクトの遅延マッピングがあります。

GUIクラスとコンポーネントの場合、部分的に継承された同じ拡張性。ボタンなどの追加。

あなたの場合、検証はそれが拡張されているかどうかを調べ、拡張に対してそれ自体を検証する必要があります。1つの場合にのみ拡張ポイントを導入すると、理解できないコードが追加されてしまい、良くありません。

したがって、解決策はありませんが、現在のコンテキストで作業しようとします。


0

「隠されたフロー制御」は、私には手に余計に波打っています。
コンテキストから取り出された構成要素または要素は、その特性を持つ場合があります。

抽象化は良いことです。私はそれらを2つのガイドラインで調整します。

  • あまりに早く抽象化しない方が良い。抽象化する前に、パターンの例が増えるのを待ちます。「もっと」はもちろん主観的で、難しい状況に特有のものです。

  • 抽象化が優れているという理由だけで、抽象化のレベルが多すぎないようにします。プログラマーは、コードベースを掘り下げて12レベルの深さになるので、新規または変更されたコードのためにそれらのレベルを頭に留めておかなければなりません。抽象化されたコードへの要望は、多くの人々が理解するのが難しいほど多くのレベルにつながる可能性があります。これは、「忍者が保守するだけ」のコードベースにもつながります。

どちらの場合も、「more」と「too many」は固定数ではありません。場合によります。それが難しいのです。

サンディメッツからのこの記事も好きです

https://www.sandimetz.com/blog/2016/1/20/the-wrong-abstraction

重複は間違った抽象化よりもはるかに安いです
し、
間違った抽象化の上に重複を好みます

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.