六大设计原则

简介: 本文介绍了六大设计原则,包括单一职责、开闭原则、里氏替换、接口隔离、依赖倒置和迪米特法则。每项原则均通过定义、设计要点及代码示例进行说明。单一职责强调类的功能应单一;开闭原则提倡对扩展开放、对修改封闭;里氏替换要求子类能无缝替换父类;接口隔离主张拆分专用接口;依赖倒置鼓励面向抽象编程;迪米特法则减少类间依赖。掌握这些原则有助于编写高质量、可维护的代码,并为学习23种设计模式奠定基础。

theme: v-green

六大设计原则

评价代码的质量,一般都是从代码是否可维护,是否简单易读,可拓展性,灵活性,简洁性等方面来评价。设计模式是代码设计经验的总结,学习设计模式可以有效提高程序员的coding能力,让你写出质量上乘的代码。设计模式一共有23种,虽然数量不少,但是它们基本上都是这六大设计原则的实现方式。可以说,这六大设计原则就是23种设计模式的基本设计思想。本文简要介绍六种设计原则,并通过实际代码示例加以说明。
image.png

1. 单一职责原则(Single Responsibility Principle)

定义:一个类应该只有一个引起它变化的原因,即一个类只负责完成一个职责或者功能。

设计要点:避免设计大而全的类,要设计功能单一,粒度适中的类。

判断标准

  • 类中代码行数过多,代码功能职责不清晰;
  • 类中包含过多的函数和属性,导致代码维护困难;
  • 类的依赖过多,导致程序耦合度过高;
  • 类的私有方法过多且这些方法集中操作少数属性。

重构建议

当类变得庞大且功能不单一时,应该把它拆分成多个职责单一的小类。

代码示例

违反单一职责原则的类

// User类同时处理用户信息和用户数据存储
public class User {
   
    private String name;
    private String email;

    // 获取用户名称
    public String getName() {
   
        return name;
    }

    // 设置用户名称
    public void setName(String name) {
   
        this.name = name;
    }

    // 获取用户邮箱
    public String getEmail() {
   
        return email;
    }

    // 设置用户邮箱
    public void setEmail(String email) {
   
        this.email = email;
    }

    // 保存用户到数据库
    public void save() {
   
        // 代码实现保存用户到数据库
    }
}

遵循单一职责原则的类

// 仅负责用户信息的User类
public class User {
   
    private String name;
    private String email;

    // 获取用户名称
    public String getName() {
   
        return name;
    }

    // 设置用户名称
    public void setName(String name) {
   
        this.name = name;
    }

    // 获取用户邮箱
    public String getEmail() {
   
        return email;
    }

    // 设置用户邮箱
    public void setEmail(String email) {
   
        this.email = email;
    }
}

// 负责用户数据存储的UserRepository类
public class UserRepository {
   
    // 保存用户到数据库
    public void save(User user) {
   
        // 代码实现保存用户到数据库
    }
}

说明:通过将用户信息和数据存储职责分离,User类仅关注用户数据,而UserRepository类负责数据的持久化操作,提高了代码的可维护性和可扩展性。

2. 开闭原则(Open-Closed Principle)

定义:软件实体(类,模块,函数等)应对拓展开放,对修改封闭。

核心思想

  • 对拓展开放:允许通过拓展来增加新的功能。
  • 对修改封闭:在需求变化时,不通过直接修改已有功能代码,而是通过增加拓展模块来实现新的功能。

实现方法

使用抽象类或接口来定义稳定的结构,具体功能的拓展通过实现这些抽象类或接口来实现。

重要性

  • 符合开闭原则的软件可以更好地适应变化,维护成本更低;
  • 所有的设计模式都追求这一原则,以确保系统开发和维护的可靠性。

代码示例

违反开闭原则的类

public class GraphicEditor {
   
    public void drawShape(Shape shape) {
   
        if (shape.type == 1) {
   
            drawCircle((Circle) shape);
        } else if (shape.type == 2) {
   
            drawRectangle((Rectangle) shape);
        }
        // 如果新增形状,需要修改此方法
    }

    private void drawCircle(Circle circle) {
   
        // 绘制圆形的代码
    }

    private void drawRectangle(Rectangle rectangle) {
   
        // 绘制矩形的代码
    }
}

public class Shape {
   
    int type;
}

public class Circle extends Shape {
   
    public Circle() {
   
        type = 1;
    }
}

public class Rectangle extends Shape {
   
    public Rectangle() {
   
        type = 2;
    }
}

遵循开闭原则的类

// 抽象类Shape,定义绘制方法
public abstract class Shape {
   
    public abstract void draw();
}

