01-设计模式系列之---七大原则助你高效开发(完整版)

本设计模式系列旨在以循序渐进的方式,帮助读者全面掌握设计模式的核心思想及其实际应用。内容分为四个阶段:

UML类图
从基础入手,讲解 UML 类图的基本概念与用法,包括类间关系(如继承、组合、关联等)。通过实例分析,帮助读者理解如何用 UML 描述系统结构,为后续学习奠定基础。


设计原则
深入解析面向对象设计的七大原则(如单一职责原则、开闭原则等),指导读者编写高内聚、低耦合的代码,并为理解设计模式提供理论支撑。


设计模式
系统讲解常见设计模式,按创建型、结构型和行为型分类,结合 UML 类图与代码示例,分析其应用场景、优缺点及实现方法。


项目实战
通过真实项目案例,展示设计模式的实际应用(如单例管理配置、工厂动态创建对象等)。结合主流框架(如 Spring、MyBatis)分析其原理,帮助读者将理论转化为实践能力。
通过这四个阶段的学习,读者不仅能掌握设计模式的理论知识,还能灵活运用于实际项目中,提升代码质量与开发能力。无论初学者还是开发者,都能从中受益,进一步提高软件开发水平。

1. 设计模式理解

1.1. 什么是设计模式?

对通用软件设计问题的可复用解决方案,是经验总结的代码设计模板。

1.2. 设计模式的作用?

  1. 加速开发:复用已验证方案(如单例、代理模式)
  2. 提升扩展性:隔离变化点,支持功能扩展
  3. 增强可维护性:规范代码结构,降低理解成本
  4. 统一沟通语言:设计模式术语提高团队协作效率

2. 七大原则

2.1. 开闭原则

2.1.1. 开闭原则的定义

开闭原则(OCP)由勃兰特·梅耶于1988年在《面向对象软件构造》中提出,其核心思想是:软件实体应对扩展开放,对修改关闭

软件实体包括模块、类与接口、方法。开闭原则指在需求变化时,无需修改源码或二进制代码,即可扩展功能以满足新需求。

2.1.2. 开闭原则的作用

开闭原则是面向对象设计的核心目标,它让软件既灵活适应变化,又保持稳定和可持续性。其主要作用包括:

  1. 简化软件测试
    遵循开闭原则时,只需测试新增或修改的部分,原有代码的测试结果不受影响。
  2. 提升代码复用性
    通过原子化设计和抽象编程,代码粒度更小,复用性更高。
  3. 增强软件可维护性
    符合开闭原则的软件更稳定、易扩展,从而降低维护成本。

2.1.3. 开闭原则的实现方法

通过“抽象约束、封装变化”实现开闭原则,即利用接口或抽象类构建稳定的抽象层,将可变因素封装在具体实现类中。抽象层合理设计后能保持架构稳定,而易变细节可通过扩展实现类来应对变化,需求变更时只需新增或调整实现类即可。
 

以 Windows 桌面主题为例说明开闭原则的应用。
示例:Windows 桌面主题设计
分析:Windows 主题由背景图片、窗口颜色和声音等元素组成,用户可自定义或下载新主题。这些主题具有共同特性,可以用一个抽象类(Abstract Subject)表示,每个具体主题(Specific Subject)作为其子类。用户可选择或添加新主题,而无需修改现有代码,符合开闭原则。类图如图 1 所示。


图1 Windows的桌面主题类图

public interface AbstractSubject {
    void display();

}
public class SpecificSubject1 implements AbstractSubject{
    @Override
    public void display() {
        System.out.println("具体主题1");
    }
}
public class SpecificSubject2 implements AbstractSubject{
    @Override
    public void display() {
        System.out.println("具体主题2");
    }
}
public class Windows {
    public void display(AbstractSubject abstractSubject) {
        abstractSubject.display();
    }
}
public class Client {
    public static void main(String[] args) {
        Windows windows = new Windows();
        windows.display(new SpecificSubject1());
        windows.display(new SpecificSubject2());
    }
}

2.1.4. 代码案例

根据开闭原则,构建一个可扩展的图形绘制系统,支持轻松添加新图形类型并在GraphicEditor中绘制。

2.1.4.1. 图形绘制系统原始方案
public class Ocp {
    public static void main(String[] args) {
        //使用看看存在的问题
        GraphicEditor graphicEditor = new GraphicEditor();
        graphicEditor.drawShape(new Rectangle());
        graphicEditor.drawShape(new Circle());
        graphicEditor.drawShape(new Triangle());

    }
}

//这是一个用于绘图的类 [使用方]
class GraphicEditor {
    //接收Shape对象,然后根据type,来绘制不同的图形
    public void drawShape(Shape s) {
        if (s.m_type == 1)
            drawRectangle(s);
        else if (s.m_type == 2)
            drawCircle(s);
        else if (s.m_type == 3)
            drawTriangle(s);
    }

    //绘制矩形
    public void drawRectangle(Shape r) {
        System.out.println(" 绘制矩形 ");
    }

    //绘制圆形
    public void drawCircle(Shape r) {
        System.out.println(" 绘制圆形 ");
    }

    //绘制三角形
    public void drawTriangle(Shape r) {
        System.out.println(" 绘制三角形 ");
    }
}

