【大神之路】大话设计模式 - 设计模式与七大原则

一、设计模式 - Design Patterns

1、设计模式的前世今生

“设计模式” 并不是计算机软件设计专业提出的名词, 其本身来源于建筑行业。

直到 1990 年, 计算机行业内才真正开始了 对于设计模式的讨论(即使3年前, 设计模式已经被提出应用在软件行业)。

五年后, 艾瑞克·伽马(ErichGamma)、理査德·海尔姆(Richard Helm)、拉尔夫·约翰森(Ralph Johnson)、约翰·威利斯迪斯(John Vlissides)四位作者发表了著作 Design Patterns: Elements of Reusable Object-Oriented Software (《设计模式:可复用面向对象软件的基础》) 中收录归纳了 23 个 设计模式。成为了设计模式领域里程碑的事件。

后期人们所谓的 GoF 的23种设计模式中的 GoF 指的就是 上面的四位大师。全称为 Gang of Four (四人帮)

本质上来说, 设计模式就是在面向对象的特征(封装、继承、多态)上 以及 类关系(关联关系 和 组合关系) 的终极体现。利用面向对象的各种特点总结而来的 “代码套路”, 从而解决各种现实问题。

2、设计模式的基本要素

要想能够更好的理解掌握设计模式, 最重要的一点就是要抓住设计模式的核心问题, 关于设计模式, 其最主要也是最关键的点有四个, 称为 设计模式的 四大核心。

  • 模式名称

    每个模式都会有一个自己的名字, 有的模式是用其作用命名, 有的是其特点、解决方案、要解决的问题等。基本上就是1到2个词组成

  • 问题

    问题描述了模式的应用环境,即在什么样一个情况下使用该模式。从数学的角度来说就是已知条件, 根据已知条件就能够得到数学模型(设计模式)从而解出结果(解决方案)。

  • 解决方案

    模式问题的解决方案包括设计的组成成分、它们之间的相互关系及各自的职责和协作方式。简答来说就是解体公式, 前面通过问题总结出了数学模型, 后面就会有对应的解题步骤, 到了这一步, 程序员只需要根据"解题步骤"就能够得到正确答案了。

  • 效果

    对前面的总结, 包括模式的优势, 缺点,需要改进的地方,灵活度, 扩展性等等各个方面。 对于模式解决的问题的各个指标进行评估。

3、GoF 的 23 设计模式

在这里插入图片描述

二、程序设计七大原则 (7 Principles)

1、开篇

程序设计原则, 程序设计这一世界 圣经 般的存在, 我们现实所知的所有框架技术包括后面要讲的设计模式, 都是因这七个原则而出现的。由此,可以看出这七大原则的重要程度和地位。

有的地方也会写为六大原则或者五大原则, 小C出于 宁可错杀一百,绝不放过一个 的原因, 列出了全部七个原则,为的是让读者们有一个更清晰的认知。

2、开闭原则 - OCP

开闭原则(Open Closed Principle,OCP): 软件实体应当对扩展开放,对修改关闭。

Software entities like classes,modules and functions should be open for extension but closed for modifications

这是一切软件设计的立命之本, 所谓的对扩展开对修改闭指的是: 当应用的需求改变时,在不修改软件实体的源代码或者二进制代码的前提下,可以扩展模块的功能,使其满足新的需求。

一般情况下, 通过“抽象约束、封装变化”来实现开闭原则,即通过接口或者抽象类为软件实体定义一个相对稳定的抽象层,而将相同的可变因素封装在相同的具体实现类中。

开闭原则的经典案例:

之前,为一家公司开发了一个CMS(内容管理系统)。 系统中要对数据使用图表进行展示。 于是有了如下的代码

/**
 * 饼图
 * @author CoWioSug
 */
public class PieChart {
    public void display(Object data){
        System.out.println("饼图信息展示");
    }
}
/**
 * 图表展示类
 * @author CoWioSug
 */
public class ChartDisplayer {
    public void display(PieChart chart){
        chart.display();
    }
}

后面发现, 还有折线图

/**
 * 折线图
 * @author CoWioSug
 */
public class LineChart {
    public void display(Object data){
        System.out.println("折线图数据展示");
    }
}

完了, 图表展示类也出问题了。改一改

/**
 * 图表展示类
 * @author CoWioSug
 */
public class ChartDisplayer {
    public void display(Object chart){
        if( null != chart){
            if(chart instanceof LineChart){
                ((LineChart) chart).display();
            }else if(chart instanceof PieChart){
                ((PieChart) chart).display();
            }else {
                throw new RuntimeException("该图表暂时不支持");
            }
        }
    }
}

