当前位置: 首页 > news >正文

系统设计——项目设计经验总结1

摘要

  1. 在系统设计的时候,注意域的区分,功能区分、类的区分、方法区分范围和定义。
  2. 在系统设计的时候的,需要思考类、方法在什么情况下会涉及到修改,遵循记住:一个类应该只有一个原因被修改! 当不满足,可能就考虑拆分的问题。
  3. 学会T泛型使用,因为泛型是通用类型?使用泛型(通用、公共方法,不涉及业务逻辑)、使用具体类型(涉及业务相关使用的具体实现类)。
  4. 使用对象抽象能力。

1. 什么是低耦合,高内聚

低耦合(Low Coupling)和高内聚(High Cohesion)是软件设计中的两个重要原则,它们有助于提高代码的可维护性、可复用性和扩展性。

1.1. 低耦合(Low Coupling)

耦合指的是模块或组件之间的依赖程度。低耦合意味着不同模块之间的依赖性较小,修改一个模块时不会影响或最小影响其他模块。

低耦合的特点:

  • 接口清晰:模块之间通过接口进行交互,而不是直接依赖具体实现。
  • 减少依赖:一个模块的变化不会导致多个模块需要修改。
  • 提高可扩展性:可以独立替换或修改某个模块,而不会影响整体系统。

如何实现低耦合?

  • 使用接口和抽象类,而不是直接依赖具体类。
  • 依赖倒置原则(DIP):依赖于抽象(接口),而不是具体实现。
  • 单一职责原则(SRP):每个模块只负责一个明确的功能,减少不必要的依赖。
  • 避免全局变量和静态方法,降低模块之间的隐藏依赖。

1.2. 高内聚(High Cohesion)

内聚指的是模块内部各个功能之间的关联程度。高内聚意味着一个模块内的功能紧密相关,模块内部的代码共同完成一个明确的任务,而不是负责多个不相关的功能。

高内聚的特点:

  • 单一职责:一个模块专注于完成一项任务,而不是承担多个不同的职责。
  • 增强可读性和可维护性:代码容易理解和修改。
  • 减少代码重复:相似功能集中在同一个模块内,而不是散落在不同模块中。

如何实现高内聚?

  • 遵循单一职责原则(SRP),一个模块只负责一件事。
  • 模块内部方法紧密相关,不包含与主要功能无关的代码。
  • 减少对外暴露的接口,尽量在模块内部解决问题,避免对外部造成不必要的依赖。

1.3. 低耦合 vs. 高内聚示例

二者相辅相成:

  • 高内聚使得模块内部功能紧密相关,保证模块内部的一致性。
  • 低耦合减少模块之间的依赖,使得模块可以独立修改和维护。

1.3.1. 示例反例(高耦合、低内聚)

public class OrderService {public void processOrder() {// 处理订单System.out.println("处理订单");// 发送通知sendEmail();sendSMS();// 记录日志logOrder();}private void sendEmail() {System.out.println("发送邮件通知");}private void sendSMS() {System.out.println("发送短信通知");}private void logOrder() {System.out.println("记录订单日志");}
}
  • 订单处理(核心业务逻辑)和通知(邮件、短信)耦合在一起,修改通知方式需要改 OrderService
  • 订单逻辑、日志记录、通知都混在 OrderService 里,导致内聚度低。

1.3.2. 优化(低耦合、高内聚)

public class OrderService {@Autowiredprivate final NotificationService notificationService;public void processOrder() {System.out.println("处理订单");notificationService.sendNotification();}
}public class NotificationService {public void sendNotification() {System.out.println("发送邮件通知");System.out.println("发送短信通知");}
}
  • 低耦合OrderService 依赖 NotificationService 接口,而不是直接调用通知方法。
  • 高内聚:订单逻辑在 OrderService,通知相关的逻辑在 NotificationService,各自只关注自己的职责。

1.4. 低耦合,高内聚总结

原则

低耦合

高内聚

定义

模块之间的依赖性低

模块内部功能紧密相关

作用

提高系统的灵活性,易于扩展和维护

使模块更易于理解、修改和复用

实现方式

依赖抽象、接口隔离、减少直接依赖

遵循单一职责原则,把相关功能放在一起

典型示例

使用接口、依赖注入(DI)、事件驱动

业务逻辑和工具类分开,方法职责清晰

在实际开发中,低耦合和高内聚是软件设计的重要目标,合理设计可以提高系统的稳定性和可维护性。

2. 什么是单一职责原则(SRP)

定义:一个类(或者模块、方法)应该只有一个引起它变化的原因,即只负责一个职责

这个原则的核心思想是高内聚、低耦合,避免一个类承担过多的职责,从而提高代码的可读性、可维护性和可复用性。

2.1. 如果一个类承担多个职责,就会导致:

