ステレオタイプエッセイの設計原則

単一義務の原則

第 1 章 単一責任の原則

単一責任の原則では、インターフェイスまたはクラスが変更を引き起こす理由が 1 つだけであることが必要です。つまり、インターフェイスまたはクラスの責任は 1 つだけであり、1 つのことに対して責任を負います。

1.1 私は「牛」ですが、複数の立場に就くことはできますか?

単一責任原則の英語名は、Single Responsibility Principle、略して SRP です。この設計原則には議論の余地がありますが、他人と議論したり、怒ったり、口論したりしたい場合には、この原則は実証済みです。あなたが上司で、インターフェイスやクラスが何らかの方法で設計されていることを確認した場合は、「あなたが設計したクラスは SRP 原則に準拠していますか?」と尋ねることができます。あなたは、「上司は確かに賢明だ」と心の中で思ってください。この原則のどこが議論の余地があるのでしょうか? それは責任の定義、クラスの責任とは何か、クラスの責任をどのように分割するかです。単一責任原則とは何かを説明するために例を挙げてみましょう。

プロジェクトを実行している限り、ユーザー、組織、およびロール管理のこれらのモジュールに触れる必要があります。基本的には、RBAC モデル (ロールベースのアクセス制御、ロールベースのアクセス制御) を使用し、権限付与を完了します。ロールの割り当てとキャンセルによるユーザー権限の管理、およびアクションの主体 (ユーザー) をリソースの動作 (権限) から切り離すキャンセルは、確かに優れたソリューションです。ここで説明するのは、ユーザー管理、ユーザー情報の変更、組織の追加 (個人が複数の組織に所属する)、ロールの追加などです。ユーザーには維持する必要がある情報と動作が非常に多いため、これらをインターフェイスに記述します。これらはすべてユーザー管理クラスです。まず、図 1-1 に示すクラス図を見てみましょう。

画像

図1-1 利用者情報保持クラス図

クラス図は簡単すぎます。このインターフェースの設計に問題があることは、ジュニア プログラマーでもわかると思います。ユーザーの属性とユーザーの行動が分離されていません。これは重大な間違いです。このインターフェースは実にめちゃくちゃに設計されており、ユーザーの情報をBO(Business Object、ビジネスオブジェクト)に抽出し、動作をBiz(Business Logic、ビジネスロジック)に抽出するという考え方に従えば、クラスは図 1-2 に示すように、図を修正する必要があります。

画像

図 1-2 役割分担後のクラス図

2 つのインターフェイスに再パッケージ化された IUserBO はユーザー属性を担当します。簡単に言うと、IUserBO の責任はユーザー属性情報の収集とフィードバックであり、IUserBiz はユーザーの行動を担当し、ユーザー情報の保守と変更を完了します。これは私が実際の仕事で使用する User クラスとはまだ違う、と言いたいかもしれません。心配しないで、2 つのインターフェイスに分割する方法を見てみましょう。OK、今はインターフェイス指向でプログラミングしているので、この UserInfo オブジェクトを生成した後は、もちろん IUserBO インターフェイスとして使用できます。使用する場所に応じて、IUserBiz インターフェイスとしても使用できます。コードリスト1-1に示すように、ユーザー情報を取得したい場合はIUserBOの実装クラスとして、ユーザー情報を保持したい場合はIUserBizの実装クラスとして使用できます。

コードリスト 1-1 役割分担後のコード例

......
IUserInfo userInfo = new UserInfo();
//我要赋值了,我就认为它是一个纯粹的BO
IUserBO userBO = (IUserBO)userInfo;
userBO.setPassword("abc");
//我要执行动作了,我就认为是一个业务逻辑类
IUserBiz userBiz = (IUserBiz)userInfo;
userBiz.deleteUser();
......

これは確かに事実であり、問​​題は解決されましたが、先ほどのアクションを分析してみましょう。なぜインターフェースを 2 つに分割する必要があるのでしょうか。実際、実際の使用では、2 つの異なるクラスまたはインターフェイスを使用することを好みます: 1 つは IUserBO で、もう 1 つは IUserBiz です。クラス図を図 1-3 に示します。

画像

図 1-3 プロジェクトでよく使用される SRP クラス図

インターフェイスを 2 つのインターフェイスに分割する上記のアクションは、単一責任の原則に基づいています。では、単一責任の原則とは何でしょうか? 単一責任原則の定義は、クラス変更の理由は 1 つだけである必要があるということです。

1.2 従来の考え方を打ち破るユニークなスキル

この説明の時点で、あなたはすでに非常に軽蔑していると思います。「カット!なぜそんな簡単なことについて話さなければならないのですか?!」 さて、複雑なことについて話しましょう。SRP の本来の説明は次のとおりです。

クラスを変更する理由は決して 1 つだけであってはなりません。

この文章は中学生でも理解できるので多くは言いませんが、理解することと実行することは別のことです。上記の例は理解しやすいですが、実際のプロジェクトでは誰もがすでにこれを行っているので、次の例が理解しやすいかどうかを確認してください。現代人にとって電話は欠かすことのできないものです。電話をかける際には、ダイヤルする、発信する、応答する、切るという 4 つのプロセスが発生します。そしてインターフェイスを記述します。そのクラス図は図 1-4 に示されています。

画像

図 1-4 電話のクラス図

iPhone を傷つけるつもりはありません。同じ名前が付いているのはまったくの偶然です。コード リスト 1-2 に示すように、このプロセスのコードを見てみましょう。

コードリスト 1-2 呼び出しプロセス