// 圆形类
public class Circle extends Shape {
   
    @Override
    public void draw() {
   
        // 绘制圆形的代码
    }
}

// 矩形类
public class Rectangle extends Shape {
   
    @Override
    public void draw() {
   
        // 绘制矩形的代码
    }
}

// 图形编辑器类,不需要修改即可支持新形状
public class GraphicEditor {
   
    public void drawShape(Shape shape) {
   
        shape.draw();
    }
}

说明:通过使用抽象类ShapeGraphicEditor类无需修改即可支持新的形状,只需新增继承自Shape的类,实现draw方法即可。


3. 里氏替换原则(Liskov Substitution Principle)

定义:子类对象可以替换父类对象的任何地方,并保证程序的逻辑行为不变。

理解方式

  • 替换依赖于面向对象语言中的多态特性,子类对象应该可以在父类出现的任何地方替换父类对象。
  • 子类的实现应该遵守接口或父类定义的方法规范,即方法的输入和输出保持一致(保证行为不变)。

代码示例

违反里氏替换原则的类

// 父类鸟
public class Bird {
   
    public void fly() {
   
        // 鸟类飞行的实现
    }
}

// 企鹅类继承自鸟,但企鹅不会飞
public class Penguin extends Bird {
   
    @Override
    public void fly() {
   
        throw new UnsupportedOperationException("企鹅不会飞");
    }
}

遵循里氏替换原则的类

// 抽象类动物
public abstract class Animal {
   
    // 一般动物的共同行为
}

// 可以飞行的动物接口
public interface Flyable {
   
    void fly();
}

// 鸟类实现Flyable接口
public class Bird extends Animal implements Flyable {
   
    @Override
    public void fly() {
   
        // 鸟类飞行的实现
    }
}

// 企鹅类不实现Flyable接口
public class Penguin extends Animal {
   
    // 企鹅的特有行为
}

说明:通过将会飞的行为抽象为Flyable接口,企鹅类不再继承不适用的fly方法,避免了因子类行为不一致导致的问题,符合里氏替换原则。


4. 接口隔离原则(Interface Segregation Principle)

定义:为各个类设计它们需要的专用接口,而不是设计一个庞大的通用接口供所有的类调用。

设计要点

  • 避免创建“胖接口”,而是要根据具体需求将接口拆分成对应的多个专一的接口。
  • 提高内聚性,降低耦合性,使类与接口之间的依赖关系更清晰。

与单一职责原则的区别:单一职责原则注重类的职责单一,接口隔离原则侧重于接口依赖的隔离。

代码示例

违反接口隔离原则的接口

public interface Worker {
   
    void work();
    void eat();
}

public class HumanWorker implements Worker {
   
    @Override
    public void work() {
   
        // 人类工作实现
    }

    @Override
    public void eat() {
   
        // 人类吃饭实现
    }
}

public class RobotWorker implements Worker {
   
    @Override
    public void work() {
   
        // 机器人工作实现
    }

    @Override
    public void eat() {
   
        // 机器人不需要吃饭,但必须实现该方法
        throw new UnsupportedOperationException("机器人不需要吃饭");
    }
}

遵循接口隔离原则的接口

// 工作接口
public interface Workable {
   
    void work();
}

// 吃饭接口
public interface Eatable {
   
    void eat();
}

public class HumanWorker implements Workable, Eatable {
   
    @Override
    public void work() {
   
        // 人类工作实现
    }

    @Override
    public void eat() {
   
        // 人类吃饭实现
    }
}

public class RobotWorker implements Workable {
   
    @Override
    public void work() {
   
        // 机器人工作实现
    }
}

说明:通过将Worker接口拆分为WorkableEatable,机器人类不再需要实现与其不相关的eat方法,符合接口隔离原则。


5. 依赖倒置原则(Dependency Inversion Principle)

定义:高层模块不应该依赖于低层模块,两者都应该依赖于抽象。抽象不应该依赖细节,细节应该依赖抽象。

实现方式

  • 面向接口编程,而不是面向具体实现。
  • 通过依赖注入的方式,将低层模块的实现注入到高层模块的抽象中。

目的

降低模块之间的耦合度,提高系统的灵活性和可维护性。

代码示例

违反依赖倒置原则的类

// 低层模块:数据库日志记录
public class DatabaseLogger {
   
    public void log(String message) {
   
        // 将日志记录到数据库
    }
}

// 高层模块:用户服务
public class UserService {
   
    private DatabaseLogger logger = new DatabaseLogger();

    public void createUser(String username) {
   
        // 创建用户的逻辑
        logger.log("User created: " + username);
    }
}

遵循依赖倒置原则的类

