设计模式之七大设计原则

一、简介

  当谈论Java编程和面向对象设计时,通常提到的七个设计原则是指五个SOLID原则加两个独立的设计原则,这是一组用于创建更具扩展性和维护性的软件设计的原则。这七个原则是:

  • 单一职责原则(Single Responsibility Principle - SRP):一个类应该只有一个引起它变化的原因。这意味着一个类应该只负责一项具体的任务或职责。
  • 开闭原则(Open/Closed Principle - OCP):软件实体(类、模块、函数等)应该对扩展开放,对修改关闭。这意味着应该通过扩展现有代码来添加新功能,而不是修改现有代码。
  • 里氏替换原则(Liskov Substitution Principle - LSP):子类应该能够替代其基类而不引起错误。这确保了继承关系的正确性和稳定性。
  • 接口隔离原则(Interface Segregation Principle - ISP):不应该强迫客户端依赖于它们不使用的接口。接口应该小而精确,而不是大而臃肿。
  • 依赖倒置原则(Dependency Inversion Principle - DIP):高层模块不应该依赖于低层模块,它们都应该依赖于抽象。抽象不应该依赖于细节,细节应该依赖于抽象。
  • 合成复用原则(Composition Over Inheritance - COI):尽量使用组合而不是继承来实现代码重用。这可以降低耦合性并提高灵活性。
  • 最小知识原则(Law of Demeter - LoD):一个对象应该对其他对象有尽可能少的了解,不应该直接调用其他对象的方法,而是通过自己的方法或代理来进行间接通信。

  这些原则有助于编写可维护、灵活和可扩展的代码,是面向对象设计的基本准则。合成复用原则最小知识原则是两个独立的设计原则,并不属于 SOLID 原则的范畴。

二、浅析

2.1、单一职责原则(Single Responsibility Principle - SRP)

  当遵循单一职责原则时,一个类应该只有一个引起它变化的原因。这意味着一个类应该负责一项明确定义的任务,而不应该具有多个不相关的职责。以下是一个示例:

  假设有一个图形类,既负责绘制图形,又负责计算图形的面积。

public class ShapeDrawerAndAreaCalculator {
    public void drawShape() {
        // 绘制图形的逻辑
    }

    public void calculateArea() {
        // 计算图形面积的逻辑
    }
}

在这个例子中,ShapeDrawerAndAreaCalculator 类承担了绘制图形和计算图形面积的多个职责,这会导致以下问题:

  • 难以维护:如果需要更改任一职责,可能会影响其他部分的代码。
  • 难以测试:难以编写单元测试,因为这些职责紧密耦合在一起。
  • 缺乏灵活性:难以复用或扩展其中一个职责而不影响其他部分。

  为了遵循单一职责原则,你应该将不同的职责分离为不同的类。在这个例子中,这两种职责可以分开成两个类,一个负责绘制图形,另一个负责计算图形的面积。

// 负责绘制图形
public class ShapeDrawer {
    public void drawShape() {
        // 绘制图形的逻辑
    }
}

// 负责计算图形面积
public class AreaCalculator {
    public void calculateArea() {
        // 计算图形面积的逻辑
    }
}

  通过这种方式,每个类都有一个清晰的职责,提高了代码的可维护性、可测试性和灵活性。这是单一职责原则的一个简单示例。

2.2、开闭原则(Open/Closed Principle - OCP)

  当遵循开闭原则时,软件实体(类、模块、函数等)应该对扩展开放,对修改关闭。这意味着你应该能够通过扩展现有代码来添加新功能,而不是修改已有代码。以下是一个示例:

假设你正在编写一个图形绘制应用,最初只支持绘制矩形。你可能会创建一个类如下:

public class Rectangle {
	public void drawRectangle() {
        // 绘制矩形的逻辑
    }
}

  这个类负责绘制矩形,但随着时间的推移,你需要添加对绘制其他形状(例如圆形)的支持。如果不遵循开闭原则,你可能会修改 Rectangle 类以支持新的形状:

public class Rectangle {
	public void drawRectangle() {
        // 绘制矩形的逻辑
    }

    public void drawCircle() {
        // 绘制圆形的实现
    }

}

  这种做法破坏了开闭原则,如果需要新增一个三角形,按照开闭原则,我们不应该修改原有的代码,而是通过扩展来实现。我们可以创建一个抽象类或接口 Shape 来表示不同形状的图形,并让具体的图形类去实现它。

public interface Shape {
    void draw();
}

public class Circle implements Shape {
    @Override
    public void draw() {
        // 绘制圆形的逻辑
    }
}

public class Rectangle implements Shape {
    @Override
    public void draw() {
        // 绘制矩形的逻辑
    }
}

// 新增的三角形类
public class Triangle implements Shape {
    @Override
    public void draw() {
        // 绘制三角形的逻辑
    }
}

  现在,我们通过创建新的 Triangle 类来实现对三角形的绘制,而不需要修改其他类。这样,通过添加新的形状类,我们实现了对图形绘制器的扩展,同时保持了原有代码的稳定性,符合开闭原则。这种设计方式可以有效地支持代码的扩展和维护,使得系统更具弹性和可扩展性。