  • 代码难以维护:一个职责的修改可能影响另一个不相关的职责。
  • 代码耦合度高:不同职责之间存在隐式依赖,修改一部分可能导致整个类的修改。
  • 测试困难:一个类承担多个职责,测试时可能需要处理不必要的复杂性。

通过遵循 SRP,我们可以:

提高代码可读性:一个类的功能清晰,易于理解。
降低修改成本:只需修改受影响的部分,而不会影响其他功能。
提高复用性:模块职责清晰,可以在不同场景下复用。

2.2. 如何判断一个类是否违反 SRP?

  • 是否有多个原因导致它需要修改?
  • 类中的方法是否处理多个不同的逻辑?
  • 类的功能是否可以拆分成多个独立的部分?
  • 是否可以将不同的功能分配给不同的类?

如果一个类满足以上几个条件,就可能违反了 SRP,需要拆分。

2.3. 代码示例

public class OrderService {public void processOrder() {System.out.println("处理订单");}public void sendEmailNotification() {System.out.println("发送邮件通知");}public void saveOrderToDatabase() {System.out.println("订单数据存入数据库");}
}

问题分析:

  • OrderService 既负责订单处理,又负责通知,还负责数据库操作,承担了多个职责。
  • 如果需要修改通知方式(比如从邮件改成短信),就必须修改 OrderService,影响了订单处理的核心逻辑。

循 SRP 的优化:拆分为三个独立的类,每个类只负责一个职责:

// 订单处理类
public class OrderService {@Autowiredprivate NotificationService notificationService;@Autowiredprivate OrderRepository orderRepository;public void processOrder() {System.out.println("处理订单");orderRepository.saveOrder();notificationService.sendNotification();}
}// 订单数据存储类
public class OrderRepository {public void saveOrder() {System.out.println("订单数据存入数据库");}
}// 通知服务类
public class NotificationService {public void sendNotification() {System.out.println("发送邮件通知");}
}

优化后的好处:

  • 职责分离OrderService 只负责订单处理,OrderRepository 负责数据库存储,NotificationService 负责通知。
  • 修改影响范围小:如果要修改通知方式,只需修改 NotificationService,不会影响 OrderService
  • 可测试性更强:每个类都可以单独测试,避免不相关的代码影响测试。

2.4. 什么时候该拆分?

并不是所有的类都必须拆分,如果拆分过度,会导致代码结构过于复杂,影响可读性。

适合拆分的情况:

  • 职责明显不同:比如订单处理、日志记录、支付等功能应该分开。
  • 不同职责会频繁变更:如果两个功能的变更频率不同,应该拆分。例如,订单逻辑可能经常变化,但日志逻辑可能一直稳定。
  • 职责之间的依赖很弱:如果两个功能可以独立开发、测试和维护,应该拆分。

2.5. SRP 在方法层面的应用

不仅仅是类,方法也应该遵循单一职责原则。

违反 SRP 的方法:

public void processOrder() {// 处理订单System.out.println("处理订单");// 记录日志System.out.println("记录订单日志");// 发送通知System.out.println("发送邮件通知");
}

遵循 SRP 的方法拆分:

public void processOrder() {handleOrder();logOrder();sendNotification();
}private void handleOrder() {System.out.println("处理订单");
}private void logOrder() {System.out.println("记录订单日志");
}private void sendNotification() {System.out.println("发送邮件通知");
}

这样,每个方法只负责一项具体任务,代码更清晰、更易维护。

2.6. SRP 与其他设计原则的关系

  • 与开闭原则(OCP):SRP 使类职责单一,减少对原有代码的修改,提高扩展性。
  • 与依赖倒置原则(DIP):通过拆分职责,可以让高层模块依赖抽象,而不是具体实现。
  • 与接口隔离原则(ISP):如果一个接口承担了多个职责,应该拆分成多个独立的接口。

2.7. 单一职责原则总结

原则

单一职责原则(SRP)

定义

一个类或方法应该只有一个引起它变化的原因,即只负责一个职责。

核心思想

高内聚、低耦合,避免一个类承担过多职责,提高代码的可读性、可维护性。

违反的表现

一个类或方法承担多个不同的功能,需要经常修改多个部分。

如何优化

拆分为多个职责单一的类或方法,每个类/方法只负责一件事。

好处

代码更清晰、可读性更高、易扩展、易测试、低耦合。

记住:一个类应该只有一个原因被修改!

3. 什么是开放-封闭原则?

3.1. 开放-封闭原则定义

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

