设计模式的六大设计原则
任何方向的程序员在设计程序时都应以六大设计原则为参考以构建出更健壮、灵活、可维护的软件系统。无论是移动端开发或是后端开发,架构模式是这些原则在特定场景中的具体体现和应用。主流架构模式(如 MVC、MVP、MVVM等)的设计本质上都是为了遵循六大设计原则,解决开发中的耦合、可维护性、可测试性等问题。因此理解六大设计原则是掌握架构模式的基础,也是程序员的基本功之一,本文结合示例与讲解详述设计模式的六大原则
1. 单一职责原则
单一职责原则是说一个类应该只有一个引起它变化的原因,即一个类只负责一项职责。
优点:
- 提高可维护性 :一个类只负责一项职责,代码的功能会更加清晰。当需求发生变化时,只需要修改与该职责相关的类,不会对其他不相关的功能产生影响,从而降低了维护的复杂度。例如在学生管理系统中,
Student
类专门负责存储学生信息,StudentManager
类专门负责学生信息的管理操作,若要修改学生信息的存储方式,只需关注Student
类即可。 - 增强可扩展性 :每个类的职责明确,当需要添加新功能时,可以很方便地创建新的类来承担相应的职责,而不会破坏原有的代码结构。比如要添加学生成绩管理功能,可创建一个新的
StudentGradeManager
类,而不影响现有的Student
和StudentManager
类。
typescript
// 学生类,只负责存储学生信息
class Student {
private String id;
private String name;
public Student(String id, String name) {
this.id = id;
this.name = name;
}
public String getId() {
return id;
}
public String getName() {
return name;
}
}
// 学生管理类,只负责学生信息的管理
import java.util.ArrayList;
import java.util.List;
class StudentManager {
private List<Student> students = new ArrayList<>();
public void addStudent(Student student) {
students.add(student);
}
public void removeStudent(Student student) {
students.remove(student);
}
public Student findStudentById(String id) {
for (Student student : students) {
if (student.getId().equals(id)) {
return student;
}
}
return null;
}
在上述代码中,Student
类只负责存储学生的基本信息,而 StudentManager
类只负责学生信息的管理操作,如添加、删除和查询。这样,每个类的职责都非常明确,当需求发生变化时,只需要修改相应的类即可。
2. 开闭原则
开闭原则是六大原则中核心的原则要求软件实体(类、模块、函数等)应该对扩展开放,对修改关闭。
优点:
- 提高软件的可扩展性:软件实体(类、模块、函数等)可以通过增加新的代码来扩展功能,而不需要修改已有的代码。这使得软件在面对不断变化的需求时,能够更加灵活地进行功能扩展,适应业务的发展和变化。
- 保持软件的稳定性:避免了对原有代码的修改,从而减少了因修改而可能导致的错误和风险,保持了软件系统的稳定性和可靠性。已经经过测试和验证的代码可以继续稳定运行,不会因为新功能的添加而受到影响。
我们举例来看不遵循开闭原则的问题代码
csharp
// 每次添加新图形都需要修改这个类
class ShapeDrawer {
public void draw(String shapeType) {
if ("circle".equals(shapeType)) {
System.out.println("绘制圆形");
} else if ("rectangle".equals(shapeType)) {
System.out.println("绘制矩形");
}
// 添加新图形需要修改这里
}
}
一旦有新需求就需要修改该类代码,一个遵循开闭原则的代码应像下面这样
该类依赖于抽象接口
typescript
public class ShapeDrawer {
// 依赖于抽象接口
public void drawShape(Shape shape) {
shape.draw();
}
}
抽象接口,只能拓展功能,不能进行修改
csharp
// 抽象接口 - 对修改关闭
public interface Shape {
void draw();
}
抽象的具体实现
typescript
public class Rectangle implements Shape {
@Override
public void draw() {
System.out.println("绘制矩形");
}
}
操作数据时,如果要添加新功能,直接新增一个接口的实现类即可,无需改动任何源代码
typescript
public class Client {
public static void main(String[] args) {
ShapeDrawer drawer = new ShapeDrawer();
// 绘制已有的图形
drawer.drawShape(new Circle());
drawer.drawShape(new Rectangle());
// 添加新图形时,只需新增实现类,无需修改原有代码
drawer.drawShape(new Triangle());
}
}
// 新增三角形,无需修改原有任何代码
class Triangle implements Shape {
@Override
public void draw() {
System.out.println("绘制三角形");
}
}
3. 里氏替换原则
里氏替换原则,即子类可以替换其父类并且不会影响程序的正确性。
里氏替换原则是实现开闭原则(对扩展开放,对修改关闭) 的重要基石之一。该原则要求子类可以增添功能但是不能修改父类原有的功能,构造参数要比父类更宽松或相同,方法的返回值要比父类更具体或相同。
为使代码不违背里氏替换原则,在决定使用继承前,先问自己:"子类是父类的一种吗?" ,"子类能完全替代父类在所有场合下的使用吗?",如果答案是否定的,最好还是避免继承的使用。
优点:
- 保证系统的稳定性 :子类可以替换父类,使得在使用父类的地方可以透明地使用子类对象,不会影响程序的正确性。这保证了系统在扩展和维护过程中的稳定性,避免了因子类替换父类而导致的程序崩溃或错误。例如在学生管理系统中,
NormalStudent
和ExcellentStudent
类都可以替换AbstractStudent
类,而不会影响StudentManager
类的正常运行。 - 促进代码复用:通过继承和多态,父类的代码可以被子类复用,减少了代码的重复编写。子类可以在父类的基础上进行扩展,实现自己的独特功能,提高了代码的复用率和开发效率。
- 便于系统的扩展和维护:遵循里氏替换原则,使得系统的设计更加灵活,当需要添加新的子类时,不会对现有的代码产生影响,便于系统的扩展和维护。
typescript
// 抽象类
abstract class AbstractStudent {
protected String id;
protected String name;
public AbstractStudent(String id, String name) {
this.id = id;
this.name = name;
}
public abstract String getInfo();
}
// 普通学生类
class NormalStudent extends AbstractStudent {
public NormalStudent(String id, String name) {
super(id, name);
}
@Override
public String getInfo() {
return "Normal Student - ID: " + id + ", Name: " + name;
}
}
// 优秀学生类
class ExcellentStudent extends AbstractStudent {
public ExcellentStudent(String id, String name) {
super(id, name);
}
@Override
public String getInfo() {
return "Excellent Student - ID: " + id + ", Name: " + name;
}
}
// 学生管理类使用抽象学生类
class StudentManager {
private List<AbstractStudent> students = new ArrayList<>();
public void addStudent(AbstractStudent student) {
students.add(student);
}
public void displayStudents() {
for (AbstractStudent student : students) {
System.out.println(student.getInfo());
}
}
}
在上述代码中,NormalStudent
和 ExcellentStudent
类都继承自 AbstractStudent
类。StudentManager
类使用 AbstractStudent
类来管理学生信息,这样无论是 NormalStudent
还是 ExcellentStudent
对象都可以替换 AbstractStudent
对象,而不会影响程序的正确性。
4. 依赖倒置原则
依赖倒置原则要求高层模块(负责实现业务逻辑和整体策略 )不应该依赖低层模块(数据库连接、文件操作、网络通信等工具类或组件),二者都应该依赖抽象。
优点:
-
降低模块间的耦合度 :高层模块不依赖低层模块,二者都依赖抽象,减少了模块之间的直接依赖关系。当低层模块发生变化时,不会影响到高层模块,提高了系统的独立性和可维护性。例如在学生管理系统中,
StudentViewer
类依赖StudentInfoProvider
接口,而不是具体的StudentInfoProviderImpl
类,当需要更换学生信息的获取方式时,只需实现新的StudentInfoProvider
接口实现类,而无需修改StudentViewer
类。 -
提高系统的可测试性:依赖抽象使得可以更容易地对模块进行单元测试。可以使用模拟对象来替代具体的实现类,对模块进行独立测试,提高了测试的效率和准确性。
-
增强系统的可扩展性 :通过依赖抽象,系统可以方便地引入新的实现类,扩展系统的功能。例如在学生管理系统中,当需要添加新的学生信息获取方式时,只需实现
StudentInfoProvider
接口,而无需修改现有的代码。下面是不遵循依赖倒置原则的实现
csharp// 低层模块:具体的数据库实现 class MySQLDatabase { public void connect() { System.out.println("连接MySQL数据库"); } } // 高层模块:业务逻辑,直接依赖具体实现 class UserService { // 直接依赖具体的MySQLDatabase,耦合度高 private MySQLDatabase db = new MySQLDatabase(); public void getUser() { db.connect(); System.out.println("查询用户数据"); } }
这样的问题是,如果需要更换数据库(如换成 Oracle),就必须修改 UserService 类,违反了开闭原则,且高层模块与低层模块耦合紧密。
typescript// 抽象接口:包含完整的数据库操作抽象方法 interface Database { void connect(); // 增加查询抽象方法,参数和返回值使用通用类型 ResultSet query(String sql); void close(); } // MySQL实现 class MySQLDatabase implements Database { @Override public void connect() { System.out.println("连接MySQL数据库"); } @Override public ResultSet query(String sql) { // MySQL特有的查询实现 System.out.println("使用MySQL语法执行查询: " + sql); return new MySQLResultSet(); // 返回MySQL特有的结果集 } @Override public void close() { System.out.println("关闭MySQL连接"); } } // Oracle实现 class OracleDatabase implements Database { @Override public void connect() { System.out.println("连接Oracle数据库"); } @Override public ResultSet query(String sql) { // Oracle特有的查询实现 System.out.println("使用Oracle语法执行查询: " + sql); return new OracleResultSet(); // 返回Oracle特有的结果集 } @Override public void close() { System.out.println("关闭Oracle连接"); } } // 通用结果集接口,屏蔽不同数据库的结果集差异 interface ResultSet { String getString(String column); int getInt(String column); } // MySQL结果集实现 class MySQLResultSet implements ResultSet { @Override public String getString(String column) { return "MySQL数据:" + column; } @Override public int getInt(String column) { return 100; // 模拟数据 } } // Oracle结果集实现 class OracleResultSet implements ResultSet { @Override public String getString(String column) { return "Oracle数据:" + column; } @Override public int getInt(String column) { return 200; // 模拟数据 } } // 高层模块:只依赖抽象,不关心具体数据库实现 class UserService { private Database db; public UserService(Database db) { this.db = db; } public void getUser(int userId) { db.connect(); // 高层模块只定义查询需求,不涉及具体数据库语法 ResultSet result = db.query("SELECT * FROM users WHERE id = " + userId); System.out.println("用户名: " + result.getString("name")); System.out.println("年龄: " + result.getInt("age")); db.close(); } } // 客户端 public class Client { public static void main(String[] args) { // 使用MySQL UserService mysqlUserService = new UserService(new MySQLDatabase()); mysqlUserService.getUser(1); // 切换到Oracle,高层模块代码无需修改 UserService oracleUserService = new UserService(new OracleDatabase()); oracleUserService.getUser(1); } }
5. 接口隔离原则
接口隔离原则强调客户端不应该依赖它不需要的接口。
优点:
-
提高系统的内聚性:每个接口只包含客户端需要的方法,使得接口的职责更加单一,提高了系统的内聚性。内聚性高的系统更容易维护和扩展。
-
增强代码的灵活性:当需求发生变化时,只需要修改相关的接口和实现类,不会影响到其他不相关的接口和类,提高了代码的灵活性和可维护性。
csharp// 一个臃肿的接口,包含了所有设备的操作方法 interface Device { void print(); // 打印 void scan(); // 扫描 void fax(); // 传真 void call(); // 通话 } // 打印机只需要print方法,但被迫实现其他不需要的方法 class Printer implements Device { @Override public void print() { System.out.println("打印文档"); } // 被迫实现不需要的方法 @Override public void scan() { throw new UnsupportedOperationException("打印机不支持扫描"); } @Override public void fax() { throw new UnsupportedOperationException("打印机不支持传真"); } @Override public void call() { throw new UnsupportedOperationException("打印机不支持通话"); } } // 电话机只需要call方法,但也被迫实现其他方法 class Phone implements Device { @Override public void call() { System.out.println("拨打电话"); } // 被迫实现不需要的方法 @Override public void print() { throw new UnsupportedOperationException("电话机不支持打印"); } @Override public void scan() { throw new UnsupportedOperationException("电话机不支持扫描"); } @Override public void fax() { throw new UnsupportedOperationException("电话机不支持传真"); } }
上述代码除了实现类被迫实现不需要的方法的问题,还有很重要的一点是,当接口发生变化时,所有实现类都需要修改,违反开闭原则。来看下面的代码
csharp// 拆分后的小型专用接口 interface Printable { void print(); // 打印功能 } interface Scannable { void scan(); // 扫描功能 } interface Faxable { void fax(); // 传真功能 } interface Callable { void call(); // 通话功能 } // 普通打印机:只实现需要的接口 class SimplePrinter implements Printable { @Override public void print() { System.out.println("打印文档"); } } // 多功能打印机:实现多个需要的接口 class MultiFunctionPrinter implements Printable, Scannable, Faxable { @Override public void print() { System.out.println("打印文档"); } @Override public void scan() { System.out.println("扫描文档"); } @Override public void fax() { System.out.println("传真文档"); } } // 智能手机:实现需要的接口 class SmartPhone implements Callable, Scannable { @Override public void call() { System.out.println("拨打电话"); } @Override public void scan() { System.out.println("扫描二维码"); } } // 客户端代码:只依赖需要的接口 public class Client { // 打印服务:只依赖Printable接口 public static void usePrinter(Printable printer) { printer.print(); } // 通话服务:只依赖Callable接口 public static void usePhone(Callable phone) { phone.call(); } public static void main(String[] args) { usePrinter(new SimplePrinter()); usePrinter(new MultiFunctionPrinter()); usePhone(new SmartPhone()); } }
6. 迪米特法则
迪米特法则也称为最少知识原则,它要求一个对象应该对其他对象有最少的了解。
其核心思想是:一个对象应该对其他对象保持最少的了解。也就是说,一个类应该尽量少地了解其他类的内部实现,只与直接的朋友进行交互。这里的 "直接的朋友" 指:
- 当前对象本身
- 作为参数传入的对象
- 当前对象创建的对象
- 当前对象的成员变量
优点:
- 降低系统的耦合度 :一个对象对其他对象有最少的了解,减少了对象之间的直接交互,降低了系统的耦合度。当一个对象发生变化时,不会对其他对象产生过多的影响,提高了系统的独立性和可维护性。例如在学生管理系统中,
StudentViewer
类只和StudentManager
类进行交互,而不直接和Student
类进行交互。 - 提高系统的可维护性:由于对象之间的交互减少,系统的结构更加清晰,当需要修改某个对象时,只需要关注该对象及其直接关联的对象,降低了维护的难度。
- 增强系统的安全性 :减少对象之间的直接交互,避免了不必要的信息传递,提高了系统的安全性。例如在学生管理系统中,
StudentViewer
类只能通过StudentManager
类获取学生信息,不能直接访问Student
类的内部信息,保护了学生信息的安全。
下面看这段代码
csharp
// 订单详情类
class OrderDetail {
private String productName;
private int quantity;
// 构造方法和getter
public OrderDetail(String productName, int quantity) {
this.productName = productName;
this.quantity = quantity;
}
public String getProductName() { return productName; }
public int getQuantity() { return quantity; }
}
// 订单类
class Order {
private List<OrderDetail> details;
public Order(List<OrderDetail> details) {
this.details = details;
}
public List<OrderDetail> getDetails() { return details; }
}
// 库存管理类
class InventoryManager {
// 直接操作了非直接朋友OrderDetail,违反迪米特原则
public void updateInventory(Order order) {
// 这里直接访问了Order的内部对象OrderDetail
for (OrderDetail detail : order.getDetails()) {
System.out.println("更新库存: " + detail.getProductName() +
", 数量: " + detail.getQuantity());
}
}
}
这种设计的问题是:InventoryManager
不仅知道 Order
的存在,还深入了解了 Order
内部的 OrderDetail
结构,导致它们之间耦合度高。当 Order
的内部实现(如存储订单详情的方式)发生变化时,InventoryManager
也需要修改。再看下面的代码
arduino
// 订单详情类
class OrderDetail {
private String productName;
private int quantity;
public OrderDetail(String productName, int quantity) {
this.productName = productName;
this.quantity = quantity;
}
// 只暴露必要的方法给直接朋友
public String getProductName() { return productName; }
public int getQuantity() { return quantity; }
}
// 订单类:负责自己的内部操作
class Order {
private List<OrderDetail> details;
public Order(List<OrderDetail> details) {
this.details = details;
}
// 提供高层方法,避免外部直接访问内部结构
public void updateInventory(InventoryManager inventory) {
for (OrderDetail detail : details) {
// 由Order自己处理与内部对象的交互
inventory.reduceStock(detail.getProductName(), detail.getQuantity());
}
}
}
// 库存管理类:只与直接朋友交互
class InventoryManager {
// 只接收必要的参数,不关心Order的内部结构
public void reduceStock(String productName, int quantity) {
System.out.println("更新库存: " + productName + ", 数量: " + quantity);
}
}
// 客户端代码
public class Client {
public static void main(String[] args) {
// 创建订单详情
List<OrderDetail> details = Arrays.asList(
new OrderDetail("手机", 2),
new OrderDetail("电脑", 1)
);
// 创建订单
Order order = new Order(details);
// 创建库存管理器
InventoryManager inventory = new InventoryManager();
// 订单自己负责更新库存,客户端不需要知道细节
order.updateInventory(inventory);
}
}
总结
最后强调一下,在这六大原则中,开闭原则 通常被认为是面向对象设计中最核心、最基础的原则之一,其他原则很多时候可以看作是实现它的具体手段或在不同层面的体现。同时设计原则不能只看不练,在学习了设计原则后可以回顾自己之前的项目思考是否符合六大原则、如何进行修改改善,并在以后的项目中尽量地践行六大原则。