这个时候就发现问题了,后面如果有更多的图形, ChartDisplayer 将压力山大。于是, 我们决定重构这一部分代码。所以, 有了下面的代码

/**
 * 图形接口
 * @author CoWioSug
 */
public interface Chart{
    void display();
}
/**
 * 折线图
 * @author CoWioSug
 */
public class LineChart implements Chart{
    @Override
    public void display() {
        System.out.println("折线图展示...");
    }
}
/**
 * 饼图
 * @author CoWioSug
 */
public class PieChart implements Chart{
    @Override
    public void display() {
        System.out.println("饼图展示...");
    }
}
/**
 * 图表展示类
 * @author CoWioSug
 */
public class Test {
    public void display(Chart chart) {
        if (null != chart) {
            chart.display();
        }
    }
}

这样, 今后再有新的图形, 只要添加一个新类, 同样去implements Chart就OK了。

所以, 对于开闭原则, 最核心思想就是抽象, 使用接口或者抽象类来抽象不同的特性, 讲不同的行为移到具体实现类中去完成。

3、里氏替换原则 - LSP

里氏替换原则 (Liskov Substitution Principle,LSP): 继承必须确保超类所拥有的性质在子类中仍然成立

该原则是 麻省理工学院计算机科学实验室的里斯科夫(Liskov)女士提出, 所以用其名字命名

该原则原定义为: Inheritance should ensure that any property proved about supertype objects also holds for subtype objects

更加直观一点,就是说:子类可以扩展父类的功能,但不能改变父类原有的功能。也就是说:子类继承父类时,除添加新的方法完成新增功能外,尽量不要重写父类的方法。

里氏替换原则最经典的例子是 正方形不是长方形 , 这可能与我们学的东西有所相悖。 我们在学习面向对象的时候,经常举例说 存在 人类(class Person) 和 他的两个子类 男人 class Man extends Person 和 婴儿 class Baby extends Person。 其类关系如下:

在这里插入图片描述

其中的move方法为移动方法: 男人的移动方式为行走 ,而婴儿的移动方式为 爬 , 所以需要重写 move方法。

/**
 * 人类 
 * @author CoWioSug
 */
public class Person{
    public void move(){
        System.out.println("移动");
    }
}
/**
 * 男人类 
 * @author CoWioSug
 */
class Man extends Person{
    @Override
    public void move() {
        System.out.println("步行");
    }
}

/**
 * 婴儿类 
 * @author CoWioSug
 */
class Baby extends Person{
    @Override
    public void move() {
        System.out.println("爬行");
    }
}

显然, 这种做法违背了 LSP , 是不应该重写方法么? 还是LSP本身有问题呢? 都不对! 出现上面的问题是:类的结构错了!我们来看下面这个例子:

在这里插入图片描述

这样就不再违背 LSP 了。

/**
 * 人类接口 
 * @author CoWioSug
 */
public interface Person {
    /**
   	 * 定义移动能力
   	 */
    void move();
}
/**
 * 成年人类 
 * @author CoWioSug
 */
public class Adult implements Person {
    @Override
    public void move() {
        System.out.println("步行");
    }
}
// 人类继承 成年人, 无需重写移动方法, 满足LSP
public class Man extends Adult {}
/**
 * 婴儿类 
 * @author CoWioSug
 */
public class Baby implements Person{
    @Override
    public void move() {
        System.out.println("爬行");
    }
}

4、依赖倒置原则 - DIP

依赖倒置原则 (Dependence Inversion Principle): 高层模块不应该依赖低层模块,两者都应该依赖其抽象;抽象不应该依赖细节,细节应该依赖抽象。

High level modules should not depend upon low level modules. Both should depend upon abstractions. Abstractions should not depend upon details. Details should depend upon abstractions

简单来说, DIP 就是 要面向接口编程,不要面向实现编程。通过接口来降低类间的耦合性

是实现开闭原则的重要途径之一, 所以, 你会发现, 下面的例子同样适用于 开闭原则

例如:饭店原来有个厨子老王

在这里插入图片描述

/**
 * 厨师老王
 * @author CoWioSug
 */
public class CookerWang {
    public void cook(String what){
        System.out.println("老王做"+what);
    }
}
/**
 * 饭店
 * @author CoWioSug
 */