//Shape类,基类
class Shape {
    int m_type;
}

class Rectangle extends Shape {
    Rectangle() {
        super.m_type = 1;
    }
}

class Circle extends Shape {
    Circle() {
        super.m_type = 2;
    }
}

//新增画三角形
class Triangle extends Shape {
    Triangle() {
        super.m_type = 3;
    }
}

原始方案的优缺点:

  • 优点:简单直观,易于理解和操作。
  • 缺点:违反了设计模式的开闭原则(OCP),即对扩展开放、对修改关闭。新增功能(如增加三角形)时需要修改现有代码,改动较大。

改进思路

  1. Shape类设计为抽象类,并提供一个抽象的draw方法,由子类实现。新增图形时,只需创建新子类继承Shape并实现draw方法,使用方无需修改代码,满足开闭原则。
  2. 定义一个Shape抽象类作为基础设计。
2.1.4.2. 图形绘制系统开闭原则 (符合OCP原则)
public class Ocp {
    public static void main(String[] args) {
        GraphicEditor graphicEditor = new GraphicEditor();
        graphicEditor.drawShape(new Rectangle());
        graphicEditor.drawShape(new Circle());
        graphicEditor.drawShape(new Triangle());
        graphicEditor.drawShape(new OtherGraphic());

    }
}

/**
 * 这是一个用于绘图的类 [使用方]
 */
class GraphicEditor {
    /**
     * 接收Shape对象,调用draw方法
     */
    public void drawShape(Shape s) {
        s.draw();
    }


}

/**
 * Shape类,基类
 */
abstract class Shape {
    /**
     * 抽象方法
     */
    public abstract void draw();
}

class Rectangle extends Shape {
    Rectangle() {
    }

    @Override
    public void draw() {
        System.out.println(" 绘制矩形 ");
    }
}

class Circle extends Shape {
    Circle() {
    }

    @Override
    public void draw() {
        System.out.println(" 绘制圆形 ");
    }
}

/**
 * 新增画三角形
 */
class Triangle extends Shape {
    Triangle() {
    }

    @Override
    public void draw() {
        System.out.println(" 绘制三角形 ");
    }
}

/**
 * 新增一个图形
 */
class OtherGraphic extends Shape {
    OtherGraphic() {
    }

    @Override
    public void draw() {
        System.out.println(" 绘制其它图形 ");
    }
}

2.2. 里氏替换原则

2.2.1. 里氏替换原则的定义

里氏替换原则(LSP)由芭芭拉·里斯科夫于1987年提出,核心思想是:子类应能无缝替换父类而不影响程序的正确性(即避免重写父类方法)。它规范了继承关系,明确了基类与子类的职责,补充了开闭原则,并为抽象设计的具体实现提供了指导。简而言之,子类必须能够完全替代父类且保持程序功能正常

2.2.2. 里氏替换原则的作用

里氏替换原则的作用可以简化为以下几点:

  1. 是实现开闭原则的重要手段。
  2. 解决了继承中重写父类导致复用性差的问题。
  3. 保证行为正确性,避免类扩展引入新错误,降低代码出错风险。
  4. 提升程序健壮性、兼容性和可维护性,降低需求变更带来的风险。

关键点

  • 子类不重写父类非抽象方法
  • 方法前置条件(参数)更宽松,后置条件(返回值)更严格

2.2.3. 里氏替换原则的实现方法

里氏替换原则的核心是:子类可以扩展父类的功能,但不能改变父类原有的行为。具体来说:

  1. 子类可以实现父类的抽象方法,但不应覆盖父类的非抽象方法
  2. 子类可以新增自己的特有方法
  3. 重载父类方法时,输入参数应更宽松;实现或重写父类方法时,返回值应更严格或相等
    如果随意重写父类方法,虽然实现起来简单,但会导致继承体系的可复用性变差,尤其是在多态场景下,容易引发运行错误。
    违背里氏替换原则的后果是:子类对象在基类预期的地方可能导致程序出错。此时应重新设计两者关系,取消不合适的继承。

里氏替换原则的核心例子常被概括为“正方形不是长方形”。类似地,生活中也有不少案例:从生物学角度看,企鹅、鸵鸟和几维鸟属于鸟类,但由于它们无法继承“鸟”会飞的特性,因此在类的继承关系中不能定义为“鸟”的子类。同样,“气球鱼”不会游泳,不能归为“鱼”的子类;“玩具炮”不能炸敌人,也不能视为“炮”的子类。
接下来,以“几维鸟不是鸟”为例,进一步说明里氏替换原则的应用。
【例1】里氏替换原则在“几维鸟不是鸟”实例中的应用。
分析:鸟一般都会飞行,如燕子的飞行速度大概是每小时 120 千米。但是新西兰的几维鸟由于翅膀退化无法飞行。假如要设计一个实例,计算这两种鸟飞行 300 千米要花费的时间。显然,拿燕子来测试这段代码,结果正确,能计算出所需要的时间;但拿几维鸟来测试,结果会发生“除零异常”或是“无穷大”,明显不符合预期,其类图如图 1 所示。


图1 “几维鸟不是鸟”实例的类图