2.3、里氏替换原则(Liskov Substitution Principle - LSP)

  里氏替换原则(Liskov Substitution Principle - LSP)是面向对象编程中的五个SOLID原则之一,它强调子类应该能够替代其基类而不引起错误。更具体地说,它提出以下要求:

  • 子类必须具有与其基类相同的接口。
  • 子类可以扩展基类的行为,但不应该改变基类的原有行为。
  • 子类的方法参数类型应该与基类方法参数类型相同或更宽松。
  • 子类的方法返回类型应该与基类方法返回类型相同或更具体。

  LSP的目标是确保通过基类类型引用的对象,无论是基类还是子类,都可以正确地工作,而不引发错误或意外行为。这有助于提高代码的可重用性和可扩展性。以下是一个示例来说明LSP:

  假设有一个图形类层次结构,其中有一个基类 Shape 和两个子类 CircleRectangle。按照LSP,任何使用 Shape 类的地方都应该能够无缝地使用 CircleRectangle 对象。

class Shape {
    // 公共图形属性和方法
}

class Circle extends Shape {
    // 圆形特有的属性和方法
}

class Rectangle extends Shape {
    // 矩形特有的属性和方法
}

根据LSP,可以这样使用这些对象:

Shape circle = new Circle();
Shape rectangle = new Rectangle();

  在这种情况下,无论是基类 Shape 还是子类 CircleRectangle 都可以正确地工作,因为它们都遵循LSP的原则。

  总之,里氏替换原则有助于确保对象之间的替代性和互换性,从而提高代码的可扩展性和可维护性。它强调了对继承关系的正确建模和设计。具体来说,里氏替换原则强调子类应该能够替代其基类,这涉及到多态的概念。多态允许你使用基类类型的引用来引用子类对象,然后根据实际运行时类型来动态调用方法。这使得代码更具灵活性,支持多态性。

  在上述示例中,我们使用了多态性,允许使用 Shape 类的引用来引用 CircleRectangle 对象,而不需要知道具体的子类类型。这是里氏替换原则的一个示例,它强调了多态如何帮助实现对象的替代性。

  因此,如果子类重写了父类的方法,但是重写后的行为与原有的契约不符合或者破坏了原有的功能,那就违反了里氏替换原则。另一方面,如果子类仅仅是扩展了父类的方法,添加了新的功能而不破坏原有行为,那么就是符合里氏替换原则的。

2.4、接口隔离原则(Interface Segregation Principle - ISP)

  接口隔离原则(Interface Segregation Principle,ISP)要求客户端不应该依赖于它们不使用的接口。这意味着接口应该小而精确,而不是大而臃肿,以确保客户端只需知道与其相关的方法。让我通过一个示例来说明接口隔离原则:

  假设你正在创建一个多媒体播放器应用,其中有不同类型的媒体文件(音频和视频)。你可能会设计一个媒体播放器接口如下:

public interface MediaPlayer {
    void playAudio();
    void playVideo();
    void pause();
    void stop();
}

  然后你创建了一个实现了该接口的媒体播放器类。这个接口包含了音频和视频播放的方法,但问题是不是所有的媒体播放器都需要实现这两个方法。

  如果你创建一个仅支持音频的媒体播放器,它仍然需要实现 playVideo() 方法,尽管它根本不会用到这个方法。这违反了接口隔离原则。

  更好的方式是将接口拆分成多个小的接口,每个接口代表一个独立的功能。例如:

public interface AudioPlayer {
    void playAudio();
    void pause();
    void stop();
}

public interface VideoPlayer {
    void playVideo();
    void pause();
    void stop();
}

  现在,你的音频播放器只需实现 AudioPlayer 接口,而视频播放器只需实现 VideoPlayer 接口。这符合接口隔离原则,因为每个类只需实现与其相关的方法,不会被迫实现它们不需要的方法。这提高了代码的灵活性和可维护性,同时遵循了接口隔离原则。

2.5、依赖倒置原则(Dependency Inversion Principle - DIP)

  依赖倒置原则(Dependency Inversion Principle - DIP)强调高层模块不应该依赖于低层模块,它们都应该依赖于抽象。抽象不应该依赖于细节,细节应该依赖于抽象。这意味着你应该编写代码以依赖于接口或抽象类,而不是依赖于具体的实现。让我通过一个示例来说明:

  假设我们有一个通知服务,负责向用户发送通知消息,最初的设计可能是直接在高层模块(例如业务逻辑)中依赖于具体的发送实现,如下所示:

// 高层模块
public class NotificationService {
    private SMSNotification smsNotification;

    public NotificationService() {
        this.smsNotification = new SMSNotification();
    }

    public void sendNotification(String message, String recipient) {
        smsNotification.send(message, recipient);
    }
}

// 低层模块
public class SMSNotification {
    public void send(String message, String recipient) {
        // 发送短信的具体实现
        System.out.println("Sending SMS to " + recipient + ": " + message);
    }
}

  这个设计存在问题,因为高层模块 NotificationService 直接依赖于低层模块 SMSNotification,违反了DIP。为了符合DIP,我们可以通过引入一个抽象接口,让高层模块依赖于抽象而不是具体的实现,如下所示:

