【设计模式系列24】GoF23种设计模式总结及软件设计7大原则

设计模式总结及软件设计七大原则

  • 设计模式系列总览
  • 前言
  • 软件设计7大原则
    • 开闭原则(Open-Closed Principle,OCP)
    • 里氏替换原则(Liskov Substitution Principle,LSP)
    • 依赖倒置原则(Dependence Inversion Principle,DIP)
    • 单一职责原则(Single Responsibility Principle,SRP)
    • 接口隔离原则(Interface Segregation Principle,ISP)
    • 迪米特法则(Law of Demeter,LoD)
    • 合成复用原则(Composite Reuse Principle,CRP)
  • 设计模式总结
    • 创建型设计模式
    • 结构型设计模式
    • 行为型设计模式
  • 总结

设计模式系列总览

设计模式 飞机票
三大工厂模式 登机入口
策略模式 登机入口
委派模式 登机入口
模板方法模式 登机入口
观察者模式 登机入口
单例模式 登机入口
原型模式 登机入口
代理模式 登机入口
装饰者模式 登机入口
适配器模式 登机入口
建造者模式 登机入口
责任链模式 登机入口
享元模式 登机入口
组合模式 登机入口
门面模式 登机入口
桥接模式 登机入口
中介者模式 登机入口
迭代器模式 登机入口
状态模式 登机入口
解释器模式 登机入口
备忘录模式 登机入口
命令模式 登机入口
访问者模式 登机入口
软件设计7大原则和设计模式总结 登机入口

前言

前面我们已经介绍完了全部的GoF23种设计模式,而介绍过程中其实也可以发现很多设计模式都是很相似的,设计模式的思想都是相通的,而且设计模式大都不是单独出现的,一般都是你中有我,我中有你,而设计模式的学习主要还是体会设计模式的思想,有些时候我们并不需要严格遵循设计模式的写法,只要能把设计模式的思想融入到日常的代码当中,自然就可以提升代码的质量。

本文主要会对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("我还会游用");
    }
}

这样就实现了接口隔离,不会具备一些无用的行为。

迪米特法则(Law of Demeter,LoD)

迪米特法则又叫作最少知道原则(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斤");
    }
}

这时候普通做法可以在Boss类里面集成Cabbage类,这样就可以拿到售卖记录,但是这就违背了迪米特法则,因为老板不应该直接和商品打交道,要不然商品一多,老板哪有闲情自己一个个去查,所以一般老板可以找对应的经理获取结果。
我们再新建一个经理类:

package com.zwx.design.principle.lod;

public class Manager {
     
    private Cabbage cabbage;
    public void getCabbageSaleMoney(){
     
        cabbage.saleRecord();
    }
}

最后再新建Boss类:

package com.zwx.design.principle.lod;

public class Boss {
     
    public void getCabbageSaleRecord(Manager manager){
     
        manager.getCabbageSaleMoney();
    }
}

可以看到Boss完全不需要和青菜打交道,找经理就好了,这就是迪米特法则,不该知道的不要知道,我只要让该知道的人知道就好了,你想知道那你就去找那个该知道的人。而实际上中介者模式就是一种典型的遵守了迪米特法则的设计模式。

合成复用原则(Composite Reuse Principle,CRP)

合成复用原则又叫组合/聚合复用原则(Composition/Aggregate Reuse Principle,CARP)。指的是在软件复用时,要尽量先使用组合(has-a)或者聚合(contains-a)等关联关系来实现,这样可以使系统更加灵活,降低类与类之间的耦合度,一个类的变化对其他类造成的影响相对较少。

继承通常也称之为白箱复用,相当于把所有的实现细节暴露给子类。组合/聚合也称之为黑箱复用,对类以外的对象是无法获取到实现细节的。

这个原则还是非常好理解的,像我们开发中经常用的依赖注入,其实就是组合,所以在这里就不再举例子了。

设计模式总结

学完设计模式之后,其实我们也应该知道,设计模式就是在某种场景下,针对某种问题的某种解决方法,而GoF23种设计模式源于《设计模式》一书:《Design Patterns:Elements of Resuable Object-Oriented Software》是由Erich Gamma、Richard Helm、Ralph Johnson、John Vlissides四个人一起合著的,发表于1995年。这四位作者也被称之为“四人组(Gang of Four)”,所以这本书也被称之为:四人组(或GoF)书,而这本书里面就介绍了23种设计模式,这就是我们常说的GoF23种设计模式

23种设计模式大致可以分为三大类,创建型结构型行为型

创建型设计模式

创建型设计模式顾名思义就是用来创建对象的,回忆23种设计可以很容易得出其主要有如下设计模式(加粗的表示高频使用设计模式):工厂方法模式抽象工厂模式单例模式建造者模式,原型模式。

设计模式 一句话总结
工厂方法模式 由子类来决定创建具体对象
抽象工厂模式 允许使用者创建对象的家族,无需指定具体类
单例模式 只为对象提供一个全局的访问点(世上只有一个地球)
建造者模式 允许使用者自由搭配组合对象
原型模式 通过复制原对象来创建新对象

结构型设计模式

结构型模式主要包括以下模式(加粗的表示高频使用设计模式):代理模式门面模式装饰器模式享元模式组合模式适配器模式,桥接模式。

设计模式 一句话总结
代理模式 增强对象功能
门面模式 统一访问入口(拨开云雾见天日)
装饰器模式 为对象添加新的功能
享元模式 共享资源池(节省资源,从我做起)
组合模式 统一整体和个体的处理方式
适配器模式 兼容转换(我所做的一切,只是为了配得上你)
桥接模式 将抽象与具体分离开来

行为型设计模式

结构型模式主要包括以下模式(加粗的表示高频使用设计模式):模板方法模式策略模式责任链模式状态模式,迭代器模式,命令模式,备忘录模式,中介者模式,解释器模式,观察者模式,访问者模式。

设计模式 一句话总结
模板方法模式 定义完整流程,只允许子类微调
策略模式 定义可互相替换策略,使用者爱用哪个用哪个(条条道路通罗马)
责任链模式 链路上每个对象只处理自己能处理的(个人自扫门前雪,休管他人瓦上霜)
状态模式 绑定状态和行为
迭代器模式 统一对集合的访问方式
命令模式 将请求与处理进行解耦(运筹帷幄之中,决胜千里之外)
备忘录模式 备份对象(专卖“后悔药”)
中介者模式 将网状结构统一通过中介进行管理
解释器模式 实现特定语法的解析(我的地盘我做主)
观察者模式 解耦观察者和被观察者,状态发生改变时竹筒通知观察者
访问者模式 解耦数据结构和数据操作

总结

本文主要介绍了软件设计的7大原则,并分别通过一个例子给予了示范,最后我们将23种设计模式分为了创建型,结构型和行为型三大类,并对每种设计模式分别用一句话进行了简单的概括。

设计模式系列到这里就算彻底结束了,请关注我其他系列文章,和孤狼一起学习进步

你可能感兴趣的:(设计模式,设计模式,java,软件设计原则,7大原则,一句话总结设计模式)