public class LSPtest {
    public static void main(String[] args) {
        Bird bird1 = new Swallow();
        Bird bird2 = new BrownKiwi();
        bird1.setSpeed(120);
        bird2.setSpeed(120);
        System.out.println("如果飞行300公里:");
        try {
            System.out.println("燕子将飞行" + bird1.getFlyTime(300) + "小时.");
            System.out.println("几维鸟将飞行" + bird2.getFlyTime(300) + "小时。");
        } catch (Exception err) {
            System.out.println("发生错误了!");
        }
    }
}
//鸟类
class Bird {
    double flySpeed;
    public void setSpeed(double speed) {
        flySpeed = speed;
    }
    public double getFlyTime(double distance) {
        return (distance / flySpeed);
    }
}
//燕子类
class Swallow extends Bird {
}
//几维鸟类
class BrownKiwi extends Bird {
    public void setSpeed(double speed) {
        flySpeed = 0;
    }
}
如果飞行300公里:
燕子将飞行2.5小时.
几维鸟将飞行Infinity小时。

程序运行错误是因为几维鸟重写了鸟类的 setSpeed(double speed) 方法,违反了里氏替换原则。正确的解决方法是:将几维鸟和鸟的共同特性提取到一个更通用的父类(如动物类)中,取消几维鸟对鸟类的直接继承。几维鸟虽然不能飞(飞行速度为 0),但可以奔跑(奔跑速度不为 0),因此可以通过其奔跑速度计算出跑完 300 千米所需时间。类图见图 2。


图2 “几维鸟是动物”实例的类图

  1. 子类型定义:若类型 T2 的对象能替换 T1 的对象且程序行为不变,则 T2 是 T1 的子类型。即,子类对象可透明地替代父类对象。
  2. 里氏替换原则:继承时应尽量避免重写父类方法,确保子类完全兼容父类的行为。
  3. 继承与耦合:继承会增加类间耦合,建议在适当情况下用聚合、组合或依赖代替继承。
public class Liskov {
    public static void main(String[] args) {
        // TODO Auto-generated method stub
        A a = new A();
        System.out.println("11-3=" + a.func1(11, 3));
        System.out.println("1-8=" + a.func1(1, 8));

        System.out.println("-----------------------");
        B b = new B();
        System.out.println("11-3=" + b.func1(11, 3));//这里本意是求出11-3
        System.out.println("1-8=" + b.func1(1, 8));// 1-8
        System.out.println("11+3+9=" + b.func2(11, 3));


    }
}


// A类
class A {
    // 返回两个数的差
    public int func1(int num1, int num2) {
        return num1 - num2;
    }
}

// B类继承了A
// 增加了一个新功能:完成两个数相加,然后和9求和
// 无法透明替换为父类A,因为方法func1重写后含义由父类的求差变成了求和,含义变了
class B extends A {
    //这里,重写了A类的方法, 可能是无意识
    public int func1(int a, int b) {
        return a + b;
    }

    public int func2(int a, int b) {
        return func1(a, b) + 9;
    }
}
11-3=8
1-8=-7
-----------------------
11-3=14
1-8=9
11+3+9=23

解决方法

  1. 我们发现原来运行正常的相减功能发生了错误。原因就是类B无意中重写了父类的 方法,造成原有功能出现错误。在实际编程中,我们常常会通过重写父类的方法完成新的功能,这样写起来虽然简单,但整个继承体系的复用性会比较差。特别是运行多态比较频繁的时候
  2. 通用的做法是:原来的父类和子类都继承一个更通俗的基类,原有的继承关系去掉, 采用依赖,聚合,组合等关系代替.
public class Liskov {
    public static void main(String[] args) {
        // TODO Auto-generated method stub
        A a = new A();
        System.out.println("11-3=" + a.func1(11, 3));
        System.out.println("1-8=" + a.func1(1, 8));

        System.out.println("-----------------------");
        B b = new B();
        //因为B类不再继承A类,因此调用者,不会再认为func1是求减法
        //调用完成的功能就会很明确
        System.out.println("11+3=" + b.func1(11, 3));//这里本意是求出11+3
        System.out.println("1+8=" + b.func1(1, 8));// 1+8
        System.out.println("11+3+9=" + b.func2(11, 3));


        //使用组合仍然可以使用到A类相关方法
        System.out.println("11-3=" + b.func3(11, 3));// 这里本意是求出11-3

    }
}

//创建一个更加基础的基类
class Base {
    //把更加基础的方法和成员写到Base类
}

// A类
class A extends Base {
    // 返回两个数的差
    public int func1(int num1, int num2) {
        return num1 - num2;
    }
}


// B类继承了A
// 增加了一个新功能:完成两个数相加,然后和9求和
class B extends Base {
    //如果B需要使用A类的方法,使用组合关系
    private A a = new A();

    //这里,重写了A类的方法, 可能是无意识 B与A没有继承关系 目的性不一样
    public int func1(int a, int b) {
        return a + b;
    }

    public int func2(int a, int b) {
        return func1(a, b) + 9;
    }

    //我们仍然想使用A的方法
    public int func3(int a, int b) {
        return this.a.func1(a, b);
    }
}
11-3=8
1-8=-7
-----------------------
11+3=14
1+8=9
11+3+9=23
11-3=8

2.3. 依赖倒置原则

2.3.1. 依赖倒置原则的定义