// 抽象日志记录接口
public interface Logger {
   
    void log(String message);
}

// 低层模块:数据库日志记录实现
public class DatabaseLogger implements Logger {
   
    @Override
    public void log(String message) {
   
        // 将日志记录到数据库
    }
}

// 低层模块:文件日志记录实现
public class FileLogger implements Logger {
   
    @Override
    public void log(String message) {
   
        // 将日志记录到文件
    }
}

// 高层模块:用户服务
public class UserService {
   
    private Logger logger;

    // 通过构造函数注入依赖
    public UserService(Logger logger) {
   
        this.logger = logger;
    }

    public void createUser(String username) {
   
        // 创建用户的逻辑
        logger.log("User created: " + username);
    }
}

使用示例

public class Main {
   
    public static void main(String[] args) {
   
        // 选择具体的日志记录实现
        Logger logger = new DatabaseLogger();
        // 或者使用文件日志记录
        // Logger logger = new FileLogger();

        // 通过依赖注入将Logger传递给UserService
        UserService userService = new UserService(logger);
        userService.createUser("JohnDoe");
    }
}

说明:高层模块UserService依赖于抽象Logger接口,而不是具体的DatabaseLoggerFileLogger,从而实现了依赖倒置,增强了系统的灵活性和可扩展性。

6. 迪米特法则(Law of Demeter)

定义:一个类应该尽量少地依赖其他的类,减少类之间的相互依赖关系。

设计要点

  • 避免直接与不必要的类进行通信,减少类之间的直接依赖关系。
  • 降低类之间的耦合度,提高模块的独立性和可维护性。

代码示例

违反迪米特法则的类

public class OrderService {
   
    private OrderRepository orderRepository = new OrderRepository();

    public void processOrder(int orderId) {
   
        Order order = orderRepository.getOrderById(orderId);
        Customer customer = order.getCustomer();
        Address address = customer.getAddress();
        // 直接访问Customer和Address的细节
        sendEmail(address.getEmail(), "Your order has been processed.");
    }

    private void sendEmail(String email, String message) {
   
        // 发送邮件的逻辑
    }
}

遵循迪米特法则的类

// Customer类增加发送邮件的方法
public class Customer {
   
    private Address address;

    public Address getAddress() {
   
        return address;
    }

    public void sendEmail(String message) {
   
        // 发送邮件的逻辑,内部获取地址信息
        String email = address.getEmail();
        // 发送邮件
    }
}

// Order类增加获取Customer的方法
public class Order {
   
    private Customer customer;

    public Customer getCustomer() {
   
        return customer;
    }
}

// OrderRepository类
public class OrderRepository {
   
    public Order getOrderById(int orderId) {
   
        // 获取订单的逻辑
        return new Order();
    }
}

// OrderService类遵循迪米特法则
public class OrderService {
   
    private OrderRepository orderRepository = new OrderRepository();

    public void processOrder(int orderId) {
   
        Order order = orderRepository.getOrderById(orderId);
        // 通过Order直接调用Customer的方法,而不需要了解Customer的内部结构
        order.getCustomer().sendEmail("Your order has been processed.");
    }
}

说明:通过将发送邮件的职责委托给Customer类,OrderService类不再需要了解CustomerAddress的内部结构,减少了类之间的耦合,符合迪米特法则。


掌握这些设计原则,并在实际编码中灵活运用,将为后续学习23种设计模式打下坚实的基础,帮助你编写出高质量、可维护、可扩展的代码。

