[5 + 1]リヒター代替原則(2)

序文

オブジェクト指向のSOLID設計原則と、Dimitの規則は、私たちがよく言う5 +1の設計原則です。640?wx_fmt = png&tp = webp&wxfrom = 5&wx_lazy = 1&wx_co = 1↑5つ、もう1つは5 +1です。ハハハ。
これらの6つの設計原則の位置付けは少し圧倒的です。原理的および理論的な指針の重要性の観点から、それらはカプセル化、継承、抽象化、または高い凝集性と低い結合より劣っているため、コードまたはコードレビューを作成するときに、「これを行うべき」または「これを行うべきではない」と説得することは困難です。力の理由。柔軟性と実用的な操作ガイドラインの点で、設計パターンやアーキテクチャパターンより劣っているため、特定のコードが特定の原則に違反していることがわかっても、何が間違っているのか、どのように変更するのかを明確に指摘するのは難しい場合があります。
そこで、ここでは、これら6つの設計原則の「理由」と「方法」について説明します。ちなみに、オブジェクト指向のデザインアイデアの一環として、抽象化、高凝集性と低結合性、カプセル化と継承の多型との関係についてもお話ししたいと思います。



[5 + 1]リヒター置換原理(1)

前の記事を読むために私をクリックしてください。



リスコフ置換原理


よりリッチな置換とオブジェクト指向

「結果指向」と「プロセス制御」は、プロジェクト管理の一般的なアイデアです。
例として製品の需要を取り上げます。
結果指向の観点からは、要件で提案されたビジネス機能を達成するだけで済みます。どの技術を使用し、どのようなデザインを作成するかは、それほど重要ではありません。
プロセス制御の観点から、私たちは最終結果だけでなく、プロジェクトの進捗プロセス、計画の詳細、マイルストーン製品にも責任を負います。
明らかに、この2つは正反対で統一されています。一方の側面を一方的に強調し、他方の役割を無視すると、プロジェクトに不要な問題が発生します。「両手でつかむ」ことによってのみ、「両手が固くなる」ことができます。
リヒター置換の原則は、オブジェクト指向の設計におけるこれら2つのプロジェクト管理のアイデアの具体化です。
結果指向の観点から、インターフェイス定義の入力パラメータと出力パラメータを処理する限り、これで完了です。インターフェイスが「神の実装クラス」、テンプレートクラスの実装クラス、またはプロキシクラスの実装クラスのいずれであるかについては、実際には問題ではありません。
プロセス制御の観点から、私たちは最終的なインターフェースだけでなく、クラス階層、コード品質、読みやすさ、および保守性のプロセスにも責任があります。
この2つは、リヒター置換の原則で統一されています。
リヒター置換の原則では、子クラスが親クラスの動作を変更しないことが必要であり、基本的に同じ結果を得るには同じパラメーターが必要です。ただし、関数の実際の実装者が親であるか子であるかは関係ありません。これは単に「関数の正しい実装が必要であり、コードがLaoWangまたはXiaoLiのどちらによって書かれたかを気にしない」だけではありませんか?結果重視の考え方ではないでしょうか。
同時に、サブクラスと親の両方が同じ結果を取得できるようにするために、リヒター置換の原則は、「サブクラスは親から継承する」操作に関する多くの要件を提唱しています。サブクラスは、親クラスの非抽象メソッドをオーバーライドすることはできませんが、独自の処理メソッドなどを追加することはできます。これらはすべて、プロセス制御の具体的な手段です。
結果指向とプロセス管理および制御を組み合わせることで、プロジェクト管理を適切に行うことができます。同様に、これら2つの考え方の助けを借りて、オブジェクト指向も成功する可能性があります。リヒター置換の原則は、オブジェクト指向を普及させる唯一の方法です。



よりリッチな置換と抽象化

