里氏替换原则(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的行为方式不同,已经变味了,所以应该通过其他的方式比如增加抽象类或者组合、依赖、委托等方式进行处理。

 

那么如何判断一个类的方法的行为是否一致呢。使用前置条件和后置条件的方式进行限制,基类的前置条件要比实现类该方法的前置条件要严格,基类的后置条件要比实现类该方法的后置条件要宽泛。在行为一样的情况下,只要满足这两个条件就说明执行的效果是一致的。

 

所以,使用继承的方式在同一某一种行为,客户类使用抽象类去调用这个行为,只有这样才能保证客户类使用的所有实现类都能实现了这个行为,同时还保证这个行为是不变味的,如果变味,说明要增加新的类型,使用新的客户类去使用他了。

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值