依赖倒置原则(DIP)由罗伯特·马丁于1996年提出,核心思想是:高层模块和低层模块都应依赖抽象,而非具体实现;抽象不应依赖细节,细节应依赖抽象。简而言之,就是面向接口编程,而非实现。遵循该原则可降低耦合度,提升系统的可扩展性和可维护性。由于抽象比具体实现更稳定,基于抽象构建的架构更为稳固,接口或抽象类用于定义规范,具体实现由实现类完成,高、低层模块均依赖抽象。

2.3.2. 依赖倒置原则的作用

依赖倒置原则的主要作用包括:

  • 降低类间耦合性
  • 提高系统稳定性
  • 减少并行开发风险
  • 增强代码可读性与可维护性

2.3.3. 依赖倒置原则的实现方法

依赖倒置原则的目的是通过要面向接口的编程来降低类间的耦合性,所以我们在实际编程中只要遵循以下4点,就能在项目中满足这个规则。

  1. 每个类尽量提供接口或抽象类,或者两者都具备。
  2. 变量的声明类型尽量是接口或者是抽象类。
  3. 任何类都不应该从具体类派生。
  4. 使用继承时尽量遵循里氏替换原则。

【例1】依赖倒置原则在“顾客购物程序”中的应用。
分析:本程序反映了 “顾客类”与“商店类”的关系。商店类中有 sell() 方法,顾客类通过该方法购物以下代码定义了顾客类通过韶关网店 ShaoguanShop 购物:

class Customer {
    public void shopping(ShaoguanShop shop) {
        //购物
        System.out.println(shop.sell());
    }
}

但是,这种设计存在缺点,如果该顾客想从另外一家商店(如婺源网店 WuyuanShop)购物,就要将该顾客的代码修改如下:

class Customer {
    public void shopping(WuyuanShop shop) {
        //购物
        System.out.println(shop.sell());
    }
}

顾客每更换一家商店,都要修改一次代码,这明显违背了开闭原则。存在以上缺点的原因是:顾客类设计时同具体的商店类绑定了,这违背了依赖倒置原则。解决方法是:定义“婺源网店”和“韶关网店”的共同接口 Shop,顾客类面向该接口编程,其代码修改如下:

class Customer {
    public void shopping(Shop shop) {
        //购物
        System.out.println(shop.sell());
    }
}

这样,不管顾客类 Customer 访问什么商店,或者增加新的商店,都不需要修改原有代码了,其类图如图 1 所示。


图1 顾客购物程序的类图

package principle;
public class DIPtest {
    public static void main(String[] args) {
        Customer wang = new Customer();
        System.out.println("顾客购买以下商品:");
        wang.shopping(new ShaoguanShop());
        wang.shopping(new WuyuanShop());
    }
}
//商店
interface Shop {
    public String sell(); //卖
}
//韶关网店
class ShaoguanShop implements Shop {
    public String sell() {
        return "韶关土特产:香菇、木耳……";
    }
}
//婺源网店
class WuyuanShop implements Shop {
    public String sell() {
        return "婺源土特产:绿茶、酒糟鱼……";
    }
}
//顾客
class Customer {
    public void shopping(Shop shop) {
        //购物
        System.out.println(shop.sell());
    }
}

顾客购买以下商品:
韶关土特产:香菇、木耳……
婺源土特产:绿茶、酒糟鱼……

2.4. 单一职责原则

2.4.1. 单一职责原则的定义

单一职责原则(SRP)由罗伯特·C. 马丁提出,强调一个类应只有一个变更原因,即只承担一个职责。若一个类职责过多,会导致两个问题:一是职责间的变更可能相互影响;二是使用时会引入不必要的功能,造成代码冗余。因此,每个类应专注于单一职责。

2.4.2. 单一职责原则的优点

单一职责原则的核心是控制类的粒度,解耦对象并提高内聚性。遵循这一原则有以下好处:

  • 降低复杂度:一个类只负责一项职责,逻辑更简单。
  • 增强可读性:复杂度降低,代码更易读。
  • 提升可维护性:可读性提高,系统更易于维护。
  • 减少变更风险:修改某一功能时,对其他功能的影响更小。

2.4.3. 单一职责原则的实现方法

单一职责原则看似简单,但实际运用却较难,要求设计人员识别类的多重职责并将其分离,分别封装到不同类或模块中。这需要设计人员具备较强的分析设计能力和重构经验。以下通过大学学生工作管理程序为例,说明该原则的应用。

2.4.3.1. 【例1】用户管理系统

假设我们正在设计一个简单的用户管理系统,需要实现用户的创建、修改和删除功能。

public class UserManager {

    // 用户管理类负责管理用户的存储和读取
    public void saveUser(User user) {
        // 将用户信息存入数据库
    }

    public User getUser(int userId) {
        // 从数据库中获取用户信息
        return null;
    }

    // 用户管理类还负责发送邮件的功能
    public void sendEmail(User user, String content) {
        // 发送电子邮件给用户
    }

    // 用户管理类负责生成用户报表
    public void generateUserReport(User user) {
        // 生成用户报表
    }
}

在上面的例子中,UserManager 类不仅负责用户的存取操作,还包含发送邮件和生成报表的功能。这违反了单一职责原则,因为一个类不应该有多个不相关的原因导致它发生变化。
 

