面向对象基本原则(二)——里式替换原则和依赖倒置原则面向对象基本原则(一)——单一职责原则和接口隔离原则面向对象基本原则(二)——李式替换原则和依赖倒置原则面向对象基本原则(三)——最少知道原则和开闭原则继承是不可或缺的优秀语言机制。它有以下优点:代码共享,减少创建类的工作量,每个子类都有父类的方法和属性。提高代码的可重用性。孩子可以与父母相似,但与父母不同。“龙生龙,凤生凤,老鼠生来打洞”就是说孩子有爸爸的“种”。“世界上没有两片完全相同的树叶。”它指明子与父的不同。为了提高代码的可扩展性,可以通过实现父类的方法来“为所欲为”。你知道吗,很多开源框架的扩展接口都是通过继承父类来完成的;提高产品或项目的开放性。自然界万物都有优点和缺点,继承的缺点如下:继承是侵入性的。只要继承,就必须拥有父类的所有属性和方法。降低代码灵活性。子类必须具有父类的属性和方法,这就对子类的自由世界施加了更多的约束。增强耦合。当修改父类的常量、变量和方法时,需要考虑子类的修改,而在没有规范的情况下,这种修改可能会带来非常糟糕的结果——一大段代码需要重构。一、里氏代换原则简介里氏代换原则英文名称为LiskovSubstitutionPrinciple,简称LSP。Listylesubstitutionprinciple的英文定义是:使用指针或引用基类的函数必须能够在不知情的情况下使用派生类的对象。这意味着:所有对基类的引用必须能够透明地使用其子类对象。通俗地说,只要父类能出现,子类就可以出现,用子类替换也不会出现任何错误或异常。用户可能不需要知道它是父类还是子类。然而,反过来是不可能的。有子类的地方,父类不一定能适配。2.Liskov替换原则定义了良好的继承,规范的子类必须完全实现父类的方法/***Gunabstractclass*ClassAbstractGun*/abstractclassAbstractGun{publicabstractfunctionshoot();}/***Pistol*ClassHandgun*/classHandgunextendsAbstractGun{publicfunctionshoot(){echo"手枪射击\n";}}/***Rifle*ClassRifle*/classRifleextendsAbstractGun{publicfunctionshoot(){echo"Rifleshooting\n";}}}如果子类不能完全实现父类的方法,或者父类的一些方法在子类中已经被“扭曲”了,建议打破父子继承关系,使用依赖、聚合、关系等作为组合取代继承。比如你要创建一个玩具手枪类,玩具手枪不能用来射击,所以AbstractGun的shoot()方法就不能实现。子类可以有自己独特的方法和属性。Liskov替换原则可以积极使用,但反之则不行。父类能出现的地方,子类就可以出现,但子类出现的地方,父类未必能胜任。狙击步枪类AUG继承Rifle类classAUGextendsRifle{publicfunctionshoot(){echo"AUGshooting\n";}/***狙击步枪特有的行为*/publicfunctionzoomOut(){echo"通过望远镜看敌人...\n";}}创建士兵类SoldierclassSoldier{/**@varAbstractGun*/private$_gun;公共函数setGun(AbstractGun$gun){$this->_gun=$gun;}publicfunctionkillEnemy(){echo"士兵开始杀敌\n";$this->_gun->shoot();}}实例化一个士兵,让他用步枪杀人//生成三毛这个士兵$sanMao=newSoldier();//给三毛一把步枪$sanMao->setGun(newRifle());$sanMao->杀死敌人();士兵开始杀敌,并开枪射击。根据Richter替换原则,父类可以出现的地方类可以出现,尝试给一个士兵一把狙击枪//生成士兵三毛$sanMao=newSoldier();//给三毛一把狙击枪枪$sanMao->setGun(newAUG());$sanMao->killEnemy();小兵开始杀敌,AUG射击可见。使用子类(AUG)代替父类(Rifle)是没有问题的。创建一个狙击手类SnipperclassSnipper{/**@varAUG*/private$_gun;公共功能setGun(AUG$gun){$this->_gun=$gun;}publicfunctionkillEnemy(){echo"狙击手开始杀敌\n";//先看敌人情况$this->_gun->zoomOut();//开始射击$this->_gun->shoot();}}实例化一个狙击手,让他用狙击枪杀人//生成三毛作为狙击手$sanMao=newSnipper();//给三毛一把狙击枪$sanMao->setGun(newAUG());$三毛->killEnemy();狙击手开始杀敌。通过望远镜观察敌人……AUG射击。但是,如果给狙击手一把步枪,狙击手将无法使用,因为步枪没有观察敌人的望远镜。//创建狙击手三毛$sanMao=newSnipper();//给三毛一把步枪$sanMao->setGun(newRifle());$sanMao->killEnemy();PHPFatalerror:UncaughtTypeError:Argument1传递给Snipper::setGun()的必须是AUG的实例,Riflegiven3的实例。最佳实践在使用里氏代换原则时,尽量避免子类的“个性化”。一旦子类有了“个性”,子类和父类的关系就很难调和了。使用子类作为父类会扼杀子类的“个性”——有点委屈;单独使用子类作为业务会使代码之间的耦合关系混乱——类替换缺乏标准。四、依赖倒置原则1.依赖倒置原则简介依赖倒置原则英文名称为DependenceInversionPrinciple,简称DIP。DependencyInversionPrinciple的英文定义是:Highlevelmodulesshouldnotdependonlowlevelmodules。模块,两者都应该依赖于它的抽象。抽象不应依赖于细节。细节应该依赖于抽象。什么是高层模块?什么是低级模块?每个逻辑的实现都是由原子逻辑组成的。不可分割的原子逻辑是低层模块,原子逻辑的重组是高层模块。什么是抽象?又是什么详情?抽象是指接口或抽象类,两者都不能直接实例化。detail就是实现类,实现接口或者继承抽象类生成的类就是detail,它的特点是可以直接实例化。抽象是对实现的约束,也是对依赖的一种规则。它不仅约束自己,也约束自己与外界的关系。规则(抽象)一起发展。只要抽象的基线存在,细节就离不开这个圈子。什么是“反转”?先说什么是“正”。直白的依赖就是类之间的依赖,是类之间依赖的实际实现,也就是面向实现的编程。这也是正常人的思维方式。我要开奔驰,就靠奔驰。如果我要用笔记本电脑,我直接依赖笔记本电脑。写程序需要的是对现实世界中的事物进行抽象。抽象的需要产生了抽象之间的依赖关系,取代了人们传统思维中事物之间的依赖关系,“倒置”由此而来。2.面向接口编程面向接口编程OOD(Object-OrientedDesign),是面向对象设计的精髓之一。依赖倒置原则的表现其实就是面向接口编程。模块之间的依赖是通过抽象产生的,实现类之间没有直接的依赖。通过接口或抽象类产生依赖;接口或抽象类不依赖于实现类;实现类依赖于接口或抽象类。3.依赖倒置原则的优点是降低了类之间的耦合度,提高了系统的稳定性。降低并行开发带来的风险。提高代码的可读性和可维护性。4.依赖的三种写法构造函数传递依赖对象在类中通过构造函数声明依赖对象。根据依赖注入,这种方法称为构造函数注入。接口ICar{publicfunctionrun();}interfaceIDriver{publicfunctiondrive();}classDriverimplementsIDriver{private$_car;公共函数__construct(ICar$car){$this->_car=$car;}publicfunctiondrive(){$this->_car->run();}}Setter方法传递依赖对象在抽象类中设置Setter方法来声明依赖。根据依赖注入,这是Setter依赖注入。interfaceICar{publicfunctionrun();}interfaceIDriver{publicfunctionsetCar(ICar$car);publicfunctiondrive();}classDriverimplementsIDriver{private$_car;publicfunctionsetCar(ICar$car){$this->_car=$car;}publicfunctiondrive(){$this->_car->run();}}接口声明依赖对象在接口构造方法中声明依赖对象,这种方法称为接口注入。interfaceICar{publicfunctionrun();}interfaceIDriver{publicfunction__construct(ICar$car);publicfunctiondrive();}5.最佳实践依赖倒置原则的本质是让各个类或模块的实现相互独立,互不影响,实现模块之间的松耦合。需要遵守以下规则。每个类尽可能有一个接口或一个抽象类,或者抽象类和接口都应该有。这是依赖倒置的基本要求。接口和抽象类都是抽象的,只有抽象才有依赖倒置的可能。任何类都不应该从具体类派生如果一个项目在开发中,确实不应该从具体类派生出子类,但这不是绝对的,因为人会犯错误,有时设计缺陷是不可避免,因此在某些情况下继承是可以容忍的。特别是在项目维护阶段,维护工作基本上是扩展开发和修复行为。通过继承关系,重写一个方法就可以修复一个大bug,何必去继承最高基类呢?(当然,在不了解父类或无法获取父类代码的情况下,尽量不要重写基类的方法。)如果基类是一个抽象类并且已经实现了这个方法,那么子类应该尽量不要覆盖。类间依赖是抽象的,重写抽象方法会对依赖的稳定性产生一定的影响。结合Liskov替换原则使用接口负责定义公共属性和方法,并声明与其他对象的依赖关系。抽象类负责公共构造部分的实现,实现类准确实现业务逻辑,同时在适当的时候细化父类。6.显示代码汽车接口ICar和驱动程序接口IDriverinterfaceICar{publicfunctionrun();}interfaceIDriver{publicfunction__construct(ICar$car);publicfunctiondrive();}Driver类实现IDriver接口类Driver实现IDriver{private$_car;公共函数__construct(ICar$car){$this->_car=$car;}publicfunctiondrive(){echo"老司机准备上路\n";$this->_car->run();}}Benz和BMW分别实现了ICar接口\n";}}司机张三开着奔驰在路上//创建一辆奔驰$benz=newBenz();//创建司机张三和givetheMercedes-BenztoZhangSan$??zhangSan=newDriver($benz);//张三开着奔驰$zhangSan->drive();老司机准备上路,奔驰startstostart.司机李斯开着一辆宝马上路。//创建一辆宝马$bmw=newBMW();//创建一个司机李斯,给李斯$lisi=newDriver($bmw);//李四开着宝马车$lisi->drive();老司机准备上路,宝马车发动。参考:《设计模式之禅》