public Interface IPhone { //電話にダイヤルするpublic void digial(String PhoneNumber); //電話をかけるpublic void chat(Object o); //通話後、電話を切るpublic void Hangup(); }






実装クラスも比較的単純なのでこれ以上は書きませんが、このインターフェースに問題がないか見てみましょう。ほとんどの読者は「これで問題ない!」とおっしゃると思いますが、私も以前そうしていましたし、ある本にも書いてありましたし、他のソースコードもこう書いてあります!はい、このインターフェイスは完璧に近いです。はっきりと見ると、「近い」です。単一責任の原則では、インターフェイスまたはクラスが変更を引き起こす理由が 1 つだけであること、つまり、インターフェイスまたはクラスの責任は 1 つだけであり、1 つのことだけを担当する必要があります。上記のインターフェイスが 1 つのことだけを担当しているかどうかわかりますか? 変化の原因は一つだけなのでしょうか?それはないようです!

iPhone インターフェイスには 1 つの役割だけではなく、2 つの役割があります。1 つはプロトコル管理で、もう 1 つはデータ送信です。dial() と Hangup() の 2 つのメソッドは、それぞれダイヤルアップと電話の切断を担当するプロトコル管理を実装し、chat() は、通話内容をアナログ信号またはデジタル信号に変換して相手に伝えるデータ送信を実装します。次に、相手から送信された信号を私たちが理解できる言語に復元します。この問題は次のように考えることができます。プロトコル接続の変更により、このインターフェイスまたは実装クラスも変更されるでしょうか。しましょう!データ送信の変更 (電話は通話できるだけでなく、インターネット サーフィンも可能です) の変更により、このインターフェイスまたは実装クラスも変更されるでしょうか? しましょう!それは簡単です。クラスチェンジの理由は 2 つあります。これら 2 つの責任は相互に影響しますか? 電話ダイヤルの場合、テレコム プロトコルかネットコム プロトコルかに関係なく、接続できれば十分です。電話が接続された後も、どのようなデータが送信されているかを気にする必要がありますか? このような分析により、クラス図上の IPhone インターフェイスには 2 つの責任が含まれており、これら 2 つの責任の変更は相互に影響を及ぼさないことが判明したため、図 1-5 に示すように、それを 2 つのインターフェイスに分割することを検討します。

画像

図 1-5 明確な責任を伴う電話クラス図

画像

図 1-6 明確な責任を伴う簡潔で明確な電話クラス図

这个类图看上去有点复杂了,完全满足了单一职责原则的要求,每个接口职责分明,结构清晰,但是我相信你在设计的时候肯定不会采用这种方式,一个手机类要把ConnectionManager和DataTransfer组合在一块才能使用。组合是一种强耦合关系,你和我都有共同的生命期,这样的强耦合关系还不如使用接口实现的方式呢,而且还增加了类的复杂性,多了两个类。经过这样的思考后,我们再修改一下类图,如图1-6所示。

这样的设计才是完美的,一个类实现了两个接口,把两个职责融合在一个类中。你会觉得这个Phone有两个原因引起变化了呀,是的,但是别忘记了我们是面向接口编程,我们对外公布的是接口而不是实现类。而且,如果真要实现类的单一职责,这个就必须使用上面的组合模式了,这会引起类间耦合过重、类的数量增加等问题,人为地增加了设计的复杂性。

通过上面的例子,我们来总结一下单一职责原则有什么好处:

● 类的复杂性降低,实现什么职责都有清晰明确的定义;

● 可读性提高,复杂性降低,那当然可读性提高了;

● 可维护性提高,可读性提高,那当然更容易维护了;

● 变更引起的风险降低,变更是必不可少的,如果接口的单一职责做得好,一个接口修改只对相应的实现类有影响,对其他的接口无影响,这对系统的扩展性、维护性都有非常大的帮助。

電話機の例を見た後、以前のデザインに何か問題があったのか考えてみませんか? いいえ、いいえ、自分の技術力を疑う必要はありません。単一責任原則の最も難しい分割は責任です。一つの責任は一つのインターフェースですが、問題は「責任」の定量的な基準がないことです。これらの責任はどのように改善されるべきでしょうか? 改良後のインターフェースやクラスはありますか? これらはすべて実際のプロジェクトから考慮する必要がある 機能的には、iPhone インターフェースを定義することに問題はありません 電話の機能を実現しており、設計は非常にシンプルです インターフェースと実装クラスは 1 つだけです実際のプロジェクトでは誰もがこれを行うと思います。プロジェクトには考慮すべき変数と不変、およびそれに関連する利益とコストの比率があるため、iPhone インターフェイスを設計することにも問題はないかもしれません。しかし、純粋に「学問」理論だけで分析すると問題があり、一つのインターフェースに変えられる理由が2つあり、将来の変化に対するリスクを伴う。将来、アナログ電話からデジタル電話にアップグレードした場合、当社が提供するインターフェース iPhone を変更する必要がありますか? インターフェイスの変更は他の Invoker クラスに大きな影響を与えますか?

単一責任の原則は、「責任」または「変更の理由」を使用してインターフェイスまたはクラスが適切に設計されているかどうかを測定する、プログラムを作成するための標準を提案していますが、「責任」と「変更の理由」は測定可能ではなく、状況によって異なることに注意してください。プロジェクトからプロジェクトへ。これは環境によって異なります。

1.3 私は単純だから幸せです

インターフェイスの場合は単一で設計する必要がありますが、実装クラスの場合は多くの側面を考慮する必要があります。単一責任の原則を機械的に適用するとクラスが急増して保守に手間がかかりますし、責任が細分化されすぎるとシステムが人為的に複雑になります。1 つのクラスで実現できる動作を強制的に 2 つのクラスに分割し、集約や組み合わせによって結合することで、人為的にシステムの複雑さが生まれます。したがって、原則は死んだ、人々は生きている、この文は理にかなっています。

单一职责原则很难在项目中得到体现,非常难,为什么?在国内,技术人员的地位和话语权都比较低,因此在项目中需要考虑环境,考虑工作量,考虑人员的技术水平,考虑硬件的资源情况,等等,最终妥协的结果是经常违背单一职责原则。而且,我们中华文明就有很多属于混合型的产物,比如筷子,我们可以把筷子当做刀来使用,分割食物;还可以当叉使用,把食物从盘子中移动到口中。而在西方的文化中,刀就是刀,叉就是叉,你去吃西餐的时候这两样肯定都是有的,刀就是切割食物,叉就是固定食物或者移动食物,分工很明晰。这种文化的差异很难一步改造过来,但是我相信随着技术的深入,单一职责原则必然会深入到项目的设计中,而且这个原则是那么的简单,简单得不需要我们更加深入地思考,单从字面上大家都应该知道是什么意思,单一职责嘛!

单一职责适用于接口、类,同时也适用于方法,什么意思呢?一个方法尽可能做一件事情,比如一个方法修改用户密码,不要把这个方法放到“修改用户信息”方法中,这个方法的颗粒度很粗,比如图1-7中所示的方法。

画像

图1-7 一个方法承担多个职责

在IUserManager中定义了一个方法changeUser,根据传递的类型不同,把可变长度参数changeOptions修改到userBO这个对象上,并调用持久层的方法保存到数据库中。在我的项目组中,如果有人写了这样一个方法,我不管他写了多少程序,花了多少工夫,一律重写!原因很简单:方法职责不清晰,不单一,不要让别人猜测这个方法可能是用来处理什么逻辑的。比较好的设计如图1-8所示。

通过类图可知,如果要修改用户名称,就调用changeUserName方法;要修改家庭地址,就调用changeHomeAddress方法;要修改单位电话,就调用changeOfficeTel方法。每个方法的职责非常清晰明确,不仅开发简单,而且日后的维护也非常容易,大家可以逐渐养成这样的习惯。

画像

图1-8 一个方法承担一个职责

したがって、インターフェイス、クラス、メソッドに対して単一責任の原則を使用すると、自分だけでなく、プロジェクト チームのメンバーも簡単に楽しく開発でき、上司も変更によって生じる作業負荷が軽減されます。が削減され、人員や資金の無駄な消費が削減されます。もちろん、昇進が待っているかもしれないので、あなたが一番幸せかもしれません。

要約する

単一の責任はインターフェイス、クラスだけでなくメソッドにも適用されます。どういう意味ですか? メソッドはユーザーのパスワードを変更するメソッドなど、できるだけ 1 つのことを実行します。このメソッドを「ユーザー情報の変更」メソッドに含めないでください。このメソッドの粒度は非常に粗いです。

単一責任の原則に関して、私の提案は、インターフェイスには単一の責任が必要であり、変更の理由が 1 つだけになるようにクラスの設計を設計する必要があるということです。

はい、クラスの単一の責任は確かに多くの要因によって制限されます。純粋に言えば、この原則は非常に優れていますが、実際には現実的な困難があります。プロジェクトの期間、コスト、担当者の技術レベル、ハードウェアの条件、ネットワークを考慮する必要があります。条件、さらには場合によっては政府の政策、独占協定、その他の要因も考慮する必要があります。例えば、2004年に私が取り組んだプロジェクトでは暗号化処理を行っていましたが、Aさんは「何も気にする必要はない、このAPIを呼び出すだけでいい、通信プロトコルや例外処理などを考える必要もない」と言っていました。 、安全な接続など。

リスコフ置換原理

第 2 章 リスコフ置換原理

2.1 父と息子の愛憎関係

オブジェクト指向言語では、継承は不可欠かつ優れた言語メカニズムであり、次のような利点があります。

● コード共有により、クラス作成の作業負荷が軽減され、各サブクラスは親クラスのメソッドと属性を持ちます。

● コードの再利用性を向上させます。

● サブタイプは親のタイプに似ていても、親のタイプとは異なる場合があります。「龍は龍を生み、フェニックスはフェニックスを生み、ネズミは穴を開けるために生まれる」ということは、子が父親の「種」を持っていることを意味します。世界に二つの同じ葉がある。」は、息子と父の違いを示しています。

● コードの拡張性を高めるために、親クラスのメソッドを実装することで「やりたい放題」にできる 多くのオープンソースフレームワークの拡張インターフェースは、親クラスを継承することで完成していることをご存知ですか?

● 製品またはプロジェクトのオープン性を向上させます。

自然界のあらゆるものには長所と短所があり、卵でも骨を拾うことがありますが、遺伝する短所は次のとおりです。

● 継承は侵襲的です。継承する限り、親クラスのすべてのプロパティとメソッドが必要です。

● コードの柔軟性が低下します。サブクラスの自由な世界にはより多くの制約があるように、サブクラスは親クラスのプロパティとメソッドを持たなければなりません。

● 增强了耦合性。当父类的常量、变量和方法被修改时,需要考虑子类的修改,而且在缺乏规范的环境下,这种修改可能带来非常糟糕的结果——大段的代码需要重构。

Java使用extends关键字来实现继承,它采用了单一继承的规则,C++则采用了多重继承的规则,一个子类可以继承多个父类。从整体上来看,利大于弊,怎么才能让“利”的因素发挥最大的作用,同时减少“弊”带来的麻烦呢?解决方案是引入里氏替换原则(Liskov Substitution Principle,LSP),什么是里氏替换原则呢?它有两种定义:

● 第一种定义,也是最正宗的定义:If for each object o1 of type S there is an object o2 of type T such that for all programs P defined in terms of T,the behavior of P is unchanged when o1 is substituted for o2 then S is a subtype of T.(如果对每一个类型为S的对象o1,都有类型为T的对象o2,使得以T定义的所有程序P在所有的对象o1都代换成o2时,程序P的行为没有发生变化,那么类型S是类型T的子类型。)

● 第二种定义:Functions that use pointers or references to base classes must be able to use objects of derived classes without knowing it.(所有引用基类的地方必须能透明地使用其子类的对象。)

第二个定义是最清晰明确的,通俗点讲,只要父类能出现的地方子类就可以出现,而且替换为子类也不会产生任何错误或异常,使用者可能根本就不需要知道是父类还是子类。但是,反过来就不行了,有子类出现的地方,父类未必就能适应。

2.2 纠纷不断,规则压制

里氏替换原则为良好的继承定义了一个规范,一句简单的定义包含了4层含义。

1.子类必须完全实现父类的方法

我们在做系统设计时,经常会定义一个接口或抽象类,然后编码实现,调用类则直接传入接口或抽象类,其实这里已经使用了里氏替换原则。我们举个例子来说明这个原则,大家都打过CS吧,非常经典的FPS类游戏,我们来描述一下里面用到的枪,类图如图2-1所示。

画像

图2-1 CS游戏中的枪支类图

枪的主要职责是射击,如何射击在各个具体的子类中定义,手枪是单发射程比较近,步枪威力大射程远,机枪用于扫射。在士兵类中定义了一个方法killEnemy,使用枪来杀敌人,具体使用什么枪来杀敌人,调用的时候才知道,AbstractGun类的源程序如代码清单2-1所示。

代码清单2-1 枪支的抽象类

public abstract class AbstractGun {
    
    
   //枪用来干什么的?杀敌!
   public abstract void shoot();
}

手枪、步枪、机枪的实现类如代码清单2-2所示。

代码清单2-2 手枪、步枪、机枪的实现类

public class Handgun extends AbstractGun {
    
     
   //手枪的特点是携带方便,射程短
   @Override
   public void shoot() {
    
    
       System.out.println("手枪射击...");
   }
}
public class Rifle extends AbstractGun{
    
     
   //步枪的特点是射程远,威力大
   public void shoot(){
    
    
       System.out.println("步枪射击...");
   }
}
public class MachineGun extends AbstractGun{
    
      
   public void shoot(){
    
    
       System.out.println("机枪扫射...");
   }
}

有了枪支,还要有能够使用这些枪支的士兵,其源程序如代码清单2-3所示。

代码清单2-3 士兵的实现类

public class Soldier {
    
    
   //定义士兵的枪支
   private AbstractGun gun;
   //给士兵一支枪
   public void setGun(AbstractGun _gun){
    
    
       this.gun = _gun; 
   }
   public void killEnemy(){
    
    
       System.out.println("士兵开始杀敌人...");
       gun.shoot();
   }
}

注意粗体部分,定义士兵使用枪来杀敌,但是这把枪是抽象的,具体是手枪还是步枪需要在上战场前(也就是场景中)前通过setGun方法确定。场景类Client的源代码如代码清单2-4所示。

代码清单2-4 场景类

public class Client {
    
    
   public static void main(String[] args) {
    
    
       //产生三毛这个士兵
       Soldier sanMao = new Soldier();
       //给三毛一支枪
       sanMao.setGun(new Rifle());
       sanMao.killEnemy();
   }
}

有人,有枪,也有场景,运行结果如下所示。

士兵开始杀敌人…

步枪射击…

在这个程序中,我们给三毛这个士兵一把步枪,然后就开始杀敌了。如果三毛要使用机枪,当然也可以,直接把sanMao.setGun(new Rifle())修改为sanMao.setGun(new MachineGun())即可,在编写程序时Solider士兵类根本就不用知道是哪个型号的枪(子类)被传入。

注意 在类中调用其他类时务必要使用父类或接口,如果不能使用父类或接口,则说明类的设计已经违背了LSP原则。

我们再来想一想,如果我们有一个玩具手枪,该如何定义呢?我们先在类图2-1上增加一个类ToyGun,然后继承于AbstractGun类,修改后的类图如图2-2所示。

画像

图2-2 枪支类图

まず、おもちゃの銃は射撃には使えない、人を殺すことはできないと考えており、そのことを射撃方法に書くべきではないと考えます。新しく追加された ToyGun のソース コードをリスト 2-5 に示します。

コードリスト 2-5 トイガンのソースコード

public class ToyGun extends AbstractGun {
    
    
   //玩具枪是不能射击的,但是编译器又要求实现这个方法,怎么办?虚构一个呗!
   @Override
   public void shoot() {
    
    
       //玩具枪不能射击,这个方法就不实现了
   }
}

新しいサブクラスの導入により、このクラスはシーン クラスでも使用され、クライアントはわずかに変更されています (リスト 2-6 にソース コードを示します)。

コードリスト 2-6 シーンクラス

public class Client {
    
       
   public static void main(String[] args) {
    
    
       //产生三毛这个士兵
       Soldier sanMao = new Soldier();
       sanMao.setGun(new ToyGun());
       sanMao.killEnemy();
   }
}

太字の部分を修正し、おもちゃの銃をサンマオに渡して敵を倒す コード操作の結果は以下の通りです。

兵士たちは敵を殺し始めます...

壊れていて、兵士たちは敵を倒すためにおもちゃの銃を持っていますが、弾は撃てません。CS の試合でこのようなことが起こったら、ヘッドショットを待って、惨めに倒れるのを見ることになります。この場合、ビジネス コール クラスに問題があり、通常のビジネス ロジックが実行できなくなっていることがわかりました。では、どうすればよいでしょうか? 扱いは簡単ですが、解決策は 2 つあります。

● Soldierクラスにinstanceofの判定を追加 おもちゃの銃であれば敵を倒すためには使用しません。この方法で問題は解決できますが、プログラムにクラスを追加するたびに、この親クラスに関連するすべてのクラスを変更する必要があることを知っておく必要があります。あなたの製品でこの問題が発生した場合、そのようなバグは修正されているため、この親クラスに関連するすべてのクラスは判定を追加する必要があり、顧客は飛び上がってあなたと一緒に戦う必要があります。それでも顧客に忠実になってもらいたいですか? 明らかに、この計画は拒否されました。

● ToyGun は継承から脱却して独立した親クラスを確立し、コードの再利用を実現するために、図 2-3 に示すように AbastractGun との関連付け委任関係を確立できます。

画像

図 2-3 玩具銃と実銃の区別のクラス図

たとえば、音と形状を AbstractGun に委ねることを AbstractToy で宣言できます。たとえば、シミュレートされた銃は実際の銃と同じ形状と音を持ち、2 つの基本クラスの下のサブクラスは影響を与えることなく自由に拡張できます。お互い。

Javaの基礎知識で継承について触れていますが、Javaの3大特徴としてカプセル化、継承、ポリモーフィズムがあります。継承とは、親クラスのメソッドとプロパティを持つように指示することで、親クラスのメソッドをオーバーライドできます。継承の原則によれば、おもちゃの銃が AbstractGun を継承することはまったく問題ありません。おもちゃの銃も銃ですが、特定のアプリケーション シナリオでは、サブクラスが親のビジネスを完全に実現できるかどうかを考慮する必要があります。クラス、そうでない場合は、銃で敵を殺すが、それがおもちゃの銃であることが判明する、上記のようなジョークが発生します。

サブクラスが親クラスのメソッドを完全に実装できない場合、または親クラスの一部のメソッドがサブクラス内で「歪められている」場合は、親子継承関係を解除し、依存関係、集約、合成、および依存関係を使用することをお勧めします。継承の代わりに他の関係。

2. サブクラスは独自の個性を持つことができます

もちろん、サブクラスは独自の動作や外観、つまりメソッドやプロパティを持つことができますが、なぜここでそれについて言及するのでしょうか? それは、リスコフ置換原則を積極的に使用することはできますが、その逆はできないからです。サブクラスが出現する場合、親クラスが必ずしも有能であるとは限りません。先ほどの銃の例を例に挙げると、AK47、AUG スナイパーライフルなど、ライフルにはいくつかの「うるさい」モデルがあります。これら 2 種類の銃を導入した後のライフルのサブクラス図は、図 2 に示されています。 4. 表示します。

画像

図 2-4 AK47 と AUG を追加した後のライフルのサブクラス図

非常に単純で、AUG は Rifle クラスを継承し、スナイパー (Snipper) は AUG スナイパー ライフルを直接使用します。ソース コードはリスト 2-7 に示されています。

コードリスト 8 月 2 ~ 7 日のスナイパーライフルのソースコード

public class AUG extends Rifle {
    
    
   //狙击枪都携带一个精准的望远镜
   public void zoomOut(){
    
    
       System.out.println("通过望远镜察看敌人...");
   }
   public void shoot(){
    
    
       System.out.println("AUG射击...");
   }
}

スナイパーガンがあるところにはスナイパーも存在する スナイパークラスのソースコードをリスト2-8に示します。

コード リスト 2-8 AUG sniper クラスのソース コード

public class Snipper {
    
       
   public void killEnemy(AUG aug){
    
    
       //首先看看敌人的情况,别杀死敌人,自己也被人干掉
       aug.zoomOut();
       //开始射击
       aug.shoot();
   }
}

スナイパー、なぜスニッパーと呼ばれるのですか?シギはシギと訳されますが、これは「シギとハマグリが戦って、漁師が勝つ」に出てくる鳥です。イギリスの貴族はインドに狩りに行ったところ、シギがとても賢いことに気付きました。人が近づくと飛び去ってしまいました。カモフラージュするしかなかった、長距離精密射撃、それでスニッパーが誕生した。

スナイパーは敵を殺すために狙撃銃を使用します。ビジネス シナリオにおける Client クラスのソース コードをコード リスト 2-9 に示します。

コード リスト 2-9 スナイパーは AUG を使用して敵を殺す

public class Client {
    
       
   public static void main(String[] args) {
    
    
       //产生三毛这个狙击手
       Snipper sanMao = new Snipper();
       sanMao.setRifle(new AUG());
       sanMao.killEnemy();
   }
}

スナイパーは G3 を使用して敵を殺害し、実行結果は次のとおりです。

双眼鏡で敵を観察…

8月の撮影…

ここではシステムがサブクラスを直接呼び出していますが、スナイパーは銃への依存度が高く、別のモデルの銃を変更するのはもちろん、同じモデルの銃を変更しても射撃に影響するため、ここではサブクラスを直接渡しています。このとき、親クラスを直接使って渡すことはできるのでしょうか?コード リスト 2-10 に示すように、Client クラスを変更します。

コード リスト 2-10 では、親クラスをパラメータとして使用しています

public class Client {
    
       
   public static void main(String[] args) {
    
    
       //产生三毛这个狙击手
       Snipper sanMao = new Snipper();
       sanMao.setRifle((AUG)(new Rifle()));
       sanMao.killEnemy();
   }
}

表示が機能せず、実行時に java.lang.ClassCastException がスローされます。これは、ダウンキャストが安全ではないとよく言われることでもあります。リスコフ置換原則の観点から見ると、サブクラスが出現する場所です。親クラスはそうでない場合があります。必然的に現れる。

3. 親クラスのメソッドをオーバーライドまたは実装するときに入力パラメータを拡大できる

メソッド内の入力パラメーターは前提条件と呼ばれますが、これは何を意味しますか? Web サービス開発を行ったことがある人なら誰でも、「契約ファースト」の原則があることを知っているはずです。つまり、最初に WSDL インターフェイスを定義し、両者の間で開発契約を策定し、次にそれを個別に実装します。リスコフ置換原則では、親クラスまたはインターフェイスであるコントラクトの定式化も必要です。この設計方法は、Design by Contract (コントラクト設計) とも呼ばれ、リスコフ置換原則と同じ効果があります。契約成立時には、事前条件と事後条件が同時に定められますが、事前条件とは、履行してほしい場合にはこちらの条件を満たさなければならないという意味で、事後条件とは、契約後にフィードバックが必要という意味です。実行の基準は何ですか? これは理解するのがさらに難しいため、例を見てみましょう。リスト 2-11 に示すように、最初に Father クラスを定義します。

コードリスト 2-11 父クラスのソースコード

public class Father {
    
       
   public Collection doSomething(HashMap map){
    
    
       System.out.println("父类被执行...");  
       return map.values();
   }
}

このクラスは非常に単純で、HashMap を Collection 型に変換し、サブクラスを定義します。ソース コードをリスト 2-12 に示します。

コードリスト 2-12 サブクラスのソースコード

public class Son extends Father {
    
    
   //放大输入参数类型
   public Collection doSomething(Map map){
    
    
       System.out.println("子类被执行...");
       return map.values();
   }
}

太字部分は親クラスのメソッド名と同じですが、親クラスをオーバーライド(Override)するメソッドではないことに注意してください。@Override を追加して試してみると、エラーが報告されます。なぜですか? メソッド名は同じですが、メソッドの入力パラメータが異なりますが、上書きではないので、これは何ですか?オーバーロードだよ!問題ありません。クラス内にない場合、オーバーロードすることはできませんか? 継承とはどういう意味ですか? サブクラスには親クラスのすべてのプロパティとメソッドが含まれます。メソッド名は同じですが、入力パラメータの型が異なります。もちろん、オーバーロードされます。親クラスとサブクラスの両方が宣言されており、シーン クラスの呼び出しをリスト 2-13 に示します。

コードリスト 2-13 シーンクラスのソースコード

public class Client {
    
    
   public static void invoker(){
    
    
       //父类存在的地方,子类就应该能够存在
       Father f = new Father();
       HashMap map = new HashMap();
       f.doSomething(map);
   }
   public static void main(String[] args) {
    
    
       invoker();
   }
}

コードを実行した後の結果は次のようになります。

親クラスが実行されます...

リスト 2-14 に示すように、リスコフ置換原則に従って、親クラスが出現する場所にはサブクラスが出現する可能性があるため、上記の太字部分をサブクラスに変更します。

コード リスト 2-14 サブクラスが親クラスを置き換えた後のソース コード

public class Client {
    
    
   public static void invoker(){
    
    
       //父类存在的地方,子类就应该能够存在
       Son f =new Son();
       HashMap map = new HashMap();
       f.doSomething(map);
   }
   public static void main(String[] args) {
    
    
       invoker();
   }
}

結果は同じですが、何が起こっているか理解できますか? 親クラスのメソッドの入力パラメータがHashMap型、サブクラスの入力パラメータがMap型の場合は実行されません。そうです、サブクラスのメソッドを機能させたい場合は、スーパークラスのメソッドをオーバーライドする必要があります。このように考えると、親クラスが Invoker クラスに関連付けられ、親クラスのメソッドが呼び出されます。サブクラスはこのメソッドをオーバーライドしたり、このメソッドをオーバーロードしたりできます。前提条件は、前提条件を拡張することです。つまり、入力パラメータの型が親クラスの型カバレッジよりも広いです。このように理解するのは難しいかもしれませんが、逆に考えてみましょう。ファーザークラスの入力パラメータの型がサブクラスの入力パラメータの型よりも広い場合はどうなるでしょうか。親クラスが存在する場合、サブクラスが存在しない可能性があります。これは、サブクラスがパラメータとして渡されると、呼び出し元がサブクラスのメソッド カテゴリに入る可能性が高いためです。上記の例を変更して、親クラスの前提条件を拡張してみましょう (リスト 2-15 にソース コードを示します)。

コード リスト 2-15 親クラスの前提条件が大きい

public class Father {
    
    
   public Collection doSomething(Map map){
    
    
       System.out.println("父类被执行...");
       return map.values();
   }
}

親クラスの前提条件を Map 型に変更し、サブクラス メソッドの入力パラメーターを変更して、親クラスと比較して入力パラメーターの型の範囲を減らす、つまり前提条件を減らします。ソース コードは次のとおりです。リスト 2-16 。

リスト 2-16 サブクラスの前提条件は小さい

public class Son extends Father {
    
    
   //缩小输入参数范围
   public Collection doSomething(HashMap map){
    
    
       System.out.println("子类被执行...");
       return map.values();
   }
}

親クラスの前提条件がサブクラスの前提条件より大きい場合、ビジネス シナリオのソース コードはコード リスト 2-17 に示されます。

コード リスト 2-17 サブクラスの前提条件は小さい

public class Client {
    
    
   public static void invoker(){
    
    
       //有父类的地方就有子类
       Father f= new Father();
       HashMap map = new HashMap();
       f.doSomething(map);
   } 
   public static void main(String[] args) {
    
    
       invoker();
   }
}

コードを実行した結果は次のようになります。

親クラスが実行されます...

では、リスコフ置換原理を導入するとどうなるでしょうか? 親クラスがある場合は、サブクラスを使用できます。それでは、Client クラスを変更しましょう。ソース コードをリスト 2-18 に示します。

コード リスト 2-18 リスコフ置換原則を採用した後のビジネス シナリオ クラス

public class Client {
    
    
   public static void invoker(){
    
    
       //有父类的地方就有子类
       Son f =new Son();
       HashMap map = new HashMap();
       f.doSomething(map);
   }
   public static void main(String[] args) {
    
    
       invoker();
   }
}

コードを実行した後の結果は次のようになります。

サブクラスが実行されます...

もう終わりですか?サブクラスが親クラスのメソッドをオーバーライドしないという前提で、サブクラスのメソッドが実行されますが、実際のアプリケーションでは一般に親クラスは抽象クラスであり、サブクラスは抽象クラスであるため、ビジネスロジックの混乱が生じます。このような実装クラスは親クラスの意図を「歪め」、予期しないビジネス ロジックの混乱を引き起こすため、サブクラスのメソッドの前提条件は、サブクラスのメソッドの前提条件と同じかそれよりも高くなければなりません。スーパークラス内のオーバーライドされたメソッドが緩んでいます。

  1. 親クラスのメソッドをオーバーライドまたは実装するときに出力を絞り込むことができます

これは何を意味しますか。親クラスのメソッドの戻り値が型 T で、サブクラスの同じメソッド (オーバーロードまたはオーバーライドされた) の戻り値が S である場合、リスコフ置換原則では S がより小さくなければなりません。 T 以上、つまり S と T が同じ型であるか、S が T のサブクラスであるのはなぜですか? オーバーライドの場合、親クラスとサブクラスの同名のメソッドの入力パラメータが同じである場合、2 つのメソッドの範囲値 S が T 以下である場合の 2 つのケースがあります。オーバーライドの要件、これが最も重要なことですが、サブクラスは親クラスのメソッドをオーバーライドしますが、これは正当化されます。オーバーロードされている場合、メソッドの入力パラメーターの型または数は異なる必要があります。リスコフ置換原則では、サブクラスの入力パラメーターは親クラスの入力パラメーター以上である必要があります。たとえば、あなたが書いたメソッドは呼び出されません。上記の前提条件を参照してください。

Liskov 置換原則を採用する目的は、プログラムの堅牢性を強化し、バージョンがアップグレードされたときに非常に良好な互換性を維持することです。サブクラスが追加された場合でも、元のサブクラスは引き続き実行できます。実際のプロジェクトでは、各サブクラスは異なるビジネスの意味に対応し、親クラスをパラメータとして使用し、異なるサブクラスを渡して異なるビジネス ロジックを完成させます。これで完璧です。

2.3 ベストプラクティス

プロジェクトでリスコフ置換原理を使用する場合は、サブクラスの「個性」を避けるようにしてください。サブクラスに「個性」があると、サブクラスと親クラスの関係を調整するのが難しくなります。サブクラスを親クラスを使うと、サブクラスの「個性」が消えてしまう - ちょっと間違っている; サブクラスだけをビジネスとして使うとコード間の結合関係が混乱する - クラス置換の基準が不足している。

依存関係逆転の原則

第 3 章 依存性逆転の原則

3.1 依存性逆転原理の定義

依存性反転原理 (DIP) という名前は少しわかりにくいですが、「依存性」と「反転」とはどういう意味ですか? 依存関係逆転の原則の元の定義は次のとおりです。

高レベルのモジュールは低レベルのモジュールに依存すべきではありません。両方とも抽象化に依存すべきです。抽象化は詳細に依存すべきではありません。詳細は抽象化に依存すべきです。

翻訳すると、次の 3 つの意味が含まれます。

● 高レベルのモジュールは低レベルのモジュールに依存すべきではなく、両方ともその抽象化に依存する必要があります。

● 抽象化は詳細に依存すべきではありません。

● 詳細は抽象化に依存する必要があります。

上位モジュールと下位モジュールは理解しやすく、各ロジックの実装はアトミックロジックで構成されており、分割できないアトミックロジックが下位モジュール、アトミックロジックを再組み立てしたものが上位モジュールとなります。では、抽象化とは何でしょうか? 詳細は何ですか? Java言語における抽象化とは、直接インスタンス化できないインタフェースや抽象クラスのことを指しますが、詳細は実装クラス、インタフェースの実装や抽象クラスの継承によって生成されるクラスは詳細であり、直接インスタンス化できるのが特徴です。つまり、キーワード new を追加してオブジェクトを生成できます。Java 言語における依存関係逆転原理のパフォーマンスは次のとおりです。

● モジュール間の依存関係は抽象化を通じて発生します。実装クラス間には直接の依存関係はなく、依存関係はインターフェイスまたは抽象クラスを通じて生成されます。

● インターフェースまたは抽象クラスは実装クラスに依存しません。

● クラスの実装はインターフェースまたは抽象クラスに依存します。

より合理化された定義は、「インターフェイス指向プログラミング」の本質の 1 つである OOD (オブジェクト指向設計、オブジェクト指向設計) です。

3.2 約束を守るなら契約が必要すぎる

依存関係逆転の原則を使用すると、クラス間の結合を減らし、システムの安定性を向上させ、並列開発によって引き起こされるリスクを軽減し、コードの可読性と保守性を向上させることができます。

定理が正しいかどうかを証明するには、提案された主題に基づいていくつかの議論を経て、定理と同じ結論を導き出す、つまり推論方法の 2 つの一般的な方法が使用されます。もう 1 つは、まず次のことを仮定します。提案された命題が偽の命題であり、既知の条件と相互に矛盾する不合理な結論を導き出す、矛盾による証明方法です。今日は、矛盾による証明を使用して、依存関係逆転の原理がいかに優れているか、優れているかを証明します。

トピック: 依存関係逆転の原則により、クラス間の結合が軽減され、システムの安定性が向上し、並列開発によって引き起こされるリスクが軽減され、コードの可読性と保守性が向上します。

反論: 依存関係逆転の原則を使用せずに、クラス間の結合を軽減し、システムの安定性を向上させ、並列開発によって引き起こされるリスクを軽減し、コードの可読性と保守性を向上させることができます。

例を使用して、アンチテーゼが成り立たないことを説明します。「今日、車はますます安くなってきています。トイレ代で良い車が買えます。車を持っていれば、誰かがそれを運転します。メルセデス・ベンツを運転するドライバーのクラス図を図 3 に示します。」 1.

画像

図 3-1 メルセデス・ベンツを運転するドライバーのクラス図

Mercedes-Benz では、車両の走行を表現するメソッド run を提供することができ、その実装プロセスをコード リスト 3-1 に示します。

コードリスト 3-1 ドライバーのソースコード

public class Driver {
    
       
   //司机的主要职责就是驾驶汽车
   public void drive(Benz benz){
    
    
       benz.run();
   }
}

ドライバーは Mercedes-Benz の run メソッドを呼び出して Mercedes-Benz を起動します。そのソース コードをリスト 3-2 に示します。

コードリスト 3-2 Mercedes-Benz のソースコード

public class Benz {
    
    
   //汽车肯定会跑
   public void run(){
    
    
       System.out.println("奔驰汽车开始运行...");
   }
}

有车,有司机,在Client场景类产生相应的对象,其源代码如代码清单3-3所示。

代码清单3-3 场景类源代码

public class Client {
    
    
   public static void main(String[] args) {
    
    
       Driver zhangSan = new Driver();
       Benz benz = new Benz();
       //张三开奔驰车
       zhangSan.drive(benz);
   }
}

通过以上的代码,完成了司机开动奔驰车的场景,到目前为止,这个司机开奔驰车的项目没有任何问题。我们常说“危难时刻见真情”,我们把这句话移植到技术上就成了“变更才显真功夫”,业务需求变更永无休止,技术前进就永无止境,在发生变更时才能发觉我们的设计或程序是否是松耦合。我们在一段貌似磐石的程序上加上一块小石头:张三司机不仅要开奔驰车,还要开宝马车,又该怎么实现呢?麻烦出来了,那好,我们走一步是一步,我们先把宝马车产生出来,实现过程如代码清单3-4所示。

代码清单3-4 宝马车源代码

public class BMW {
    
    
   //宝马车当然也可以开动了
   public void run(){
    
    
       System.out.println("宝马汽车开始运行...");
   }
}

宝马车也产生了,但是我们却没有办法让张三开动起来,为什么?张三没有开动宝马车的方法呀!一个拿有C驾照的司机竟然只能开奔驰车而不能开宝马车,这也太不合理了!在现实世界都不允许存在这种情况,何况程序还是对现实世界的抽象,我们的设计出现了问题:司机类和奔驰车类之间是紧耦合的关系,其导致的结果就是系统的可维护性大大降低,可读性降低,两个相似的类需要阅读两个文件,你乐意吗?还有稳定性,什么是稳定性?固化的、健壮的才是稳定的,这里只是增加了一个车类就需要修改司机类,这不是稳定性,这是易变性。被依赖者的变更竟然让依赖者来承担修改的成本,这样的依赖关系谁肯承担!证明到这里,我们已经知道反论题已经部分不成立了。

注意 设计是否具备稳定性,只要适当地“松松土”,观察“设计的蓝图”是否还可以茁壮地成长就可以得出结论,稳定性较高的设计,在周围环境频繁变化的时候,依然可以做到“我自岿然不动”。

我们继续证明,“减少并行开发引起的风险”,什么是并行开发的风险?并行开发最大的风险就是风险扩散,本来只是一段程序的错误或异常,逐步波及一个功能,一个模块,甚至到最后毁坏了整个项目。为什么并行开发就有这样的风险呢?一个团队,20个开发人员,各人负责不同的功能模块,甲负责汽车类的建造,乙负责司机类的建造,在甲没有完成的情况下,乙是不能完全地编写代码的,缺少汽车类,编译器根本就不会让你通过!在缺少Benz类的情况下,Driver类能编译吗?更不要说是单元测试了!在这种不使用依赖倒置原则的环境中,所有的开发工作都是“单线程”的,甲做完,乙再做,然后是丙继续……这在20世纪90年代“个人英雄主义”编程模式中还是比较适用的,一个人完成所有的代码工作。但在现在的大中型项目中已经是完全不能胜任了,一个项目是一个团队协作的结果,一个“英雄”再牛也不可能了解所有的业务和所有的技术,要协作就要并行开发,要并行开发就要解决模块之间的项目依赖关系,那然后呢?依赖倒置原则就隆重出场了!

根据以上证明,如果不使用依赖倒置原则就会加重类间的耦合性,降低系统的稳定性,增加并行开发引起的风险,降低代码的可读性和可维护性。承接上面的例子,引入依赖倒置原则后的类图如图3-2所示。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-oarIutPp-1670180212551)(https://box.kancloud.cn/2016-08-14_57b0032281df0.jpg)]

图3-2 引入依赖倒置原则后的类图

建立两个接口:IDriver和ICar,分别定义了司机和汽车的各个职能,司机就是驾驶汽车,必须实现drive()方法,其实现过程如代码清单3-5所示。

代码清单3-5 司机接口

public interface IDriver {
    
    
   //是司机就应该会驾驶汽车
   public void drive(ICar car);
}

接口只是一个抽象化的概念,是对一类事物的最抽象描述,具体的实现代码由相应的实现类来完成,Driver实现类如代码清单3-6所示。

代码清单3-6 司机类的实现

public class Driver implements IDriver{
    
      
   //司机的主要职责就是驾驶汽车
   public void drive(ICar car){
    
    
       car.run();
   }
}

IDriverでは抽象間の依存関係をICarインターフェースで渡すことで実現しており、Driver実装クラスもICarインターフェースで渡しており、どのタイプのCarであるかは上位モジュールで宣言する必要があります。

ICar とその 2 つの実装クラスの実装プロセスをリスト 3-7 に示します。

コード リスト 3-7 Car インターフェイスと 2 つの実装クラス

public interface ICar {
    
    
   //是汽车就应该能跑
   public void run();
}
public class Benz implements ICar{
    
    
   //汽车肯定会跑
   public void run(){
    
    
       System.out.println("奔驰汽车开始运行...");
   }
}
public class BMW implements ICar{
    
       
   //宝马车当然也可以开动了
   public void run(){
    
    
       System.out.println("宝马汽车开始运行...");
   }
}

ビジネス シナリオでは、「抽象化は詳細に依存しない」を実装します。つまり、抽象化 (ICar インターフェイス) は BMW とベンツの 2 つの実装クラス (詳細) に依存しないと考えているため、上位モジュールのアプリケーションはすべての抽象化、クライアント 実装プロセスをコード リスト 3-8 に示します。

コードリスト 3-8 ビジネスシナリオ

public class Client {
    
    
   public static void main(String[] args) {
    
    
       IDriver zhangSan = new Driver();
       ICar benz = new Benz();
       //张三开奔驰车
       zhangSan.drive(benz);
   }
}

クライアントは高レベルのビジネス ロジックに属し、低レベルのモジュールへの依存は抽象化に基づいています。zhangSan のサーフェス タイプは IDriver で、Benz のサーフェス タイプは ICar です。おそらく、この高レベルのことを尋ねる必要があるでしょう。 module は、new Driver() や new Benz() などの低レベル モジュールも呼び出します。どう説明すればよいでしょうか? 確かに、zhangSan の表面タイプは IDriver であり、これは抽象的で非実体的なインターフェイスであり、後続のすべての操作では、zhangSan は IDriver のタイプで動作し、抽象化に対する詳細の影響を保護します。もちろん、Zhang San が BMW を運転したい場合も、非常に簡単です。ビジネス シナリオ クラスを変更するだけです。実装プロセスはコード リスト 3-9 に示されています。

コード リスト 3-9 BMW 車を運転する Zhang San の実装プロセス

public class Client {
    
    
   public static void main(String[] args) {
    
    
       IDriver zhangSan = new Driver();
       ICar bmw = new BMW();
       //张三开奔驰车
       zhangSan.drive(bmw);
   }
}

新たに下位モジュールを追加する場合、ビジネスシナリオクラス、つまり上位モジュールのみを変更するだけで、Driverクラスなど他の下位モジュールは変更する必要がなく、最小限の負荷で業務を実行できます。 「変化」によるリスクの拡散。

Java では、変数が定義されている限り、変数には型が必要であることに注意してください。変数には、表面型と実際の型の 2 つの型を持つことができます。表面型は、定義時に割り当てられる型であり、実際の型です。 type は、zhangSan の表面などのオブジェクトのタイプです。タイプは IDriver で、実際のタイプは Driver です。

並列開発に対する依存関係の反転の影響について考えてみましょう。2 つのクラスの間には依存関係があり、2 つの間のインターフェイス (または抽象クラス) を定式化する限り、独立して開発でき、プロジェクト間の単体テストも独立して実行でき、TDD (Test-駆動開発 (テスト駆動開発) 開発モデルは、依存関係反転原則の最も先進的なアプリケーションです。車を運転するドライバーの上記の例を引き続き見てみましょう。プログラマ A は IDriver の開発を担当し、プログラマ B は ICar の開発を担当します。インターフェイスが確立されている限り、2 人の開発者は独立して開発できます。 A の開発進捗は比較的早く完了しています IDriver とそれに関連する実装クラス Driver の開発作業は完了していますが、プログラマ B は開発が遅れており、A は単体テストを行うことができますか? 答えは「はい」です。JMock ツールを導入します。このツールの最も基本的な機能は、抽象仮想オブジェクトをテストすることです。そのテスト クラスをリスト 3-10 に示します。

コード リスト 3-10 テスト クラス

public class DriverTest extends TestCase{
    
    
   Mockery context = new JUnit4Mockery();
   @Test
   public void testDriver() {
    
    
       //根据接口虚拟一个对象
       final ICar car = context.mock(ICar.class);
       IDriver driver = new Driver();
       //内部类
       context.checking(new Expectations(){
    
    {
    
    
           oneOf (car).run();    
       }});
       driver.drive(car);
   }
}

太字の部分に注目してください。Driver クラスの単体テストには ICar インターフェイスのみが必要です。この観点から見ると、相互に依存する 2 つのオブジェクトを個別に開発し、個別に単体テストすることで、並列開発の効率と品質を確保できる、ここに TDD 開発の本質があるのではないでしょうか。テスト駆動開発では、最初に単体テスト クラスを作成し、次に実装クラスを作成します。これはコードの品質を向上させるのに非常に役立ち、特に研究開発プロジェクトやプロジェクト メンバーの全体的なレベルが比較的低い場合に適しています。

抽象化は実装に対する制約であり、依存者に対する一種の契約でもあります。それ自体を制約するだけでなく、それ自体と外部との関係も制約します。契約(抽象)は共に発展します。抽象化が存在し、細部はこの円から切り離すことができず、常にターゲットが「言葉は信じなければならず、行動は毅然としていなければならない」を達成させます。

3.3 依存関係を記述する 3 つの方法

依存関係は渡すことができます。A オブジェクトは B オブジェクトに依存し、B は C に依存し、C は D に依存します。人生に終わりはなく、依存関係は無限です。1 つだけ覚えておいてください。抽象的な依存関係が達成されている限り、マルチレイヤーの依存関係の転送も心配する必要はありません。

オブジェクトの依存関係は、以下に示す 3 つの方法で渡すことができます。

1. コンストラクターは依存オブジェクトを渡します

クラス内で依存オブジェクトをコンストラクタを通じて宣言します。依存性注入はコンストラクタ注入と呼ばれます。この注入方法に従って、IDriver と Driver の修正プログラムをリスト 3-11 に示します。

コード リスト 3-11 依存オブジェクトを渡すコンストラクター

public interface IDriver {
    
    
   //是司机就应该会驾驶汽车
   public void drive();
}
public class Driver implements IDriver{
    
    
   private ICar car; 
   //构造函数注入
   public Driver(ICar _car){
    
    
       this.car = _car;
   }
   //司机的主要职责就是驾驶汽车
   public void drive(){
    
    
       this.car.run();
   }
}

2. Setter メソッドは依存オブジェクトを転送します

Setter メソッドをアブストラクトに設定して依存関係を宣言します。依存関係の注入によると、これは Setter 依存関係の注入です。この注入方法に従って、変更された IDriver と Driver のプログラムはリスト 3-12 に示されます。

コード リスト 3-12 Setter 依存関係の挿入

public interface IDriver {
    
    
   //车辆型号
   public void setCar(ICar car);
   //是司机就应该会驾驶汽车
   public void drive();
}
public class Driver implements IDriver{
    
    
   private ICar car; 
   public void setCar(ICar car){
    
    
       this.car = car;
   }
   //司机的主要职责就是驾驶汽车
   public void drive(){
    
    
       this.car.run();
   }
}

3. インターフェース宣言に依存するオブジェクト

インターフェイスのメソッドで依存オブジェクトを宣言する セクション 3.2 の例では、インターフェイス宣言の依存関係 (インターフェイス インジェクションとも呼ばれる) のメソッドが使用されています。

3.4 ベストプラクティス

依存関係逆転の原則の本質は、抽象化 (インターフェイスまたは抽象クラス) を通じて各クラスまたはモジュールの実装を互いに独立させ、相互影響を与えず、モジュール間の疎結合を実現することです。計画?いくつかのルールに従ってください。

● 各クラスは可能な限りインターフェースか抽象クラス、あるいは抽象クラスとインターフェースの両方を持ちます。

これは依存関係の反転の基本的な要件です。インターフェイスと抽象クラスは両方とも抽象クラスであり、抽象化によってのみ依存関係の反転が可能になります。

● 変数の表面的な型は、可能な限りインターフェースまたは抽象クラスにする必要があります。

多くの本には、変数の型はインターフェイスまたは抽象クラスでなければならないと書かれていますが、これは少し絶対的です。たとえば、ツール クラス、xxxUtils では通常、インターフェイスや抽象クラスは必要ありません。また、クラスのcloneメソッドを使用する場合は、JDKが提供する仕様である実装クラスを使用する必要があります。

● 具象クラスからクラスを派生させてはなりません

プロジェクトが開発状態にある場合、具象クラスから派生したサブクラスがあってはいけないのは事実ですが、人は間違いを犯すものであり、場合によっては設計上の欠陥が避けられないため、これは絶対的なものではありません。以下の両方のレベルの継承が許容されます。特にプロジェクトの保守を担当する同志は、基本的にこのルールを無視できますが、なぜですか? メンテナンス作業は基本的に拡張開発と動作修正です。継承関係を通じて、大きなバグはメソッドをオーバーライドすることで修正できます。なぜ最上位の基本クラスを継承するのでしょうか? (もちろん、親クラスがよく理解できていない場合や、親クラスのコードが取得できない場合には、可能な限りこのような状況が発生するはずです。)

● 基本クラスのメソッドをオーバーライドしないようにしてください。

基本クラスが抽象クラスで、このメソッドが実装されている場合、サブクラスはそれをオーバーライドしないようにする必要があります。クラス間の依存関係は抽象化であり、抽象メソッドをオーバーライドすると依存関係の安定性に一定の影響を与えます。

● リスコフ置換原理と組み合わせて使用​​されます。

第 2 章では、リスコフ置換の原理を説明しました。親クラスが出現する場所にサブクラスを出現できます。この章の説明と組み合わせると、次のような一般的なルールを描くことができます: インターフェイスはパブリック プロパティとメソッドの定義を担当し、宣言します他のオブジェクトとの依存関係では、抽象クラスはパブリック構築部分の実現を担当し、実装クラスはビジネス ロジックを正確に実装し、同時に必要に応じて親クラスを改良します。

ここまで言っても、「反転」という言葉はまだ皆さん理解されていないと思われますが、「反転」とは一体何でしょうか?まず「正しさ」とは何かという話ですが、正しさに依存するということは、クラス間の依存関係を現実に実現すること、つまり実装指向プログラミングであり、これが普通の人の考え方でもあります。メルセデス・ベンツを運転したい場合はメルセデス・ベンツに依存し、ラップトップを使用したい場合は直接ラップトップに依存します。プログラムを書くために必要なのは、現実世界の物事を抽象化することです。人々の伝統的な思考において、物間の依存が物間の依存に置き換わるものであり、「逆転」はここから来ています。

依存関係反転原則の利点は、単純な SSH アーキテクチャを使用する 10 人月未満のプロジェクトなどの小規模なプロジェクトでは反映されにくく、基本的にそれほど手間をかけずに完了するため、依存関係反転原則が採用されるかどうかは重要です。効果は少ない。ただし、大規模および中規模のプロジェクトでは、特に非技術的要因によって引き起こされる問題を回避するために、依存関係逆転の原則を採用することには多くの利点があります。プロジェクトが大規模になると需要変動の確率が高くなりますが、依存関係逆転の原理で設計されたインターフェースや抽象クラスを利用して実装クラスを制約することで、需要変動による作業負荷の増加を軽減できます。大規模から中規模のプロジェクトでは人事異動が頻繁に発生しますが、設計が良く、コード構造が明確であれば、人事異動がプロジェクトに与える影響は基本的にゼロです。大規模および中規模のプロジェクトのメンテナンス サイクルは一般に非常に長く、依存関係逆転の原則を使用することで、メンテナンス担当者は簡単に拡張およびメンテナンスを行うことができます。

依存性反転原則は 6 つの設計原則の中で実装が最も困難です. オープン-クローズド原則を実現するための重要な方法です. 依存性反転原則が実現されなければ, 拡張への開放性と閉鎖への開放性を実現することはできません.変形。このプロジェクトでは、「インターフェイス指向プログラミング」であることを誰もが覚えていれば、基本的に依存関係逆転の原理の核心を捉えています。

依存関係逆転の原則の利点についてはこれまでたくさん説明してきたので、皆さんに言ってみましょう。現実の世界では、詳細の定義に依存しなければならない法律など、詳細に依存しなければならないものが確かにあります。中国の法律には古代から現代に至るまで「活殺」が存在しており [ 1]、ここでいう「殺す」とは抽象的な意味であり、どのように殺すのか、誰を殺すのか、なぜ殺すのかといった定義はない。善人が悪人を殺して命を落とすのは不公平であるという観点から、依存関係逆転の原則を実際のプロジェクトで利用する場合には、原則を把握するのではなく、状況を判断する必要があります。各原則の利点は限られており、普遍的な真実ではないため、原則に従うためだけにプロジェクトの最終目標を放棄しないでください。つまり、それを本番環境に導入して利益を上げるということです。プロジェクト マネージャーまたはアーキテクトとして、テクノロジは目標を達成するためのツールにすぎないことを理解する必要があります。デザインがどれほど美しく、コードがどれほど完璧で、プロジェクトが基準をどのように満たしていても、上司を怒らせるようなことはありません。プロジェクトはお金を失い、製品のインプットがアウトプットを上回っていれば、すべてがナンセンスです。自分自身をより良くしようとしないでください!

[ 1]漢王朝の高祖皇帝である劉邦は税関に入ったとき、庶民と 3 つの協定を結びました。その 1 つは「殺人者は死に、傷害と窃盗の罪は罰せられる」というものでした。

インターフェース分離原理

第 4 章 インターフェース分離の原則

4.1 インターフェース分離原則の定義

インターフェイス分離の原理について説明する前に、私たちの主人公であるインターフェイスを明確にしましょう。インターフェイスには次の 2 種類があります。

● インスタンス インターフェイス (オブジェクト インターフェイス)。Java でクラスを宣言し、 new キーワードを使用してインスタンスを生成します。インスタンスは、インターフェイスの一種であるものの種類の記述です。たとえば、クラス Person を定義し、次に Person zhangSan=new Person() を使用してインスタンスを生成する場合、このインスタンスが準拠する必要がある標準はクラス Person であり、その Person クラスは zhangSan のインターフェイスになります。混乱?読めない?Java 言語があまりにも長い間浸透してきたため、この観点から見ると Java のクラスも一種のインターフェイスであることがわかっていれば問題ありません。

● クラス インターフェイス (クラス インターフェイス)。Java でよく使用されるインターフェイス キーワードによって定義されるインターフェイス。

主人公が明確に設定されているので、孤立とは何か?次の 2 つの定義があります。

● クライアントは、使用しないインターフェイスに依存することを強制すべきではありません (クライアントは、必要のないインターフェイスに依存すべきではありません)。

● あるクラスから別のクラスへの依存関係は、可能な限り最小のインターフェイスに依存する必要があります (クラス間の依存関係は、最小のインターフェイスで確立される必要があります)。

新しいものの定義は一般に理解するのが難しく、曖昧であるのが普通です。これら 2 つの定義を分析してみましょう。まず最初の定義について話しましょう。「クライアントは、必要のないインターフェイスに依存すべきではない」では、クライアントは何に依存しているのでしょうか。必要なインターフェイスに依存し、クライアントが必要とするあらゆるインターフェイスを提供し、不要なインターフェイスを削除する場合は、インターフェイスを改良してその純度を確保する必要があります。2 番目の定義を見てください。「クラス間の依存関係は、最小のもので確立される必要があります。」これは、最小のインターフェイスを必要とし、インターフェイスが洗練されて純粋であることも必要とします。これは最初の定義とまったく同じですが、あるものについての 2 つの異なる記述にすぎません。

これら 2 つの定義を 1 つの文に要約すると、単一のインターフェイスを構築し、肥大化し巨大なインターフェイスを構築しないでください。もっと簡単に言うと、インターフェイスはできる限り詳細にする必要があり、インターフェイス内のメソッドはできる限り少なくする必要があります。これを見ると、これは単一責任原則と同じではないかと誰もが疑問に思うかもしれません。違います。インターフェイス分離の原則は、単一責任の観点とは異なります。単一責任には、責任に焦点を当てた、単一のクラスとインターフェイス責任が必要です。これはビジネス ロジックの分割であり、インターフェイス分離の原則では、必要なメソッドは最小限です。インターフェイス上で。たとえば、インターフェイスの役割には 10 個のメソッドが含まれる場合があります。これらの 10 個のメソッドは 1 つのインターフェイスに配置され、複数のモジュールによるアクセスに提供されます。各モジュールは、指定された権限に従ってアクセスします。システムの外部では、「未使用のメソッド」は次の制約を受けます。 「このドキュメントにはアクセスしないでください」というメッセージは、単一責任の原則に従って許可されますが、「できるだけ多くの専用インターフェースを使用する」必要があるため、インターフェース分離の原則に従って許可されません。専用インターフェースとは何を指しますか? これは、すべてのクライアント アクセスに対応するために巨大で肥大化したインターフェイスを作成するのではなく、各モジュールに単一のインターフェイスを提供し、複数のモジュールに複数のインターフェイスを持たせる必要があることを意味します。

4.2 さまざまな考え方を持つ美しい人がたくさんいる

我们举例来说明接口隔离原则到底对我们提出了什么要求。现在男生对小姑娘的称呼,使用频率最高的应该是“美女”了吧,你在大街上叫一声:“嗨,美女!”估计10个有8个回头,其中包括那位著名的如花。美女的标准各不相同,首先就需要定义一下什么是美女:首先要面貌好看,其次是身材要窈窕,然后要有气质,当然了,这三者各人的排列顺序不一样,总之要成为一名美女就必须具备:面貌、身材和气质,我们用类图体现一下星探(当然,你也可以把自己想象成星探)找美女的过程,如图4-1所示。

画像

图4-1 星探寻找美女的类图

定义了一个IPettyGirl接口,声明所有的美女都应该有goodLooking、niceFigure和great-Temperament,然后又定义了一个抽象类AbstractSearcher,其作用就是搜索美女并显示其信息,只要美女都按照这个规范定义,Searcher(星探)就轻松多了,美女类的实现如代码清单4-1所示。

代码清单4-1 美女类

public interface IPettyGirl {
//要有姣好的面孔
public void goodLooking();
//要有好身材
public void niceFigure();
//要有气质
public void greatTemperament();
}

美女的标准定义完毕,具体的美女实现类如代码清单4-2所示。

代码清单4-2 美女实现类

public class PettyGirlimplements IPettyGirl { private String name; //美しさには名前があるpublic PettyGirl(String _name){ this.name=_name; } //美しい顔public void GoodLooking() { System.out.println( this.name + "—顔はとても美しい!"); } //気質はより良いpublic void greatTemperament() { System.out.println( this.name + "—気質はとても良い!"); } //図public void niceFigure() { System.out.println( this.name + "—素晴らしい人物!"); } }

















美人の条件は3つの方法で定義されており、この基準によればルファ少女は美人の基準から除外されます。美人がいる場合は、美人を探すスカウトも存在します。具体的な実装例をリスト 4-3 に示します。

コード リスト 4-3 Scout 抽象クラスのソース コード

public abstract class AbstractSearcher { protected IPettyGirl pettyGirl; public AbstractSearcher(IPettyGirl _pettyGirl){ this.pettyGirl = _pettyGirl; } //美人を検索し、美人をリストアップpublic abstract void show(); }






スカウトの実装クラスは比較的単純で、そのソース コードをリスト 4-4 に示します。

コードリスト4-4 スカウトクラス

public class Searcher extends AbstractSearcher{ public Searcher(IPettyGirl _pettyGirl){ super(_pettyGirl); } //美人情報を表示public void show(){ System.out.println(“------美人情報は以下の通りfollow : ---------------"); //顔を表示super.pettyGirl.goodLooking(); //体型を表示super.pettyGirl.niceFigure(); //気質を表示super. pettyGirl .greatTemperament(); } }













シーンには美女とスカウトの二人のキャラクターがすでに登場していますが、これらのキャラクターを接続するためにシーンクラスを記述する必要があります。シーンクラスの実装はコードリスト 4-5 に示されています。

コードリスト 4-5 シーンクラス

public class Client { //美容情報の検索と表示public static void main(String[] args) { //美容の定義IPettyGirl yanYan = new PettyGirl("燕燕"); AbstractSearcher searcher = new Searcher(yanYan); searcher. show(); } }







スカウトマンが美女を探した結果は以下の通り。

--------美の情報は以下の通りです:---------------

ヤンヤン - 彼女はきれいな顔をしています!

ヤンヤン - フィギュアはとても良いです!

ヤンヤン - 気質はとても良いです!

スカウトが美人を見つけるためのプログラムが開発されており、実行結果は正しいです。戻って、このプログラムに問題があるかどうか考えてみましょう。IPettyGirl のインターフェイスについて考えてみましょう。このインターフェイスは最適に設計されていますか? 答えは「いいえ」です。インターフェイスも最適化できます。

私たちの美的見方は変化しており、美しさの定義も変化しています。唐の時代の楊貴妃がこの時代に生きていたら、恥を知れて死ななければならなかったのに、なぜでしょうか?肥満!しかし、太っていても中国四大美人の一人に選ばれることに影響はなかったことから、当時の美意識が現在とは異なっていたことが分かる。もちろん、時代の発展とともに私たちの美意識も変わってきており、顔は悪く、体型も平均的だが、性格がとても良い女性を見つけたとき、ほとんどの人はそのような女性を美人と呼ぶと思います。美的品質の向上により、気質的な美人が生み出されますが、私たちのインターフェースでは、美人は 3 つすべてを備えている必要があると定義されています。この基準によると、気質的な美人は美人とみなされません。おそらくあなたは、ビューティー クラスを再拡張し、greatTemperament メソッドのみを実装し、他の 2 つのメソッドは空白のままにし、何も書かない、それでいいのではないか、と言いたいのかもしれません。賢いけど、うまくいかない!なぜ?スカウト AbstractSearcher は、3 つのメソッドを持つ IPettyGirl インターフェイスに依存していますが、実装したメソッドは 2 つだけです。スカウト メソッドを変更しますか? 上記のプログラムで出力された情報には 2 つの項目が欠けていますが、スカウトはその女性が美しい女性かどうかをどうやって判断できるのでしょうか?

この点を分析した結果、IPettyGirl インターフェイスの設計には欠陥があり、大きすぎ、いくつかの変動要因を考慮していることがわかりました。インターフェイス分離原則によれば、スカウト AbstractSearcher はいくつかの特性を持つ女の子に依存する必要がありますが、これらの特性を追加しました。すべてカプセル化されてインターフェイスに入れられますが、カプセル化が多すぎます。問題が見つかったので、クラス図を再設計しましょう。変更されたクラス図が図 4-2 に示されています。

オリジナルの IPettyGirl インターフェースを 2 つのインターフェースに分割し、1 つは美しい容姿を持つ美女、IGoodBodyGirl です。この種の美しさは、素晴らしい顔と体型を特徴とし、超一流ですが、随所に唾を吐き出すなどの美的性質がなく、 2 番目は IGreatTemperamentGirl、美しい気質を持ち、非常に高いレベルの会話と自己修養を持った美人です。比較的肥大化したインターフェイスを 2 つの特化したインターフェイスに分割し、柔軟性と保守性を向上させ、美少女が欲しい場合でも、将来的に美少女が欲しい場合でも、PettyGirl を通じて簡単に定義できます。2 つのタイプの美しさの定義をリスト 4-6 に示します。

画像

図 4-2 美しい女性を探すスカウトの修正されたクラス図

コード リスト 4-6 2 種類の美しさの定義

public Interface IGoodBodyGirl { //顔が良いpublic void GoodLooking(); //体型が良いpublic void niceFigure(); } public Interface IGreatTemperamentGirl { //気質が良いpublic void greatTemperament(); }








顔、体型、気質に応じて美しさを考慮し、実装クラスはコードリスト4-7に示すように2つのインターフェースを実装します。

コードリスト 4-7 最も標準的な美しさ

public class PettyGirlimplements IGoodBodyGirl,IGreatTemperamentGirl { private String name; //美しさには名前があるpublic PettyGirl(String _name){ this.name=_name; } //美しい顔public void goodLooking() { System.out.println( this. name + "—顔はとても美しい!"); } //気質は良いですpublic void greatTemperament() { System.out.println( this.name + "—気質はとても良いです!"); } //図の方が優れていますpublic void niceFigure( ) { System.out.println( this.name + "—素晴らしい図です!"); } }

















このようなリファクタリングを行うと、将来的に気質の美しい女性が欲しい場合でも、美しい容姿が欲しい場合でも、インターフェースを安定させることができます。もちろん、美的観点は将来的に変わる可能性があると言いたいかもしれません。見た目が良いだけが美しさです。そうすると、IGoodBody インターフェイスはまだ修正する必要があります。それは本当ですが、デザインには限界があります。将来の変更を無限に考慮することはできず、そうしないとデザインの泥沼にはまってしまい、そこから抜け出すことができなくなります。

肥大化したインターフェイスを 2 つの独立したインターフェイスに変更するという前述の原理はインターフェイス分離の原理であり、これにより、AbstractSearcher は包括的なインターフェイスに依存するよりも 2 つの専用インターフェイスに依存する方がより柔軟になります。インターフェースとは設計時に外部に提供する契約であり、複数のインターフェースを分散して定義することで将来の変更の波及を防ぎ、システムの柔軟性や保守性を向上させることができます。

4.3 インターフェースの純度を保証する

インターフェイス分離の原則はインターフェイスを規制することであり、次の 4 つの意味が含まれます。

● インターフェースはできるだけ小さくする必要があります。

これはインターフェイス分離原則の核となる定義です。ファット インターフェイスはありませんが、「小ささ」には限界があります。第一に、単一責任の原則に違反することはできません。これは何を意味しますか? 単一責任の原則で iPhone の例について説明しました。ここでは、単一責任の原則を使用して 2 つの責任を 2 つのインターフェイスに分解します。クラス図は図 4-3 に示されています。

画像

図 4-3 電話のクラス図

IConnectionManager インターフェイスの分割を継続できるかどうかを慎重に分析してください。電話を切る方法には 2 つあります。1 つは通常に電話を切る方法、もう 1 つは異常に電話を切る方法です。これら 2 つのメソッドの処理は異なるはずですが、なぜですか? 通常通りに電話を切ると、相手が切断信号を受信し、課金システムが課金を停止します。電話機が停電した場合は方法が異なります。これは信号損失であり、中継サーバーがそれを検出し、その後、請求システムに停止を通知します。請求、そうでないとコストが異常に増加しますか?

これについて考えると、IConnectionManager インターフェイスを 2 つに展開し、1 つのインターフェイスが接続を担当し、もう 1 つのインターフェイスが電話の切断を担当する必要があるでしょうか。これでいいでしょうか?ちょっと待って、もう一度考えてみましょう ビジネスロジックの観点から見ると、コミュニケーションの確立と終了はすでに最小のビジネス単位であり、さらに細分化されるため、分割されている場合は単一責任の原則に適合しませんはビジネス、またはプロトコルの分割 (他のビジネス ロジック) です。考えてみてください。電話機は 3G プロトコルやリレー サーバーなどを考慮する必要がありますが、この電話機はどのように設計できるのでしょうか? ビジネスの観点から見ると、そのような設計は失敗した設計です。ある原則は解体する必要がありますが、別の原則は解体してはいけない場合、どうすればよいでしょうか? インターフェース分離の原則に従ってインターフェースを分割する場合、まず単一責任の原則を満たさなければなりません。

● 界面の凝集力が高いこと

高い凝集力とは何ですか?高い凝集性とは、インターフェイス、クラス、モジュールの処理能力を向上させ、外部との対話を減らすことを意味します。たとえば、あなたは部下に「オバマ大統領のオフィスに×××の文書を盗みに行く」と指示し、部下が「はい、その任務を完了することを約束します!」と毅然とした口調で答えるのを聞いて、1 か月後には、部下は本当に×××を入れた ファイルは机の上に置かれている このように、条件を言わずにすぐに仕事を終わらせる行動は、結束力の高さの表れです。インターフェイス分離原則に特有のこととして、インターフェイス内で公開するパブリック メソッドをできる限り少なくする必要があります。インターフェイスは外部コミットメントです。コミットメントが少なければ少ないほど、システム開発はより良くなり、変更のリスクも少なくなります。コスト削減にもつながります。

● カスタマイズされたサービス

システムまたはシステム内のモジュール間には結合が必要であり、結合がある場合は、相互アクセスのためのインターフェースが必要です (必ずしも Java で定義されたインターフェースである必要はなく、クラスまたは単純なデータ交換の場合もあります)。訪問者(クライアント)ごとにサービスをカスタマイズする必要がありますが、カスタマイズサービスとは何ですか?カスタマイズサービスとは、個人に対して優れたサービスを提供することです。システム設計を行う際には、システム間やモジュール間のインターフェースにカスタマイズされたサービスの利用も考慮する必要があります。カスタマイズされたサービスを使用するには、訪問者が必要とするメソッドのみを提供するという要件があるはずですが、これは何を意味しますか? 例を挙げて説明します。たとえば、管理者が書籍をクエリするのに便利なクエリ インターフェイスを備えた図書館管理システムを開発しました。クラス図を図 4-4 に示します。

画像

図 4-4 ブッククエリのクラス図

複数のクエリ メソッドがインターフェイスで定義されており、それぞれ著者、タイトル、発行者、カテゴリごとにクエリを実行でき、最後に混合クエリ メソッドも提供されます。プログラムを作成して運用を開始した後、ある日突然、システムが非常に遅いことに気づき、それを苦労して分析し始めました。そして最終的に、アクセス インターフェイスの complexSearch (Map マップ) メソッドの同時実行性が高すぎることがわかりました。アプリケーション サーバーのパフォーマンスが低下したため、追跡を続けたところ、これらのクエリはすべてパブリック ネットワークから開始されていることがわかりました。さらに分析した結果、問題は、パブリック ネットワーク (パブリック ネットワーク) に提供されているクエリ インターフェイスであることがわかりました。プロジェクトは別のプロジェクト チームによって開発されています) は、システムの管理担当者に提供されるインターフェイスと同じです。これらはすべて IBookSearcher インターフェイスですが、権限が異なります。システム管理者は、インターフェイスの complexSearch メソッドを通じてすべての書籍をクエリできます。ただし、パブリック ネットワーク上のこのメソッドは制限されており、値を返しません。設計中に口頭で制限されています。メソッドを呼び出すことはできませんが、パブリック ネットワーク プロジェクト チームの過失により、このメソッドはまだ公開されています。結果は得られませんが、これは、インターフェイスの肥大化によって引き起こされるパフォーマンス障害です。

問題が見つかった場合、インターフェイスをリファクタリングする必要があり、IBookSearcher は 2 つのインターフェイスに分割され、それぞれ 2 つのモジュールにカスタマイズされたサービスを提供します。変更されたクラス図は図 4-5 に示されています。

画像

図 4-5 変更されたブック クエリ クラス図

管理者に提供する実装クラスは ISimpleBookSearcher と IComplexBookSearcher の 2 つのインターフェイスを同時に実装しており、元のプログラムを変更する必要はありませんが、パブリック ネットワークに提供するインターフェイスは単純なクエリのみを許可し、サービスを個別にカスタマイズする ISimpleBookSearcher になります。 、起こり得るリスクを軽減します。

● インターフェイスのデザインが制限される

インターフェイス設計の粒度が小さいほど、システムの柔軟性が高まることは議論の余地のない事実です。ただし、柔軟性は構造の複雑化、開発の難易度の上昇、保守性の低下をもたらしますが、これはプロジェクトや製品が期待しているものではないため、インターフェース設計では節度を考慮する必要があります。経験と常識から判断すると、確固たる測定可能な基準はありません。

4.4 ベストプラクティス

インターフェイス分離の原則はインターフェイスの定義であり、クラスの定義でもあり、インターフェイスとクラスは可能な限りアトミック インターフェイスまたはアトミック クラスを使用して組み立てられる必要があります。ただし、この原子をどのように分割するかは設計パターンにおける大きな問題であり、実際には次のルールに従って測定できます。

● インターフェイスは 1 つのサブモジュールまたはビジネス ロジックのみを提供します。

● ビジネス ロジックを通じてインターフェイス内のパブリック メソッドを圧縮し、インターフェイスを時々見直して、インターフェイスを「太い」メソッドではなく「筋肉と骨で満たした」ものにするように努めます。

● 汚染されたインターフェースを可能な限り変更し、変更のリスクが高い場合はアダプター モードを使用して変換します。

● 環境を理解し、盲従を拒否します。すべてのプロジェクトや製品には特定の環境要因があります。マスターがこのようにやっているのを見て、ただ真似しないでください。環境が異なり、インターフェース分割の基準も異なります。ビジネスロジックを深く理解し、最高のインターフェイスデザインをあなたの手から生み出します。

インターフェイス分離原則は、他の設計原則と同様に、設計と計画に多くの時間と労力を必要としますが、設計の柔軟性がもたらされ、ビジネス担当者からの「不当な」要件に簡単に対処できるようになります。インターフェイス分離の原則を実装する最善の方法は、1 つのインターフェイスと 1 つのメソッドを使用して、インターフェイス分離の原則に完全に準拠していることを確認することです (単一責任の原則に準拠していない可能性があります)。しかし、あなたはそれを採用しますか? いや、あなたが狂っていない限り!では、インターフェース分離の原則を正しく使用するにはどうすればよいでしょうか? 答えは、経験と常識に基づいてインターフェイスの粒度を決定することです。インターフェイスの粒度が小さすぎると、インターフェイスのデータが急激に増加し、開発者はインターフェイスの海の中で窒息死してしまいます。インターフェースの粒度が大きすぎると、柔軟性が低下し、カスタマイズされたサービスを提供できなくなり、予期せぬリスクが生じます。

インターフェース分離の原則を正確に実践するにはどうすればよいでしょうか? 実践、経験、そして洞察力!

デメテルの法則

第5章 ディミットの法則

5.1 デメテルの法則の定義

デメテルの法則 (LoD) は、最小知識原則 (LKP) とも呼ばれ、名前は異なりますが、オブジェクトは他のオブジェクトについて最小限の知識を持っている必要があるという同じルールを説明しています。平たく言えば、クラスは、結合または呼び出される必要があるクラスについて最低限知っている必要があります。あなたの内部 (結合または呼び出されるクラス) がどれほど複雑であるかは、私には関係ありません。それはあなたの仕事です。非常に多くのパブリック メソッドがあるため、私は非常に多くのメソッドを呼び出しますが、他のメソッドは気にしません。

5.2 私についての知識は少ないほど良い

ディミットの法則は、クラスの低結合に対する明確な要件を提示しており、これには次の 4 つの意味が含まれています。

  1. 友達とのみ通信する

迪米特法则还有一个英文解释是:Only talk to your immediate friends(只与直接的朋友通信。)什么叫做直接的朋友呢?每个对象都必然会与其他对象有耦合关系,两个对象之间的耦合就成为朋友关系,这种关系的类型有很多,例如组合、聚合、依赖等。下面我们将举例说明如何才能做到只与直接的朋友交流。

传说中有这样一个故事,老师想让体育委员确认一下全班女生来齐没有,就对他说:“你去把全班女生清一下。”体育委员没听清楚,就问道:“呀,……那亲哪个?”老师无语了,我们来看这个笑话怎么用程序来实现,类图如图5-1所示。

画像

图5-1 老师要求清点女生类图

Teacher类的commond方法负责发送命令给体育会员,命令他清点女生,其实现过程如代码清单5-1所示。

代码清单5-1 老师类

public class Teacher {
//老师对学生发布命令,清一下女生
public void commond(GroupLeader groupLeader){
List listGirls = new ArrayList();
//初始化女生
for(int i=0;i<20;i++){
listGirls.add(new Girl());
}
//告诉体育委员开始执行清查任务
groupLeader.countGirls(listGirls);
}
}

老师只有一个方法commond,先定义出所有的女生,然后发布命令给体育委员,去清点一下女生的数量。体育委员GroupLeader的实现过程如代码清单5-2所示。

代码清单5-2 体育委员类实现过程

public class GroupLeader {
//清查女生数量
public void countGirls(List listGirls){
System.out.println(“女生数量是:”+listGirls.size());
}
}

老师类和体育委员类都对女生类产生依赖,而且女生类不需要执行任何动作,因此定义一个空类,其实现过程如代码清单5-3所示。

代码清单5-3 女生类

public class Girl {
}

故事中的三个角色都已经有了,再定义一个场景类来描述这个故事,其实现过程如代码清单5-4所示。

代码清单5-4 场景类

public class Client {
public static void main(String[] args) {
Teacher teacher= new Teacher();
//老师发布命令
teacher.commond(new GroupLeader());
}
}

运行结果如下所示:

女生数量是:20

体育委員会は先生の要求に従って女子生徒の数を数え、その数を入手しました。戻って、このプログラムの何が問題なのか考えてみましょう。まず、Teacher クラスには複数のフレンド クラスがあり、フレンド クラスは GroupLeader だけであることを確認してください。なぜガールは友達クラスではないのですか?先生も依存症!フレンドクラスの定義は次のとおりです。メソッドのメンバ変数および入出力パラメータに現れるクラスをメンバフレンドクラスと呼び、メソッド本体に現れるクラスはフレンドクラスに属しません。 、クラス Girl は共通メソッド body に出現するため、Teacher クラスのフレンド クラスではありません。デメテルの法則によれば、クラスはフレンド クラスとのみ通信しますが、定義した commond メソッドは Girl クラスと通信し、List 動的配列を宣言します。つまり、見知らぬクラス Girl と通信するため、クラスの堅牢性が破壊されます。教師。メソッドはクラスの動作であり、そのクラスはその動作が他のクラスと依存関係にあることを知りません。これは許可されておらず、ディミットの法則に大きく違反します。

問題が見つかったので、図 5-2 に示すように、プログラムを変更してクラス図を少し変更しましょう。

画像

図 5-2 変更されたクラス図

クラス図の Girl クラスに対する Teacher の依存関係を削除すると、変更された Teacher クラスがリスト 5-5 に示されます。

コード リスト 5-5 変更された教師クラス

public class Teacher { //教師は生徒たちに女子生徒を片づけるよう命令を出したpublic void command(GroupLeader groupLeader){ //スポーツ委員会にチェックタスクを開始するよう指示groupLeader.countGirls(); } }





変更された GroupLeader クラスは、清朝のコード 5 ~ 6 に示されています。

コードリスト 5-6 修正されたスポーツコミッショナークラス

public class GroupLeader { private List listGirls; //クラス内の女子全員を参加させるpublic GroupLeader(List _listGirls){ this.listGirls = _listGirls; } //女子の数を確認するpublic void countGirls(){ System.out .println("女の子の数は: "+this.listGirls.size()); } }









コンストラクターは GroupLeader クラスで定義され、依存関係はコンストラクターを通じて渡されます。同時に、リスト 5-7 に示すように、シーン クラスにいくつかの変更が加えられています。

コード リスト 5-7 変更されたシーン クラス

public class Client { public static void main(String[] args) { //ガールズグループリストを生成 listGirls = new ArrayList(); //ガールズを初期化for(int i=0;i<20;i++){ listGirls.add (new Girl()); }教師 Teacher= new Teacher(); //教師は命令を出しましたTeacher.commond(new GroupLeader(listGirls)); } }











プログラムに簡単な変更を加え、Teacher の List の初期化をシーン クラスに移動するとともに、GroupLeader に Girl のインジェクションを追加し、見知らぬユーザーが Teacher クラスにアクセスすることを回避しました。クラス Girl を使用し、システム間の結合を減らし、システムの堅牢性を向上させます。

クラスは友人とのみ通信し、馴染みのないクラスとは通信しないことに注意してください。getA().getB().getC().getD() は行わないでください (この種のアクセスは、極端な場合には許可されます。ドット以降の型は同じです)、クラス間の関係はメソッド間ではなくクラス間で確立されるため、メソッドはクラスに存在しないオブジェクトを導入しないようにする必要があります。もちろん、JDK API にはクラス例外が用意されています。

  1. 友達との間に距離がある

人と人との間には距離があり、遠すぎると次第に疎遠になり、やがて他人になってしまい、近すぎると刺し合います。友達同士の関係を説明するのに最も適切な話は、「2 匹のハリネズミが暖をとっているが、暖を保つには遠すぎるし、刺し合うのが近すぎるので、刺し合わずに暖を保つことができる距離を保たなければならない」というものです。ディミットの法則はこの距離を表すもので、友人であってもすべてを話し、すべてを知ることはできません。

ソフトウェアをインストールするとき、多くの場合、ガイド アクションがあります。最初のステップはインストールするかどうかの確認、2 番目のステップはライセンスの確認、そしてインストール ディレクトリの選択です。これは一般的な一連のアクションです。プログラムの詳細は次のとおりです: 1 つ以上のクラスを呼び出し、最初に最初のメソッドを実行し、次に 2 番目のメソッドを実行し、次に返された結果に従って 3 番目のメソッドまたは 4 番目のメソッドを呼び出せるかどうかを確認します。図 5-3 に示します。

画像

図 5-3 ソフトウェアのインストール プロセスのクラス図

ソフトウェアのインストールのプロセスを実現するための非常に単純なクラス図。最初のメソッドは最初のステップで何を行うかを定義し、2 番目のメソッドは 2 番目のステップで何を行うかを定義し、3 番目のメソッドは 3 番目のステップで何を行うかを定義します。実装プロセスをリスト 5-8 に示します。

コードリスト 5-8 ガイダンスクラス

public class Wizard { private Random rand = new Random(System.currentTimeMillis()); //最初のステップpublic int first(){ System.out.println("最初のメソッドを実行します..."); return rand.nextInt ( 100); } //2 番目のステップpublic int Second(){ System.out.println("2 番目のメソッドを実行..."); return rand.nextInt(100); } //3 番目のメソッドpublic int third ( ){ System.out.println("3 番目のメソッドを実行します..."); return rand.nextInt(100); } }
















Wizard クラスには 3 つのステップ メソッドが定義されています。各ステップには、指定されたタスクを完了するための関連ビジネス ロジックがあります。ランダム関数を使用して、ビジネス実行の戻り値を置き換えます。ソフトウェア インストールの InstallSoftware クラスをリスト 5-9 に示します。

コード リスト 5-9 InstallSoftware クラス

public class InstallSoftware { public void installWizard(Wizard wizard){ int first = wizard.first(); //first によって返された結果に従って、2 番目を実行する必要があるかどうかを確認しますif(first>50){ int Second = wizard .2(); if(2>50){ int third = wizard.third(); if( 3 >50){ wizard.first(); } } } }













各メソッドの実行結果に応じて、手動選択操作をシミュレートして次のメソッドに進むかどうかが決定されます。シーン クラスをリスト 5-10 に示します。

コードリスト 5-10 シーンクラス

public class Client { public static void main(String[] args) { InstallSoftware invoker = new InstallSoftware(); invoker.installWizard(new Wizard()); } }




上記のプログラムは非常に単純で、実行結果は乱数に関連付けられており、実行結果は毎回異なります。読者は自分で実行して結果を確認する必要があります。プログラムは単純ですが、隠れた問題は単純ではありません。プログラムの何が問題なのかを考えてください。Wizard クラスが InstallSoftware クラスに公開するメソッドが多すぎるため、両者の友情が近すぎてカップリング関係が非常に強くなります。Wizard クラスの最初のメソッドの戻り値の型を int から boolean に変更する場合は、InstallSoftware クラスを変更する必要があるため、変更および変更のリスクが分散されます。したがって、このような結合は非常に不適切であるため、設計をリファクタリングする必要があります。リファクタリングされたクラス図を図 5-4 に示します。

画像

図 5-4 リファクタリングされたソフトウェア インストール プロセスのクラス図

リスト 5-11 に示すように、installWizard メソッドを Wizard クラスに追加してインストール プロセスをカプセル化し、元の 3 つのパブリック メソッドをプライベート メソッドに変更します。

コード リスト 5-11 修正されたガイド クラスの実装プロセス

public class Wizard { private Random rand = new Random(System.currentTimeMillis()); //最初のステップprivate int first(){ System.out.println("最初のメソッドを実行します..."); return rand.nextInt ( 100); } //2 番目のステップprivate int Second(){ System.out.println("2 番目のメソッドを実行..."); return rand.nextInt(100); } //3 番目のメソッドprivate int third ( ){ System.out.println("3 番目のメソッドを実行します..."); return rand.nextInt(100); } //ソフトウェアのインストール処理public void installWizard(){ int first = this.first(); / /first によって返された結果に従って、2 番目を実行する必要があるかどうかを確認してくださいif(first>50){ int Second = this.second(); if(second>50){























int third = this.third();
if(3 番目 >50){ this.first(); } } } } }





3 つのステップのアクセス権をプライベートに変更し、InstallSoftware のメソッド installWizad を Wizard メソッドに移動します。このようなリファクタリングの後、Wizard クラスは public メソッドのみを公開するため、最初のメソッドの戻り値が変更されたとしても、影響を受けるのは Wizard 自体のみで、他のクラスには影響が及ばないことから、クラスの凝集性の高さがわかります。

コード リスト 5-12 に示すように、InstallSoftware クラスに小さな変更を加えます。

コード リスト 5-12 変更された InstallSoftware クラス

public class InstallSoftware { public void installWizard(Wizard wizard){ // 直接使用wizard.installWizard(); } }




コード リスト 5-10 に示すように、シーン クラス Client は変更されません。リファクタリングを行うことで、クラス間の結合関係が弱まり、構造が明確になり、変更によるリスクが小さくなります。

クラスが公開するパブリック プロパティまたはメソッドが増えるほど、変更に関わる範囲が広がり、変更によって生じるリスクの分散も大きくなります。そのため、フレンドクラス間の距離を保つためには、パブリックメソッドや属性を削減できるか、プライベート、パッケージプライベート(パッケージタイプ、クラス以前のアクセス禁止、メソッド、変数) 権限、デフォルトはパッケージ タイプ)、protected およびその他のアクセス権限、final キーワードを追加できるかどうかなど。

注意 迪米特法则要求类“羞涩”一点,尽量不要对外公布太多的public方法和非静态的public变量,尽量内敛,多使用private、package-private、protected等访问权限。

  1. 是自己的就是自己的

在实际应用中经常会出现这样一个方法:放在本类中也可以,放在其他类中也没有错,那怎么去衡量呢?你可以坚持这样一个原则:如果一个方法放在本类中,既不增加类间关系,也对本类不产生负面影响,那就放置在本类中。

  1. 谨慎使用Serializable

在实际应用中,这个问题是很少出现的,即使出现也会立即被发现并得到解决。是怎么回事呢?举个例子来说,在一个项目中使用RMI(Remote Method Invocation,远程方法调用)方式传递一个VO(Value Object,值对象),这个对象就必须实现Serializable接口(仅仅是一个标志性接口,不需要实现具体的方法),也就是把需要网络传输的对象进行序列化,否则就会出现NotSerializableException异常。突然有一天,客户端的VO修改了一个属性的访问权限,从private变更为public,访问权限扩大了,如果服务器上没有做出相应的变更,就会报序列化失败,就这么简单。但是这个问题的产生应该属于项目管理范畴,一个类或接口在客户端已经变更了,而服务器端却没有同步更新,难道不是项目管理的失职吗?

5.3 最佳实践

迪米特法则的核心观念就是类间解耦,弱耦合,只有弱耦合了以后,类的复用率才可以提高。其要求的结果就是产生了大量的中转或跳转类,导致系统的复杂性提高,同时也为维护带来了难度。读者在采用迪米特法则时需要反复权衡,既做到让结构清晰,又做到高内聚低耦合。

「見知らぬ 2 人の人間の間には最大 6 人が存在する、つまり 6 人しか結びつかない」という理論を聞いたことがあるかどうかはわかりませんが、これが有名な「6 次の隔たり」です。仮説"。この理論をプロジェクトに適用すると、私と呼び出したいクラスの間には最大 6 つのパスが存在します。これは単なる遊びです。実際のアプリケーションでは、クラスが別のクラスにアクセスするために 2 回以上ジャンプする場合、それをリファクタリングする方法を見つける必要があります。なぜ 2 回以上ジャンプするのでしょうか? なぜなら、システムの成功は基準や原則によって決まるだけでなく、多くの外部要因によっても決まるからです。ジャンプが増えるほどシステムは複雑になり、維持するのが難しくなります。 2 回を超えない すべてが許容可能ですが、特定の問題についての具体的な分析が必要です。

デメテルの法則ではクラス間の分離が必要ですが、コンピューターのバイナリ 0 と 1 の最小単位でない限り、分離には限界があります。実際のプロジェクトでは、この原則を適切に考慮する必要があり、原則を適用するためだけにプロジェクトを実行しないでください。この原則はあくまで参考です。この原則に違反してもプロジェクトが失敗するわけではありません。この原則を採用する際には全員が繰り返し測定する必要があります。これを守らないのは間違いです。厳格に実施するのは「行き過ぎ」です。

開閉原理

第 6 章 オープンクローズの原則

6.1 オープンクローズ原則の定義

哲学では、矛盾の法則、つまり対立物の統一の法則は、唯物弁証法の最も基本的な法則です。この章で議論する開始と終了の原則は、同様に重要かつ普遍的なものですか? 実際、オープンとクローズの原則は、Java の世界における最も基本的な設計原則です。これは、安定した柔軟なシステムを構築する方法を示します。まず、オープンとクローズの原則の定義を見てみましょう。

クラス、モジュール、関数などのソフトウェア エンティティは、拡張に対してオープンである必要がありますが、変更に対してはクローズされている必要があります (クラス、モジュール、関数などのソフトウェア エンティティは、拡張に対してオープンであり、変更に対してクローズである必要があります)。

初めてこの定義を見たとき、あなたは混乱するかもしれません。これは拡張の余地があるのでしょうか? 何を開く?変更のために閉じられています。閉じるにはどうすればよいですか? 大丈夫、私があなたの疑問を一歩ずつ解決へと導きます。

私たちが 1 つのことを行うとき、または方向性を選択するとき、一般に 3 つのステップを経る必要があります。What—何を、Why—なぜ、How—どのように行うかです (3W 原則と呼ばれ、How は最後の w をとります)。開閉の原理についても、開閉の原理とは何か、なぜ開閉の原理を使用するのか、開閉の原理をどのように使用するのか、この 3 つのステップを使用して分析します。 。

6.2 廬山の開閉原理の正体

オープンクローズ原則の定義は非常に明確に示しています。ソフトウェア エンティティは拡張に対してオープンであり、変更に対してクローズである必要があります。つまり、ソフトウェア エンティティは既存のコードの変更ではなく、拡張を通じて変更を達成する必要があります。では、ソフトウェアエンティティとは何でしょうか? ソフトウェア エンティティは次の部分で構成されます。

● プロジェクトまたはソフトウェア製品内で特定の論理規則に従って分割されたモジュール。

● 抽象化とクラス。

●方法。

「ソフトウェア製品はその存続期間がある限り変化します。変化は既成の事実であるため、プロジェクトの安定性と柔軟性を向上させ、真の実現を実現するために、設計中にこれらの変化に適応するよう最善を尽くす必要があります。」変化を受け入れること。」オープンとクローズの原則は、既存のコードを変更するのではなく、ソフトウェア エンティティの動作を可能な限り拡張することによって変更を実現する必要があることを示しており、現在の開発設計を制約するためにソフトウェア エンティティの将来の出来事のために定式化された原則です。開閉原理とは何かを説明するために、書籍を販売する書店を例として、そのクラス図を図 6-1 に示します。

画像

図 6-1 書店の販売クラス図

IBook は、データの 3 つの属性 (名前、価格、作成者) を定義します。小説クラス NovelBook は特定の実装クラスであり、これはすべての小説本の一般名であり、BookStore は書店を指し、IBook インターフェイスはコード リスト 6-1 に示されています。

コードリスト 6-1 ブックインターフェイス

public Interface IBook { //本には名前がありますpublic String getName(); //本には価格がありますpublic int getPrice(); //本には著者がありますpublic String getAuthor(); }






リスト 6-2 に示すように、現在、書店ではフィクションの本のみを販売しています。

コードリスト 6-2 フィクション

public class NovelBookimplements IBook { //本の名前private String name; //本の価格private int Price; //本の著者private String author; //コンストラクターを介して本のデータを転送public NovelBook(String _name,int _price,String _author){ this.name = _name; this.price = _price; this.author = _author; } //著者が誰であるかを取得しますpublic String getAuthor() { return this.author; } //本の名前は何ですかpublic String getName ( ) { return this.name ; } // 本の価格を取得public int getPrice() { return this.price; } }
























なお、価格を int 型で定義するのは間違いではありませんが、非金融プロジェクトで通貨を扱う場合は、一般的に 2 桁の精度が使用され、計算過程で 100 倍に拡張するのが一般的な設計方法です。表示する必要がある場合は 100 分の 1 に縮小して、精度による誤差を軽減します。

書店で書籍を販売するプロセスをリスト 6-3 に示します。

コードリスト 6-3 書店の書籍

public class BookStore { private Final static ArrayList bookList = new ArrayList(); //static static モジュールがデータを初期化します。実際のプロジェクトでは通常、永続層によって行われますstatic{ bookList.add(new NovelBook("Dragon Babu",3200, " 金庸")); bookList.add(new NovelBook("ノートルダム", 5600, "ヒューゴ")); bookList.add(new NovelBook("レ・ミゼラブル", 3500, "ヒューゴ")); bookList.add (new NovelBook("Jin Ping Mei", 4300, "Lan Ling Xiao Xiao Sheng")); } //書店をシミュレートして書籍を購入しますpublic static void main(String[] args) { NumberFormat formatter = NumberFormat.getCurrencyInstance(); formatter.setMinimumFractionDigits (2); System.out.println("-----------書店で販売されている書籍は次のように記録されます: -----------"); for (IBook ブック: bookList){ System.out.println("ブック名:" + book.getName()+"\tブック著者:" +















book.getAuthor()+"\t本の価格:"+ formatter.format(book.getPrice()/
100.0)+"元");
}
}
}

データの初期化を実現するために BookStore で静的モジュールを宣言します。この部分は永続層から生成し、永続層フレームワークで管理する必要があります。実行結果は次のとおりです。

---------------書店で販売されている書籍は以下の通り収録されています。

書名:天龍八書 著者:金庸 書籍価格:¥25.60

書籍名:ノートルダム・ド・パリ 書籍著者:ユゴー 書籍価格:¥50.40

書籍名:レ・ミゼラブル 書籍著者名:ユゴー書籍価格:¥28.00

書名:金平梅書 著者:蘭陵暁暁生 書籍価格:¥38.70

プロジェクトは動き出し、本は普通に売れ、書店も利益を得ることができました。2008年から世界経済が低迷し始め、小売業界も大きな影響を受け、書店は生き残るために値引き販売を始め、40元以上の本はすべて10%引き、その他の本は定価で販売するようになった。 20%割引。すでに運用が開始されているプロジェクトにとって、これは変化ですが、このような需要の変化にどのように対応すべきでしょうか? この問題を解決するには 3 つの方法があります。

● インターフェースを変更する

IBook に特別に割引処理に使用される新しいメソッド getOffPrice() を追加し、すべての実装クラスがこのメソッドを実装します。ただし、この変更の結果として、実装クラス NovelBook を変更する必要があり、BookStore の main メソッドも変更する必要がありますが、同時に、インターフェースとしての IBook は安定していて信頼性が高く、頻繁に変更すべきではありません。そうしないと、コントラクトとしてのインターフェイスの機能が有効性を失います。したがって、プログラムは拒否されました。

● 実装クラスを変更する

NovelBookクラスのメソッドを修正し、getPrice()に直接値引き処理を実装する 良い方法です プロジェクトでは誰でもよく使う方法だと思います 業務変更の一部(または不具合修正)は可能ですクラスファイルを置き換えることで完了します。)。この方法は、プロジェクトに明確な憲章 (チーム内の制約) がある場合、または優れたアーキテクチャ設計がある場合には非常に良い方法ですが、この方法にはまだ欠陥があります。例えば、本の購入者も価格を見る必要がありますが、この方法では割引価格を実現しているため、購入者が見ているのは割引価格であり、情報の非対称性による意思決定の誤りが生じます。したがって、このスキームは最適なスキームではありません。

● 拡張による変化

サブクラス OffNovelBook を追加し、getPrice メソッドをオーバーライドし、高レベル モジュール (つまり、静的静的モジュール領域) が OffNovelBook クラスを通じて新しいオブジェクトを生成して、ビジネスの変更に備えたシステムの最小限の開発を完了します。これは変更が少なく、リスクも少ない良い方法です。変更されたクラス図を図 6-2 に示します。

画像

図 6-2 拡張された書店販売クラス図

OffNovelBook クラスは NovelBook を継承し、元のコードを変更せずに getPrice メソッドをオーバーライドします。新しく追加されたサブクラス OffNovelBook をリスト 6-4 に示します。

コードリスト 6-4 小説の割引価格

public class OffNovelBook extends NovelBook { public OffNovelBook(String _name,int _price,String _author){ super(_name,_price,_author); } //販売価格をオーバーライド@Override public int getPrice(){ //元の価格int selfPrice = super .getPrice(); int offPrice=0; if(selfPrice>4000){ //元の価格が 40 元を超える場合、10%オフ offPrice = selfPrice * 90 /100; }else{ offPrice = selfPrice * 80 / 100; }価格をオフに戻す ; } }
















非常に簡単で、getPrice メソッドをオーバーライドし、拡張機能を通じて新しく追加されたビジネスを完了するだけです。書店クラス BookStore はサブクラスに依存する必要があり、コードはリスト 6-5 に示すように少し変更されています。

コードリスト 6-5 書店の割引販売クラス

public class BookStore { private Final static ArrayList bookList = new ArrayList(); //static static モジュールがデータを初期化します。実際のプロジェクトでは通常、永続層によって行われますstatic{ bookList.add(new OffNovelBook("Dragon Babu", 3200, " 金庸")); bookList.add(new OffNovelBook("ノートルダム", 5600, "ヒューゴ")); bookList.add(new OffNovelBook("レ・ミゼラブル", 3500, "ヒューゴ")); bookList.add (new OffNovelBook("Jin Ping Mei", 4300, "Lan Ling Xiao Xiao Sheng")); } //書店をシミュレートして書籍を購入しますpublic static void main(String[] args) { NumberFormat formatter = NumberFormat.getCurrencyInstance(); formatter.setMinimumFractionDigits (2); System.out.println("-----------書店で販売されている書籍は次のように記録されます: -----------"); for (IBook 本: bookList){














System.out.println("書籍名:" + book.getName()+"\t書籍著者:" + book.getAuthor()+ "\t書籍価格:" + formatter.format (book.getPrice()/ 100.0) +"元");
}
}
}

太字部分のみ変更し、その他の部分は変更せずに実行した結果は以下の通りです。

------------------------ 書店での書籍販売実績は以下の通りです。 ----- ---- --

書名:天龍八書 著者:金庸 書籍価格:¥25.60

書籍名:ノートルダム・ド・パリ 書籍著者:ユゴー 書籍価格:¥50.40

書籍名:レ・ミゼラブル 書籍著者名:ユゴー書籍価格:¥28.00

書名:金平梅書 著者:蘭陵暁暁生 書籍価格:¥38.70

OK、割引販売の展開は完了です。これを見て、次のアイデアが得られるかもしれません。OffNoveBook クラスを追加した後もビジネス ロジックは変更されており、静的静的モジュール領域も変更されています。この部分は確かに変更されています。この部分は上位モジュールに属し、永続層によって生成されます。ビジネス ルールが変更されると、新しいビジネスに適応するために上位モジュールを部分的に変更する必要があります。変更は次のようになります。変更リスクの拡散を防ぐため、可能な限り少ないものにします。

オープンとクローズの原則は拡張に対してオープンであり、変更に対してクローズであることに注意してください。これは、変更が行われないという意味ではありません。低レベルのモジュールへの変更は高レベルのモジュールと結合する必要があります。そうでない場合、それは孤立した無意味なコードになります。断片。

変化は次の 3 つのタイプに分類できます。

● 論理的な変更

他のモジュールを関与させることなく 1 つのロジックのみが変更されます。たとえば、元のアルゴリズムは b+c でしたが、今度は b*cに変更する必要があります。これは、元のクラスのメソッドを変更することで実行できます。すべての依存クラスまたは関連クラスが同じロジックに従って処理されるということです。

● サブモジュールの変更

モジュールの変更は他のモジュールに影響を及ぼしますが、特に低レベルのモジュールの変更は必然的に高レベルのモジュールの変更につながります。そのため、拡張によって変更が完了した場合、高レベルのモジュールの変更は避けられません。処理モジュールと同様に、この部分の変更はインターフェースの変更を引き起こす可能性もあります。

● 目に見えるビューの変更

ビューは、JSP プログラム、Swing インターフェイスなど、顧客に提供されるインターフェイスであることがわかります。この部分の変更は一般に連鎖反応を引き起こします (特に中国のプロジェクトや、ヨーロッパや米国のアウトソーシング プロジェクトの場合)一般的にはあまり影響はありません)。インターフェイス上のボタンや文字の配置を変えるだけなら簡単ですが、最も多いのは業務結合の変更ですが、これは何を意味するのでしょうか?データを表示するためのリストは、当初の要件では 6 列でしたが、ある日突然 1 列が追加され、この列は N 個のテーブルにまたがる必要があり、M 個のロジックを処理して表示できるようになり、このような変更は非常に恐ろしいですが、まだ 変更は拡張によって実現できますが、それは元の設計が柔軟であるかどうかによって決まります。

書店の販売プログラムを振り返ってみましょう。まず、かなり柔軟な設計になっています (柔軟性のなさとはどのようなものですか? IBook を使用する BookStore 内のすべての場所がすべて実装クラスに変更され、その後 ComputerBook ブックが拡張されます。何が柔軟性に欠けているかを知ることができます)、次に要件に変更があり、サブクラスを拡張することでその変更を受け入れ、最終的にサブクラスを動作環境に導入し、新しいロジックが正式に運用環境に導入されます。分析の結果、元のモジュール コードは変更されておらず、IBook インターフェイスも変更されておらず、NovelBook クラスも変更されていないことがわかり、これは既存のビジネス コードに属しており、履歴の純粋性が維持されています。履歴を変更するという考えを放棄する プロジェクトの基本的な流れは次のようになります: プロジェクト開発、リファクタリング、テスト、生産、運用保守 リファクタリングでは、元の設計とコードを変更し、運用保守を行うことができます元のコードの変更は、過去のコードの純度を維持し、システムの安定性を向上させます。

6.3 オープンクローズ原則を使用する理由

あらゆるものの誕生には存在の必然性があり、存在には合理性があり、開閉原理の存在にも合理性があるのに、なぜそう言えるのでしょうか?

まず、オープンとクローズの原理は非常に有名ですが、オブジェクト指向プログラミングをしている限り、Java、C++、Smalltalkなど、どの言語であっても、オープンとクローズの原理は同じです。開発中に言及されました。

第二に、開閉の原則は最も基本的な原則であり、最初の 5 章で紹介された原則は、開閉の原則の具体的な形式であり、つまり、最初の 5 つの原則は、指針となるツールと方法です。デザインと開閉の原理はその精神的なリーダーです。別の観点から見ると、Java 言語の名前の通り、オープンとクローズの原則は抽象クラスであり、他の 5 つの原則は具体的な実装クラスです。この設計は力学におけるニュートンの第一法則の設計に似ており、幾何学におけるストックの法則と特殊相対性理論における質量エネルギー方程式の位置は比類のないものです。

最後に、開閉の原理は非常に重要であり、その重要性は次の側面からも理解できます。

  1. テストに対するオープンクローズ原則の影響

運用環境に導入されたすべてのコードは意味があり、システム ルールの対象となります。そのようなコードは、ロジックが正しいことを確認するだけでなく、過酷な条件 (高圧、例外) が発生しないことを確認するために、「強化された」テスト プロセスを通過する必要があります。 、エラー)は「有害なコード」(Poisonous Code)を生成しないため、変更が提案される場合、元の堅牢なコードを修正せずに拡張のみで変更できるかどうかを検討する必要があります。そうでない場合は、元のテスト プロセスを再び戻す必要があり、単体テスト、機能テスト、統合テスト、さらには受け入れテストが必要になります。自動テスト ツールが強く推奨されていますが、依然として手動テストに代わることはできません。

前述の書店を例に挙げます。IBook インタフェースが作成された後、実装クラス NovelBook も作成されます。テスト用のテスト クラスを作成する必要があります。テスト クラスをリスト 6-6 に示します。

コードリスト 6-6 新規クラスの単体テスト

public class NovelBookTest extends TestCase { private String name = "Ordinary World"; private int Price = 6000; private String author = "Lu Yao"; private IBook NovelBook = new NovelBook(name,price,author); //test getPrice メソッドpublic void testGetPrice() { //元の価格での販売、入力値と出力値が等しいかどうかに応じてアサートしますsuper.assertEquals(this.price, this.novelBook.getPrice()); } }









単体テストが成功すると、緑色のバーが表示されます。単体テストでは、「コードをきれいに保つためにバーを緑色に保つ」という非常に有名な格言があります。つまり、緑色のバーを維持することはコードをきれいにするのに役立ちます。これは何を意味するのでしょうか。緑色のバーは、Junit 操作の 2 つの結果のうちの 1 つです。赤色のバーは単体テストが失敗し、緑色のバーは単体テストが成功しました。通常、1 つのメソッドに対して 3 つ以上のテスト メソッドが存在します。まず通常のビジネス ロジックをテストし、次に境界条件、例外をテストする必要があります。重要なメソッドには 10 を超えるテスト メソッドがあり、単体テストはクラスのテストです。メソッドの結合は許可されます。このような条件では、変更を完了するために 1 つのメソッドまたは複数のメソッド コードを変更することを考えている場合、それは基本的に夢です。このクラスのすべてのテスト メソッドはリファクタリングする必要があります。大量のコードをリファクタリングするのはどのような感じですか?馴染みがない!

書店での書籍の販売の例では、割引販売の需要が追加されます。ビジネス要件の変更を実現するために getPrice メソッドを直接変更する場合は、単体テスト クラスを変更する必要があります。考えてみてください、私たちが挙げた例は非常に単純ですが、それが複雑なロジックの場合は、テスト クラスを認識できないほど変更する必要があります。また、実際のプロジェクトでは、通常、クラスにはテスト クラスが 1 つしかなく、その中には多数のテスト メソッドが存在する可能性があり、すでに複雑なアサーションの束に多くの変更が加えられた場合、テスト漏れが発生することは避けられません。 . こちらはプロジェクトマネージャーです。耐え難いです。

したがって、ビジネス ロジックの変更は、変更ではなく拡張機能によって実装する必要があります。上記の例では、サブクラス OffNovelBook を追加することでビジネス要件の変更が完了していますが、これはテストにどのようなメリットがありますか? テスト ファイル OffNovelBookTest を再生成し、getPrice をテストします。単体テストは分離されたテストです。提供するメソッドが正しい限り、他のものは気にしません。OffNovelBookTest はコード リスト 6-7 に示されています。

リスト 6-7. 販売中の小説の単体テスト

public class OffNovelBookTest extends TestCase { private IBook Below40NovelBook = new OffNovelBook("Ordinary World", 3000, "Lu Yao"); private IBook Above40NovelBook = new OffNovelBook("Ordinary World", 6000, "Lu Yao"); // かどうかをテストします。 40 元未満のデータは 20% 割引ですpublic void testGetPriceBelow40() { super.assertEquals(2400, this.below40NovelBook.getPrice()); } //40 元を超える書籍が 10% 割引かどうかをテストしますpublic void testGetPriceAbove40( ){ super.assertEquals(5400, this.above40NovelBook.getPrice()); } }










新しく追加されたクラスが正しい限り、新しく追加されたクラス、新しく追加されたテスト メソッド。

  1. オープンクローズ原理により再利用性が向上

オブジェクト指向設計では、ビジネス ロジックをクラスに個別に実装するのではなく、すべてのロジックがアトミック ロジックから結合されます。この方法でのみコードを再利用でき、粒度が小さいほど再利用される可能性が高くなります。では、なぜ再利用するのでしょうか?コードの量を減らし、同じロジックが複数の隅に散在することを避け、将来のメンテナが小さな欠陥を修正したり新しい機能を追加したりするためにプロジェクト全体で関連コードを検索し、開発者に「極度の失望」を与えることを防ぎます。感情。では、どうすれば再利用率を高めることができるのでしょうか?ロジックを分割できなくなるまでロジックの粒度を減らします。

  1. オープンクローズ原理によりメンテナンス性が向上

ソフトウェアが運用開始された後、メンテナの仕事はデータをメンテナンスするだけでなく、プログラムを拡張することでもあります。メンテナが最も喜んで行うことは、クラスを変更するのではなく、クラスを拡張することです。元のコードの良し悪しに関係なく、メンテナが元のコードを読んで修正するのは非常に苦痛です。修正する前に元のコードの海を泳がせないでください。苦痛と荒廃の。

  1. オブジェクト指向開発の要件

すべてはオブジェクトです。すべてをオブジェクトに抽象化し、そのオブジェクトを操作する必要がありますが、すべては動いています。動きがあるときは変化があり、変化に対処するための戦略があり、変化に素早く対処する方法があります。 ? これには、設計の開始時に考えられるすべての変化要因を考慮に入れ、その後インターフェースを離れ、「可能性」が「現実」になるのを待つ必要があります。

6.4 オープンクローズ原則の使用方法

開閉の原理は非常に想像上の原理です。最初の 5 つの原理は開閉の原理を具体的に説明したものですが、開閉の原理はそれだけに限定されるものではありません。一生懸命勉強しなさいということですが、実際はそうではありません。何をどう学ぶべきかを教えてください。経験して習得する必要があります。開閉の原則はスローガンでもありますが、このスローガンを実際の仕事にどのように適用するか?

  1. 抽象的な制約

抽象化とは、特定の実装を持たないもののグループの一般的な説明です。これは、抽象化には多くの可能性があり、要件の変更に応じて変化する可能性があることを意味します。したがって、変更される可能性のある動作のグループは、インターフェイスまたは抽象クラスを通じて制約することができ、拡張に対してオープンにすることができます。これには 3 つの意味が含まれます。まず、拡張はインターフェイスまたは抽象クラスを通じて制約され、拡張の境界は制限されます。また、インターフェイスや抽象クラスに存在しないパブリック メソッドにそれらを出現させることはできません。2 番目に、パラメータの型や参照オブジェクトの実装クラスの代わりにインターフェイスや抽象クラスを使用するようにしてください。3 番目に、抽象化層を安定した状態に保つようにしてください。可能な限り決定し、一度決定したら変更は許可されません。書店を例に挙げると、現在は小説本のみを販売しています。結局のところ、一度の操作ではリスクが伴います。そのため、書店はコンピュータ書籍を追加しました。書籍のタイトル、著者、価格などの情報が含まれているだけではありません。この本だけでなく、次のような固有の属性もあります。 指向性 フィールドとは何か、つまり、プログラミング言語関連、データベース関連など、その範囲です。 変更されたクラス図を図 6-3 に示します。

画像

図 6-3 業態追加後の書店売上クラス図

インターフェイス IComputerBook と実装クラス Computer-Book が追加され、BookStore は何も変更せずに書店でコンピュータ書籍を販売するビジネスを完了できます。コンピュータ ブックのインターフェイスをコード リスト 6 ~ 8 に示します。

コード リスト 6-8 コンピュータ ブック インターフェイス

public Interface IComputerBook extends IBook{ //コンピュータ ブックにはスコープがあるpublic String getScope(); }


非常に単純ですが、コンピュータ ブックには、ブックの範囲を取得し、IBook インターフェイスを継承するメソッドが追加されます。結局のところ、コンピュータ ブックもブックであり、その実装はコード リスト 6 ~ 9 に示されています。

コードリスト 6-9 コンピュータ書籍

public class ComputerBook は IComputerBook { private String 名を実装します。プライベート文字列スコープ。プライベート文字列作成者。プライベート int 価格; public ComputerBook(String _name,int _price,String _author,String _scope){ this.name=_name; this.price = _price; this.author = _author; this.scope = _scope; public String getScope() { this.scope を返します。public String getAuthor() { this.author を返します。public String getName() { this.nameを返しますpublic int getPrice() { this.price を返します。} }






















これも非常に簡単で、コード リスト 6 ~ 10 に示すように、IComputerBook を実装するだけで、BookStore クラスは何も変更せず、静的な静的モジュールにデータを追加するだけです。

コード リスト 6-10 コンピューター書籍を販売する書店

public class BookStore { private Final static ArrayList bookList = new ArrayList(); //static static モジュールがデータを初期化します。実際のプロジェクトでは通常、永続層によって行われますstatic{ bookList.add(new NovelBook("Dragon Babu",3200, " 金庸")); bookList.add(new NovelBook("ノートルダム", 5600, "ヒューゴ")); bookList.add(new NovelBook("レ・ミゼラブル", 3500, "ヒューゴ")); bookList.add (new NovelBook("Jin Ping Mei", 4300, "Lanling Xiaoxiaosheng")); //コンピューター書籍を追加bookList.add(new ComputerBook("Think in Java", 4300, "Bruce Eckel", "Programming Language" )) ; } // 書籍を販売する書店をシミュレートしますpublic static void main(String[] args) { NumberFormat formatter = NumberFormat.getCurrencyInstance(); formatter.setMinimumFractionDigits(2); System.out.println("------ -- ---書店で販売された書籍は次のように記録されます:--------」);















for(IBook book:bookList){ System.out.println("書籍名:" + book.getName()+"\t書籍著者:" + book.getAuthor()+ "\t書籍価格:" + formatter.format ( book.getPrice()/100.0)+"元"); } } }



書店ではコンピュータ書籍の販売を開始し、その実績は以下のとおりです。

------------------------ 書店で販売されている書籍は次のように記録されています。 ------------------- --

書名:天龍八書 著者:金庸 書籍価格:¥32.00

書籍名:ノートルダム・ド・パリ 書籍著者名:ユゴー 書籍価格:¥56.00

書籍名:レ・ミゼラブル 書籍著者名:ユゴー書籍価格:¥35.00

書名:金平梅書 著者:蘭陵暁暁生 書籍価格:¥43.00

書籍名:Javaで考える本 著者:Bruce Eckel 書籍価格:¥43.00

私がメンテナンスを担当するのであれば、このようなことは他の業務と連動する必要がなく、簡単な作業なのでとても喜んでやります。元のコードにレンガやタイルを追加するだけで、ビジネスの変化を実現できます。このコードがどのような意味を持っているかを見てみましょう。

まず、ComputerBook クラスは、IComputerBook インターフェイスを介して渡される制約である IBook の 3 つのメソッドを実装する必要があります、つまり、作成した IBook インターフェイスは、拡張クラス ComputerBook に対する拘束力を持っています。 BookStore クラスが大量のリビジョンを実装する必要がないバインド力。

次に、元のプログラム設計でインターフェイスの代わりに実装クラスを使用した場合、どのような問題が発生するでしょうか。次のコードに示すように、BookStore クラスのプライベート変数 bookList を変更してみましょう。

プライベート最終静的 ArrayList bookList = new ArrayList();

元の IBook の依存関係を NovelBook 実装クラスの依存関係に変更します。考えてみましょう。今回は拡張を続けることができますか? このように設計すると、拡張することはまったくできず、元のビジネス ロジック (つまり main メソッド) を変更する必要があり、そのような拡張は基本的に役に立ちません。

最後に、IBook に getScope メソッドを追加すれば可能でしょうか? 答えは「いいえ」です。元の実装クラス NovelBook はすでに実稼働および運用されているため、このメソッドは必要ありません。また、インターフェイスは他のモジュールと通信するためのコントラクトであり、コントラクトを変更することは、他のモジュールにコントラクトの変更を許可することと同等です。 。したがって、インターフェイスまたは抽象クラスが定義されたら、すぐに実行する必要があり、完全な作り直しでない限り、インターフェイスを変更することは考えるべきではありません。

したがって、拡張に対するオープン性を実現するには、抽象的な制約が最初の前提条件となります。

  1. メタデータはモジュールの動作を制御します

プログラミングは非常に難しくて疲れる仕事ですが、どうすればストレスを軽減できるでしょうか? 答えは、メタデータをできる限り使用してプログラムの動作を制御し、開発の繰り返しを減らすことです。メタデータとは何ですか? 環境とデータを記述するために使用されるデータは、平たく言えば、ファイルまたはデータベースから取得できる構成パラメーターです。非常に単純な例を挙げると、ログイン メソッドは次のようなロジックを提供します。まず、IP アドレスが許可されたアクセスのリストに含まれているかどうかを確認し、次にデータベース内のパスワードを検証するかどうかを決定します (SSH アーキテクチャが使用されている場合は、データベース内のパスワードを検証するかどうかを決定します)。 Struts 実装によってインターセプトされる)、この動作はメタデータ制御モジュールの動作の典型的な例であり、その中で究極は制御の反転 (Inversion of Control) であり、最もよく使用されるのは Spring コンテナであり、SpringContext 設定ファイルでは次のような基本的な設定が行われます。コードとしてはリスト 6-11 に示されています。

コード リスト 6-11 SpringContext の基本構成ファイル

次に、Father クラスのサブクラス Son を作成して新しいビジネスを完成させ、同時に SpringContext ファイルを変更します (コード リスト 6-12 を参照)。

コード リスト 6-12 拡張 SpringContext 構成ファイル

サブクラスを拡張して設定ファイルを変更することで業務変更が完了するのもフレームワーク導入のメリットです。

  1. プロジェクト憲章を作成する

チームでは、プロジェクト憲章を確立することが非常に重要です。憲章には、すべての担当者が従う必要がある規則が指定されているためです。プロジェクトの場合、規則は構成よりも優れています。誰もがプロジェクトを行ったことがあると思いますが、プロジェクトによって大量の構成ファイルが生成されることに気づくでしょう。簡単な例で言うと、SSHプロジェクト開発を例に挙げると、プロジェクト内にはBean設定ファイルが多数存在し、管理が非常に面倒です。拡張する必要がある場合は、サブクラスを追加し、SpringContext ファイルを変更する必要があります。ただし、プロジェクトでそのような憲章を指定すると、すべての Bean が自動的に注入され、アノテーションを使用して組み立てられ、拡張する場合はサブクラスを記述するだけで済み、オブジェクトは永続化レイヤーによって生成されます。これにはプロジェクト内の制約が必要であり、プロジェクトメンバー全員がそれを遵守しなければならないが、この方法はチームの高い自覚と長期間の慣らし運転が必要である。ルールを使用すると、インターフェイスや抽象クラスを使用するよりも優れており、制約はより効率的であり、スケーラビリティはまったく低下しません。

  1. パッケージ変更

変更のカプセル化には 2 つの意味があります: 1 つ目は、同じ変更が 1 つのインターフェイスまたは抽象クラスにカプセル化されること、2 つ目は、異なる変更が異なるインターフェイスまたは抽象クラスにカプセル化されることであり、2 つの異なる変更が同じインターフェイスまたは抽象クラスに現れるべきではないことです。 。変更、つまり保護されたバリエーション (保護されたバリエーション) をカプセル化し、変化または不安定になると予想される点を特定し、これらの変更点に対する安定したインターフェイスを作成します。正確には、予測された、または「第六感の場合」の可能性のある変更をカプセル化します。変化をさまざまな角度からカプセル化した 23 のデザインパターンを、パターンごとにステップごとに説明します。

6.5 ベストプラクティス

ソフトウェア設計における最大の問題は要件の変化に対処することですが、要件の複雑な変化は予測できません。私たちは予測不可能な事態に備える必要があり、それ自体が非常に苦痛なことですが、達人たちはそれでも、将来の変化を「カプセル化」するために 6 つの非常に優れた設計原則と 23 の設計パターンを提案しました。最初の 5 章では、次の設計原則が示されています。について議論されました。

● 単一責任の原則: 単一責任の原則

● オープンクローズド原則: オープンとクローズドの原則

● リスコフ置換原理: リスコフ置換原理

●デメテルの法則:デメテルの法則

● Interface Segregation Principle:接口隔离原则

● Dependence Inversion Principle:依赖倒置原则

把这6个原则的首字母(里氏替换原则和迪米特法则的首字母重复,只取一个)联合起来就是SOLID(solid,稳定的),其代表的含义也就是把这6个原则结合使用的好处:建立稳定、灵活、健壮的设计,而开闭原则又是重中之重,是最基础的原则,是其他5大原则的精神领袖。我们在使用开闭原则时要注意以下几个问题。

● 开闭原则也只是一个原则

开闭原则只是精神口号,实现拥抱变化的方法非常多,并不局限于这6大设计原则,但是遵循这6大设计原则基本上可以应对大多数变化。因此,我们在项目中应尽量采用这6大原则,适当时候可以进行扩充,例如通过类文件替换的方式完全可以解决系统中的一些缺陷。大家在开发中比较常用的修复缺陷的方法就是类替换,比如一个软件产品已经在运行中,发现了一个缺陷,需要修正怎么办?如果有自动更新功能,则可以下载一个.class文件直接覆盖原有的class,重新启动应用(也不一定非要重新启动)就可以解决问题,也就是通过类文件的替换方式修正了一个缺陷,当然这种方式也可以应用到项目中,正在运行中的项目发现需要增加一个新功能,通过修改原有实现类的方式就可以解决这个问题,前提条件是:类必须做到高内聚、低耦合,否则类文件的替换会引起不可预料的故障。

● 项目规章非常重要

如果你是一位项目经理或架构师,应尽量让自己的项目成员稳定,稳定后才能建立高效的团队文化,章程是一个团队所有成员共同的知识结晶,也是所有成员必须遵守的约定。优秀的章程能带给项目带来非常多的好处,如提高开发效率、降低缺陷率、提高团队士气、提高技术成员水平,等等。

● 预知变化

在实践中过程中,架构师或项目经理一旦发现有发生变化的可能,或者变化曾经发生过,则需要考虑现有的架构是否可以轻松地实现这一变化。架构师设计一套系统不仅要符合现有的需求,还要适应可能发生的变化,这才是一个优良的架构。

開閉の原則は究極の目標であり、マスターを含めて誰もそれを100%達成することはできませんが、この方向に取り組むことでシステムの構造を大幅に改善し、真に「変化を受け入れる」ことができます。

おすすめ

転載: blog.csdn.net/greek7777/article/details/128180223