架构师学习
第4篇
写在前面
说实话,刚开始接触SOLID原则的时候,我觉得这些都是些”虚头巴脑”的理论。直到临时接手一个离职同事的代码——一个类上千行,改一行bug到处冒,而且代码完全没有秩序,我才真正体会到这些原则的价值。
SOLID原则是我CLAUDE.md其中一个规范,是由Robert C. Martin(Uncle Bob)提出的五个面向对象设计原则。简单来说,它们能帮我写出更好维护、更好扩展、更好测试的代码。
因为在软件开发的早期,我们往往更关注功能的实现,而忽视了代码的设计质量。随着项目规模的增长,糟糕的代码设计会变得越来越难以维护,每一次修改都可能引发意想不到的问题。SOLID原则正是为了解决这些问题而诞生的。
为什么要学习SOLID原则?
SOLID原则能够帮助我们:
- 降低代码耦合度:让模块之间的依赖关系更加清晰
- 提高代码可维护性:单一职责让修改更加聚焦
- 增强代码可扩展性:开闭原则让功能扩展更安全
- 改善代码可测试性:依赖倒置让单元测试更容易编写
学习资料
单一职责原则(Single Responsibility Principle,SRP)
原则定义
一个类应该只有一个引起它变化的原因,换句话说,一个类应该只有一个职责。
直观理解
想象一个瑞士军刀,它有太多功能:刀、剪刀、开瓶器等。虽然功能强大,但当你只需要用刀的时候,带着整个军刀就显得笨重了。同样,一个类承担太多职责时,任何职责的变化都可能影响其他职责,导致系统变得脆弱。
反例代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
|
public class UserService {
public void register(String username, String password) { System.out.println("用户注册:" + username); }
public void login(String username, String password) { System.out.println("用户登录:" + username); }
public void logToFile(String message) { try { FileWriter writer = new FileWriter("app.log", true); writer.write(message + "\n"); writer.close(); } catch (IOException e) { e.printStackTrace(); } }
public void sendEmail(String to, String subject, String content) { System.out.println("发送邮件给:" + to); System.out.println("主题:" + subject); System.out.println("内容:" + content); } }
|
问题分析:
1. UserService类承担了用户管理、日志记录、邮件发送三个职责
2. 修改日志格式需要修改UserService
3. 更换邮件服务提供商需要修改UserService
4. 这违反了单一职责原则,导致类的变化原因过多
正例代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51
|
public class UserService { private Logger logger; private EmailService emailService;
public UserService(Logger logger, EmailService emailService) { this.logger = logger; this.emailService = emailService; }
public void register(String username, String password) { logger.log("用户注册:" + username); emailService.sendEmail(username, "注册成功", "欢迎注册"); }
public void login(String username, String password) { logger.log("用户登录:" + username); } }
public class Logger { public void log(String message) { try { FileWriter writer = new FileWriter("app.log", true); writer.write(message + "\n"); writer.close(); } catch (IOException e) { e.printStackTrace(); } } }
public class EmailService { public void sendEmail(String to, String subject, String content) { System.out.println("发送邮件给:" + to); System.out.println("主题:" + subject); System.out.println("内容:" + content); } }
|
重构收益:
1. 每个类都有明确的单一职责
2. 修改日志实现只需修改Logger类
3. 更换邮件服务只需修改EmailService类
4. UserService类保持稳定,不受其他职责变化影响
开闭原则(Open-Closed Principle,OCP)
原则定义
软件实体(类、模块、函数等)应该对扩展开放,对修改封闭。
这意味着当我们需要添加新功能时,应该通过扩展现有代码来实现,而不是修改已有的代码。
直观理解
想象一个插线板,它有多个插座。当你需要使用新电器时,你只需要插上新的插头,而不需要拆开插线板重新布线。开闭原则就是让我们的代码像插线板一样,能够轻松”插入”新功能而不需要修改核心代码。
反例代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
|
public class DiscountCalculator {
public double calculateDiscount(String discountType, double price) { switch (discountType) { case "NONE": return price; case "TEN_PERCENT": return price * 0.9; case "TWENTY_PERCENT": return price * 0.8; default: return price; } } }
|
违反开闭原则的后果:
1. 每次添加新的折扣类型都需要修改DiscountCalculator类
2. 修改已有代码可能引入新的bug
3. 需要重新测试整个折扣计算功能
4. 违反了对修改封闭的原则
正例代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86
|
public interface DiscountStrategy { double calculate(double price); }
public class NoDiscount implements DiscountStrategy { @Override public double calculate(double price) { return price; } }
public class TenPercentDiscount implements DiscountStrategy { @Override public double calculate(double price) { return price * 0.9; } }
public class TwentyPercentDiscount implements DiscountStrategy { @Override public double calculate(double price) { return price * 0.8; } }
public class DiscountCalculator { private DiscountStrategy discountStrategy;
public DiscountCalculator(DiscountStrategy discountStrategy) { this.discountStrategy = discountStrategy; }
public double calculate(double price) { return discountStrategy.calculate(price); }
public void setDiscountStrategy(DiscountStrategy discountStrategy) { this.discountStrategy = discountStrategy; } }
public class Main { public static void main(String[] args) { DiscountCalculator calculator = new DiscountCalculator(new NoDiscount()); System.out.println("无折扣价格:" + calculator.calculate(100));
calculator.setDiscountStrategy(new TenPercentDiscount()); System.out.println("10%折扣后价格:" + calculator.calculate(100));
calculator.setDiscountStrategy(new TwentyPercentDiscount()); System.out.println("20%折扣后价格:" + calculator.calculate(100));
calculator.setDiscountStrategy(new ThirtyPercentDiscount()); System.out.println("30%折扣后价格:" + calculator.calculate(100)); } }
public class ThirtyPercentDiscount implements DiscountStrategy { @Override public double calculate(double price) { return price * 0.7; } }
|
重构收益:
1. 添加新的折扣类型只需创建新的策略类
2. DiscountCalculator类无需修改,符合开闭原则
3. 通过策略模式实现了对扩展开放、对修改封闭
4. 每个折扣策略都是独立的,易于测试和维护
里氏替换原则(Liskov Substitution Principle,LSP)
原则定义
所有引用基类的地方必须能够透明地使用其子类的对象,子类可以替换父类出现在父类能够出现的任何地方,而不破坏程序的正确性。
直观理解
如果你有一个正方形和一个长方形,从几何上讲,正方形是特殊的长方形。但在编程中,如果让正方形继承长方形类,可能会出现问题。因为正方形的长宽必须相等,这违反了长方形”长宽可以不同”的基本约定。
反例代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80
|
public class Rectangle { protected double width; protected double height;
public Rectangle(double width, double height) { this.width = width; this.height = height; }
public void setWidth(double width) { this.width = width; }
public void setHeight(double height) { this.height = height; }
public double getWidth() { return width; }
public double getHeight() { return height; }
public double getArea() { return width * height; } }
public class Square extends Rectangle {
public Square(double size) { super(size, size); }
@Override public void setWidth(double width) { this.width = width; this.height = width; }
@Override public void setHeight(double height) { this.width = height; this.height = height; } }
public class LSPTest { public static void main(String[] args) { Rectangle rectangle = new Rectangle(5, 10); System.out.println("长方形面积:" + rectangle.getArea());
rectangle.setWidth(10); rectangle.setHeight(5); System.out.println("长方形面积:" + rectangle.getArea());
Rectangle square = new Square(5); System.out.println("正方形面积:" + square.getArea());
square.setWidth(10); square.setHeight(5); System.out.println("正方形面积:" + square.getArea());
} }
|
违反里氏替换原则的问题:
1. Square继承Rectangle后,破坏了Rectangle的行为约定
2. 当使用Square替换Rectangle时,程序结果不一致
3. 客户端代码无法正确预测继承后的行为
4. 这种继承关系在设计上就是错误的
正例代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81
|
public interface Shape { double getArea(); }
public class Rectangle implements Shape { private double width; private double height;
public Rectangle(double width, double height) { this.width = width; this.height = height; }
public void setWidth(double width) { this.width = width; }
public void setHeight(double height) { this.height = height; }
@Override public double getArea() { return width * height; } }
public class Square implements Shape { private double size;
public Square(double size) { this.size = size; }
public void setSize(double size) { this.size = size; }
@Override public double getArea() { return size * size; } }
public class ShapeUtils { public static void printArea(Shape shape) { System.out.println("形状面积:" + shape.getArea()); } }
public class LSPTestCorrect { public static void main(String[] args) { Rectangle rectangle = new Rectangle(5, 10); Square square = new Square(5);
ShapeUtils.printArea(rectangle); ShapeUtils.printArea(square);
List<Shape> shapes = Arrays.asList(rectangle, square); for (Shape shape : shapes) { ShapeUtils.printArea(shape); } } }
|
重构收益:
1. Rectangle和Square都实现Shape接口,各自保持独立性
2. 任何使用Shape的地方都可以透明地使用Rectangle或Square
3. 避免了不合理的继承关系
4. 符合里氏替换原则,保证程序行为的正确性
接口隔离原则(Interface Segregation Principle,ISP)
原则定义
客户端不应该被迫依赖于它不使用的接口,接口应该被拆分为更小和更具体的部分,这样客户端只需要知道它们所需的部分。
直观理解
想象一个万能遥控器,上面有电视、空调、音响等各种设备的按钮。当你只需要控制电视时,面对这么多无关的按钮会很困扰。接口隔离原则就是让每个接口都专注于特定的功能,避免”胖接口”。
反例代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
|
public interface Machine { void print(String document); void fax(String document); void scan(String document); void photocopy(String document); }
public class OldPrinter implements Machine {
@Override public void print(String document) { System.out.println("打印:" + document); }
@Override public void fax(String document) { throw new UnsupportedOperationException("不支持传真功能"); }
@Override public void scan(String document) { throw new UnsupportedOperationException("不支持扫描功能"); }
@Override public void photocopy(String document) { throw new UnsupportedOperationException("不支持复印功能"); } }
|
违反接口隔离原则的问题:
1. Machine接口过于臃肿,包含了太多操作
2. OldPrinter只需要打印功能,但被迫实现其他方法
3. 客户端可能调用不支持的方法,导致运行时异常
4. 接口设计不够灵活,无法适应不同的设备组合
正例代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112
|
public interface Printer { void print(String document); }
public interface Fax { void fax(String document); }
public interface Scanner { void scan(String document); }
public interface Photocopier { void photocopy(String document); }
public class SimplePrinter implements Printer { @Override public void print(String document) { System.out.println("打印:" + document); } }
public class MultiFunctionPrinter implements Printer, Fax, Scanner, Photocopier {
@Override public void print(String document) { System.out.println("打印:" + document); }
@Override public void fax(String document) { System.out.println("传真:" + document); }
@Override public void scan(String document) { System.out.println("扫描:" + document); }
@Override public void photocopy(String document) { System.out.println("复印:" + document); } }
public class PrintScanCombo implements Printer, Scanner {
@Override public void print(String document) { System.out.println("打印:" + document); }
@Override public void scan(String document) { System.out.println("扫描:" + document); } }
public class ISPTest { public static void main(String[] args) { Printer simplePrinter = new SimplePrinter(); simplePrinter.print("简单文档");
MultiFunctionPrinter mfp = new MultiFunctionPrinter(); usePrinter(mfp); useFax(mfp); useScanner(mfp);
PrintScanCombo combo = new PrintScanCombo(); usePrinter(combo); useScanner(combo); }
public static void usePrinter(Printer printer) { printer.print("使用打印机"); }
public static void useFax(Fax fax) { fax.fax("使用传真机"); }
public static void useScanner(Scanner scanner) { scanner.scan("使用扫描仪"); } }
|
重构收益:
1. 接口被拆分为多个小而专注的接口
2. 客户端只依赖它需要的接口,避免不必要的方法
3. 实现类可以选择性地实现需要的接口
4. 符合接口隔离原则,提高了系统的灵活性和可维护性
依赖倒置原则(Dependency Inversion Principle,DIP)
原则定义
高层模块不应该依赖低层模块,两者都应该依赖抽象。抽象不应该依赖细节,细节应该依赖抽象。
直观理解
想象一个公司组织架构,总经理(高层)不应该直接管理员工(低层)的每一个具体工作。相反,总经理应该制定标准和接口,员工按照这些标准工作。这样,更换员工不会影响公司的整体运作。
反例代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
|
public class MySQLConnection { public void connect() { System.out.println("连接到MySQL数据库"); }
public void executeQuery(String sql) { System.out.println("在MySQL中执行查询:" + sql); } }
public class UserService { private MySQLConnection dbConnection;
public UserService() { this.dbConnection = new MySQLConnection(); }
public void getUser(String userId) { dbConnection.connect(); dbConnection.executeQuery("SELECT * FROM users WHERE id = " + userId); } }
|
违反依赖倒置原则的问题:
1. UserService直接依赖MySQLConnection具体类
2. 如果要更换数据库(如PostgreSQL),需要修改UserService
3. 高层模块被低层模块的具体实现所束缚
4. 难以进行单元测试(无法mock数据库连接)
正例代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98
|
public interface DatabaseConnection { void connect(); void executeQuery(String sql); }
public class MySQLConnection implements DatabaseConnection { @Override public void connect() { System.out.println("连接到MySQL数据库"); }
@Override public void executeQuery(String sql) { System.out.println("在MySQL中执行查询:" + sql); } }
public class PostgreSQLConnection implements DatabaseConnection { @Override public void connect() { System.out.println("连接到PostgreSQL数据库"); }
@Override public void executeQuery(String sql) { System.out.println("在PostgreSQL中执行查询:" + sql); } }
public class MongoDBConnection implements DatabaseConnection { @Override public void connect() { System.out.println("连接到MongoDB数据库"); }
@Override public void executeQuery(String sql) { System.out.println("在MongoDB中执行查询:" + sql); } }
public class UserService { private DatabaseConnection dbConnection;
public UserService(DatabaseConnection dbConnection) { this.dbConnection = dbConnection; }
public void getUser(String userId) { dbConnection.connect(); dbConnection.executeQuery("SELECT * FROM users WHERE id = " + userId); }
public void setDbConnection(DatabaseConnection dbConnection) { this.dbConnection = dbConnection; } }
public class DIPTest { public static void main(String[] args) { DatabaseConnection mysqlConnection = new MySQLConnection(); DatabaseConnection pgConnection = new PostgreSQLConnection(); DatabaseConnection mongoConnection = new MongoDBConnection();
UserService userService1 = new UserService(mysqlConnection); userService1.getUser("1");
UserService userService2 = new UserService(pgConnection); userService2.getUser("2");
UserService userService3 = new UserService(mongoConnection); userService3.getUser("3");
userService1.setDbConnection(mongoConnection); userService1.getUser("4"); } }
|
重构收益:
1. UserService依赖DatabaseConnection抽象接口,而非具体实现
2. 可以轻松切换不同的数据库实现,无需修改UserService
3. 高层模块和低层模块都依赖抽象,降低了耦合度
4. 便于单元测试,可以轻松mock DatabaseConnection
面试中被问到SOLID原则怎么办?
有次在Reddit上看到一个吐槽帖,说某公司招聘人员让候选人”凭空解释5个SOLID原则”,结果博主直接拒绝回答。其实这问题挺常见的,尤其是面试初级到中级岗位的时候。
答题思路
别死记硬背。先说总体理解,然后逐个简述:
| 原则 |
一句话解释 |
| S |
一个类只做一件事 |
| O |
加功能别改老代码,用扩展加 |
| L |
子类能完美替代父类 |
| I |
接口别太臃肿,该拆就拆 |
| D |
依赖接口,别依赖具体实现 |
然后补一句:”这些原则我实际项目里也在用,比如最近做的XX项目,用策略模式实现XX功能,就体现了开闭原则…”
这样回答既展示了理论知识,又证明了实践经验。
记忆技巧
- SRP:Single Responsibility → 单一职责
- OCP:Open/Closed → 对扩展开放,对修改封闭
- LSP:Liskov Substitution → 里氏替换
- ISP:Interface Segregation → 接口隔离
- DIP:Dependency Inversion → 依赖倒置(依赖抽象)
总结
SOLID原则是面向对象设计的基石,它们相互关联、相互补充。掌握这些原则能够帮助我们设计出更加优雅、灵活、可维护的软件系统。
SOLID原则的关系图
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| ┌─────────────────────────────────────────────────┐ │ SOLID 原则 │ ├─────────────────────────────────────────────────┤ │ │ │ SRP ──────── 单一职责 ───────── 一个类只有一个职责 │ │ ↓ │ │ OCP ──────── 开闭原则 ────── 对扩展开放,对修改封闭 │ │ ↓ │ │ LSP ──────── 里氏替换 ──── 子类可以透明替换父类 │ │ ↓ │ │ ISP ──────── 接口隔离 ──── 接口应该小而专注 │ │ ↓ │ │ DIP ──────── 依赖倒置 ── 依赖抽象而非具体实现 │ │ │ └─────────────────────────────────────────────────┘
|
实践建议
如何在项目中应用SOLID原则:
- 逐步重构:不要试图一次性重构所有代码,逐步应用这些原则
- 识别坏味道:学会识别违反SOLID原则的代码坏味道
- 设计优先:在编写新代码时,优先考虑SOLID原则
- 团队共识:确保团队成员都理解并认同这些原则
- 适度应用:不要过度设计,根据实际情况灵活应用
重要提示:
SOLID原则是指导原则,不是绝对的规则。在实际项目中,需要根据具体情况灵活应用。过度遵循这些原则可能导致过度设计,增加系统复杂度。