摘要
- 在系统设计的时候,注意域的区分,功能区分、类的区分、方法区分范围和定义。
- 在系统设计的时候的,需要思考类、方法在什么情况下会涉及到修改,遵循记住:一个类应该只有一个原因被修改! 当不满足,可能就考虑拆分的问题。
- 学会T泛型使用,因为泛型是通用类型?使用泛型(通用、公共方法,不涉及业务逻辑)、使用具体类型(涉及业务相关使用的具体实现类)。
- 使用对象抽象能力。
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 实现方式
除了继承 + 多态,还有:
- 使用接口
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 + " 元");}
}
- 扩展新支付方式(如
ApplePay
),无需修改老代码,符合 OCP! - 使用策略模式(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. 限定类型(extends
和 super
)
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. 泛型的限制
- 泛型不能用于基本数据类型:
List<int> list = new ArrayList<>(); // ❌ 错误
需要使用包装类型:
List<Integer> list = new ArrayList<>(); // ✅ 正确
- 不能创建泛型数组:
T[] array = new T[10]; // ❌ 错误
需要使用 Object[]
代替:
Object[] array = new Object[10]; // ✅ 正确
- 不能实例化泛型类型:
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. 泛型总结
特性 | 泛型的作用 |
类型安全 | 通过编译时检查,避免 |
代码复用 | 相同逻辑可适用于不同的数据类型 |
可读性提高 | 代码更清晰,无需强制类型转换 |
性能优化 | 避免不必要的类型检查,提高运行效率 |
泛型是 Java 通用编程的强大工具,可以在类、方法、接口等场景中使用,提升代码的安全性、复用性和可维护性。🚀
5. 在编写接口时,选择泛型还是具体类型?
在编写接口时,选择泛型还是具体类型,主要取决于以下几个因素:
- 是否需要增强通用性(支持不同的数据类型)
- 是否需要约束返回值或参数类型(限制为某种具体类型)
- 接口的使用场景(是否依赖于特定业务逻辑)
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();
好处:
UserRepository
和ProductRepository
可以共用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) | 使用具体类型 |
适用场景 | 需要支持多种类型 | 仅适用于特定类型 |
灵活性 | 高,可扩展 | 低,局限于特定类型 |
代码复用 | 代码可复用 | 代码可能重复 |
安全性 | 编译时检查类型 | 仅适用于特定类型 |
典型示例 |
, |
, |
5.4. 设计决策总结
✅ 使用泛型(通用、公共方法,不涉及业务逻辑)
- 如果接口适用于多个类型,且与具体类型无关(如
Repository<T>
) - 如果返回值或参数类型可以变化(如
Converter
) - 如果方法或接口需要提供通用能力(如
List<T>
)
✅ 使用具体类型(涉及业务相关使用的具体实现类)
- 如果接口逻辑特定于某个实体(如
UserService
) - 如果方法返回值不需要变化(如
PaymentService
) - 如果接口涉及特定领域业务逻辑(如
OrderProcessor
)