オブジェクト指向の抽象化は安定している必要があり、日々変更することはできないと繰り返し述べてきました。ほとんどの場合、メソッドシグネチャを変更せず、入力および出力パラメータタイプを変更しないなど、コンパイル中のコードの安定性について説明します。リヒター代替原理は、より高い安定性基準、つまり動作中の機能的安定性を提唱しています。
メソッドパラメータの追加など、コンパイル時の安定性を損なうと、コードのコンパイル時にJavaがエラーアラートを表示します。ただし、実行時の安定性を損なうと、「この場所に問題があります」と誰もが耳にすることはありません。ユニットテストや統合テストを何度も行っても、問題を見逃しがちです。
たとえば、私たちのシステムの1つは、バッチ処理にSpringBatch + HIbernateを使用しています。コードと構成を簡素化するために、同僚は、プロセッサーに配置する必要のあるコードをカスタムライターに配置しました。

public class UpdateOverDueDaysWriter extends HibernateItemWriter <Record> {/ ***延滞日数を更新* / @Override public void write(List <Record> items){for(Record r:items){Plan plan = r.getPlan(); //返済レコードの延滞日数を計算するif(plan.getState()== State.RESERVED){//計算ロジックはわずかにint overDueDays = caclulate(); // HibernateのSession自動フラッシュメカニズムの助けを借りて、設定後に自動的に更新できますデータベース内。r.setOverDueDays(overDueDays);}}}}


この同僚のビジョンによると、UpdateOverDueDaysWriterクラスはデータベースを明示的に更新しませんが、r.setOverDueDays(overDueDays)の後、Hibernateのトランザクションマネージャーは自動的にSession.flush()を呼び出して、新しいoverDueDaysを変更できるはずです。値が更新されて保存されます。結局のところ、その親クラスHIbernateItemWriterはこの関数を実装しています。したがって、SpringBatch仕様に準拠していませんが、このクラスの機能は問題ありません。

私は2つの「すべき」を使用しました-「大丈夫なはず」と「問題ないはず」。実際、これら2つの「はず」はすべて失敗しました。データベースのoverDueDaysは更新されていません。
どうしてこれなの?
親クラスHibernateItemWriterの主要なソースコードを見てみましょう。

public class HibernateItemWriter<T> implements ItemWriter<T>, InitializingBean {
 private SessionFactory sessionFactory;
 private boolean clearSession = true;
 @Override  public void write(List<? extends T> items) {    
   doWrite(sessionFactory, items);
    
    // 注意这个地方:这里手动调用了Flush方法
   
   sessionFactory.getCurrentSession().flush();
   
   if(clearSession) {
     
     sessionFactory.getCurrentSession().clear();
   
   }
 
 }
 
}

コメントを追加した行に注意してください。HibernateItemWriterは、Hibernateのトランザクション管理メカニズムに渡すのではなく、ここでSession.flush()メソッドを明示的に呼び出します。理由はわかりませんが、これは重要な注意事項です。Session.flush()メソッドはHIbernateトランザクションマネージャーによって自動的に呼び出されませんが、HIbernateセッションのデータがデータベースに更新されるようにコード表示呼び出しが必要です。

ただし、サブクラスUpdateOverDueDaysWriterがHIbernateItemWriter.wite()メソッドを書き換えると、インターフェイス、メソッドシグネチャ、または戻り値のタイプは変更されませんが、親クラスメソッドでSession.flush()を呼び出すコードは、サブクラスによって完全に破棄されます。サブクラスは親クラスメソッドの機能を書き換えて変更するため、データを更新および保存できなくなります。
言い換えると、サブクラスUpdateOverDueDaysWriterがHIbernateItemWriter.wite()メソッドを書き換えると、Richter置換の原則に違反し、write()メソッドの機能の安定性が損なわれ、最終的に機能の欠如とオンラインのバグが発生します。
さらに恐ろしいのは、別のバッチタスクがこのバッチタスクと同時に開始されたため、コードコンパイル、静的チェック、コードレビュー、ユニットテスト、QAテスト、UATテスト、およびオンライン展開でこの問題が検出されなかったことです。このフィールドも更新されます。後者は実際に更新されて保存されます。2番目のバッチ処理タスクが正常に完了し、削除されたコードがオフラインになったのは2年後のことでしたが、2日が経過し、overDueDaysフィールドが更新されなかったのはなぜですか。
幸い、事件から2日以内にこの問題を発見して解決しました。NationalDayまたはSpringFestival中にオンラインのバグが発生した場合、その結果は想像を絶するものです。
これは、機能の安定性を損なうことについてのひどいことです。オンライン障害が発生する前に問題が見つかることを保証する方法はありません。実際、リヒターの代替規則ではこの問題を解決できないため、修理後から予防前に変更するという考え方が変わりました。抽象関数の安定性は、リヒター置換の原則に準拠する厳密さの程度に正比例するとさえ言えます。



より豊かな置換と高い凝集性と低い結合

リヒター置換の原理は、高凝集性と低結合性とは強く関連していません。リヒター置換の原則に従って、低凝集性と高結合性のコードを書くことも可能です。
ただし、リヒター置換の原則では、クラス間の階層関係をより深く調べ、コードと関数をより適切な位置に配置する必要があります。これを行った後、通常、より高い凝集力と低い結合クラスを得ることができます。
たとえば、次のようなクラスがあるとします。


パブリッククラスSomeServiceはSomeInterfaceを実装します{@Overridepublic Result service(Param param){valid(param); ServiceDO sDo = doService(param); transToResult(SDo);を返します。} protected void valid(Param param)throws ValidException {//略} protected ServiceDO doService(Param param){//略} protected Result transToResult(ServiceDO sDo){//略}}


新しいビジネスを追加する必要がある場合は、サブカテゴリを追加するだけです。


public class OtherService extends SomeService {@Override protected void valid(Param param)throws ValidException {//別の検証ロジック。省略} @Override protected ServiceDO doService(Param param){//別のサービスロジック、省略}}


高凝集性と低結合の観点からは、これはif-elseアプローチよりも優れていますが、それでも「まだ失敗」しています。SomeServiceとOtherServiceの間の不要なサブクラス結合です。SomeServiceが独自のビジネス上の理由でコードの一部を変更した場合、OtherServiceも影響を受けます。

これらの2つのカテゴリは、明らかにリヒター置換の原則に違反しています。サブクラスOtherServiceは、親クラスによって実装されたメソッドvalid()およびdoService()をオーバーライドします。検証ルールを2つのメソッドに同時に追加する場合は、明らかに、親クラスSomeServiceを変更することは無意味です。
リヒター置換原理のガイダンスに従って、クラス構造を次のように調整できます。


パブリック抽象クラスAbstractServiceはSomeInterfaceを実装します{@Overridepublic Result service(Param paran){valid(param); ServiceDO sDo = doService(param); transToResult(SDo);を返します。} protected abstract void valid(Param param)throws ValidExcption; 保護された抽象ServceDOdoService(Param param);


このようなクラス階層は、Richter置換原則の要件にさらに一致しています。同時に、SomeServiceとOtherServiceの間の結合も低くなります。コードの量を増やすことに加えて、誰もが満足しています。



よりリッチな置換とカプセル化継承多型

リヒターの置換原理と継承および多形性との関係は言うまでもありません。それは、継承および多形性の「ベストプラクティス」として説明することができます。しかし、それとパッケージングの関係はそれほど明白ではありません。
「カプセル化」に関しては、通常、パブリック/保護/プライベートなどの可視性修飾子について考えます。実際、これらはカプセル化ツールであり、カプセル化自体ではありません。Zenマスターが言ったように、これは「仏教の問題」ではなく、単なる「仏教への道」です。
「カプセル化」の場合、いわゆる「仏になるもの」とは、「封印」と「設置」のことです。このクラスでは、クラスに属するものは「封印」され、クラスに属するものは「インストール」されます。このクラスでは。これらの2つのポイントが達成される限り、「カプセル化」は達成されます。
では、リヒターの代替原則は何を「封印」するのでしょうか。答えは「親クラスの非抽象メソッド」です。この非抽象メソッドの修飾子がパブリック、デフォルト、または保護されている場合でも、この非抽象メソッドが最終メソッドでなくても、リヒター置換の原則により、サブクラスがそれをオーバーライドすることは禁止されています。これも一種の「封印」ではないですか?
リヒターの代替原則は、「エンクロージャー」の原則であると同時に「インストール」の原則でもあります。サブクラスは親クラスの非抽象メソッドをオーバーライドできないことを規定します。また、クラス内の非抽象メソッドがサブクラスによってオーバーライドされる場合、このメソッドを現在のクラスに配置しないことも規定します。このメソッドの抽象定義を「インストール」するために新しいクラスを定義する必要があります。同時に、抽象メソッドの2つの異なる実装を「インストール」するために、元の2つのクラスにこの新しいクラスを継承させます。
しかし、リヒターの代用原則は、「封印」であろうと「設置」であろうと、基本的には人が規制することしかできず、文法やコンパイラなどのツールを使って制約を加えることは難しい。これはおそらく、リヒター置換の原則が実際にほとんど言及されていないもう1つの理由です。



リヒター交換およびその他の設計原則
リヒター交換および単一の責任

リヒター置換原理とカプセル化の関係を理解することで、リヒター置換原理と単一責任原理の関係が実際に明らかになります。つまり、メソッドを子クラスから親クラスに、または2つのクラスから親子クラスが兄弟クラスに変換されるとき、私たちはリヒター置換の原則に従うだけでなく、単一の責任の原則に従います。
前のコードを例として取り上げます。サブクラスが親クラスの非抽象メソッドを書き換えると、それらのクラス構造が次の図に示されます。

640?wx_fmt = png&tp = webp&wxfrom = 5&wx_lazy = 1&wx_co = 1

上記の構造では、ClassAは2つの責任を負います。それは、独自のビジネス機能と、ClassBのプロセステンプレートを定義する責任です。また、ClassBは、独自のビジネス機能とClassAの他のビジネス機能の2つの責任も負います。明らかに、彼らは両方とも彼ら自身ではないいくつかの機能的責任を引き受けます。したがって、これら2つのカテゴリは、リヒター代替原則に準拠していないだけでなく、単一責任原則にも準拠していません。
リヒター置換原理の要件に従う場合は、上記のクラス構造を次のように変換します。

640?wx_fmt = png&tp = webp&wxfrom = 5&wx_lazy = 1&wx_co = 1

変換後、ClassCは特定のビジネス機能ではなく、プロセステンプレートを定義する責任のみを引き受けます。ClassAは独自のビジネス機能のみを引き受け、プロセステンプレートを定義する責任を引き受けなくなります。ClassBも独自のビジネス機能のみを引き受けます。 ClassAのビジネス機能は含まれなくなりました。現時点では、次のように言うことができます。これらの3つのカテゴリは、リヒターの代替原則に従うだけでなく、単一の責任の原則にも準拠しています。



よりリッチな交換と開閉

開閉の原則は、「新しい追加を開き、変更を閉じる」ことを要求します。リヒター置換の原則は、親クラスと子クラスの間でこの原則を改良します。継承レベルを追加したり、「新しい追加を受け入れる」サブクラスメソッドを追加したりできますが、親クラスの実装を変更することはできません。これが「変更間近」です。



以前に抽象化について説明したときに、抽象化は階層的であると述べました。継承を通じて、抽象を「垂直に」複数のレベルに分割できます。多態性の助けを借りて、抽象を「水平に」同じレベル内の複数の実装クラスに分割できます。この垂直方向と水平方向の間で、適切に処理されない場合、複雑な親子結合によってコードが混乱します。リヒターの代替原理の助けを借りて、抽象的な複雑さを徐々に分割し、それを目に見えないものに排除することができます。
したがって、リヒターの代替原則は少し混乱し、使用するのが難しいですが、それは確かに魔法の武器であり、それを習得するために私たちの時間の価値があります。
もちろん、複雑な抽象化を複数の単純な抽象化に分割するなど、抽象的な複雑さに対処する他の方法もあります。ただし、これは次の章の内容であるインターフェイス分離の原則です。では、次回は分解してみましょう。

qrcode?scene = 1000004&size = 102&__ biz = MzUzNzk0NjI1NQ ==&mid = 2247488431&idx = 1&sn = 9462f09f48b68e3ec97119a8e9d012aa&send_time =


おすすめ

転載: blog.51cto.com/winters1224/2540777