设计模式原则分析
1. 单一职责原则(Single Responsibility Principle)
1.1 定义
单一职责原则是指一个类应该只有一个责任(即一个功能),一个类应该只负责一项任务。换句话说,如果一个类承担了多个职责,这些职责之间会有不同的变化原因,改变其中某个职责时,可能会影响到其他职责,导致系统更加复杂和难以维护。
1.2 不符合SRP原则举例
假设有一个类同时处理数据存储和报告生成的功能。当存储方式变化时(如从数据库变为文件存储),我们需要修改这个类的存储相关代码;而当报告生成的需求变化时(如格式从 PDF 变为 Excel),我们也需要修改这个类的报告生成代码。此时,这个类有两个职责,它们各自有不同的变化原因,导致这个类的维护变得复杂。
// 违反 SRP 原则:一个类同时负责数据存储和报告生成
public class UserManager {
public void saveUser(User user) {
// 保存用户到数据库
System.out.println("Saving user to database");
}
public void generateUserReport(User user) {
// 生成用户报告
System.out.println("Generating report for user");
}
}
1.3 遵守SRP原则举例
// 遵守 SRP 原则:将职责分离到不同的类中
public class UserRepository {
public void saveUser(User user) {
// 保存用户到数据库
System.out.println("Saving user to database");
}
}
public class UserReportGenerator {
public void generateUserReport(User user) {
// 生成用户报告
System.out.println("Generating report for user");
}
}
在这个例子中,UserRepository
只负责数据存储,而 UserReportGenerator
只负责生成报告。这样,每个类只负责一个功能,系统的职责划分更加清晰,修改某一功能时只需要修改对应的类,而不影响其他部分。
2. 开闭原则(Open/Closed Principle)
2.1 定义
开放-封闭原则指的是软件实体(类、模块、函数等)应该对扩展开放,对修改封闭。这意味着当我们需要为系统添加新功能时,应该通过扩展已有的代码,而不是直接修改已有的代码。
2.2 不符合OCP原则举例
假设我们有一个计算薪水的程序,原本只考虑基本工资和奖金两个因素。如果以后需要加入更多的薪酬计算因素(如补贴、扣款等),如果我们每次都修改原有的计算逻辑,就会违反开放-封闭原则。
// 违反 OCP 原则:修改原有类以支持新的薪资计算方式
public class SalaryCalculator {
public double calculateSalary(Employee employee) {
// 计算基本工资和奖金
return employee.getBaseSalary() + employee.getBonus();
}
}
假设现在我们需要新增一个“补贴”因素,每次添加新因素时都需要修改 SalaryCalculator
类的 calculateSalary
方法。随着需求变化,SalaryCalculator
类会变得越来越复杂,而且容易出错。这就是违反了开放-封闭原则。
2.3 符合OCP原则举例
为了遵守开放-封闭原则,我们可以引入抽象类或接口,将不同的薪资计算策略封装成不同的类,而不是修改 SalaryCalculator
类。
// 遵守 OCP 原则:将薪资计算逻辑通过策略模式封装,便于扩展
public interface SalaryCalculatorStrategy {
double calculateSalary(Employee employee);
}
public class BaseSalaryAndBonusCalculator implements SalaryCalculatorStrategy {
@Override
public double calculateSalary(Employee employee) {
// 计算基本工资和奖金
return employee.getBaseSalary() + employee.getBonus();
}
}
public class BaseSalaryBonusAndSubsidyCalculator implements SalaryCalculatorStrategy {
@Override
public double calculateSalary(Employee employee) {
// 计算基本工资、奖金和补贴
return employee.getBaseSalary() + employee.getBonus() + employee.getSubsidy();
}
}
public class SalaryCalculator {
private SalaryCalculatorStrategy strategy;
public SalaryCalculator(SalaryCalculatorStrategy strategy) {
this.strategy = strategy;
}
public double calculateSalary(Employee employee) {
return strategy.calculateSalary(employee);
}
}
我们将计算薪水的逻辑提取到不同的策略类中,并定义了一个共同的 SalaryCalculatorStrategy
接口。每个薪资计算策略都实现了这个接口,分别处理不同的计算逻辑(如仅包含基本工资和奖金,或包含补贴等)。通过策略模式,我们能够灵活地扩展薪资计算方式,而无需修改现有的代码。如果以后还需要新增其他薪资计算方式,只需要新建一个实现 SalaryCalculatorStrategy
接口的类,而不需要修改 SalaryCalculator
类。
3. 里氏替换原则 (LSP)
3.1 定义
里氏替换原则是指子类对象应该能够替换父类对象,且程序的行为不受影响。换句话说,子类必须完全继承父类的行为,并且在替换父类实例时不会破坏父类的行为和功能。
3.2 符合LSP原则举例
假设我们有一个形状类 Shape
,其中有一个方法 area()
来计算图形的面积。然后我们有两个子类:Rectangle
和 Square
。根据几何学,矩形的面积是 宽 * 高
,而正方形的面积是 边长 * 边长
。但是,假如我们在正方形类中重写了 setWidth
和 setHeight
方法,使其不再符合父类 Rectangle
的行为,那么就违反了里氏替换原则。
// 违反 LSP 原则:正方形类破坏了矩形类的预期行为
class Rectangle {
private double width;
private double height;
public void setWidth(double width) {
this.width = width;
}
public void setHeight(double height) {
this.height = height;
}
public double area() {
return width * height;
}
}
class Square extends Rectangle {
@Override
public void setWidth(double width) {
super.setWidth(width);
super.setHeight(width); // 强制宽度和高度相等,违反矩形的行为
}
@Override
public void setHeight(double height) {
super.setHeight(height);
super.setWidth(height); // 强制宽度和高度相等,违反矩形的行为
}
}
正方形是矩形的一种特殊情况,但正方形并不等同于矩形。矩形的宽度和高度是独立的,而正方形的宽度和高度始终相等。如果我们让正方形继承矩形类,并且让 setWidth
和 setHeight
强制设置相等的值,这将破坏矩形类的预期行为。当我们将一个 Square
对象作为 Rectangle
类型的对象传递时,程序的行为会不符合预期,setWidth
和 setHeight
会强制设置相等的值,而这在矩形的情况下并不应该这样。
3.3 符合LSP原则举例
为了遵守里氏替换原则,我们应该避免让 Square
直接继承 Rectangle
,因为它违反了矩形的行为。相反,我们可以通过创建一个新的抽象类 Shape
来统一接口,Rectangle
和 Square
各自继承并实现具体的行为。
// 遵守 LSP 原则:通过抽象类和接口来正确设计
abstract class Shape {
public abstract double area();
}
class Rectangle extends Shape {
private double width;
private double height;
public Rectangle(double width, double height) {
this.width = width;
this.height = height;
}
public double area() {
return width * height;
}
}
class Square extends Shape {
private double side;
public Square(double side) {
this.side = side;
}
public double area() {
return side * side;
}
}
Shape 类作为一个抽象类,定义了所有图形的公共接口 area()
方法。
Rectangle
和 Square
都继承 Shape
类,但它们分别根据自己的特点实现 area()
方法,而不再强行使用不合适的继承关系。
正确的继承:Rectangle
和 Square
的行为各自独立,满足了不同的需求。正方形和矩形之间的差异得到了合理的隔离。
4. 接口隔离原则(ISP)
4.1 定义
接口隔离原则指的是不应该强迫一个类去依赖它不需要的接口。换句话说,接口应该尽可能地小,避免设计过于庞大和臃肿的接口。客户端不应该被迫实现它不使用的方法。
简单理解:
- 如果一个类依赖了一个接口,并且这个接口中有一些方法是该类不需要的,那么就违反了接口隔离原则。
- 应该将一个大接口拆分成多个小接口,每个接口承担一个独立的职责,这样类只需要实现它需要的接口,而不是实现整个庞大的接口。
4.2 不符合ISP原则举例
假设我们有一个 Worker
接口,包含了多种不同的功能方法,像是 work
、eat
、sleep
等。然而,某些具体的实现类(比如 Robot
)并不需要所有这些方法,比如它不需要 eat
和 sleep
方法。这个接口设计就违反了接口隔离原则。
// 违反 ISP 原则:接口包含了不需要的方法
public interface Worker {
void work();
void eat();
void sleep();
}
public class Human implements Worker {
@Override
public void work() {
System.out.println("Human working...");
}
@Override
public void eat() {
System.out.println("Human eating...");
}
@Override
public void sleep() {
System.out.println("Human sleeping...");
}
}
public class Robot implements Worker {
@Override
public void work() {
System.out.println("Robot working...");
}
@Override
public void eat() {
// Robot doesn't need to implement this method
}
@Override
public void sleep() {
// Robot doesn't need to implement this method
}
}
在这个例子中,Worker
接口定义了 eat
和 sleep
方法,但对于 Robot
类而言,这些方法并不适用,因此需要实现这些无用的方法,违反了接口隔离原则。Robot
类并不需要 eat
和 sleep
方法,但是由于接口设计不合理,它不得不提供这些空实现,从而导致了不必要的代码。
4.3 符合ISP原则举例
为了解决这个问题,我们可以将接口拆分成多个小接口,每个接口仅包含客户端需要的方法。Human
类实现包含 work
、eat
、sleep
的接口,而 Robot
类只实现 work
方法相关的接口。
// 遵守 ISP 原则:将接口拆分成多个小接口
public interface Workable {
void work();
}
public interface Eatable {
void eat();
}
public interface Sleepable {
void sleep();
}
public class Human implements Workable, Eatable, Sleepable {
@Override
public void work() {
System.out.println("Human working...");
}
@Override
public void eat() {
System.out.println("Human eating...");
}
@Override
public void sleep() {
System.out.println("Human sleeping...");
}
}
public class Robot implements Workable {
@Override
public void work() {
System.out.println("Robot working...");
}
}
5. 依赖倒置原则(Dependency Inversion Principle)
5.1 定义
依赖倒置原则是指高层模块不应该依赖低层模块,二者都应该依赖于抽象;抽象不应该依赖细节,细节应该依赖于抽象。
简单来说,依赖倒置原则强调的是通过依赖于抽象(如接口或抽象类),而不是依赖于具体实现,从而实现高层模块和低层模块的解耦。
5.2 不符合DIP原则举例
假设我们有一个 UserService
类,它依赖于 UserRepository
类来进行数据存储。如果 UserService
直接依赖于 UserRepository
类,这样就违反了依赖倒置原则,因为高层模块(UserService
)直接依赖于低层模块(UserRepository
)。
// 违反 DIP 原则:高层模块直接依赖低层模块
class UserRepository {
public void save(User user) {
// 保存用户到数据库
System.out.println("User saved to database.");
}
}
class UserService {
private UserRepository userRepository;
public UserService() {
userRepository = new UserRepository(); // 直接依赖具体实现
}
public void registerUser(User user) {
// 注册用户
userRepository.save(user);
}
}
UserService
直接依赖于UserRepository
的实现。这样,如果我们需要更改UserRepository
的实现,比如从数据库改为从文件中保存数据,UserService
就需要修改其代码,违反了高层模块不应该依赖低层模块的原则。- 如果以后需要实现不同类型的存储方式,比如
FileRepository
或MongoDBRepository
,每次都需要修改UserService
,导致代码不容易维护和扩展。
5.3 符合DIP原则举例
为了遵守依赖倒置原则,我们应该引入一个抽象接口,UserRepository
应该是一个接口,而 UserService
只依赖这个接口,而不依赖具体实现类。具体的实现类可以通过依赖注入的方式传入。
// 遵守 DIP 原则:通过接口隔离高层模块和低层模块
interface UserRepository {
void save(User user);
}
class UserRepositoryImpl implements UserRepository {
@Override
public void save(User user) {
// 保存用户到数据库
System.out.println("User saved to database.");
}
}
class FileRepository implements UserRepository {
@Override
public void save(User user) {
// 保存用户到文件
System.out.println("User saved to file.");
}
}
class UserService {
private UserRepository userRepository;
// 通过构造器注入依赖
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
public void registerUser(User user) {
// 注册用户
userRepository.save(user);
}
}
6. 合成复用原则(Composite Reuse Principle)
6.1 定义
合成复用原则是指优先使用对象的合成(组合)而非继承来实现代码复用。这意味着,通过将已有的类作为成员对象嵌入到新类中,来增强新类的功能,而不是通过继承扩展原有类的功能。
组合优于继承:当需要复用某些功能时,尽量通过组合来实现,而不是通过继承。这是因为继承在很多情况下会导致类之间的耦合性增加,而组合则更灵活且易于扩展。
6.2 不符合CRP原则举例
假设我们有一个 Bird
类和一个 Penguin
类,Penguin
是 Bird
的子类。我们在 Bird
类中定义了一些行为(如飞行),但是企鹅是不能飞的。如果我们用继承来实现复用,这样就会导致不合理的设计。
// 违反合成复用原则:企鹅类继承了鸟类,导致不适用的行为
class Bird {
public void fly() {
System.out.println("Bird is flying.");
}
}
class Penguin extends Bird {
// Penguin 是鸟类的一种,但企鹅不能飞
@Override
public void fly() {
// 企鹅不能飞,但继承了飞行行为,导致设计不合理
System.out.println("Penguins can't fly.");
}
}
6.3 符合CRP原则举例
为了遵守合成复用原则,我们可以将飞行行为抽象出来,并将其作为一个接口或者类的成员。这样,企鹅类就不再需要继承鸟类,而是根据实际需要来组合不同的行为。
// 遵守合成复用原则:通过组合来复用功能
interface Flyable {
void fly();
}
class Bird implements Flyable {
@Override
public void fly() {
System.out.println("Bird is flying.");
}
}
class Penguin {
// 企鹅并不具备飞行能力,不需要继承 Bird 类
// 如果需要飞行,可以通过组合一个 Flyable 对象
private Flyable flyable;
public Penguin(Flyable flyable) {
this.flyable = flyable;
}
public void tryToFly() {
// 企鹅不能飞,但如果需要,可以选择其他飞行方式
if (flyable != null) {
flyable.fly();
} else {
System.out.println("Penguins can't fly.");
}
}
}
class Test {
public static void main(String[] args) {
// 创建一个可以飞的鸟类
Bird bird = new Bird();
Penguin penguin = new Penguin(null); // 企鹅没有飞行能力
penguin.tryToFly(); // 输出:Penguins can't fly.
// 如果需要,可以通过组合飞行的能力
Penguin flyingPenguin = new Penguin(bird);
flyingPenguin.tryToFly(); // 输出:Bird is flying.
}
}
7. 最少知识原则(迪米特法则)
7.1 定义
最少知识原则,又称迪米特法则,其核心思想是一个对象应该对其他对象有尽可能少的了解。也就是说,一个对象应该只与自己直接的朋友(即直接相关的对象)进行交互,而不应该与“陌生人”或“陌生人的朋友”进行交互。
7.2 不符合迪米特法则举例
假设我们有一个 Customer
类,它有一个 Order
类属性,而 Order
类又包含 Product
类。如果 Customer
直接访问 Product
类的属性,这样就违反了最少知识原则。
class Product {
public String name;
public double price;
}
class Order {
private Product product;
public Order(Product product) {
this.product = product;
}
public Product getProduct() {
return product;
}
}
class Customer {
private Order order;
public Customer(Order order) {
this.order = order;
}
// 违反最少知识原则:直接访问了 Product 对象的属性
public void printProductName() {
System.out.println(order.getProduct().name); // 访问了 order -> product -> name
}
}
7.3 符合迪米特法则举例
class Product {
private String name;
private double price;
public Product(String name, double price) {
this.name = name;
this.price = price;
}
public String getName() {
return name;
}
}
class Order {
private Product product;
public Order(Product product) {
this.product = product;
}
public String getProductName() {
return product.getName();
}
}
class Customer {
private Order order;
public Customer(Order order) {
this.order = order;
}
// 遵守最少知识原则:只通过 Order 类与 Product 交互
public void printProductName() {
System.out.println(order.getProductName());
}
}