// 抽象
public interface Notification {
    void send(String message, String recipient);
}

// 低层模块实现抽象
public class SMSNotification implements Notification {
    public void send(String message, String recipient) {
        // 发送短信的具体实现
        System.out.println("Sending SMS to " + recipient + ": " + message);
    }
}

// 高层模块依赖于抽象
public class NotificationService {
    private Notification notification;

    public NotificationService(Notification notification) {
        this.notification = notification;
    }

    public void sendNotification(String message, String recipient) {
        notification.send(message, recipient);
    }
}

  现在,NotificationService 高层模块依赖于抽象 Notification 接口,而不是直接依赖于具体的 SMSNotification。这使得系统更灵活,可以轻松地扩展和替换不同的通知方式,比如邮件:

// 另一种低层模块实现抽象
public class EmailNotification implements Notification {
    public void send(String message, String recipient) {
        // 发送邮件的具体实现
        System.out.println("Sending Email to " + recipient + ": " + message);
    }
}

2.6、合成复用原则(Composition Over Inheritance - COI)

  合成复用原则(Composition Over Inheritance - COI)鼓励使用组合(composition)而不是继承(inheritance)来实现代码的重用。让我通过一个示例来说明:

  假设你正在编写一个手机类,可能手机可以拍照,显示,处理等。你可能会考虑使用继承来实现这些形状:

public class Phone {
	public void takePhoto() {
        // 拍照逻辑
    }
    
    public void display() {
        // 显示逻辑
    }

    public void gyroscope() {
        // 陀螺仪
    }
}

public class Smartphone extends Phone{
	// 省略方法实现
}

  这种方式看起来没问题,但是如果说,中间某部分功能就没有,比如某个手机没陀螺仪或者其他功能,就会很多的冗余和耦合。这违反了合成复用原则,因为继承并不总是最好的代码重用方式。更好的方式是使用组合如下:

// 摄像头类
public class Camera {
    public void takePhoto() {
        // 拍照逻辑
    }
}

// 屏幕类
public class Screen {
    public void display() {
        // 显示逻辑
    }
}

// 处理器类
public class Gyroscope {
    public void process() {
        // 处理逻辑
    }
}


public class Smartphone {
    private Camera camera;
    private Screen screen;
    private Gyroscope gyroscope;

    public Smartphone() {
        this.camera = new Camera();
        this.screen = new Screen();
        this.gyroscope = new Gyroscope();
    }

    public void takePhoto() {
        camera.takePhoto();
    }

    public void display() {
        screen.display();
    }

    public void process() {
        gyroscope.process();
    }

    // 可以添加其他手机功能的方法
}

  现在,通过组合的方式将不同的功能组合到一个类中,提高了代码的灵活性、可维护性和可扩展性。比如你没有拍照功能,直接移除即可。不是说继承不能用,但是通常不是很通用的情况下,都是优先组合。

2.7、最小知识原则(Law of Demeter - LoD)

  最小知识原则(也称为迪米特法则)的主要重点在于减少对象之间的直接依赖关系,将依赖限制在最小的范围内,以降低代码的耦合度。这通常通过以下方式来实现:

  • 对于对象的方法,只调用其自身的方法或属性,而不调用其他对象的方法。
  • 将具体的操作委托给依赖对象,而不是直接访问依赖对象的内部状态。

  这有助于确保对象之间的关系更加清晰,减少了代码中的隐藏依赖,使代码更容易理解、维护和修改。最小知识原则通常在大型、复杂的系统中更为重要,以确保代码的可维护性和可扩展性。在小型程序中,有时可能会看到最小知识原则与其他原则重叠,因为简化问题通常会导致更少的依赖关系。

  当遵循最小知识原则时,对象应该限制其通信范围,只与直接的朋友互动,而不与陌生人互动。所谓的直接的朋友是指以下对象:

  1. 该对象本身
  2. 该对象的参数
  3. 该对象的实例变量

  最小知识原则的主要思想是,一个对象应该知道越少越好,只与它的直接朋友互动,而不与陌生对象互动。这有助于减少对象之间的耦合,提高代码的灵活性和可维护性。

下面是一个更清晰的示例:

public class Customer {
    private String name;

    public Customer(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }
}

public class Order {
    private Customer customer;

    public Order(Customer customer) {
        this.customer = customer;
    }

	// 不直接调用,而通过订单自身的方法
    public String getCustomerName() {
        return customer.getName();
    }
}

  在这个示例中,Order 类只与其直接的朋友 Customer 类互动,而不直接与任何其他对象(如邮件服务、数据库等)互动。这符合最小知识原则,减少了对象之间的依赖关系,使系统更具灵活性。

三、结语

  设计模式中的七大设计原则为面向对象设计提供了指导,帮助设计出灵活、可维护、可扩展的软件系统。它们强调了代码的模块化、松耦合性、高内聚性以及对变化的适应能力。在实际设计中,这些原则通常是相互关联的,一起使用以达到更好的设计效果,接下来,开始我们设计模式之旅。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值