六大设计模式原则

  本文介绍为什么有“面向抽象编程”,以及六大原则。

  工作中,为什么新人要用好多时间,而且项目进展还越来越慢,问题越来越多的;而高手却是前面慢,后面项目越来越顺利的?

  这里排除技术上的因素外,主要就是编程思想的影响了。下面简单理论分析,暂不提供具体代码。

为什么需要编程思想

举例做超市收银系统

  一开始卖的是水果。有西瓜、苹果、香蕉等等,每种价格不同,包装方式不同等等好多不同,所以拆成多个水果class+main函数、计费系统&结算系统。

需求①:给每种水果标价格

  增加函数int Sell()获得。在main使用switch选择使用哪种水果。

需求②:卖相差的水果打折

  修改函数Sell,进行选择是否打折。

需求③:促销,买3个苹果送一个香蕉

  main函数中严格按照先苹果后香蕉的顺序收费,先计算多少个苹果,同时跨class,传递该数字给香蕉,香蕉个数减优惠。这里可以使用全局static,为了防止出问题,增加计费前的置零+结算后的置零操作函数;或者在main中传递参数,增加main中判断是否需要传递参数的代码。也要修改Sell函数!

  当需求变成买2个西瓜送苹果时,同样方式增加修改函数。现在3个class发生交叉,也出现了重复函数,最重要是main增加了计费顺序判断的大量代码,Sell函数也交错复杂。

需求④:买10元水果送优惠券,下次消费可以使用

  在main函数水果计费后,结算前的位置,第一次插入代码:检查是否有超过10元,并提示是否进行优惠减免。

….

项目回顾

  在增加N个需求后,回顾项目代码,发现简单几个功能而已,就十几个函数,十几个控制状态参数,甚至class间跨越交互,main函数更加是几千行的判断跳转代码。

  随着项目发展,后期加功能越来越难,而且加功能后,还会影响到前期的功能。

  而且如果这时候新拿了卖鱼的收银系统,怎么办?全部代码不能重用,得全部重写!

  软件真这么差吗?为什么网页在IE能打开,在360也能打开,在手机也能打开,那是他们做了十几套这样的页面来让我们打开吗?事实只有一个html。但是这里怎么都是收银系统,我就得分卖水果,然后卖鱼又得重新做了?

引入设计模式解决困境

  目前该收银系统,是买东西,东西有很多种,有水果,有肉等等,但是收银系统,归根到底就是卖东西,分统计价格,结算两个步骤。

  统计价格,这里应该笼统成一个抽象—“顾客买的东西”。可以细分成很多种东西,但是它们只提供int Sell()函数出来。在main中统一调用该函数即可。所以这里创建根抽象类IGoods,提供对外int Sell()函数。然后由具体的物品类来继承重写该类。

  但是这样我怎么知道要使用哪个具体类?这就是商品条码的作用了。

  计费时,会嘀一下商品条码,这时候我得到了一个标识,这个标识是唯一的,能区分它是哪个具体计费类。所以在抽象类IGoods中增加传递该条码的函数。然后在子类中判断是否该条码对应的是自己,是的话才执行计费,不是就返回。

  这样的好处是:

  1. 同前面需求②,水果打折,那我直接增加新的类来处理就行,因为要打折的水果,条码是可以和普通水果不同来区分的。
  2. 需求③呢,一样,新增计费类(appleaddbanana),内部通过实例化原有水果类来处理,这样对原先存在的那些代码不修改,而这个打折,也可以通过条码不同来区分。
  3. 需求④呢?给优惠券新增计费类,不过int Sell()出来的是负数而已。对原main代码保持不变!当然,这里你可能会说,没传递价格给优惠券,怎么知道要不要给优惠券的,等下被人恶意刷券怎么办?

  那就拆分吧,计费系统分成两个抽象:要卖东西+优惠。

  这样换卖鱼的系统,两个抽象类、main函数,计费函数和结算函数都不用修改,只需要新增具体计费类就行。如果牛逼一点,把这个“具体计费类”,通过反射、公式计算等等方式,搞成不用写代码就能实现具体类的那种,那更加简单了!拿到卖金条收银系统的项目,这个系统直接带过去,现场问一下有那些计费方式,整个excel,让程序读一下,马上就能运行正常计费收费了!

编程思想是什么

  这个比较复杂,表现形式上,就是能让项目良好运行下去的思想。

  像前面的收银系统,抽象出来“卖的东西”+“优惠”,就解决了main函数中几千行switch的问题,甚至跨行业收银都不成问题了。这样的项目才有发展的可能性,才能衍生出卖鱼收银系统,卖金条收银系统等等多种多样的项目。不然,像未优化前那样,来多几个水果,整个系统就跑不动了,项目哪里还进行得下去?更别说思想传承了。

  而上面的利用“抽象”,实质就是课本上经常提到的“面向抽象编程”!编程思想很虚很广博,但是归根到底,就是“抽象”两个字。

  计算机,就是由1010这样的二进制组成的,在这个基础上,才衍生出了python、Lua、php,甚至中文的易语言等等众多的高级语言,它们表现层完全不一样,能实现的功能也不一样,但是抽象出来,都是同一个二进制。

  现在功能丰富的电子产品,功能越来越多,但是剖析下去,再剖析下去,就只是电路板上的1010。

  编程思想,实际就是抽象。有了抽象,才能有具体的实现,进而有丰富多彩的实现。

  编程思想由“六大原则”组成,是一个基于“抽象”,实现“高内聚,低耦合”目的的思想。

六大原则

单一责任原则

