系统设计——项目设计经验总结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 {
    
    @Autowired
    private 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 {
    @Autowired
    private NotificationService notificationService;
    
    @Autowired
    private 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; }
    @Override
    public 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; }
    @Override
    public 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; }
    @Override
    public 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()); // Hello

Box<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

博文参考

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

庄小焱

我将坚持分享更多知识

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值