記事のディレクトリ
リヒターの置換原則とは何ですか?なぜリヒター置換原則?
定義された原則(リスコフの置換原則)のを置き換えるリヒターを見てみましょう:
機能は、ベースクラスへの使用のポインタまたは参照は、それを知らずに、派生クラスのオブジェクトを使用することができなければならないということ。 |
地元の基底クラスへのすべての参照は、そのサブクラス透過オブジェクトを使用することができます。
素人の面では、サブクラスは親クラスの機能を拡張することができますが、親クラスの本来の機能を変更することはできません。
これは女性バーバラ・リスコフは1988年に作られている - これは、ほとんど元の定義であります:
「何、ここで望まれることは次の置換プロパティのようなものです:タイプの各オブジェクトO1のための01のときPがTの用語で定義されたすべてのプログラムのために、Pの動作が変更されないような型TのオブジェクトO2があるS場合O2の代わりに、その後Sは「Tのサブタイプであります |
S O1オブジェクトの各種類は、O2は、型Tのオブジェクトを有し、そしてTは、全てO2置換によって置換されO1 Pオブジェクトプログラムの全ては、プログラムPの挙動が変更されていないように定義されている場合は、タイプSはタイプTのサブタイプであります
オブジェクト指向言語では、継承は間違いなく優れた特性である、それは、コードの再利用性を向上させるために利点があり、スケーラビリティ、Baibiweixiaは、しかし、それはまた、いくつかの欠点をもたらす:
●継承は侵襲的です。長い継承などとして、あなたは親クラスのすべての属性とメソッドを持っている必要があります。
●コードの柔軟性を減らします。サブクラスは自由世界のサブクラスを作ることはより多くの制約となっている、親クラスの属性とメソッドを持っている必要があります。
●強化カップリングを。親クラスの定数、変数やメソッドが変更されたとき、私たちは、サブクラスを改正検討する必要があり、標準化された環境が存在しない場合に、この変更は非常に悪い結果をもたらす可能性があります-リファクタリングの大きな塊を。
弱点を避けるためには、代替原則リヒターを導入する必要があります。
リヒター解明
リヒターの置換原則は4つの意味が含まれています。
*サブクラスは完全に親クラスのメソッドを実装する必要があります
システム設計を行うことで、頻繁にして達成するためにコーディング、インターフェースや抽象クラスを定義し、その後、直接インターフェースや抽象クラスにクラスを呼び出し、実際には、リヒターの原則を置き換えるためにそこに使用されています。
、銃を記述するために使用され、例えば、図2-1に示すクラス図をCSゲームこの原理を説明するための例として:
銃の主な責任は、各特定のサブクラスで定義されている方法を撮影するために、撮影された、シングルイシューピストルは比較的近いプロセスへの機銃掃射のため、非常に強力な長距離ライフル、機関銃です。兵士たちは、敵を殺すために銃を使用して、クラスメソッドkillEnemyで定義されている、特定の使用銃は敵を殺すために何を、いつ呼び出すかを知っています。
銃器抽象クラス:
public abstract class AbstractGun {
//枪用来干什么的?杀敌!
public abstract void shoot();
}
拳銃、ライフル、機関銃、実装クラス:
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("机枪扫射...");
}
}
兵士の実装クラス:
public class Soldier {
//定义士兵的枪支
private AbstractGun gun; //给士兵一支枪
public void setGun(AbstractGun _gun){
this.gun = _gun;
}
public void killEnemy(){
System.out.println("士兵开始杀敌人...");
gun.shoot();
}
}
敵兵の定義は銃を使用するが、銃は抽象的で、ピストルやライフル、特定のニーズ(つまりシナリオ)は、戦場で前に前setGun方法を決定します。クライアントのシーンカテゴリ次のように:
シーンカテゴリ:
public class Client {
public static void main(String[] args) {
//产生三毛这个士兵
Soldier sanMao = new Soldier(); //给三毛一支枪
sanMao.setGun(new Rifle());
sanMao.killEnemy();
}
}
このプログラムでは、兵士にライフルサン真央を与え、そして敵が始まりました。あなたは機関銃サン真央を使用したい場合は、もちろん、直接置くことがsanMao.setGun(新しいライフル())sanMao.setGun(新しいMACHINEGUN())に変更することができ、プログラム兵士クラスの兵士の準備で、単にどのモデルを知りません銃(サブクラス)が渡されます。
あなたはおもちゃの銃を持っている場合、どのようにそれを定義するために、もう一度考えてみて?我々図でファーストクラスの増加2-1クラスToyGun、次に継承AbstractGunクラス、図2-2に示す変形クラス図。
玩具枪是不能用来射击的,杀不死人的,这个不应该写在shoot方法中。
public class ToyGun extends AbstractGun {
//玩具枪是不能射击的,但是编译器又要求实现这个方法,怎么办?虚构一个呗!
@Override
public void shoot() {
//玩具枪不能射击,这个方法就不实现了
}
}
由于引入了新的子类,场景类中也使用了该类,Client类稍作修改:
public class Client {
public static void main(String[] args) {
//产生三毛这个士兵 Soldier sanMao = new Soldier();
sanMao.setGun(new ToyGun()); sanMao.killEnemy();
}
}
结果:
士兵开始杀敌人...
玩具枪开始杀人了!在这种情况下,我们发现业务调用类已 经出现了问题,正常的业务逻辑已经不能运行,那怎么办?有两种办法。
● 在Soldier类中增加instanceof的判断,如果是玩具枪,就不用来杀敌人。这个方法可以 解决问题,但是,在程序中,每增加一个类,所有与这个父类有关系的类都必须修改,显然,这个方案被否定了。
● ToyGun脱离继承,建立一个独立的父类,为了实现代码复用,可以与AbastractGun建 立关联委托关系,如图2-3所示:
例如,可以在AbstractToy中声明将声音、形状都委托给AbstractGun处理,然后两个基类下的子类自由延展,互不影响。
如果子类不能完整地实现父类的方法,或者父类的某些方法在子类中已经发 生“畸变”,则建议断开父子继承关系,采用依赖、聚集、组合等关系代替继承。
* 子类可以有自己的个性
子类可以有自己的行为和外观了,也就是方法和属性—— 里氏替换原则可以正着用,但是不能反过来用。在子类出现的地方,父类未必就可以胜任。还是以刚才的关于枪支的例子为例,步枪有几个比较“响亮”的型号,比如AK47、AUG 狙击步枪等,把这两个型号的枪引入后的Rifle子类图如图2-4所示:
AUG继承了Rifle类,狙击手(Snipper)则直接使用AUG狙击步枪。
AUG狙击枪类代码:
public class AUG extends Rifle {
//狙击枪都携带一个精准的望远镜
public void zoomOut(){
System.out.println("通过望远镜察看敌人...");
}
public void shoot(){
System.out.println("AUG射击...");
}
}
狙击手类:
public class Snipper {
public void killEnemy(AUG aug){
//首先看看敌人的情况,别杀死敌人,自己也被人干掉
aug.zoomOut();
//开始射击
aug.shoot();
}
}
狙击手使用狙击枪来杀死敌人,业务场景Client类如下:
public class Client {
public static void main(String[] args) {
//产生三毛这个狙击手
Snipper sanMao = new Snipper();
sanMao.setRifle(new AUG());
sanMao.killEnemy();
}
}
运行结果:
通过望远镜察看敌人...
AUG射击...
在这里,系统直接调用了子类,狙击手是很依赖枪支的,别说换一个型号的枪了,就是换一个同型号的枪也会影响射击,所以这里就直接把子类传递了进来。这个时候,我们能不 能直接使用父类传递进来呢?修改一下Client类:
public class Client {
public static void main(String[] args) {
//产生三毛这个狙击手
Snipper sanMao = new Snipper();
sanMao.setRifle((AUG)(new Rifle()));
sanMao.killEnemy();
}
}
显然是不行的,会在运行期抛出java.lang.ClassCastException异常,这也是大家经常说的 向下转型(downcast)是不安全的,从里氏替换原则来看,就是有子类出现的地方父类未必就可以出现。
* 覆盖或实现父类的方法时输入参数可以被放大
方法中的输入参数称为前置条件,这是什么意思呢?大家做过Web Service开发就应该知道有一个“契约优先”的原则,也就是先定义出WSDL接口,制定好双方的开发协议,然后再各自实现。里氏替换原则也要求制定一个契约,就是父类或接口,这种设计方法也叫做 Design by Contract(契约设计),与里氏替换原则有着异曲同工之妙。契约制定了,也就同 时制定了前置条件和后置条件,前置条件就是你要让我执行,就必须满足我的条件;后置条 件就是我执行完了需要反馈,标准是什么。这个比较难理解,来看一个例子。
先定义一个Father类:
public class Father {
//把HashMap转换为Collection集合类型
public Collection doSomething(HashMap map){
System.out.println("父类被执行...");
return map.values();
}
}
再定义一个子类:
public class Son extends Father {
//放大输入参数类型
public Collection doSomething(Map map){
System.out.println("子类被执行...");
return map.values();
}
}
子类的doSomething方法与父类的方法名相同,但又不是覆写(Override)父类的方法。方法名虽然相同,但方法的输入参数不同,就不是覆写,是重载(Overload)!——继承,子类拥有父类的所有属性和方法,方法名相同,输入参数类型又不 相同,当然是重载。
场景类的调用如下:
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();
}
}
运行结果:
父类被执行...
根据里氏替换原则,父类出现的地方子类就可以出现,修改场景类:
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类中关联了一个父类,调用了一个父 类的方法,子类可以覆写这个方法,也可以重载这个方法,前提是要扩大这个前置条件,就是输入参数的类型宽于父类的类型覆盖范围。这样说可能比较难理解,我们再反过来想一 下,如果Father类的输入参数类型宽于子类的输入参数类型,会出现什么问题呢?会出现父类存在的地方,子类就未必可以存在,因为一旦把子类作为参数传入,调用者就很可能进入 子类的方法范畴。
把上面的例子修改一下,扩大父类的前置条件:
public class Father {
public Collection doSomething(Map map){
System.out.println("父类被执行...");
return map.values();
}
}
把父类的前置条件修改为Map类型,再修改一下子类方法的输入参数,相对父类缩小输入参数的类型范围,也就是缩小前置条件:
public class Son extends Father {
//缩小输入参数范围
public Collection doSomething(HashMap map){
System.out.println("子类被执行...");
return map.values();
}
}
在父类的前置条件大于子类的前置条件的情况下,业务场景:
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类修改一下:
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();
}
}
运行结果:
子类被执行...
子类在没有覆写父类的方法的前提下,子类方法被执行了,这会引起业务 逻辑混乱,因为在实际应用中父类一般都是抽象类,子类是实现类,你传递一个这样的实现 类就会“歪曲”了父类的意图,引起一堆意想不到的业务逻辑混乱,所以子类中方法的前置条 件必须与超类中被覆写的方法的前置条件相同或者更宽松。
* 覆写或实现父类的方法时输出结果可以被缩小
親クラスのメソッドの戻り値は、タイプT、同じサブクラス(ヘビーデューティまたはオーバーライド)Sの戻り値であり、その後、原則リヒターは、交換がT S、Sのいずれかに等しい未満でなければならない必要そしてTは同じ型で、いずれかのSはTのサブクラスであり、その理由は?オーバーライドが、同じ名前の親クラスとサブクラスのメソッドの入力パラメータが同じである場合、2つの場合、2つの方法の値の範囲は、これが最も重要未満または上書き要件で等しいT S、です、不変、親クラスをオーバーライドするサブクラス。それがオーバーロードされている場合、この方法は、入力パラメータが同じタイプまたは数量ではありません必要があり、原則の下でリヒターの交換の要件に、それは、あなたがこのメソッドを書くことをどの手段があり、入力パラメータサブクラスより広いか、親クラスの入力パラメータに等しいです、上記の前提条件を参照して言えば、それは呼び出されません。
リヒター置換原理を使用する目的は、アップグレードが非常に良好な適合性を維持することができたときに、プログラムの堅牢性を向上させることです。でも増加サブクラスならば、オリジナルのサブクラスが実行し続けることができます。実際のプロジェクトでは、各パラメータの異なるサブクラス異なるサービスロジックとして渡された親クラスを使用して、異なるサービス・サブクラスの意味に対応します。
参考:
[1]:「デザインパターンの禅」
[2]:リヒター交代原理(リスコフの置換原則)
[3]:デザインパターンは、6つの原則(2):リヒター置換原則
[4]:「西のデザインパターン」