public class Restaurant{
    private CookerWang cookerWang = new CookerWang();
    public void business(){
        cookerWang.cook("酸菜鱼");
        cookerWang.cook("糖醋里脊");
    }
}
public class Test {
    public static void main(String[] args) {
        Restaurant r = new Restaurant();
        r.business();
    }
}

突然有一天, 老王不干了, 饭店新招了一个厨子老张,此时要新添加一个类 CookerZhang, 并且修改Restaurant源代码

/**
 * 厨师老张
 * @author CoWioSug
 */
public class CookerZhang {
    public void cook(String what){
        System.out.println("老张做"+what);
    }
}
/**
 * 饭店类
 * @author CoWioSug
 */
public class Restaurant{
    private CookerZhang cookerZhang = new CookerZhang();
    public void business(){
        cookerZhang.cook("酸菜鱼");
        cookerZhang.cook("糖醋里脊");
    }
}

这是极度不友好的, 也不满足 DIP 。此时 , 我们应该把代码修改为:

在这里插入图片描述

/**
 * 厨师接口
 * @author CoWioSug
 */
public interface Cooker{
    void cook(String what);
}
/**
 * 厨师老张
 * @author CoWioSug
 */
public class CookerZhang implements Cooker{
    @Override
    public void cook(String what){
        System.out.println("老张做"+what);
    }
}
/**
 * 厨师老王
 * @author CoWioSug
 */
public class CookerWang implements Cooker{
    @Override
    public void cook(String what) {
        System.out.println("老王做"+what);
    }
}
/**
 * 饭店
 * @author CoWioSug
 */
public class Restaurant{
    private Cooker cooker;

    public void setCooker(Cooker cooker) {
        this.cooker = cooker;
    }

    public void business(){
        cooker.cook("酸菜鱼");
        cooker.cook("糖醋里脊");
    }
}
/**
 * 测试类
 * @author CoWioSug
 */
public class Test {
    public static void main(String[] args) {
        Restaurant r = new Restaurant();
        r.setCooker(new CookerWang());
        r.business();
        r.setCooker(new CookerZhang());
        r.business();
    }
}

5、单一职责原则 - SRP

单一职责原则 / 单一功能原则 (Single Responsibility Principle,SRP): 规定一个类应该有且仅有一个引起它变化的原因,否则类应该被拆分

There should never be more than one reason for a class to change

简单来说 就是一个类应该只关心一种事情, 比如说: service 就应该只关心核心业务逻辑, controller (action / servlet )就应该只关心显示逻辑和数据交互, mapper ( dao )只关心 数据持久化操作

如果代码不满足这个原则, 并不会导致大面积的代码重写, 只不过:

  1. 一个职责的变化可能会削弱或者抑制这个类实现其他职责的能力;
  2. 当客户端需要该对象的某一个职责时,不得不将其他不需要的职责全都包含进来,从而造成冗余代码或代码的浪费。

例如

在这里插入图片描述

一个工具类中包含了所有的工具方法, 凌乱不堪, 无法维护, 我们可以做的是

在这里插入图片描述

一个类只关心一种类型的工具方法。

注意:单一职责同样也适用于方法。一个方法应该尽可能做好一件事情。如果一个方法处理的事情太多,其颗粒度会变得很粗,不利于重用。

6、接口隔离原则 - ISP

接口隔离原则(Interface Segregation Principle,ISP): 客户端不应该被迫依赖于它不使用的方法 / 一个类对另一个类的依赖应该建立在最小的接口上

Clients should not be forced to depend on methods they do not use

The dependency of one class to another one should depend on the smallest possible interface

简单来说, 不要为了省事, 将不同的功能定义在同一个接口中 / 接口中的方法应该是高内聚的。 即:为不同的功能或者能力或者规范(接口的作用就是 定义一种规范 或者表示一种能力)去定义不同的接口。如果只用一个接口定义供所有的类使用, 会使得这个接口非常庞大并且难以维护。

细心的读者会发现, ISP 表面上很像 SRP , 两者在提高类的内聚性 , 降低类与类之间的耦合性上是一致的, 但是, 两者之间也存在着本质上的区别。

  • SRP 更加关注的是职责 即一个类 中的内聚性。 是对类的约束, 主要针对的是具体的实现和细节。
  • ISP 更加关注的是对接口依赖的隔离。是对接口的约束, 主要针对的是程序架构。

例如: 还是前面那个厨子。

在这里插入图片描述

突然有一天, 老板说:一道好菜, 从食材原料上就应该严格控制!CookWang, 采购你也一起负责了吧!于是, 有了下面的结构