public class UserManager {

    // 用户管理类只负责管理用户的存储和读取
    public void saveUser(User user) {
        // 将用户信息存入数据库
    }

    public User getUser(int userId) {
        // 从数据库中获取用户信息
        return null;
    }
}

public class EmailService {

    // 邮件服务类负责发送邮件
    public void sendEmail(User user, String content) {
        // 发送电子邮件给用户
    }
}

public class ReportGenerator {

    // 报表生成类负责生成用户报表
    public void generateUserReport(User user) {
        // 生成用户报表
    }
}

在上面的修改后的设计中,我们将功能拆分到了三个类中:

  1. UserManager 管理用户信息的存取操作。
  2. EmailService 负责发送邮件的功能。
  3. ReportGenerator 负责生成报表的功能。

每个类都只负责一种特定的功能,符合单一职责原则。这样的设计使得每个类更加清晰、可维护,当需求变化时,只需要修改相应的类而不会影响到其他部分。

2.4.3.2. 【例 2】大学学生工作管理程序。

分析:大学学生工作主要分为生活辅导和学业指导两部分。生活辅导包括班委建设、出勤统计、心理辅导、费用催缴和班级管理等任务;学业指导则涵盖专业引导、学习辅导、科研指导和学习总结等内容。将这些工作交由一位老师负责显然不合理,应由辅导员负责生活辅导,学业导师负责学业指导,其类图如图1所示。


图1 大学学生工作管理程序的类图


单一职责原则同样适用于方法:一个方法应专注于完成一件任务。如果一个方法承担过多功能,其粒度会变粗,影响代码的可重用性。

注意事项:

  1. 降低类的复杂度,确保一个类只负责一项职责。
  2. 提升类的可读性和可维护性。
  3. 减少因变更引发的风险。
  4. 通常应遵守单一职责原则,但在逻辑简单时,可在代码层面适度放宽;若类中方法数量较少,可在方法层面保持单一职责。

2.5. 接口隔离原则

2.5.1. 接口隔离原则的定义

接口隔离原则(ISP)主张将大接口拆分为小而具体的接口,确保每个接口只包含必要的方法。根据罗伯特·C. 马丁的定义,客户端不应依赖不需要的方法,类间依赖应基于最小接口。简言之,为每个类提供专门接口,避免宽泛的大接口。
虽然 ISP 和单一职责原则都旨在提升内聚性并降低耦合度,但关注点不同:单一职责聚焦于类的功能单一性,适用于实现细节;而 ISP 关注接口层面,强调最小化依赖,更适合抽象设计和系统架构规划。

2.5.2. 接口隔离原则的优点

接口隔离原则通过减少类对接口的依赖,带来以下核心优势:

  1. 提升灵活性与可维护性:将大接口拆分为小而具体的接口,防止外部变更影响扩散。
  2. 降低耦合度:减少不必要的交互,增强系统内聚力。
  3. 平衡接口设计:合理划分接口大小,保持系统稳定,避免过细或过大带来的问题。
  4. 清晰关系层级:通过多接口设计和继承机制,明确对象间的关系。
  5. 减少代码冗余:避免实现大接口时编写无用方法,提高代码效率。

2.5.3. 接口隔离原则的实现方法

在应用接口隔离原则时,可遵循以下关键规则:

  1. 接口最小化:一个接口应专注于服务单一模块或业务逻辑,但需适度。
  2. 按需定制:只为调用者提供必要方法,隐藏无关功能。
  3. 结合实际:根据具体项目环境和业务逻辑决定接口拆分标准,避免盲目套用。
  4. 高内聚低耦合:设计接口时,尽量减少对外交互,用最少的方法实现最多功能。

下面以学生成绩管理程序为例介绍接口隔离原则的应用。

2.5.3.1. 【例1】学生成绩管理程序。

分析:学生成绩管理程序一般包含插入成绩、删除成绩、修改成绩、计算总分、计算均分、打印成绩信息、査询成绩信息等功能,如果将这些功能全部放到一个接口中显然不太合理,正确的做法是将它们分别放在输入模块、统计模块和打印模块等 3 个模块中,其类图如图 1 所示。


图1 学生成绩管理程序的类图

package principle;
public class ISPtest {
    public static void main(String[] args) {
        InputModule input = StuScoreList.getInputModule();
        CountModule count = StuScoreList.getCountModule();
        PrintModule print = StuScoreList.getPrintModule();
        input.insert();
        count.countTotalScore();
        print.printStuInfo();
        //print.delete();
    }
}
//输入模块接口
interface InputModule {
    void insert();
    void delete();
    void modify();
}
//统计模块接口
interface CountModule {
    void countTotalScore();
    void countAverage();
}
//打印模块接口
interface PrintModule {
    void printStuInfo();
    void queryStuInfo();
}
//实现类
class StuScoreList implements InputModule, CountModule, PrintModule {
    private StuScoreList() {
    }
    public static InputModule getInputModule() {
        return (InputModule) new StuScoreList();
    }
    public static CountModule getCountModule() {
        return (CountModule) new StuScoreList();
    }
    public static PrintModule getPrintModule() {
        return (PrintModule) new StuScoreList();
    }
    public void insert() {
        System.out.println("输入模块的insert()方法被调用!");
    }
    public void delete() {
        System.out.println("输入模块的delete()方法被调用!");
    }
    public void modify() {
        System.out.println("输入模块的modify()方法被调用!");
    }
    public void countTotalScore() {
        System.out.println("统计模块的countTotalScore()方法被调用!");
    }
    public void countAverage() {
        System.out.println("统计模块的countAverage()方法被调用!");
    }
    public void printStuInfo() {
        System.out.println("打印模块的printStuInfo()方法被调用!");
    }
    public void queryStuInfo() {
        System.out.println("打印模块的queryStuInfo()方法被调用!");
    }
}

