私は、コードベースの抽象化が多すぎる(または少なくともそれを処理している)と感じている問題に直面しています。コードベースのほとんどのメソッドは、コードベースの最上位の親Aを取り込むように抽象化されていますが、この親の子Bには、これらのメソッドの一部のロジックに影響を与える新しい属性があります。問題は、入力がAに抽象化されているため、これらのメソッドでこれらの属性をチェックできないことです。もちろん、Aにはこの属性がありません。 Bを別の方法で処理する新しいメソッドを作成しようとすると、コードの重複のために呼び出されます。私の技術リーダーによる提案は、ブール型パラメーターを受け取る共有メソッドを作成することですが、これに関する問題は、これを「隠された制御フロー」と見なす人がいることです。 、さらにこの共有メソッドは、将来的に属性を追加する必要がある場合、たとえそれが小さな共有メソッドに分解されたとしても、過度に複雑/複雑になります。これはまた、カップリングを増やし、結束を減らし、チームの誰かが指摘した単一責任の原則に違反します。
基本的に、このコードベースの抽象化の多くはコードの重複を減らすのに役立ちますが、メソッドを拡張/変更することにより、最高の抽象化を行うことが難しくなります。このような状況ではどうすればよいですか?私は非難の中心にいますが、他の誰もが彼らが良いと思うことに同意することはできませんが、結局私を傷つけています。
Bを別の方法で処理する新しいメソッドを作成しようとすると、コードの重複のために呼び出されます。
すべてのコードの複製が同じように作成されるわけではありません。
2つのパラメーターを受け取り、それらを一緒に追加するtotal()
というメソッドがあるとします。 add()
と呼ばれる別のものがあるとします。それらの実装は完全に同一に見えます。それらを1つのメソッドにマージする必要がありますか?番号!!!
Don't-Repeat-Yourself または DRY Principle は、繰り返しコードに関するものではありません。それは、決定やアイデアを広めることです。そのため、アイデアを変更した場合は、そのアイデアを広めるあらゆる場所で書き直す必要があります。ブレグ。それはひどい。しないでください。代わりにDRYを使用して 1つの場所で決定を行う を使用してください。
DRY(Do n't Repeat Yourself))原則は次のように述べています:
すべての知識は、システム内で単一の明確で信頼できる表現を持つ必要があります。
しかし、DRYは、どこか他の場所のコピーと貼り付けのように見える、同様の実装を探すコードをスキャンする習慣に破損する可能性があります。これは、DRYの頭の悪い形です。地獄、あなたは静的分析ツールでこれを行います。DRYのpointを無視するため、役に立ちませんコードに柔軟性を持たせます。
合計要件が変わった場合、total
の実装を変更する必要があるかもしれません。私のadd
実装を変更する必要があるという意味ではありません。誰かがそれらを一緒に1つの方法にスムーズにした場合、私は今や少し不必要な痛みを感じています。
どれくらいの痛み?確かに、コードをコピーして、必要なときに新しいメソッドを作成することはできます。だから大したことない?マラキー!他に何もない場合は、私にいい名前を付けてください!良い名前はなかなか手に入れられず、意味をいじるとうまく反応しません。意図を明確にする良い名前は、メソッドに正しい名前を付けた方が正直に修正しやすいバグをコピーしたリスクよりも重要です。
したがって、私のアドバイスは、同様のコードに対する反応がひどく、コードベースを結び目で結ぶことをやめることです。メソッドが存在するという事実を無視して、代わりにwilly nillyをコピーして貼り付けてもよいと言っているのではありません。いいえ、各メソッドには、それに関する1つのアイデアをサポートするすてきな名前が必要です。その実装がたまたま他の優れたアイデアの実装と一致した場合、今、今日、誰が気にかけているのでしょうか?
一方、sum()
と同じまたは異なる実装さえあるtotal()
メソッドがあり、合計要件が変わるたびにsum()
を変更する必要がある場合、これらは2つの異なる名前で同じ考えである可能性が高いです。マージされたコードはより柔軟になるだけでなく、使用するのに混乱が少なくなります。
ブール型パラメーターについては、はい、それは厄介なコードのにおいです。その制御フローが問題を引き起こすだけでなく、悪いことに、抽象化が不適切なポイントで行われたことを示しています。抽象化は物事をより複雑にするのではなく、より使いやすくするためのものです。メソッドにブール値を渡してその動作を制御することは、実際に呼び出すメソッドを決定する秘密の言語を作成するようなものです。わぁ!私にそれをしないでください。 正直多態性 が続いている場合を除き、各メソッドに独自の名前を付けます。
今、あなたは抽象化に燃え尽きているようです。抽象化は上手くいけば素晴らしいことなので、それは残念です。何も考えずにたくさん使っています。ラックアンドピニオンシステムを理解する必要なく車を運転するたび、OSの割り込みを考慮せずに印刷コマンドを使用するたび、および個々の剛毛について考えることなく歯を磨くたびに。
いいえ、あなたが直面しているように見える問題は、抽象化が悪いことです。ニーズとは異なる目的を果たすために作成された抽象化。複雑なオブジェクトへのシンプルなインターフェースが必要です。これにより、それらのオブジェクトを理解する必要なく、ニーズを満たすことができます。
別のオブジェクトを使用するクライアントコードを作成すると、ニーズが何であり、そのオブジェクトから何が必要かがわかります。そうではありません。これが、クライアントコードがインターフェイスを所有する理由です。あなたがクライアントであるとき、あなたのニーズが何であるかをあなたに伝えることは何もありません。あなたはあなたのニーズが何であるかを示すインターフェースを出し、あなたに手渡されるものは何でもそれらのニーズを満たすことを要求します。
それが抽象化です。クライアントとして私は知らない何を話しているのか私はそれから何が必要かを知っています。それが意味する場合は、インターフェースを変更するために何かをまとめてから、私にそれを渡す必要があります。私は気にしません。必要なことをしてください。複雑にするのはやめてください。
抽象化の内部を調べて使用方法を理解する必要がある場合、抽象化は失敗しました。それがどのように機能するかを知る必要はありません。それが機能するだけです。わかりやすい名前を付けてください。内部を確認しても、見つけたものに驚かないでください。使い方を思い出すために中を見続けさせないでください。
抽象化がこのように機能すると主張する場合、その背後にあるレベルの数は重要ではありません。抽象化の背後を見ない限り。あなたは抽象化がそれに適応しないあなたのニーズに準拠していると主張しています。これを機能させるには、使いやすく、適切な名前を付ける必要があります leak ではありません。
それがDependency Injectionを生み出した態度です(または、私のような古い学校の場合は、参照渡しのみ)。 継承よりも構成と委任を優先する でうまく機能します。態度は多くの名前で行きます。私のお気に入りは 教えて、聞かないで です。
私は一日中あなたを原則で溺死させることができました。そして、あなたの同僚はすでにそうです。しかし、ここに問題があります。他のエンジニアリング分野とは異なり、このソフトウェアは100年も経っていません。私たちはまだそれを理解しています。だから、威圧的な響きのある本をたくさん持っている人に、読みにくいコードを書くようにいじめさせないでください。それらに耳を傾けますが、彼らは理にかなっていると主張します。信仰については何も取らないでください。なぜすべてを最大の混乱にするのかわからないまま、こう言われたからといって何らかの方法でコーディングする人々。
私たちはみんなここを読んでいるといういつものことわざがあります。
すべての問題は、抽象化の別のレイヤーを追加することで解決できます。
まあ、これは真実ではありません!あなたの例はそれを示しています。したがって、少し変更したステートメントを提案します(自由に再利用してください;-)):
すべての問題は、正しいレベルの抽象化を使用して解決できます。
あなたの場合には2つの異なる問題があります:
両方が関連しています:
Shape
がsurface()
を特殊な方法で計算できることを理解する問題は誰にもありません。一般的な一般的な行動パターンがある操作を抽象化する場合、2つの選択肢があります。
さらに、このアプローチは、設計レベルで抽象的なカップリング効果をもたらす可能性があります。ある種の新しい特殊な動作を追加するたびに、それを抽象化し、抽象親を変更して、他のすべてのクラスを更新する必要があります。これは、希望する種類の変更の伝播ではありません。そして、それは、(少なくとも設計において)専門化に依存しない抽象化の精神に含まれているわけではありません。
私はあなたのデザインを知りません、そしてそれ以上は助けることができません。おそらくそれは本当に非常に複雑で抽象的な問題であり、これ以上の方法はありません。しかし、オッズは何ですか?過剰一般化の症状はここにあります。もう一度見直す時がきたかもしれません、そして considercomposition over generalization ?
動作がパラメーターの型に切り替わるメソッドを見るときはいつでも、そのメソッドが実際にメソッドパラメーターに属しているかどうかをすぐに検討します。たとえば、次のようなメソッドの代わりに:
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
}
}
私たちはそれをいつ使うべきかを知っているところに振る舞います。 real抽象化を作成します。実装のタイプや詳細を知る必要はありません。状況によっては、このメソッドを元のクラス(O
と呼びます)から型A
に移動し、型B
でオーバーライドする方が理にかなっています。メソッドが一部のオブジェクトでdoIt
と呼ばれる場合は、doIt
をA
に移動し、B
の異なる動作でオーバーライドします。 doIt
が最初に呼び出された場所からデータビットがある場合、またはメソッドが十分な場所で使用されている場合は、元のメソッドを残してデリゲートできます。
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
のタイプを知っている必要があります。通常、これはそこでインスタンスを作成するか、そのタイプのインスタンスをパラメーターとして受け取ることを意味します。ここでO
を取るB
でメソッドを作成すると機能します。コンパイラーはどの方法を選択するかを知っています。このような変更を進めているとき、 間違った抽象化を作成するよりも複製の方が優れています 、少なくとも実際にどこに向かっているのかがわかるまでは。もちろん、この時点までに何を変更しても、実際には完了していないことをお勧めします。
A
とB
の関係をさらに詳しく調べる必要があります。一般に、私たちは 継承よりも構成を優先する とすべきだと言われています。これはすべての場合に当てはまるわけではありませんが、一度調査すると驚くほど多くの場合に当てはまります。B
はA
から継承します。つまり、B
はA
。 B
は、動作が少し異なることを除いて、A
と同じように使用する必要があります。しかし、それらの違いは何ですか?違いにもっと具体的な名前を付けることはできますか? B
はA
ではありませんが、本当にA
にはX
があり、A'
またはB'
になる可能性がありますか?そうした場合、コードはどのように見えるでしょうか?
前述のようにメソッドをA
に移動した場合、X
のインスタンスを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
があり、これはA'
またはB'
のいずれかであると、以前に仮定しました。しかし、この仮定でさえ正しくないかもしれません。 A
とB
の違いが重要なのはここだけですか?もしそうなら、おそらく私たちは少し異なるアプローチを取ることができます。 X
はまだA'
またはB'
のいずれかですが、A
には属していません。気にするのはO.doIt
だけなので、O.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
の方が知識が少ないため、さらに簡単です。それは抽象的なものではありません。
コードベースの重複を減らすことは重要ですが、最初に重複が発生する理由を考慮する必要があります。重複は、抜け出そうとしているより深い抽象化の兆候である可能性があります。
Inheritanceによる抽象化は、かなり醜くなります。典型的なファクトリーを持つ並列クラス階層。リファクタリングは頭痛の種になる可能性があります。そして、その後の開発、あなたがいる場所。
別の方法があります:拡張ポイント、厳密な抽象化、および段階的なカスタマイズ。特定の都市のカスタマイズに基づいて、政府顧客のカスタマイズを1つ言ってください。
警告:残念ながら、これはすべて(またはほとんど)のクラスが拡張されている場合に最適に機能します。あなたには選択肢がない、おそらく小さいでしょう。
この拡張性は、拡張可能なオブジェクトの基本クラスに拡張機能を持たせることで機能します。
void f(CreditorBO creditor) {
creditor.as(AllowedCreditorBO.class).ifPresent(allowedCreditor -> ...);
}
内部的には、拡張クラスによる拡張オブジェクトへのオブジェクトの遅延マッピングがあります。
GUIクラスとコンポーネントの場合、一部は継承されますが、同じ拡張性があります。ボタンなどの追加。
あなたの場合、検証はそれが拡張されているかどうかを調べ、拡張に対してそれ自体を検証する必要があります。 1つの場合にのみ拡張ポイントを導入すると、理解できないコードが追加されてしまい、良くありません。
したがって、解決策はありませんが、現在のコンテキストで作業しようとします。
「隠されたフロー制御」は私には手に余りにも波打つように聞こえます。
コンテキストから取り出された構成または要素は、その特性を持っている可能性があります。
抽象化は良いことです。私はそれらを2つのガイドラインで調整します。
あまりに早く抽象化しない方が良い。抽象化する前に、パターンの例が増えるのを待ちます。 「もっと」はもちろん主観的で、難しい状況に特有のものです。
抽象化が優れているという理由だけで、抽象化のレベルが多すぎないようにします。プログラマーは、コードベースを掘り下げて12レベルの深さになるので、新規または変更されたコードのためにそれらのレベルを頭に留めておかなければなりません。抽象化されたコードへの欲求は、多くの人々が従うのが難しいほど多くのレベルにつながる可能性があります。これは、「忍者が保守するだけ」のコードベースにもつながります。
どちらの場合も、「more」と「too many」は固定数ではありません。場合によります。それが難しいのです。
サンディメッツからのこの記事も好きです
https://www.sandimetz.com/blog/2016/1/20/the-wrong-abstraction
複製は間違った抽象化よりもはるかに安価です
そして
間違った抽象化よりも複製を好む