单一责任原则(Single Responsibility Principle, SRP):一个类/函数只负责一个功能,同时只能有一个因素引起它变化。

  像上面的收银系统,抽象后,一个类对应一个条码(一个变化因素),只对应一种水果的计费方式(一个功能)。

  这里不单单在class定义上,在函数的定义上也是。举例计算机加减乘除。最好的做法,就是把四个运算分为四个函数。这样就是一个变化因素–运算符,来选择应该执行哪个具体运算的函数,这个函数只负责这种运算功能。

  概念上很抽象,但是做到后,首先是能精简代码,避免重复的代码。因为抽出来函数后,在重复的地方,就是调用这个抽出来的函数,就避免了重复。

  其次,在这个函数里面,它只需要做好这件事就行,就能控制好输入输出,提高系统容错性。   

开闭原则

开闭原则(Open-Closed Principle, OCP):一个软件实体应当对扩展开放,对修改关闭。即软件实体应尽量在不修改原有代码的情况下进行扩展。

  同样拿收银系统举例,遵循开闭原则后,就不会去修改Sell函数。如果前期系统经过了测试,后面增加新功能就不用重复测试了,因为前面功能肯定不会被影响,从而节省人力。

  增加新功能,应该是加新的类/函数来实现。不应该修改原有的已经确定正确运行的代码。

  该原则,提倡避免修改原有的正常代码,尽量增加新代码。因为增加新功能会引入新的不确定性因素,所以需要避免影响到原有正常功能,以减少排错时的复杂度。

里氏替换原则

里氏替换原则(Liskov Substitution Principle, LSP):所有引用基类(父类)的地方必须能使用其子类来替换而不报错且正常运行。

  在收银系统就用到这个,定义根抽象类IGoods,main函数里面调用它的Sell函数。后面具体实现时才用其子类。这样在替换新子类时,不影响系统,正常计费。

  这里也是C#的拆箱装箱问题。object能做的事,也能由继承它的子类来做。

  假设存入list,这样装箱操作:这里能add(object),也肯定能add(继承object的int)。

  这个原则得配合后面提到的原则,才能较好理解。

依赖倒置原则

依赖倒置原则(Dependency Inversion Principle, DIP):抽象不应该依赖于细节,细节应当依赖于抽象。换言之,要针对接口编程,而不是针对实现编程。

  需要结合里氏替换原则理解。

  在收银系统中,依靠里氏替换原则,增加新收费物品。从而计费那里,根据这里的依赖倒置原则,它是根据抽象基类IGoods来收费Sell的,不是根据具体类来收费,所以才能正常计算Sell收费。

  “依赖倒置原则”和“里氏替换原则”,是成对出现的。同时这里也实现“开闭原则”的目的。

接口隔离原则

接口隔离原则(Interface Segregation Principle, ISP):使用多个专门的接口,而不使用单一的总接口,即客户端不应该依赖那些它不需要的接口。

  例如收银系统里面,收费时分成两个抽象类:东西+优惠。不应该合并起来,因为并不是全部东西都有优惠,这样就避免了多余的无效代码。当然得结合实际情况编程,有的商品是大部分有优惠的,就可以合并了。

  这里提到的这个原则,就是避免冗余代码。结合“单一责任原则”一起用。

  例如记录存储数据,这里需要储存数据的操作:连接数据库/写本地文件/…,以及获取数据源的操作:http/界面填入/excel分析…。我可以定义成一个基类接口。但是如果遇到获取是数据库,存入却不是数据库的,就得新建子类了。这样总共有N1*N2种情况。

  而如果遵循接口隔离原则,分成两个接口,就是N1+N2种情况。两者效率,代码结构差距很大。

迪米特法则

迪米特法则(Law of Demeter, LoD):一个软件实体应当尽可能少地与其他实体发生相互作用。

  又称

最少知识原则(LeastKnowledge Principle, LKP)

  这是基于“单一责任原则”的,同时避免跨类操作,避免模块与模块间的关联。

  这个法则的目的是降低系统耦合度。减少类间的关联。

  这实际上是最难的一个原则。

  举例:收银系统中,买3个苹果送1个香蕉,这个功能需要苹果类和香蕉类关联起来。如果直接修改原苹果类,肯定不行,违背了开闭原则,而且也没遵循本原则,苹果类和香蕉类关联耦合了。那要怎么避免?只能引入第三方,第三个的责任就是处理3个苹果送1个香蕉问题。

  苹果和香蕉的代码没变,但是多了一个内部实例化引用它们两个的第三方类。

  同样的,如果买3个苹果和1个西瓜,就送1个香蕉,那就在这个第三方类中处理,当然也可以新建第三方类。

  代码上,这个第三方类归在优惠中,结算统计完后,传递统计数据给抽象优惠类,而这个优惠类的具体实现类就是这个第三方类,内部分析应该进行怎样的优惠减免计算。毕竟这个没得扫条码,而且是根据买的东西来算有没有优惠,所以是归优惠类。

  工程中常见就是设置一个manager管理类,负责不同模块类间的交互通讯。

总结

  面向抽象编程为第一准则。尽量先定义接口,再写具体函数类。

  但是在简单工程,简单功能下,为了防止过度设计,是可以直接写具体类而不抽象的。毕竟设计模式是为了更好发展项目,是建立在项目会发展得越来越复杂前提下的。

  结尾贴一篇博文,是写六大设计原则比较好。设计模式之六大原则(转载)


The End

姚佳鑫 wechat
如果您对我的文章感兴趣,可以添加我的微信
感谢您的支持