输入模块的insert()方法被调用!
统计模块的countTotalScore()方法被调用!
打印模块的printStuInfo()方法被调用!
2.5.3.2. 【例 2】 文件处理系统

假设我们正在设计一个文件处理系统,其中有不同类型的文件需要处理,例如文本文件和图片文件。我们需要实现文件的读取、写入、以及可能的格式转换功能。
错误的设计(不符合接口隔离原则)

// 定义一个通用的文件处理接口
public interface FileHandler {

    void read(String fileName);

    void write(String fileName);

    void convert(String fileName, FileType fileType);
}

// 实现文本文件处理类
public class TextFileHandler implements FileHandler {

    @Override
    public void read(String fileName) {
        // 读取文本文件的逻辑
    }

    @Override
    public void write(String fileName) {
        // 写入文本文件的逻辑
    }

    @Override
    public void convert(String fileName, FileType fileType) {
        if (fileType == FileType.IMAGE) {
            // 错误:文本文件不能转换为图片文件
        } else if (fileType == FileType.TEXT) {
            // 文本文件的格式转换逻辑
        }
    }
}

// 实现图片文件处理类
public class ImageFileHandler implements FileHandler {

    @Override
    public void read(String fileName) {
        // 读取图片文件的逻辑
    }

    @Override
    public void write(String fileName) {
        // 写入图片文件的逻辑
    }

    @Override
    public void convert(String fileName, FileType fileType) {
        if (fileType == FileType.TEXT) {
            // 错误:图片文件不能转换为文本文件
        } else if (fileType == FileType.IMAGE) {
            // 图片文件的格式转换逻辑
        }
    }
}

在上述设计中,定义了一个通用的 FileHandler 接口,它包含了读取、写入和格式转换三个方法。然后分别实现了 TextFileHandler 和 ImageFileHandler 类来处理文本文件和图片文件。
问题分析:

冗余接口方法: FileHandler 接口中的 convert 方法对于文本文件处理类和图片文件处理类来说,并不都是必需的。

例如,文本文件处理类不需要实现图片文件的转换逻辑,反之亦然。
接口污染: convert 方法使得接口过于笨重,迫使实现类去实现不必要的方法,增加了系统的复杂性和理解难度。


符合接口隔离原则的设计
为了符合接口隔离原则,我们可以将接口进行细化,每个接口只包含相关的方法,从而避免不必要的依赖和复杂性。

// 定义文件读取接口
public interface Readable {

    void read(String fileName);
}

// 定义文件写入接口
public interface Writable {

    void write(String fileName);
}

// 定义文件格式转换接口
public interface Convertible {

    void convert(String fileName, FileType fileType);
}

// 实现文本文件处理类
public class TextFileHandler implements Readable, Writable {

    @Override
    public void read(String fileName) {
        // 读取文本文件的逻辑
    }

    @Override
    public void write(String fileName) {
        // 写入文本文件的逻辑
    }
}

// 实现图片文件处理类
public class ImageFileHandler implements Readable, Writable, Convertible {

    @Override
    public void read(String fileName) {
        // 读取图片文件的逻辑
    }

    @Override
    public void write(String fileName) {
        // 写入图片文件的逻辑
    }

    @Override
    public void convert(String fileName, FileType fileType) {
        // 图片文件的格式转换逻辑
    }
}

设计分析:

  • 接口细化: 将通用的 FileHandler 接口分解为 Readable、Writable 和 Convertible 接口,每个接口专注于一个单一的功能。
  • 避免依赖: 每个文件处理类只需实现它们需要的接口,例如 TextFileHandler 只需实现 Readable 和 Writable 接口,而 ImageFileHandler 实现了 Readable、Writable 和 Convertible 接口。
    通过这样的设计,我们遵循了接口隔离原则,使得系统中的接口更加专一和灵活,减少了不必要的复杂性和耦合度,同时提高了每个类的可维护性和可测试性。

2.6. 迪米特法则

2.6.1. 迪米特法则的定义

迪米特法则(Law of Demeter,LoD),又称最少知识原则,由Ian Holland于1987年提出,后因Grady Booch和《程序员修炼之道》一书而被广泛传播。其核心思想是:只与直接关联的对象交互,避免与“陌生”对象通信。具体而言,若两个软件实体无需直接通信,则应通过第三方间接调用,以减少类之间的依赖,提高模块独立性。
这里的“直接关联的对象”包括当前对象自身、其成员对象、创建的对象、方法参数及返回值等与当前对象有直接联系的对象。

💡 tips

直接的朋友:我们称出现成员变量,方法参数,方法返回值中的类为直接的朋友,而出现在局部变量中的类不是直接的朋友。也就是说,陌生的类最好不要以局部变量 的形式出现在类的内部。

