1.里氏替换原则(LSP)
里氏替换原则(Liskov Substitution Principle, LSP)是面向对象设计中的一项重要原则,该原则的核心思想是:
如果对每一个父类对象(基类),都存在一个子类对象(派生类)能够替代它,并且程序的行为没有变化,那么这个子类就是对父类的一个正确的替代。
里氏替换原则的要点
子类应当可以替换父类:在任何使用父类的地方,都可以用子类代替而不影响程序的正确性。
保证行为一致:子类在替换父类时,必须保持父类的行为和期望。也就是说,子类不能改变父类的预期行为。
不改变父类的契约:子类不应引入比父类更严格的前置条件或更宽松的后置条件。即,子类的输入输出和异常处理应当与父类一致。
假设我们在设计一个几何图形系统,其中有一个 Rectangle
(矩形)类,并想扩展一个 Square
(正方形)类。根据里氏替换原则,Square
类应当能够替代 Rectangle
而不改变系统的行为。
// 矩形类
class Rectangle {
protected int width;
protected int height;
public void setWidth(int width) {
this.width = width;
}
public void setHeight(int height) {
this.height = height;
}
public int getArea() {
return width * height;
}
}
违反里氏替换原则的情况
如果我们让 Square
继承自 Rectangle
,并尝试通过重写 setWidth
和 setHeight
来强制保持正方形的性质,那么我们会违反里氏替换原则:
class Square extends Rectangle {
@Override
public void setWidth(int width) {
this.width = width;
this.height = width; // 保持正方形的性质
}
@Override
public void setHeight(int height) {
this.width = height;
this.height = height; // 保持正方形的性质
}
}
问题
在上述设计中,Square
的 setWidth
和 setHeight
方法修改了矩形的行为,因为在 Rectangle
中,宽和高可以分别设置,但在 Square
中,一旦设置宽或高,另一个也会跟着改变。这种改变违反了 Rectangle
类的基本假设(宽和高是独立的),所以 Square
无法替代 Rectangle
而不引发问题。
在 Square
替换 Rectangle
的情况下,square.getArea()
的值不符合预期,因为我们期望它遵循矩形的行为,但实际它的宽高被强制相等。
遵循里氏替换原则的改进
要遵循里氏替换原则,我们可以将 Rectangle
和 Square
设计为平行的类,而不是让 Square
继承 Rectangle
,比如可以使用一个 Shape
接口或抽象类来表示它们的公共特性:
abstract class Shape {
public abstract int getArea();
}
class Rectangle extends Shape {
protected int width;
protected int height;
public void setWidth(int width) {
this.width = width;
}
public void setHeight(int height) {
this.height = height;
}
@Override
public int getArea() {
return width * height;
}
}
class Square extends Shape {
private int side;
public void setSide(int side) {
this.side = side;
}
@Override
public int getArea() {
return side * side;
}
}
public class TestLSP {
public static void main(String[] args) {
Rectangle rectangle = new Rectangle();
rectangle.setWidth(5);
rectangle.setHeight(10);
System.out.println("Rectangle area: " + rectangle.getArea()); // 50
Square square = new Square();
square.setSide(5);
System.out.println("Square area: " + square.getArea()); // 25
}
}
2. 开放封闭原则(OCP)
开放封闭原则(Open-Closed Principle, OCP)是面向对象设计中的重要原则之一,它的核心理念是:软件实体(如类、模块和函数)应对扩展开放,对修改封闭。这意味着在不修改已有代码的前提下,通过扩展来实现新功能,从而提高系统的灵活性和可维护性。
假设我们正在开发一个订单系统,需要根据不同的支付方式来处理支付逻辑。最初系统只支持信用卡支付,因此我们实现了一个 OrderProcessor
类来处理信用卡支付。
没有遵循开放封闭原则的设计
初始设计时,可能会在 OrderProcessor
类中直接处理不同支付方式的逻辑。代码可能如下:
class OrderProcessor {
public void processOrder(String paymentType) {
if (paymentType.equals("creditCard")) {
System.out.println("Processing credit card payment...");
// 处理信用卡支付逻辑
} else if (paymentType.equals("paypal")) {
System.out.println("Processing PayPal payment...");
// 处理 PayPal 支付逻辑
}
// 未来可能还会加入更多的支付方式
}
}
问题
这个设计的问题在于,每当我们添加新的支付方式时(如增加 bitcoin
支付),都需要修改 OrderProcessor 的代码,这违反了开放封闭原则。每次修改都可能引入新问题,尤其当逻辑越来越复杂时,维护起来会很困难。
遵循开放封闭原则的设计
为了遵循开放封闭原则,我们可以使用策略模式来重构代码,将每种支付方式的处理逻辑独立到不同的类中。这样,OrderProcessor
不需要知道具体的支付方式,只需要调用支付接口即可。
1.创建支付方式接口
interface Payment {
void pay();
}
2.为每种支付方式创建具体实现类
class CreditCardPayment implements Payment {
@Override
public void pay() {
System.out.println("Processing credit card payment...");
}
}
class PayPalPayment implements Payment {
@Override
public void pay() {
System.out.println("Processing PayPal payment...");
}
}
// 可以轻松添加新的支付方式,比如 Bitcoin 支付
class BitcoinPayment implements Payment {
@Override
public void pay() {
System.out.println("Processing Bitcoin payment...");
}
}
3.修改 OrderProcessor
使其依赖于接口
class OrderProcessor {
private Payment payment;
public OrderProcessor(Payment payment) {
this.payment = payment;
}
public void processOrder() {
payment.pay();
}
}
4.使用示例
现在,当我们要处理不同的支付方式时,只需创建相应的支付对象并传递给 OrderProcessor
,而无需修改 OrderProcessor
本身的代码:
public class TestOCP {
public static void main(String[] args) {
// 使用信用卡支付
Payment creditCardPayment = new CreditCardPayment();
OrderProcessor orderProcessor = new OrderProcessor(creditCardPayment);
orderProcessor.processOrder();
// 使用 PayPal 支付
Payment payPalPayment = new PayPalPayment();
orderProcessor = new OrderProcessor(payPalPayment);
orderProcessor.processOrder();
// 新增的 Bitcoin 支付,不需要修改 OrderProcessor
Payment bitcoinPayment = new BitcoinPayment();
orderProcessor = new OrderProcessor(bitcoinPayment);
orderProcessor.processOrder();
}
}
3. 单一职责原则(SRP)
单一职责原则(Single Responsibility Principle, SRP)强调每个类应该只有一个引起它变化的原因,即一个类只负责一个职责。这意味着一个类只应该负责完成一个功能或逻辑,而不应该承担多个功能,以避免类变得过于复杂,提高代码的可读性和可维护性。
假设我们在开发一个用户管理系统,其中有一个 User
类,负责用户的基本信息管理,同时包含用户的持久化逻辑(如保存到数据库)和发送通知功能。
没有遵循单一职责原则的设计
class User {
private String name;
private String email;
public User(String name, String email) {
this.name = name;
this.email = email;
}
public String getName() {
return name;
}
public String getEmail() {
return email;
}
// 负责保存用户信息
public void save() {
System.out.println("Saving user to the database...");
// 数据库保存逻辑
}
// 负责发送通知
public void sendNotification() {
System.out.println("Sending notification to " + email);
// 通知逻辑
}
}
问题
在这个设计中,User
类承担了三个职责:
- 用户数据的管理(存储用户的
name
和email
)。 - 持久化(将用户数据保存到数据库)。
- 通知功能(给用户发送通知)。
这种设计违反了单一职责原则,带来了一些问题:
- 如果数据库的保存逻辑发生变化,我们需要修改
User
类。 - 如果通知方式(例如从邮件通知改为短信通知)发生变化,也要修改
User
类。 - 类的职责过多,难以理解和维护。
遵循单一职责原则的设计
为了解决上述问题,我们可以将每个职责拆分到单独的类中,使每个类只负责一个职责。
1.User
类只负责管理用户数据
class User {
private String name;
private String email;
public User(String name, String email) {
this.name = name;
this.email = email;
}
public String getName() {
return name;
}
public String getEmail() {
return email;
}
}
2.创建 UserRepository
类,专门负责用户的持久化
class UserRepository {
public void save(User user) {
System.out.println("Saving user " + user.getName() + " to the database...");
// 数据库保存逻辑
}
}
3.创建 NotificationService
类,专门负责发送通知
class NotificationService {
public void sendNotification(User user) {
System.out.println("Sending notification to " + user.getEmail());
// 通知逻辑
}
}
4.使用示例
在需要使用这些功能时,我们可以将各个类组合使用:
public class TestSRP {
public static void main(String[] args) {
User user = new User("Alice", "[email protected]");
// 保存用户
UserRepository userRepository = new UserRepository();
userRepository.save(user);
// 发送通知
NotificationService notificationService = new NotificationService();
notificationService.sendNotification(user);
}
}
4. 接口隔离原则(ISP)
接口隔离原则(Interface Segregation Principle, ISP)指出:客户端不应该被强迫依赖于它不使用的方法。也就是说,接口应该小而专,不要让一个接口承担过多的职责,而是将其分解为多个功能单一的接口,让实现类可以选择自己需要实现的接口。
这条原则的目的是避免“胖接口”(拥有过多方法的接口),使得类依赖于最小的接口集合,从而提升系统的灵活性和可维护性。
假设我们在设计一个多功能设备(如多合一的办公设备),这个设备可以提供打印、扫描、传真等功能。我们先来看看没有遵循接口隔离原则的设计。
没有遵循接口隔离原则的设计
interface MultiFunctionDevice {
void print(Document doc);
void scan(Document doc);
void fax(Document doc);
}
假设我们有一些设备实现了 MultiFunctionDevice
接口,但其中有的设备不支持所有功能,比如:
- 普通打印机只需要
print
功能。 - 扫描仪只需要
scan
功能。
在这种设计下,即便一个设备只需要部分功能(例如只有打印功能),它也必须实现 MultiFunctionDevice
接口的所有方法。代码可能如下:
class Printer implements MultiFunctionDevice {
@Override
public void print(Document doc) {
System.out.println("Printing document...");
}
@Override
public void scan(Document doc) {
throw new UnsupportedOperationException("Scan not supported");
}
@Override
public void fax(Document doc) {
throw new UnsupportedOperationException("Fax not supported");
}
}
问题
这种设计违反了接口隔离原则,带来了以下问题:
- 强迫实现无用的方法:
Printer
类不得不实现scan
和fax
方法,即使它不需要这些功能。 - 增加了代码复杂性:需要额外处理不支持的方法(比如抛出异常),使代码可读性下降。
遵循接口隔离原则的设计
为了解决这个问题,我们可以将 MultiFunctionDevice
接口拆分为多个小接口,每个接口只负责一种功能。这样,类只需实现自己所需的接口,而不必依赖无关的功能。
1.定义多个小接口
interface Printer {
void print(Document doc);
}
interface Scanner {
void scan(Document doc);
}
interface Fax {
void fax(Document doc);
}
2.让具体的设备类实现它们需要的接口
// 只实现打印功能
class SimplePrinter implements Printer {
@Override
public void print(Document doc) {
System.out.println("Printing document...");
}
}
// 只实现扫描功能
class SimpleScanner implements Scanner {
@Override
public void scan(Document doc) {
System.out.println("Scanning document...");
}
}
// 实现打印和扫描功能
class MultiFunctionPrinter implements Printer, Scanner {
@Override
public void print(Document doc) {
System.out.println("Printing document...");
}
@Override
public void scan(Document doc) {
System.out.println("Scanning document...");
}
}
3.使用示例
不同的设备可以按需组合不同的接口,而不会强迫实现无关的功能:
public class TestISP {
public static void main(String[] args) {
Document doc = new Document();
// 使用简单打印机
Printer printer = new SimplePrinter();
printer.print(doc);
// 使用多功能打印机
MultiFunctionPrinter multiFunctionPrinter = new MultiFunctionPrinter();
multiFunctionPrinter.print(doc);
multiFunctionPrinter.scan(doc);
}
}
5. 依赖倒置原则(DIP)
依赖倒置原则(Dependency Inversion Principle, DIP)是面向对象设计中的一条重要原则,核心思想是:高层模块不应该依赖于低层模块,两者都应该依赖于抽象;抽象不应该依赖于具体实现,具体实现应该依赖于抽象。简单来说,就是让代码依赖于接口或抽象类,而不是具体的实现类。
目的
依赖倒置原则的目的是降低模块之间的耦合,使得系统更灵活、更易扩展和维护。它使得高层模块(通常是业务逻辑)与低层模块(例如数据存储或第三方服务)的实现细节解耦,因而能够轻松替换或修改底层实现,而不需要更改高层模块的代码。
假设我们有一个简单的消息通知系统,其中 NotificationService
需要依赖发送消息的方式,比如电子邮件或短信。
没有遵循依赖倒置原则的设计
class EmailService {
public void sendEmail(String message) {
System.out.println("Sending email: " + message);
}
}
class NotificationService {
private EmailService emailService;
public NotificationService() {
this.emailService = new EmailService(); // 直接依赖具体的EmailService
}
public void sendNotification(String message) {
emailService.sendEmail(message);
}
}
在这个设计中:
NotificationService
直接依赖于EmailService
,这是一种强耦合关系。- 如果我们需要将
EmailService
替换为SMSService
,就必须修改NotificationService
的代码。 - 这违反了依赖倒置原则,因为高层模块
NotificationService
依赖于低层模块EmailService
。
遵循依赖倒置原则的设计
为了满足依赖倒置原则,我们可以引入一个抽象层,定义一个通用的接口 MessageService
,让 NotificationService
依赖于该接口,而不是具体的实现类。然后,我们可以实现不同的 MessageService
(例如 EmailService
和 SMSService
),并通过依赖注入的方式提供具体实现。
1.定义 MessageService
接口
interface MessageService {
void sendMessage(String message);
}
2.实现 EmailService
和 SMSService
,它们依赖于 MessageService
接口
class EmailService implements MessageService {
@Override
public void sendMessage(String message) {
System.out.println("Sending email: " + message);
}
}
class SMSService implements MessageService {
@Override
public void sendMessage(String message) {
System.out.println("Sending SMS: " + message);
}
}
3.修改 NotificationService
,依赖于 MessageService
接口
class NotificationService {
private MessageService messageService;
// 通过构造函数注入依赖
public NotificationService(MessageService messageService) {
this.messageService = messageService;
}
public void sendNotification(String message) {
messageService.sendMessage(message);
}
}
4.使用示例
在使用时,可以根据需要将具体实现传递给 NotificationService
,实现灵活配置:
public class TestDIP {
public static void main(String[] args) {
// 使用EmailService
MessageService emailService = new EmailService();
NotificationService notificationService1 = new NotificationService(emailService);
notificationService1.sendNotification("Hello via Email!");
// 使用SMSService
MessageService smsService = new SMSService();
NotificationService notificationService2 = new NotificationService(smsService);
notificationService2.sendNotification("Hello via SMS!");
}
}
6. 合成复用原则(CRP)
合成复用原则(Composite Reuse Principle, CRP)是面向对象设计的一个重要原则。它的核心思想是:优先使用对象组合(Object Composition)而不是继承(Inheritance)来达到代码复用。这一原则也叫做“组合优于继承”(Composition over Inheritance)。
目的
CRP 的目的是通过组合多个对象来扩展类的功能,使得系统更加灵活,减少了继承层次带来的耦合问题。组合比继承更加灵活,因为继承会造成父类和子类之间的强依赖,而组合允许类独立扩展和变化,不需要直接依赖父类的实现。
假设我们要设计一个图形系统,其中有不同的图形(如圆形、矩形等),每种图形可能有不同的绘制方式。
没有遵循合成复用原则的设计
在没有遵循 CRP 的设计中,我们可能会直接使用继承来复用代码,比如将 Shape
作为基类,然后创建 Circle
和 Rectangle
类。
// 基础的 Shape 类
class Shape {
public void draw() {
System.out.println("Drawing Shape");
}
}
// Circle 继承 Shape
class Circle extends Shape {
@Override
public void draw() {
System.out.println("Drawing Circle");
}
}
// Rectangle 继承 Shape
class Rectangle extends Shape {
@Override
public void draw() {
System.out.println("Drawing Rectangle");
}
}
在这个设计中,Circle
和 Rectangle
都继承了 Shape
,并且重写了 draw
方法。但是如果我们想要添加更多的行为,比如不同的填充样式或边框样式,继承会导致类层次结构变得复杂,不便于扩展。
遵循合成复用原则的设计
为了满足合成复用原则,我们可以将“图形类型”和“绘制方式”解耦,通过组合的方式来实现不同的绘制方式。具体做法是创建一个 Drawing
接口,每种绘制方式都实现该接口,然后将它组合进 Shape
类中。
1.定义 Drawing
接口
interface Drawing {
void draw();
}
2.实现不同的绘制方式
class CircleDrawing implements Drawing {
@Override
public void draw() {
System.out.println("Drawing Circle");
}
}
class RectangleDrawing implements Drawing {
@Override
public void draw() {
System.out.println("Drawing Rectangle");
}
}
3.通过组合的方式在 Shape
类中使用 Drawing
class Shape {
private Drawing drawing;
// 通过构造函数注入绘制方式
public Shape(Drawing drawing) {
this.drawing = drawing;
}
public void draw() {
drawing.draw();
}
}
4.使用示例
在使用时,可以灵活组合不同的 Drawing
实现,而不需要通过继承扩展:
public class TestCRP {
public static void main(String[] args) {
// 创建一个圆形绘制方式的 Shape
Shape circle = new Shape(new CircleDrawing());
circle.draw();
// 创建一个矩形绘制方式的 Shape
Shape rectangle = new Shape(new RectangleDrawing());
rectangle.draw();
}
}
7. 迪米特法则(LoD)
迪米特法则(Law of Demeter, LoD),又称最少知识原则(Principle of Least Knowledge),是面向对象设计中的一条重要原则。它的核心思想是:一个对象应当尽可能少地了解其他对象的细节。换句话说,模块之间应当尽量减少相互依赖,这样可以降低系统的耦合性,提高代码的可维护性和灵活性。
规则
迪米特法则主要遵循以下规则:
- 只与直接朋友通信:一个对象只应该直接与它需要交互的对象通信,而不应依赖于那些不直接相关的对象。
- 不调用陌生对象的属性或方法:避免“链式”调用,例如
a.getB().getC().doSomething()
,因为这会让对象依赖于多个不直接相关的对象的实现。
假设我们有一个公司管理系统,Company
包含多个部门(Department
),每个部门中包含员工(Employee
),公司希望获取各部门的员工姓名。
不遵循迪米特法则的设计
在不遵循迪米特法则的情况下,Company
直接访问 Department
的内部细节,并通过链式调用获取 Employee
的姓名信息:
class Employee {
private String name;
public Employee(String name) {
this.name = name;
}
public String getName() {
return name;
}
}
class Department {
private List<Employee> employees;
public Department(List<Employee> employees) {
this.employees = employees;
}
public List<Employee> getEmployees() {
return employees;
}
}
class Company {
private List<Department> departments;
public Company(List<Department> departments) {
this.departments = departments;
}
// 获取所有部门员工姓名
public List<String> getAllEmployeeNames() {
List<String> names = new ArrayList<>();
for (Department dept : departments) {
for (Employee emp : dept.getEmployees()) { // 直接访问Department内部的员工列表
names.add(emp.getName());
}
}
return names;
}
}
在这里,Company
依赖于 Department
的内部结构,并且直接访问 Employee
对象。这种设计违反了迪米特法则,因为 Company
不仅知道 Department
的细节,还依赖 Employee
,增加了类之间的耦合性。
遵循迪米特法则的设计
为了遵循迪米特法则,我们可以对 Department
类进行封装,让 Company
只调用 Department
提供的公开方法,而不直接访问 Employee
。这样 Company
就不需要知道 Employee
的存在:
class Employee {
private String name;
public Employee(String name) {
this.name = name;
}
public String getName() {
return name;
}
}
class Department {
private List<Employee> employees;
public Department(List<Employee> employees) {
this.employees = employees;
}
// 提供获取员工姓名的方法
public List<String> getEmployeeNames() {
List<String> names = new ArrayList<>();
for (Employee emp : employees) {
names.add(emp.getName());
}
return names;
}
}
class Company {
private List<Department> departments;
public Company(List<Department> departments) {
this.departments = departments;
}
// 获取所有部门员工姓名
public List<String> getAllEmployeeNames() {
List<String> names = new ArrayList<>();
for (Department dept : departments) {
names.addAll(dept.getEmployeeNames()); // 只调用Department的公开方法
}
return names;
}
}
在这个设计中:
Company
类只调用了Department
类的getEmployeeNames
方法,而不需要知道Department
的内部结构。Company
与Employee
之间没有直接依赖关系,降低了类之间的耦合性。