目录
相关文章
|
8月前
|
NoSQL 安全 Java
深入理解 RedisConnectionFactory:Spring Data Redis 的核心组件
在 Spring Data Redis 中,`RedisConnectionFactory` 是核心组件,负责创建和管理与 Redis 的连接。它支持单机、集群及哨兵等多种模式,为上层组件(如 `RedisTemplate`)提供连接抽象。Spring 提供了 Lettuce 和 Jedis 两种主要实现,其中 Lettuce 因其线程安全和高性能特性被广泛推荐。通过手动配置或 Spring Boot 自动化配置,开发者可轻松集成 Redis,提升应用性能与扩展性。本文深入解析其作用、实现方式及常见问题解决方法,助你高效使用 Redis。
861 4
|
IDE Shell 网络安全
Visual Studio 2022 git error Unable to negotiate with xx.xxx.xxxx port 22: no matching host key type found. Their offer: ssh-rsa
Visual Studio 2022 git error Unable to negotiate with xx.xxx.xxxx port 22: no matching host key type found. Their offer: ssh-rsa
810 0
Visual Studio 2022 git error Unable to negotiate with xx.xxx.xxxx port 22: no matching host key type found. Their offer: ssh-rsa
|
前端开发 Java 应用服务中间件
21张图解析Tomcat运行原理与架构全貌
【10月更文挑战第2天】本文通过21张图详细解析了Tomcat的运行原理与架构。Tomcat作为Java Web开发中最流行的Web服务器之一,其架构设计精妙。文章首先介绍了Tomcat的基本组件:Connector(连接器)负责网络通信,Container(容器)处理业务逻辑。连接器内部包括EndPoint、Processor和Adapter等组件,分别处理通信、协议解析和请求封装。容器采用多级结构(Engine、Host、Context、Wrapper),并通过Mapper组件进行请求路由。文章还探讨了Tomcat的生命周期管理、启动与停止机制,并通过源码分析展示了请求处理流程。
|
8月前
|
存储 Java 开发者
Java 中的 equals 方法:看似简单,实则深藏玄机
本文深入探讨了Java中`equals`方法的设计与实现。默认情况下,`equals`仅比较对象引用是否相同。以`String`类为例,其重写了`equals`方法,通过引用判断、类型检查、长度对比及字符逐一比对,确保内容相等的逻辑。文章还强调了`equals`方法需遵循的五大原则(自反性、对称性等),以及与`hashCode`的关系,避免集合操作中的潜在问题。最后,对比了`instanceof`和`getClass()`在类型判断中的优劣,并总结了正确重写`equals`方法的重要性,帮助开发者提升代码质量。
616 1
|
8月前
|
JSON 前端开发 Java
彻底搞定 Spring 中的 @PathVariable 和 @ResponseBody
本文深入解析了Spring MVC中`@PathVariable`和`@ResponseBody`的使用方法及适用场景。`@PathVariable`用于从URL路径提取变量,增强REST风格接口的语义性;`@ResponseBody`则将方法返回值直接写入HTTP响应体,适合返回JSON或XML数据。通过多个实际案例,展示了两者在不同场景下的应用,如删除商品、获取商品详情等。文章还总结了最佳实践,帮助开发者更好地理解与运用这两个注解,提升接口设计的清晰度与可维护性。
288 0
|
8月前
|
存储 Java 数据挖掘
Java 中数组的多种定义方式
本文深入解析了Java中数组的多种定义方式,涵盖基础的`new`关键字创建、直接初始化、动态初始化,到多维数组、`Arrays.fill()`方法以及集合类转换为数组等高级用法。通过理论与实践结合的方式,探讨了每种定义方法的适用场景、优缺点及其背后的原理,帮助开发者掌握高效、灵活的数组操作技巧,从而编写更优质的Java代码。
413 0
|
8月前
|
存储 安全 Java
Java 集合框架详解:系统化分析与高级应用
本文深入解析Java集合框架,涵盖List、Set、Map等核心接口及其常见实现类,如ArrayList、HashSet、HashMap等。通过对比不同集合类型的特性与应用场景,帮助开发者选择最优方案。同时介绍Iterator迭代机制、Collections工具类及Stream API等高级功能,提升代码效率与可维护性。适合初学者与进阶开发者系统学习与实践。
255 0
|
8月前
|
Java 编译器 API
Java Lambda 表达式:以 Foo 接口为例深入解析
本文深入解析了 Java 8 中 Lambda 表达式的用法及其背后的函数式接口原理,以 `Foo` 接口为例,展示了如何通过简洁的 Lambda 表达式替代传统匿名类实现。文章从 Lambda 基本语法、函数式接口定义到实际应用层层递进,并探讨默认方法与静态方法的扩展性,最后总结常见误区与关键点,助你高效优化代码!
196 0
|
8月前
|
SQL 前端开发 Java
深入理解 Spring Boot 项目中的分页与排序功能
本文深入讲解了在Spring Boot项目中实现分页与排序功能的完整流程。通过实际案例,从Service层接口设计到Mapper层SQL动态生成,再到Controller层参数传递及前端页面交互,逐一剖析每个环节的核心逻辑与实现细节。重点包括分页计算、排序参数校验、动态SQL处理以及前后端联动,确保数据展示高效且安全。适合希望掌握分页排序实现原理的开发者参考学习。
536 4
|
8月前
|
存储 Java API
Java Optional 完全指南:彻底告别 NullPointerException
Java 8 引入的 `Optional` 类旨在解决 `null` 带来的空指针异常问题,通过提供容器类显式处理可能为空的值,提升代码健壮性和可读性。本文从基础到进阶解析 `Optional` 的用法,涵盖创建、检查、获取值、处理值等核心功能,结合实际应用场景与最佳实践,助你彻底告别 `NullPointerException`,编写更优雅的 Java 代码。
431 0