2.6.2. 迪米特法则的优点

迪米特法则主张减少软件实体间的通信宽度和深度,其正确应用有两大好处:

  1. 降低类间耦合,增强模块独立性。
  2. 提高类的可复用性和系统的扩展性。

然而,过度应用可能导致大量中介类的产生,增加系统复杂性并降低通信效率。因此,在使用迪米特法则时需权衡利弊,确保在实现低耦合与高内聚的同时,保持系统结构清晰简洁。

2.6.3. 迪米特法则的实现方法

迪米特法则的核心是:依赖者只依赖必要的对象,被依赖者只暴露必要的方法。在应用该法则时,需注意以下六点:

  1. 弱耦合:设计类时尽量减少类之间的依赖,以增强可复用性。
  2. 降低访问权限:限制类成员的可见性,避免不必要的访问。
  3. 优先使用不变类:尽可能将类设计为不可变,提升安全性和稳定性。
  4. 减少对外部对象的引用:尽量降低对其他类的直接依赖。
  5. 封装属性:不直接暴露类的属性,而是通过访问器(如 getset 方法)进行操作。
  6. 谨慎使用序列化:小心处理 Serializable 功能,避免潜在风险。
2.6.3.1. 【例1】明星与经纪人的关系实例。

分析:明星由于全身心投入艺术,所以许多日常事务由经纪人负责处理,如与粉丝的见面会,与媒体公司的业务洽淡等。这里的经纪人是明星的朋友,而粉丝和媒体公司是陌生人,所以适合使用迪米特法则,其类图如图 1 所示。


图1 明星与经纪人的关系图
 

package principle;
public class LoDtest {
    public static void main(String[] args) {
        Agent agent = new Agent();
        agent.setStar(new Star("林心如"));
        agent.setFans(new Fans("粉丝韩丞"));
        agent.setCompany(new Company("中国传媒有限公司"));
        agent.meeting();
        agent.business();
    }
}
//经纪人
class Agent {
    // 成员变量
    private Star myStar;
    // 成员变量
    private Fans myFans;
    // 成员变量
    private Company myCompany;
    public void setStar(Star myStar) {
        this.myStar = myStar;
    }
    public void setFans(Fans myFans) {
        this.myFans = myFans;
    }
    public void setCompany(Company myCompany) {
        this.myCompany = myCompany;
    }
    public void meeting() {
        System.out.println(myFans.getName() + "与明星" + myStar.getName() + "见面了。");
    }
    public void business() {
        System.out.println(myCompany.getName() + "与明星" + myStar.getName() + "洽淡业务。");
    }
}
//明星
class Star {
    private String name;
    Star(String name) {
        this.name = name;
    }
    public String getName() {
        return name;
    }
}
//粉丝
class Fans {
    private String name;
    Fans(String name) {
        this.name = name;
    }
    public String getName() {
        return name;
    }
}
//媒体公司
class Company {
    private String name;
    Company(String name) {
        this.name = name;
    }
    public String getName() {
        return name;
    }
}

粉丝韩丞与明星林心如见面了。
中国传媒有限公司与明星林心如洽淡业务。
2.6.3.2. 【例 2】模拟学校管理打印员工信息
  1. 有一个学校,下属有各个学院和总部,现要求打印出学校总部员 工ID和学院员工的id
  2. 编程实现上面的功能, 看代码演示
public class Demeter1 {
    public static void main(String[] args) {
        //创建了一个 SchoolManager 对象
        SchoolManager schoolManager = new SchoolManager();
        //输出学院的员工id 和  学校总部的员工信息
        schoolManager.printAllEmployee(new CollegeManager());
    }
}


//学校总部员工类
class Employee {
    private String id;

    public void setId(String id) {
        this.id = id;
    }

    public String getId() {
        return id;
    }
}


//学院的员工类
class CollegeEmployee {
    private String id;

    public void setId(String id) {
        this.id = id;
    }

    public String getId() {
        return id;
    }
}


//管理学院员工的管理类
class CollegeManager {
    //返回学院的所有员工
    public List<CollegeEmployee> getAllEmployee() {
        List<CollegeEmployee> list = new ArrayList<CollegeEmployee>();
        for (int i = 0; i < 10; i++) { //这里我们增加了10个员工到 list
            CollegeEmployee emp = new CollegeEmployee();
            emp.setId("学院员工id= " + i);
            list.add(emp);
        }
        return list;
    }
}

//学校管理类
//分析 SchoolManager 类的直接朋友类有哪些 Employee、CollegeManager
//CollegeEmployee 不是 直接朋友 而是一个陌生类,这样违背了 迪米特法则
class SchoolManager {
    //返回学校总部的员工
    public List<Employee> getAllEmployee() {
        List<Employee> list = new ArrayList<Employee>();

        for (int i = 0; i < 5; i++) { //这里我们增加了5个员工到 list
            Employee emp = new Employee();
            emp.setId("学校总部员工id= " + i);
            list.add(emp);
        }
        return list;
    }

