【设计模式系列 24】GoF23 种设计模式总结及软件设计 7 大原则
在软件开发过程中,为了提高系统的可维护性和可复用性,可扩展性以及灵活性,产生了 7 大设计原则,这些原则也会贯穿体现在我们前面介绍的设计模式中,设计模式会尽量遵循这些原则,但是也可能为了某一个侧重点从而牺牲某些原则,在我们日常开发中也只能说尽量遵守,但是并不必刻意的为了遵守而遵守,要有所侧重。
开闭原则(Open-Closed Principle,OCP)
开闭原则由勃兰特·梅耶(Bertrand Meyer)在其 1988 年的著作《面向对象软件构造》(Object Oriented Software Construction)中提出:在一个软件实体(如类,函数等)应该对扩展开放,对修改关闭。
开闭原则强调的是用抽象构建框架,用实现扩展细节,这样就可以提高软件系统的可复用性和可维护性。
开闭原则是面向对象设计的最基本设计原则,而遵守开闭原则的核心思想就是面向抽象编程。
举个例子,超市里面的商品需要出售。
1、新建一个商品接口:
package com.zwx.design.principle.ocp;
import java.math.BigDecimal;
public interface IGoods {
String getName();
BigDecimal getSalePrice();
}
2、新建一个白菜商品类:
package com.zwx.design.principle.ocp;
import java.math.BigDecimal;
public class Cabbage implements IGoods {
@Override
public String getName() {
return "上海青";
}
@Override
public BigDecimal getSalePrice() {
return new BigDecimal("2.98");
}
}
这时候到了晚上了,白菜要打折清仓,只卖 1.98。这时候应该怎么做?
直接改白菜商品类的 getSalePrice 方法可行吗?不可行,可能会影响到其他地方。
那直接改接口呢,新增一个打折方法可行吗?假如有几千种商品,我就只有白菜这一个商品需要打折呢,那么这显然也不合理,再不然就直接在白菜类里面单独新增一个打折方法,这些方法看似可行,但是都违背了开闭原则中的对修改关闭。所以我们的做法是再新建一个白菜打折类:
package com.zwx.design.principle.ocp;
import java.math.BigDecimal;
public class DiscountCabbage implements IGoods {
@Override
public String getName() {
return "上海青";
}
@Override
public BigDecimal getSalePrice() {
return new BigDecimal("1.98");
}
}
这样子就符合了开发原则,扩展灵活,后面如果有其他商品需要打折可以一样处理
里氏替换原则(Liskov Substitution Principle,LSP)
里氏替换原则由麻省理工学院计算机科学实验室的芭芭拉·利斯科夫(Barbara Liskov)在 1987 年的“面向对象技术的高峰会议”(OOPSLA)上发表的一篇文章《数据抽象和层次》(Data Abstraction and Hierarchy)里提出:继承必须确保超类所拥有的性质在子类中仍然成立。也就是说如果对每一个类型为 T1 的对象 o1 都有类型为 T2 的对象 o2,使得以 T1 所定义的程序 P 在所有的对象 o1 都替换成为 o2 时,程序 P 的行为没有发生改变。
在具体一点就是说如果一个软件实体适用于一个父类的话,那么一定适用于子类,所有引用了父类的地方都必须能透明的使用其子类对象。具体的可以总结为以下几条原则:
1、子类可以实现父类的抽象方法,但是不能覆盖父类的非抽象方法。
2、子类中可以增加自己的特有方法。
3、当子类方法重载父类的方法时,方法的前置条件(即方法的输入/入参)要比父类方法输入的参数更宽松
4、当子类实现父类的方法(重载/重写/实现抽象方法),方法的后置条件(即方法的输出/返回值)要比父类更严格或者相等
遵循里氏替换原则有如下优点:
1、可以约束继承的泛滥,也是开闭原则的一种体现
2、加强了程序的健壮性,同时在变更时也做到了非常好的兼容性,提高了程序的维护性,扩展性,降低了需求变更时引入的风险。
举个栗子,我们以鸟类飞翔为例:
package com.zwx.design.principle.lsp;
public class Bird {
public void fly() {
System.out.println("我正在天上飞");
}
}
这时候我们有一个鹰类需要继承 Bird:
package com.zwx.design.principle.lsp;
public class Eagle extends Bird {
@Override
public void fly() {
System.out.println("我正在 8000 米高空飞翔");
}
}
最后我们再看看测试类:
package com.zwx.design.principle.lsp;
import com.zwx.design.principle.isp.Dog;
public class TestLsp {
public static void main(String[] args) {
Bird bird = new Bird();
bird.fly();
//替换成子类 Eagle,子类重写了父类 Bird 的 fly 方法
Eagle eagle = new Eagle();
eagle.fly();
}
}
当我们用子类替换父类的时候,因为父类的方法被重写了,所以替换之后输出结果发生了改变,这就违背了里氏替换原则。
依赖倒置原则(Dependence Inversion Principle,DIP)
依赖倒置原则是 Object Mentor 公司总裁罗伯特·马丁(Robert C.Martin)于 1996 年在 C++ Report 上发表的文章中提出。
依赖倒置原则指的是在设计代码结构时,高层模块不应该依赖低层模块,而是都应该依赖其抽象。抽象不应该依赖细节,细节应该依赖抽象。通过依赖倒置原则可以减少类与类之间的耦合性,提高系统的稳定性,提高代码的可读性和可维护性,而且能够降低修改程序所带来的的风险。
举个栗子:
比如说有一家超市里面一开始只卖青菜:
package com.zwx.design.principle.dip;
public class SuperMarket {
public void saleCabbage(){
System.out.println("我有白菜可以卖");
}
}
然后心在业务开始扩大了,要卖肉了,这时候怎么办呢,可以再加一个方法,但是这么一来底层要改,调用者也要改,不利于维护,所以应该不依赖于具体实现来编程。
进行如下改写:
新建一个商品接口:
package com.zwx.design.principle.dip;
public interface IGoods {
void sale();
}
然后新建一个白菜类:
package com.zwx.design.principle.dip;
public class Cabbage implements IGoods{
@Override
public void sale() {
System.out.println("我有白菜卖");
}
}
然后将超市类改写:
package com.zwx.design.principle.dip;
public class SuperMarket {
public void sale(IGoods goods){
goods.sale();
}
}
这时候超市已经面向接口了,而不面向具体(白菜),如果扩张业务,想要卖肉类,直接新增一个肉类就好了
单一职责原则(Single Responsibility Principle,SRP)
单一职责原则由罗伯特·C.马丁(Robert C. Martin)于《敏捷软件开发:原则、模式和实践》一书中提出。
单一职责原则指的是不要存在多于一个导致类变更的原因。假如我们有一个类里面有两个职责,一旦其中一个职责发生需求变更,
那我们我们修改其中一个职责就有可能导致另一个职责出现问题,在这种情况应该把两个职责放在两个 Class 对象之中。
单一职责可以降低类的复杂度,提高类的可读性,提高系统的可维护性,也降低了变更职责引发的风险。
举个栗子:
比如说超市里面的商品需要进货然后再卖出去,这就是两件事。
新建一个超市商品类:
package com.zwx.design.principle.srp;
public class Goods {
public void action(String type){
if ("进货".equals(type)){
System.out.println("我要去进货了");
}else if("售卖".equals(type)){
System.out.println("我要卖商品");
}
}
}
这时候一个方法里面有两个功能,假如业务逻辑非常复杂,那么一个功能发生变化需要修改有很大的风险导致另一个功能也发生异常。所以我们应该进行如下改写,将这两个职责拆分成两个类:
package com.zwx.design.principle.srp;
public class BuyGoods {
public void action(){
System.out.println("我要去进货了");
}
}
package com.zwx.design.principle.srp;
public class SaleGoods {
public void action(){
System.out.println("我要卖商品");
}
}
接口隔离原则(Interface Segregation Principle,ISP)
接口隔离原则是 2002 年由罗伯特·C.马丁提出的。接口隔离原则指的是用多个专门的接口,而不使用单一的一个总接口,客户端不应依赖它不需要的接口。
接口隔离原则符合我们所说的高内聚低耦合的设计思想,从而使得类具有很好的可读性、可扩展性和可维护性,我们在设计接口的时候应该注意以下几点:
1、一个类对其他类的依赖应建立在最小的接口之上
2、建立单一的接口,不建立庞大臃肿的接口
3、尽量细化接口,接口中的方法应适度
我们以常见的动物的行为来举个栗子:
package com.zwx.design.principle.isp;
public interface IAnimal {
void run();
void swim();
void fly();
}
这个动物接口里面包含了三个行为接口:地上走,水里游,天上飞。但是是不是所有动物都有这三种行为呢?显然不是,比如狗肯定不能天上飞,鱼只能水里游,这样没用的行为只能空着什么都不做了:
package com.zwx.design.principle.isp;
public class Dog implements IAnimal {
@Override
public void run() {
System.out.println("我跑的很快");
}
@Override
public void swim() {
System.out.println("我还会游泳");
}
@Override
public void fly() {
}
}
而如果鱼,那就得空着两个方法什么也不能做了,这就是一个臃肿的接口设计,如果遵循接口隔离原则,那么应该这么改写:
新建三个接口,每个动作都对应一个接口:
package com.zwx.design.principle.isp;
public interface IFlyAnimal {
void fly();
}
package com.zwx.design.principle.isp;
public interface IRunAnimal {
void run();
}
package com.zwx.design.principle.isp;
public interface ISwimAnimal {
void swim();
}
这时候动物狗就可以这么写:
package com.zwx.design.principle.isp;
public class Dog implements IRunAnimal,ISwimAnimal {
@Override
public void run() {
System.out.println("我跑的很快");
}
@Override
public void swim() {
System.out.println("我还会游用");
}
}
这样就实现了接口隔离,不会具备一些无用的行为。
迪米特法则又叫作最少知道原则(Least Knowledge Principle,LKP),产生于 1987 年美国东北大学(Northeastern University)的一个名为迪米特(Demeter)的研究项目,由伊恩·荷兰(Ian Holland)提出。
迪米特法则是指一个对象对其他对象应该保持最少的了解,尽量降低类与类之间的耦合。
举个栗子,比如说上面的超市售卖的商品青菜,老板(Boss)想知道卖出去了多少斤:
首先新建一个青菜商品:
package com.zwx.design.principle.lod;
public class Cabbage {
public void getName(){
System.out.println("上海青");
}
public void saleRecord(){
System.out.println("我今天卖出去了 100 斤");
评论