  • 对扩展开放(Open for extension):可以通过增加新功能来扩展现有代码的行为。
  • 对修改封闭(Closed for modification):不应该修改已有代码来实现新需求,避免影响已有功能。

👉 目标:提高代码的可扩展性稳定性,避免因修改老代码导致新 Bug。

3.2. 为什么要遵循 OCP?

减少代码变更:修改老代码容易引入 Bug,遵循 OCP 可以降低维护成本。
提高系统稳定性:不修改现有代码,避免影响已有功能。
增强可扩展性:新需求可以通过新增代码实现,而不是修改老代码。

3.3. 示例:如何应用 OCP?

3.3.1. 不遵循 OCP(错误示范)

假设我们有一个计算不同形状面积的方法:

public class AreaCalculator {public double calculateArea(Object shape) {if (shape instanceof Circle) {Circle c = (Circle) shape;return Math.PI * c.getRadius() * c.getRadius();} else if (shape instanceof Rectangle) {Rectangle r = (Rectangle) shape;return r.getWidth() * r.getHeight();}return 0;}
}

问题:

  • 每次增加新的形状(如 Triangle),都要修改 calculateArea() 方法。
  • 违反 OCP,因为要修改原来的代码,风险高,代码不稳定。

3.3.2. 遵循 OCP(正确示范 - 使用多态)

可以使用 抽象类 + 继承 让系统支持扩展,而不修改原有代码:

// 1. 创建 Shape 抽象类
abstract class Shape {public abstract double calculateArea();
}// 2. 具体形状实现各自的计算逻辑
class Circle extends Shape {private double radius;public Circle(double radius) { this.radius = radius; }public double getRadius() { return radius; }@Overridepublic double calculateArea() {return Math.PI * radius * radius;}
}class Rectangle extends Shape {private double width, height;public Rectangle(double width, double height) { this.width = width; this.height = height; }@Overridepublic double calculateArea() {return width * height;}
}// 3. 计算面积的方法
public class AreaCalculator {public double calculateArea(Shape shape) {return shape.calculateArea();}
}

好处:新增形状(如 Triangle)时,不需要修改 AreaCalculator 代码,只需要新增一个 Triangle 类即可:

class Triangle extends Shape {private double base, height;public Triangle(double base, double height) { this.base = base; this.height = height; }@Overridepublic double calculateArea() {return 0.5 * base * height;}
}

🔹 这样我们扩展了新功能,但没有修改 AreaCalculator,符合 OCP!

3.4. 其他 OCP 实现方式

除了继承 + 多态,还有:

  1. 使用接口
interface Payment {void pay(double amount);
}class WeChatPay implements Payment {public void pay(double amount) {System.out.println("使用微信支付:" + amount + " 元");}
}class AliPay implements Payment {public void pay(double amount) {System.out.println("使用支付宝支付:" + amount + " 元");}
}
  1. 扩展新支付方式(如 ApplePay),无需修改老代码,符合 OCP!
  2. 使用策略模式(Strategy Pattern):适用于有多种行为可扩展的情况(比如不同的折扣策略、支付方式)。

3.5. 什么时候使用 OCP?

