在面向对象设计中,遵循一些基本的设计原则可以帮助我们编写更加灵活、易于维护和扩展的代码。这些原则是设计模式的核心思想,帮助开发者避免常见的设计陷阱。以下是七种常见的设计原则,每个原则都有其独特的价值。
定义:一个类应该只有一个原因去改变,即一个类应该仅有一个职责。
解释:单一职责原则要求每个类应该有一个明确的职责,且该职责应当被类中的所有方法和属性所体现。如果一个类有多个职责,那么它就会有多个原因去变化,从而导致代码变得复杂且不易维护。如果我们需要修改一个类的某个职责时,可能会影响到类中的其他职责,这就违背了该原则。
例子: 假设我们设计一个处理用户信息的类。如果一个类同时负责处理用户的业务逻辑和用户的持久化存储,那么它就违反了单一职责原则。我们应该将业务逻辑和存储操作分别提取到不同的类中。
// 违反单一职责原则的例子
class UserService {
public void register(User user) {
// 业务逻辑
}
public void saveToDatabase(User user) {
// 数据库操作
}
}
// 改进后的设计
class UserService {
private UserRepository userRepository;
public void register(User user) {
// 业务逻辑
}
}
class UserRepository {
public void save(User user) {
// 数据库操作
}
}
通过将业务逻辑和持久化存储分离,我们实现了每个类只有一个变化的原因。
定义:软件实体(类、模块、函数等)应该对扩展开放,对修改关闭。
解释:开放-关闭原则要求我们在对系统进行扩展时,不需要修改已有的代码。通过实现可扩展的设计,我们可以通过添加新的功能类来扩展系统,而不需要修改现有的代码。这样可以有效减少代码的变动,降低出现新 bug 的风险,并提高系统的可维护性。
例子: 假设我们有一个支付系统,它支持不同的支付方式,如支付宝、微信支付等。如果我们要支持新的支付方式,应该创建新的支付方式类,而不是修改已有的代码。
// 违反开放-关闭原则的例子
class PaymentService {
public void processPayment(String paymentType) {
if (paymentType.equals("alipay")) {
// 处理支付宝支付
} else if (paymentType.equals("wechat")) {
// 处理微信支付
}
}
}
// 改进后的设计
interface PaymentMethod {
void pay();
}
class Alipay implements PaymentMethod {
public void pay() {
// 支付宝支付实现
}
}
class WeChatPay implements PaymentMethod {
public void pay() {
// 微信支付实现
}
}
class PaymentService {
public void processPayment(PaymentMethod paymentMethod) {
paymentMethod.pay();
}
}
通过引入接口 PaymentMethod
,每个支付方式都可以扩展到新的类中,而无需修改 PaymentService
类的代码。
定义:子类对象必须能够替换掉父类对象并且保证程序的正确性。
解释:里氏替换原则强调子类应该能够完全继承父类的行为,并且能够替换父类实例而不影响程序的正确性。具体来说,子类可以扩展父类的功能,但不能改变父类已有的行为。
例子: 假设我们有一个形状类,Rectangle
类是 Shape
类的子类。Square
类是 Rectangle
类的子类。根据里氏替换原则,Square
应该能够被替换为 Rectangle
,并且行为仍然正确。
// 违反里氏替换原则的例子
class Rectangle {
private int width;
private int height;
public void setWidth(int width) {
this.width = width;
}
public void setHeight(int height) {
this.height = height;
}
}
class Square extends Rectangle {
@Override
public void setWidth(int width) {
this.width = width;
this.height = width;
}
@Override
public void setHeight(int height) {
this.height = height;
this.width = height;
}
}
// 改进后的设计
class Shape {
public void draw() {}
}
class Rectangle extends Shape {
private int width;
private int height;
public void setWidth(int width) {
this.width = width;
}
public void setHeight(int height) {
this.height = height;
}
}
class Square extends Shape {
private int side;
public void setSide(int side) {
this.side = side;
}
}
通过让 Square
独立于 Rectangle
,我们避免了里氏替换原则的破坏。现在,Square
类与 Rectangle
类没有继承关系,它们都继承自 Shape
,因此可以安全地替换和扩展。
定义:高层模块不应该依赖低层模块,二者都应该依赖抽象;抽象不应该依赖细节,细节应该依赖抽象。
解释:依赖倒转原则强调系统中的高层模块和低层模块不应直接依赖于实现细节,而应依赖于抽象(接口或抽象类)。这意味着我们可以将实现细节交给具体的类来处理,而高层模块只关心抽象接口,从而实现灵活的系统设计。
例子: 假设我们有一个 UserService
类,它依赖于一个具体的 UserRepository
类。如果我们想要改变 UserRepository
的实现(比如使用不同的数据库),我们就需要修改 UserService
类。通过引入接口,我们可以依赖接口,而不是具体的实现。
// 违反依赖倒转原则的例子
class UserService {
private UserRepository userRepository;
public UserService() {
this.userRepository = new UserRepository(); // 直接依赖具体实现
}
public void saveUser(User user) {
userRepository.save(user);
}
}
class UserRepository {
public void save(User user) {
// 保存用户
}
}
// 改进后的设计
interface UserRepository {
void save(User user);
}
class MySQLUserRepository implements UserRepository {
public void save(User user) {
// 保存到 MySQL
}
}
class MongoUserRepository implements UserRepository {
public void save(User user) {
// 保存到 MongoDB
}
}
class UserService {
private UserRepository userRepository;
public UserService(UserRepository userRepository) {
this.userRepository = userRepository; // 依赖注入
}
public void saveUser(User user) {
userRepository.save(user);
}
}
通过依赖接口 UserRepository
,UserService
类不再依赖于具体的数据库实现,从而降低了耦合度。
定义:客户端不应该依赖它不需要的接口。
解释:接口隔离原则强调客户端只应该依赖于它实际需要的接口,而不是被迫依赖于一些不需要的接口。通过拆分大的接口,我们可以确保每个接口只包含一个特定的功能,避免不必要的耦合。
例子: 假设我们有一个 Worker
接口,它包含了多个方法,如 work()
、eat()
和 sleep()
。如果某些类只需要其中的一个方法,那么它们会被迫实现不需要的接口。我们可以将接口拆分为多个小接口,使得每个类只实现它所需要的接口。
// 违反接口隔离原则的例子
interface Worker {
void work();
void eat();
void sleep();
}
class Robot implements Worker {
public void work() {
// 工作
}
public void eat() {
// 不吃
}
public void sleep() {
// 不睡
}
}
// 改进后的设计
interface Workable {
void work();
}
interface Eatable {
void eat();
}
interface Sleepable {
void sleep();
}
class Robot implements Workable {
public void work() {
// 工作
}
}
通过拆分接口,我们让 Robot
类只实现它需要的方法,避免了实现不必要的接口。
定义:一个对象应当对其他对象有尽可能少的了解,即只与直接的朋友对象交互。
解释:迪米特法则要求对象
之间尽量少地互相了解,尤其是避免通过调用其他对象的内部方法来达到目的。它提倡“只与直接朋友交互”的设计,即对象之间应该通过简洁的接口来通信,而不应该通过链式调用或暴露过多的内部实现。
例子: 假设我们有一个 Order
类,它包含一个 Customer
对象,Customer
对象又包含一个 Address
对象。如果 Order
类直接操作 Address
类的属性,就违背了迪米特法则。
// 违反迪米特法则的例子
class Order {
private Customer customer;
public String getCustomerStreet() {
return customer.getAddress().getStreet();
}
}
// 改进后的设计
class Order {
private Customer customer;
public String getCustomerStreet() {
return customer.getStreet();
}
}
class Customer {
private Address address;
public String getStreet() {
return address.getStreet();
}
}
通过封装内部实现,我们避免了 Order
类直接操作 Address
类,从而符合迪米特法则。
定义:优先使用对象的组合(组合模式)或聚合(聚合模式)而非继承来复用代码。
解释:组合/聚合复用原则强调,通过组合或者聚合对象而不是继承来实现功能复用。组合和聚合可以提供更灵活的设计,因为它们能够在运行时更改组合的对象,而继承则是在编译时就确定了父子类关系。
例子: 假设我们有一个 Car
类,它继承了一个 Engine
类。如果我们希望能够为不同的汽车使用不同的发动机,使用组合可以避免继承带来的问题。
// 违反组合/聚合复用原则的例子
class Car extends Engine {
public void start() {
// 启动汽车
}
}
// 改进后的设计
class Engine {
public void start() {
// 启动发动机
}
}
class Car {
private Engine engine;
public Car(Engine engine) {
this.engine = engine;
}
public void start() {
engine.start();
}
}
通过组合 Engine
对象,而不是继承自 Engine
类,Car
类可以灵活地更换不同的发动机。