博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
浅谈 SOLID 原则的具体使用
阅读量:5896 次
发布时间:2019-06-19

本文共 4529 字,大约阅读时间需要 15 分钟。

SOLID 是面向对象设计5大重要原则的首字母缩写,当我们设计类和模块时,遵守 SOLID 原则可以让软件更加健壮和稳定。那么,什么是 SOLID 原则呢?本篇文章我将谈谈 SOLID 原则在软件开发中的具体使用。

单一职责原则(SRP)

单一职责原则(SRP)表明一个类有且只有一个职责。一个类就像容器一样,它能添加任意数量的属性、方法等。然而,如果你试图让一个类实现太多,很快这个类就会变得笨重。任意小的改变都将导致这个单一类的变化。当你改了这个类,你将需要重新测试一遍。如果你遵守 SRP,你的类将变得简洁和灵活。每一个类将负责单一的问题、任务或者它关注的点,这种方式你只需要改变相应的类,只有这个类需要再次测试。SRP 核心是把整个问题分为小部分,并且每个小部分都将通过一个单独的类负责。

假设你在构建一个应用程序,其中有个模块是根据条件搜索顾客并以Excel形式导出。随着业务的发展,搜索条件会不断增加,导出数据的分类也会不断增加。如果此时将搜索与数据导出功能放在同一个类中,势必会变的笨重起来,即使是微小的改动,也可能影响其他功能。所以根据单一职责原则,一个类只有一个职责,故创建两个单独的类,分别处理搜索以及导出数据。

o_srp.png

开放封闭原则(OCP)

开放封闭原则(OCP)指出,一个类应该对扩展开放,对修改关闭。这意味一旦你创建了一个类并且应用程序的其他部分开始使用它,你不应该修改它。为什么呢?因为如果你改变它,很可能你的改变会引发系统的崩溃。如果你需要一些额外功能,你应该扩展这个类而不是修改它。使用这种方式,现有系统不会看到任何新变化的影响。同时,你只需要测试新创建的类。

假设你现在正在开发一个 Web 应用程序,包括一个在线纳税计算器。用户可以访问Web 页面,指定他们的收入和费用的细节,并使用一些数学公式来计算应纳税额。考虑到这一点,你创建了如下类:

public class TaxCalculator{    public decimal Calculate(decimal income, decimal deduction, string country)    {        decimal taxAmount = 0;        decimal taxableIncome = income - deduction;        switch (country)        {            case "India":                //Todo calculation                break;            case "USA":                //Todo calculation                 break;            case "UK":                //Todocalculation                break;        }        return taxAmount;    }}

这个方法非常简单,通过指定收入和支出,可以动态切换不同的国家计算不同的纳税额。但这里隐含了一个问题,它只考虑了3个国家。当这个 Web 应用变得越来越流行时,越来越多的国家将被加进来,你不得不去修改 Calculate 方法。这违反了开放封闭原则,有可能你的修改会导致系统其他模块的崩溃。

让我们对这个功能进行重构,以符合对扩展是开放,对修改是封闭的。

o_ocp.png

根据类图,可以看到通过继承实现横向的扩展,并且不会引发对其他不相关类的修改。这时 TaxCalculator 类中的 Calculate 方法会异常简单:

public decimal Calculate(CountryTaxCalculator obj){    decimal taxAmount = 0;    taxAmount = obj.CalculateTaxAmount();    return taxAmount;}

里氏替换原则(LSP)

里氏替换原则指出,派生的子类应该是可替换基类的,也就是说任何基类可以出现的地方,子类一定可以出现。值得注意的是,当你通过继承实现多态行为时,如果派生类没有遵守LSP,可能会让系统引发异常。所以请谨慎使用继承,只有确定是“is-a”的关系时才使用继承。

假设你在开发一个大的门户网站,并提供很多定制的功能给终端用户,根据用户的级别,系统提供了不同级别的设定。考虑到这个需求,设计如下类图:

o_ISettings.png

可以看到,ISettings 接口有 GlobalSettings、SectionSettings 以及 UserSettings 三个不同的实现。GlobalSettings 设置会影响整个应用程序,例如标题、主题等。SectionSettings 适用于门户的各个部分,如新闻、天气、体育等设置。UserSettings 为特定登录用户设置,如电子邮件和通知偏好。

这样的设计没问题,但如果有另一个需求,系统需要支持游客访问,唯一区别是游客不支持系统的设定,为了满足这个需求,你可能会如下设计:

public class GuestSettings : ISettings{    public void GetSettings()    {        //get settings from database        //include guest name、ip address...    }    public void SetSettings()    {        //guests are not allowed set settings        throw new NotImplementedException();    }}

这样没问题吗?准确来说,系统存在隐患。当单独使用 GuestSettings 时,因为我们了解游客不能设置,所以我们潜意识并不会主动调用 SetSettings 方法。但是由于多态,ISettings 接口的实现可以被替换为 GuestSettings 对象,当调用SetSettings 方法时,可能会引发系统异常。

重构这个功能,拆分为两个不同的接口:IReadableSettings 和 IWritableSettings。子类根据需求实现所需的接口。

o_lsp.png

接口隔离原则(ISP)

接口隔离原则(ISP)表明类不应该被迫依赖他们不使用的方法,也就是说一个接口应该拥有尽可能少的行为,它是精简的,也是单一的。

假设你正在开发一个电子商务的网站,需要有一个购物车和关联订单处理机制。你设计一个接口 IOrderProcessor,它用包含一个验证信用卡是否有效的方法(ValidateCardInfo)以及收件人地址是否有效的方法(ValidateShippingAddress)。与此同时,创建一个OnlineOrderProcessor 的类表示在线支付。

这非常好,你的网站也能正常工作。现在让我们来考虑另一种情形,假设在线信用卡支付不再有效,公司决定接受货到付款支付。

乍一看,这个解决方案听起来很简单,你可以创建一个CashOnDeliveryProcessor 并实现 IOrderProcessor 接口。货到付款的购买方式不会涉及任何信贷卡验证,所以,CashOnDeliveryOrderProcessor 类内部的 ValidateCardInfo 方法抛出 NotImplementedException。

o_IOrderProcess.png

这样的设计在未来可能会出现的潜在问题。假设由于某种原因在线信用用卡付款需要额外的验证步骤。自然,IOrderProcessor 将被修改,它将包括那些额外的方法,于此同时 OnlineOrderProcessor 将实现这些额外的方法。然而,CashOnDeliveryOrderProcessor 尽管不需要任何的附加功能,但你必须实现这些附加的功能。显然,这违反了接口隔离原则。

你需要将这个功能重构:

o_isp.png

新的设计分成两个接口。IOrderProcessor 接口只包含两个方法:ValidateShippingAddress 和 ProcessOrder,而 ValidateCardInfo 抽象到到一个单独的接口:IOnlineOrderProcessor。现在,在线信用卡支付的任何改变只局限于IOnlineOrderProcessor 和它的子类实现,而 CashOnDeliveryOrderProcessor 是不会被影响。因此,新设计符合接口隔离原则。

依赖倒置原则(DIP)

依赖倒置原则(DIP)表明高层模块不应该依赖低层模块,相反,他们应该依赖抽象类或者接口。这意味着你不应该在高层模块中使用具体的低层模块。因为这样的话,高层模块变得紧耦合低层模块。如果明天,你改变了低层模块,那么高层模块也会被修改。根据DIP原则,高层模块应该依赖抽象(以抽象类或者接口的形式),低层模块也是如此。通过面向接口(抽象类)编程,紧耦合被移除。

那么什么是高层模块,什么是低层模块呢?通常情况下,我们会在一个类(高层模块)的内部实例化它依赖的对象(低层模块),这样势必造成两者的紧耦合,任何依赖对象的改变都将引起类的改变。

依赖倒置原则表明高层模块、低层模块都依赖于抽象,举个例子,你现在正在开发一个通知系统,当用户改变密码时,邮件通知用户。

public class UserManager{    public void ChangePassword(string username,string oldpwd,string newpwd)    {        EmailNotifier notifier = new EmailNotifier();        //add some logic and change password         //Notify the user        notifier.Notify("Password was changed on "+DateTime.Now);    }}

这样的实现在功能上没有问题,但试想一下,新的需求希望通过SNS形式通知用户,那么我们只能手动将EmaiNorifier 替换为 SNSNotifier。在这儿,UserManager 就是高层模块,而EmailNotifier 就是低层模块,他们彼此耦合。我们希望解耦,依赖于抽象 INotifier,也就是面向接口的编程。

o_dip.png

小结

本篇博客为大家介绍了面向对象设计的 SOLID 原则,并以具体的案例辅助讲解。你可以看到,继承和多态在SOLID 原则中扮演了非常重要的角色。我们的应用程序不能过度设计,当然也不能随意设计。了解基本的 SOLID 原则能让你的应用程序变得健壮。你可以在Github 上查看具体的示例代码:

转载地址:http://skasx.baihongyu.com/

你可能感兴趣的文章
笔记:认识.NET平台
查看>>
cocos2d中CCAnimation的使用(cocos2d 1.0以上版本)
查看>>
gitlab 完整部署实例
查看>>
SCCM 2016 配置管理系列(Part8)
查看>>
struts中的xwork源码下载地址
查看>>
我的友情链接
查看>>
PHP 程序员的技术成长规划
查看>>
python基础教程_学习笔记19:标准库:一些最爱——集合、堆和双端队列
查看>>
js replace,正则截取字符串内容
查看>>
javascript继承方式详解
查看>>
lnmp环境搭建
查看>>
自定义session扫描器精确控制session销毁时间--学习笔记
查看>>
仿射变换
查看>>
视频直播点播nginx-rtmp开发手册中文版
查看>>
PHP队列的实现
查看>>
单点登录加验证码例子
查看>>
[T-SQL]从变量与数据类型说起
查看>>
occActiveX - ActiveX with OpenCASCADE
查看>>
BeanUtils\DBUtils
查看>>
python模块--os模块
查看>>