里氏替换原则(LSP)
Posted 茂树24
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了里氏替换原则(LSP)相关的知识,希望对你有一定的参考价值。
一、为什么需要LSP
先看一个例子,有一个下载类,需要将要下载的file 保存在硬盘中。
<script> //硬盘类 function HardDisk () this.save = function (file) console.log('硬盘正字保存 '+file); //下载类 function Download () var _hd; Object.defineProperties(this, HardDisk: set:function (hd) if(hd instanceof HardDisk) _hd = hd; else throw new Error('类型错误'); , get: function () return _hd; ); Download.prototype.download = function (file) if(typeof file === 'string') this.HardDisk.save(file); else throw new Error("文件不符合格式"); ; (function () var d = new Download(); d.HardDisk = new HardDisk(); var file = "文字文字文字" d.download(file); )(); //硬盘正字保存 文字文字文字 </script>
当我保存文件的时候输出“硬盘正字保存 文字文字文字”,确实达到了预期的目的。
-----------------------------------------------------------------------------------------------------------------
随着社会的发展,出现了U盘CD等存储媒介……,所以需求改了,不仅要存储到硬盘,还要存储到u盘上,这个修改很简单了。
//硬盘类 function HardDisk () this.save = function (file) console.log('硬盘正字保存 '+file); //U盘类 function UDisk () this.save = function (file) console.log('U盘正字保存 '+file); //下载类 function Download () var _d; Object.defineProperties(this, disk: set:function (d) if(d instanceof HardDisk || d instanceof UDisk) _d = d; else throw new Error('类型错误'); , get: function () return _d; ); Download.prototype.download = function (file) if(typeof file === 'string') this.disk.save(file); else throw new Error("文件不符合格式"); ; (function () var d = new Download(); d.disk = new UDisk(); var file = "文字文字文字" d.download(file); )();
仅仅是在set方法中再添加一个对UDIsk判断就可以了,然后改改变量命名方式…… 如果我再添加CD存储呢,是不是也要重复这么一个过程?
如果编写这些HardDisk、 UDisk是一个人写的,Download是一个人开发的,最终整体过程调用又是一个人开发的。是不是第一个人添加了一个存储媒介类,都要通知第二个人说我是这么命名的等沟通呢?可能第一个人还会和第三个人进行沟通,这个大大降低了开发效率。所以说,如果不同的开发人员之间随着需求的修改频繁的交流的话,想必这种设计不是一个好的设计至少不会提高工作效率。
再从是否满足高内聚低耦合的角度分析一下需求改变之后的设计。在需求还没有更改的时候,仅仅是Downlaod类依赖于HardDisk类,这个是必须存在的。但是随着需求的不断扩充,DownLoad不仅依赖了HarDisk还依赖了UDisk、CDDisk如果以后扩展依赖的更多,使得设计僵化,如下图所示。也就是说,当其中一个类因为某些原因修改之后,可能会导致依赖的类也要进行修改。所以说这种方式依赖性是很高的具有很高的耦合度。这也就是为什么三个程序员频繁交流的原因。
那么问题来了能不能让download依赖于其他的类还是1,不会增加依赖,同时还能满足变化的需求呢?这种问题的解决办法就是因为HardDisk、UDisk、CDDisk都具有形同的行为,所以可以提取一个抽象类Disk来管理这些具有相似行为的概念上相近的类。使得DownLoad仅仅依赖于这一个抽象类,通过传入不同的对象,执行不同类型的存储。
<script> function extend (subClass,superClass) function F(); F.prototype = superClass.prototype; var obj = new F(); subClass.prototype = obj; subClass.superClass = superClass; if(superClass.prototype === Object.prototype) superClass.prototype = superClass; function AbstractDisk () AbstractDisk.prototype.save = function () throw new Error('要保证实现类实现该方法'); //硬盘类 var HardDisk = (function (AbstractDisk) var HardDisk = function () extend(HardDisk, AbstractDisk); HardDisk.prototype.save = function (file) console.log('硬盘正字保存 '+file); return HardDisk; )(AbstractDisk); //U盘类 var UDisk = (function (AbstractDisk) var UDisk = function () extend(UDisk, AbstractDisk); UDisk.prototype.save = function (file) console.log('U盘正字保存 '+file); return UDisk; )(AbstractDisk); //CD盘类 var CDDisk = (function (AbstractDisk) var CDDisk = function () extend(CDDisk, AbstractDisk); CDDisk.prototype.save = function (file) console.log('CD盘正字保存 '+file); return CDDisk; )(AbstractDisk); //下载类 function Download () var _disk; Object.defineProperties(this, disk: set:function (d) if(d instanceof AbstractDisk) _disk = d; else throw new Error('类型错误'); , get: function () return _disk; ); Download.prototype.download = function (file) if(typeof file === 'string') this.disk.save(file); else throw new Error("文件不符合格式"); ; (function () var d = new Download(); d.disk = new UDisk(); var file = "文字文字文字" d.download(file); var d2 = new Download(); d2.disk = new HardDisk(); var file = "^^^^^^^"; d2.download(file); )(); </script>
-----------------------------------------------------------------------------------------------------------------
当高兴不长时间的时候,又来需求了最近新产出了一种叫做云存储的存储方式,他和之前其他的方式不同,这个不是保存到本地Disk而是保存到网络中或者是云上。这时候在不能增加依赖性的基础上很简单啊,直接在让YunPan类继承AbstractDisk类不就行了这样Downlaod也能用了。真的是这样吗?想想看,之前无论是硬盘还是U盘还是CDsave方法都是存储到本地的方法实现,但是如果yunPan有save方法的话想必应该是存储到网络吧而不是存储到本地,所以save虽然都是保存,但是保存的行为变了,所以如果还让yunPan继承描述保存到本地这种抽象类的话,未免太牵强。还有一个重要的是,DownLaod在yunPan出现之前一直是按照存储到本地的模式写的,如果DownLoad使用了YunPan之后是不是能够工作?所以暂时的决定就是让YunPan独立成一个模块然后委托与AbstractDisk。但是js中没有委托机制,所以只能使用一个SaveTool抽象类来管理两个大类,一个是Disk本地存储抽象类,一个是云存储抽象类。同时DownLoad类也因该分为下载到本地的和下载到云的。
问为什么YunPan作为AbstractDisk的实现类?其实这里除了上面解析的对使用者DownLoad对save的需求是保存到本地之外,还有一层意思绝对不能作为实现类,那就是“继承”到底是什么?
所谓的继承难道就是觉得有那么几个实现类比如HardDisk、UDisk、CDDDisk都有save方法,并且都是存储媒介,可以抽象出一个基类在进行公共的管理。但是我们却忽略了一点那就是实现save的这个方法这个行为的方式应该是差不多的。所以继承有一种将一些描述共同行为的类组织在一起。比如鸟类,有飞这一个公共熟悉,所以实现类就是有麻雀等鸟,但是绝对没有企鹅吧,虽然在我们生活的逻辑中企鹅也是鸟,但是他不能飞,或者飞的方式不太一样。这个就是正如YunPan不能作为AbstractDisk原因,因为yunPan的save和另外三种存储媒介不太一样。
经过上述这个例子不知不觉中在需求不断变化的设计中就应用了LSP设计原则。那么接下来将详细介绍下这个原则。
二、什么是LSP
一般的权威的定义是:任何基类可以出现的地方,子类一定可以出现。这一句话起码涵盖了这两层意思。
(1)再调用其他类的过程中,务必使用其他类的抽象类或者接口的方式来使用。
正如上述第二次需求一样,如果增加了使用类的需求,(增加u盘、CD盘方式),如果downlaod不使用Disk这个基类作为实现的话,那么依赖性相比是非常大的,导致耦合度上升,从而导致代码的僵化严重,不易于日后的维护。
(2)子类必须继承或覆盖基类行为,并且子类保证实现的行为不会“变味”。
在之前,如果将YunPan类添加到AbstractDisk中,因为本身Download仅仅是要求存储媒介保存在磁盘中,所以下载文件的格式也是按照磁盘的格式来的。如果传入YunPan对象的话,导致DownLoad行为出错。根本原因就是YunPan不能属于AbstractDisk的实现类因为save的行为方式不同,已经变味了,所以应该通过其他的方式比如增加抽象类或者组合、依赖、委托等方式进行处理。
那么如何判断一个类的方法的行为是否一致呢。使用前置条件和后置条件的方式进行限制,基类的前置条件要比实现类该方法的前置条件要严格,基类的后置条件要比实现类该方法的后置条件要宽泛。在行为一样的情况下,只要满足这两个条件就说明执行的效果是一致的。
所以,使用继承的方式在同一某一种行为,客户类使用抽象类去调用这个行为,只有这样才能保证客户类使用的所有实现类都能实现了这个行为,同时还保证这个行为是不变味的,如果变味,说明要增加新的类型,使用新的客户类去使用他了。
以上是关于里氏替换原则(LSP)的主要内容,如果未能解决你的问题,请参考以下文章
深入理解JavaScript系列:S.O.L.I.D五大原则之里氏替换原则LSP
"围观"设计模式--里氏替换原则(LSP,Liskov Substitution Principle)