    //该方法完成输出学校总部和学院员工信息(id)
    void printAllEmployee(CollegeManager sub) {

        //分析问题
        //1. 这里的 CollegeEmployee 不是  SchoolManager的直接朋友
        //2. CollegeEmployee 是以局部变量方式出现在 SchoolManager
        //3. 违反了 迪米特法则

        //获取到学院员工
        List<CollegeEmployee> list1 = sub.getAllEmployee();
        System.out.println("------------学院员工------------");
        for (CollegeEmployee e : list1) {
            System.out.println(e.getId());
        }
        //获取到学校总部员工
        List<Employee> list2 = this.getAllEmployee();
        System.out.println("------------学校总部员工------------");
        for (Employee e : list2) {
            System.out.println(e.getId());
        }
    }
}

应用实例改进

  1. 前面设计的问题在于 SchoolManager 中,CollegeEmployee 类并不是 SchoolManager 类的直接朋友 (分析)
  2. 按照迪米特法则,应该避免类中出现这样非直接朋友关系的耦合
  3. 对代码按照迪米特法则进行改进.
public class Demeter1 {
    public static void main(String[] args) {
        //创建了一个 SchoolManager 对象
        SchoolManager schoolManager = new SchoolManager();
        //输出学院的员工id 和  学校总部的员工信息
        schoolManager.printAllEmployee(new CollegeManager());
    }
}
//学校总部员工类
class Employee {
    private String id;

    public void setId(String id) {
        this.id = id;
    }

    public String getId() {
        return id;
    }
}


//学院的员工类
class CollegeEmployee {
    private String id;

    public void setId(String id) {
        this.id = id;
    }

    public String getId() {
        return id;
    }
}


//管理学院员工的管理类
class CollegeManager {
    //返回学院的所有员工
    public List<CollegeEmployee> getAllEmployee() {
        List<CollegeEmployee> list = new ArrayList<CollegeEmployee>();
        for (int i = 0; i < 10; i++) { //这里我们增加了10个员工到 list
            CollegeEmployee emp = new CollegeEmployee();
            emp.setId("学院员工id= " + i);
            list.add(emp);
        }
        return list;
    }

    //输出学院员工的信息
    public void printEmployee() {
        //获取到学院员工
        List<CollegeEmployee> list1 = getAllEmployee();
        System.out.println("------------学院员工------------");
        for (CollegeEmployee e : list1) {
            System.out.println(e.getId());
        }
    }

}

//学校管理类

//分析 SchoolManager 类的直接朋友类有哪些 Employee、CollegeManager
//CollegeEmployee 不是 直接朋友 而是一个陌生类,这样违背了 迪米特法则
class SchoolManager {
    //返回学校总部的员工
    public List<Employee> getAllEmployee() {
        List<Employee> list = new ArrayList<Employee>();

        for (int i = 0; i < 5; i++) { //这里我们增加了5个员工到 list
            Employee emp = new Employee();
            emp.setId("学校总部员工id= " + i);
            list.add(emp);
        }
        return list;
    }

    //该方法完成输出学校总部和学院员工信息(id)
    void printAllEmployee(CollegeManager sub) {

        sub.printEmployee();

        //获取到学校总部员工
        List<Employee> list2 = this.getAllEmployee();
        System.out.println("------------学校总部员工------------");
        for (Employee e : list2) {
            System.out.println(e.getId());
        }
    }
}

迪米特法则要点:

  1. 核心是降低类之间的耦合。
  2. 注意:并非完全消除依赖,而是减少不必要的依赖,保持适度的耦合关系。

2.7. 合成复用原则

2.7.1. 合成复用原则的定义

合成复用原则(CRP)又称组合/聚合复用原则(CARP),主张在软件复用时优先通过组合或聚合实现,而非直接使用继承。若必须使用继承,则需严格遵守里氏替换原则。合成复用原则与里氏替换原则相辅相成,共同体现开闭原则的具体规范。

2.7.2. 合成复用原则的重要性

合成复用原则的重要性
类的复用分为继承复用和合成复用两种方式。虽然继承复用实现简单,但存在以下问题:

  1. 破坏封装性,子类可直接访问父类的实现细节(白箱复用)。
  2. 子类与父类高度耦合,父类改动会影响子类。
  3. 缺乏灵活性,继承关系在编译时固定,无法动态调整。
    相比之下,组合或聚合复用的优势更明显:
  4. 保持封装性,新对象无需了解被包含对象的具体实现(黑箱复用)。
  5. 降低耦合度,对象间仅通过接口交互。
  6. 提供更高灵活性,支持运行时动态调整组合关系。

2.7.3. 合成复用原则的实现方法

合成复用原则是通过将已有对象作为新对象的成员来实现复用,新对象可以调用已有对象的功能。
以汽车分类管理程序为例介绍该原则的应用:
【例1】汽车分类管理程序
分析:汽车按“动力源”可分为汽油车、电动车等;按“颜色”可分为白车、黑车、红车等。如果同时考虑这两种分类,组合会非常多。图1展示了使用继承关系实现汽车分类的类图。

图1 用继承关系实现的汽车分类的类图

从图 1 可以看出用继承关系实现会产生很多子类,而且增加新的“动力源”或者增加新的“颜色”都要修改源代码,这违背了开闭原则,显然不可取。但如果改用组合关系实现就能很好地解决以上问题,其类图如图 2 所示。


图2 用组合关系实现的汽车分类的类

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值