  • 系统需求变更频繁(避免频繁修改老代码导致 Bug)。
  • 需要支持多种类型的行为(如不同形状、不同支付方式)。
  • 核心业务逻辑比较稳定,但可能会增加新功能

4. 泛型原理与示例

是的,泛型(Generics) 是 Java 中的一种特性,允许我们编写通用的、类型安全的代码。泛型的主要目的是在编译时提供类型检查,避免强制类型转换带来的问题,同时提高代码的复用性。

4.1. 泛型的基本用法

4.1.1. 泛型类

可以在类定义时指定泛型:

public class Box<T> {private T value;public void setValue(T value) {this.value = value;}public T getValue() {return value;}
}

使用时,可以为 T 指定具体类型:

Box<String> stringBox = new Box<>();
stringBox.setValue("Hello");
System.out.println(stringBox.getValue()); // HelloBox<Integer> intBox = new Box<>();
intBox.setValue(123);
System.out.println(intBox.getValue()); // 123

4.1.2. 泛型方法

除了泛型类,还可以定义泛型方法

public class Util {// 这里泛型表示入参是一个泛型,表示可以传递类型数组(可以是String、Integer、其他类型)public static <T> void printArray(T[] array) {for (T item : array) {System.out.print(item + " ");}System.out.println();}
}

使用泛型方法:

String[] words = {"Hello", "World"};
Integer[] numbers = {1, 2, 3};Util.printArray(words);   // Hello World
Util.printArray(numbers); // 1 2 3

4.1.3. 泛型接口

可以让接口使用泛型:

//泛型接口
public interface Storage<T> {void add(T item);T get(int index);
}

实现接口时指定具体类型:

public class StringStorage implements Storage<String> {private List<String> list = new ArrayList<>();public void add(String item) {list.add(item);}public String get(int index) {return list.get(index);}
}

4.1.4. 泛型通配符 ?

当不确定具体类型时,可以使用 ? 作为通配符:

public static void printList(List<?> list) {for (Object item : list) {System.out.println(item);}
}

List<?> 表示可以接收任何类型的 List

List<String> strList = Arrays.asList("A", "B", "C");
List<Integer> intList = Arrays.asList(1, 2, 3);printList(strList);
printList(intList);

💡 注意:List<?> 不能添加元素,因为 Java 不能确定它的实际类型,只能读取。

4.1.5. 限定类型(extendssuper

4.1.5.1. 上界通配符 <? extends T>

如果只需要读取数据,可以使用 ? extends T,表示接受 T 及其子类:

public static void readList(List<? extends Number> list) {for (Number num : list) {System.out.println(num);}
}

可传入 List<Integer>List<Double>

List<Integer> intList = Arrays.asList(1, 2, 3);
List<Double> doubleList = Arrays.asList(1.1, 2.2, 3.3);readList(intList);
readList(doubleList);

💡 特点

  • 可以读取数据(Number 或其子类)。
  • 不能添加数据(除了 null)。
4.1.5.2. 下界通配符 <? super T>

如果只需要写入数据可以使用 ? super T,表示接受 T 及其父类:

java复制编辑
public static void addNumbers(List<? super Integer> list) {list.add(10);list.add(20);
}

可传入 List<Integer>List<Number>List<Object>

java复制编辑
List<Number> numberList = new ArrayList<>();
addNumbers(numberList);
System.out.println(numberList); // [10, 20]

💡 特点

  • 可以添加 Integer 及其子类数据。
  • 读取时只能当作 Object 处理

4.2. 泛型的限制

  1. 泛型不能用于基本数据类型
List<int> list = new ArrayList<>(); // ❌ 错误

需要使用包装类型

List<Integer> list = new ArrayList<>(); // ✅ 正确
  1. 不能创建泛型数组
T[] array = new T[10]; // ❌ 错误

需要使用 Object[] 代替:

Object[] array = new Object[10]; // ✅ 正确
  1. 不能实例化泛型类型
public class Box<T> {T instance = new T(); // ❌ 错误
}

需要使用构造方法传递

public class Box<T> {private T instance;public Box(Class<T> clazz) throws Exception {this.instance = clazz.getDeclaredConstructor().newInstance();}
}

4.3. 泛型总结

特性

泛型的作用

类型安全

通过编译时检查,避免 ClassCastException

代码复用

相同逻辑可适用于不同的数据类型

可读性提高

代码更清晰,无需强制类型转换

性能优化

避免不必要的类型检查,提高运行效率

泛型是 Java 通用编程的强大工具,可以在类、方法、接口等场景中使用,提升代码的安全性、复用性和可维护性。🚀

5. 在编写接口时,选择泛型还是具体类型?

在编写接口时,选择泛型还是具体类型,主要取决于以下几个因素:

  1. 是否需要增强通用性(支持不同的数据类型)
  2. 是否需要约束返回值或参数类型(限制为某种具体类型)
  3. 接口的使用场景(是否依赖于特定业务逻辑)

5.1. 什么时候使用泛型?

如果接口需要适用于多种类型,且不依赖于具体实现,就应该使用泛型,这样可以提高代码的通用性和复用性

5.1.1. 泛型适用于以下情况:

  • 接口支持多种数据类型
  • 不关心具体的实现类
  • 希望增强代码的灵活性和复用性
  • 返回值或参数的类型由调用者决定

5.1.2. 示例 1:通用存储接口

public interface Repository<T> {void save(T entity);T findById(int id);
}

这样,Repository<T> 可以用于任何数据类型:

class User {}
class Product {}Repository<User> userRepo = new UserRepository();
Repository<Product> productRepo = new ProductRepository();

好处:

  • UserRepositoryProductRepository 可以共用 Repository<T> 逻辑。
  • save(T entity) 保证了存入的对象类型安全。

5.1.3. 示例 2:泛型方法

有时候,方法本身可以使用泛型,而不是整个接口:

public interface Converter {<T> T convert(String input, Class<T> clazz);
}

这样可以支持不同类型的转换:

Converter converter = new StringConverter();
Integer num = converter.convert("123", Integer.class);
Double d = converter.convert("12.34", Double.class);

5.2. 什么时候使用具体的实例类?

如果接口的输入或输出只涉及固定的业务逻辑,且不需要支持多种类型,就应该使用具体类型

5.2.1. 具体类型适用于以下情况:

  • 接口逻辑只适用于特定数据类型
  • 接口方法需要操作具体的字段
  • 返回值必须是固定的类型

5.2.2. 示例 1:固定业务逻辑的接口

public interface UserService {void register(User user);User findById(int id);
}

这里 UserService 只针对 User,不会用于其他类型,因此不需要泛型。

5.2.3. 示例 2:固定返回值

public interface PaymentService {PaymentResult processPayment(PaymentRequest request);
}

这里 processPayment 方法总是返回 PaymentResult,不会返回其他类型,所以不需要泛型。

5.3. 泛型 vs 具体类型对比

对比项

使用泛型(T)

使用具体类型

适用场景

需要支持多种类型

仅适用于特定类型

灵活性

高,可扩展

低,局限于特定类型

代码复用

代码可复用

代码可能重复

安全性

编译时检查类型

仅适用于特定类型

典型示例

List<T>

, Repository<T>

UserService

, PaymentService

5.4. 设计决策总结

使用泛型(通用、公共方法,不涉及业务逻辑

  • 如果接口适用于多个类型,且与具体类型无关(如 Repository<T>
  • 如果返回值或参数类型可以变化(如 Converter
  • 如果方法或接口需要提供通用能力(如 List<T>

使用具体类型(涉及业务相关使用的具体实现类)

  • 如果接口逻辑特定于某个实体(如 UserService
  • 如果方法返回值不需要变化(如 PaymentService
  • 如果接口涉及特定领域业务逻辑(如 OrderProcessor

博文参考

相关文章:

  • 4.8 加密模块
  • 【动手学深度学习】1.1~1.2 机器学习及其关键组件
  • BioID技术:探索蛋白质相互作用的新方法
  • Vue3——Pinia
  • 数据分析预备篇---Pandas的DataFrame的更多操作
  • 【Linux】Linux 多线程
  • 【DeepSeek】为什么需要linux-header
  • 网络刷卡器的分类和网口通讯流程
  • 变频器如何通过Profibus DP主站转Modbus RTU/TCP接入到上位机
  • HTTP 协议的发展历程及技术演进
  • 文档债务拖累交付速度?5大优化策略文档自动化
  • 【深度学习:理论篇】--一文理解Transformer
  • Kotlin 协程 (二)
  • HomeAssistant开源的智能家居docker快速部署实践笔记(CentOS7)
  • 基于ROS2/Gazebo的室内送餐机器人系统开发实战教程
  • 生产消费者模型 读写者模型
  • 监控易一体化运维:采集集群管理,构建稳健运维基石
  • 【SPIN】高级时序规范(SPIN学习系列--6)
  • 什么是物联网 (IoT):2024 年物联网概述
  • Fiddler 指定链接断点
  • 山西资深公益人士孙超因突发急病离世,终年37岁
  • 上海国际电影节将于6月3日公布排片表,6月5日中午开票
  • 男子服用头孢后饮酒应酬致昏迷在家,救援人员破门施救后脱险
  • 大学2025丨专访西湖大学副校长邓力:如何才能培养“不惧未知”的创新者
  • 以色列总理:以哈谈判内容包括“结束战争的框架”
  • 蔡建忠已任昆山市副市长、市公安局局长