在这里插入图片描述

可是, 此时, Cooker 还是 Cooker 么 ? 厨师会采购!!!!!! 显然这样设计就有问题了, 同时也不满足 ISP了。所以, 真正的设计方式应该是

在这里插入图片描述

7、迪米特法则 / 最少知识原则 - LoD / LKP

迪米特法则 (Law of Demeter,LoD) 又名 最少知识原则 (Least Knowledge Principle,LKP): LoD 定义只与你的直接朋友交谈,不跟“陌生人”说话。

Talk only to your immediate friends and not to strangers

其含义是:如果两个软件实体无须直接通信,那么就不应当发生直接的相互调用,可以通过第三方转发该调用。其目的是降低类之间的耦合度,提高模块的相对独立性。

厨子老王又来了, 顾客到饭店吃饭, 点了一个 宫保鸡丁 , 于是老王就做了一个宫保鸡丁。

/**
 * 厨子老王
 * @author CoWioSug
 */
public class CookerWang{
    public void cook(String what){
        // 老王只会做菜
        System.out.println("老王做了" + what);
    }
}
/**
 * 顾客
 * @author CoWioSug
 */
public class Customer{
    public void order(String what){
        // 顾客点餐
        System.out.println("顾客点餐" + what);
        // 找到老王
        CookerWang cookerWang = new CookerWang();
        // 让老王做
        cookerWang.cook(what);
    }
}
/**
 * 测试类
 * @author CoWioSug
 */
public class Test {
    public static void main(String[] args) {
        // 有一个顾客
        Customer c = new Customer();
        // “自助式” 点了 宫保鸡丁
        c.order("宫保鸡丁");
    }
}

在这里插入图片描述

运行结果是对的, 但是 老王怎么和顾客打起交道了呢? 厨子不应该在厨房么?顾客到厨房找老王下单了?明显代码不合理了, 老王 和 顾客 是陌生人,两者之间不应该能够直接通信!所以, 根据 LoD , 代码应该为

/**
 * 厨子老王
 * @author CoWioSug
 */
public class CookerWang{
    // 老王只会做菜
    public void cook(String what){
        System.out.println("老王做了" + what);
    }
}
/**
 * 顾客
 * @author CoWioSug
 */
public class Customer{
    // 顾客只负责点餐
    public void order(String what){
        System.out.println("顾客点餐" + what);
    }
}
/**
 * 服务员
 * @author CoWioSug
 */
public class Waiter{

    private CookerWang cookerWang;
    private Customer customer;
	
    // 服务员通知老王做菜
    public Waiter setCookerWang(CookerWang cookerWang) {
        this.cookerWang = cookerWang;
        return this;
    }
	// 服务员服务于顾客
    public Waiter setCustomer(Customer customer) {
        this.customer = customer;
        return this;
    }
	
    // 服务
    public void service(String what){
        // 顾客点餐
        customer.order(what);
        // 老王做菜
        cookerWang.cook(what);
    }

}
public class LoDTest {
    public static void main(String[] args) {
        Waiter waiter = new Waiter()
                    .setCookerWang(new CookerWang())
                    .setCustomer(new Customer());
        waiter.service("宫保鸡丁");
    }
}

其实, 私有化属性 提供 get / set 也是迪米特法则的一个表现形式

8、合成复用原则 - CRP

合成复用原则(Composite Reuse Principle,CRP)又叫组合/聚合复用原则(Composition/Aggregate Reuse Principle,CARP)。它要求在软件复用时,要尽量先使用组合或者聚合等关联关系来实现,其次才考虑使用继承关系来实现。

简答来说 :

如果A 类 f 方法中 需要使用B类的方法 t:

  • 方法参数能搞定的(把B对象作为f方法的参数传入f方法中), 坚决不用类的属性
  • 类的属性(将B 对象 作为A的属性存在), 构造方法能传入的 坚决不要自己new
  • 类的属性能搞定的 坚决不用继承(A extends B)
  • 一定要用继承的情况下,请遵循 LSP

但是, 还有一个重要前提, 在不满足该前提的情况下, 以上规则全部作废!即: 要满足实际现实逻辑!!

三、小结

洋洋洒洒 4000 多字, 简单的给大家介绍了下什么是设计模式, 以及 程序设计的 7 大原则, 为后面23设计模式的引入开了个头, 小C会不定期的更新 大话设计模式 系列内容, 希望能够给大家带来些帮助。


在这里插入图片描述

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值