0%

面向对象编程的最佳实践是多年软件开发经验的结晶,遵循这些原则可以显著提高代码质量、可维护性和可扩展性。下面我将对这些最佳实践进行深入讲解。

一、优先使用组合而非继承

概念解析

组合(Composition)是指一个类包含另一个类的实例作为其成员变量,通过委托来使用其功能,而不是通过继承获得父类的功能。

为什么优先使用组合?

  1. 更松散的耦合:组合对象可以在运行时动态更换
  2. 更好的封装:内部实现细节被隐藏
  3. 避免继承层次过深:继承链过长会导致”脆弱基类问题”
  4. 更灵活:可以组合多个不同类的功能

代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 使用继承(不推荐)
class Engine {
void start() { /*...*/ }
}

class Car extends Engine { // Car不是Engine的一种,这是不好的设计
// ...
}

// 使用组合(推荐)
class Car {
private Engine engine; // Car有一个Engine

public Car(Engine engine) {
this.engine = engine;
}

void start() {
engine.start();
}
}

何时使用继承?

  • 当确实是”is-a”关系时(如Dog是Animal)
  • 需要多态行为时
  • 子类确实是父类的特殊化

二、遵循最小知识原则(迪米特法则)

概念解析

迪米特法则(Law of Demeter)规定:

  1. 每个单元只应该了解与它密切相关的单元
  2. 只与直接朋友交谈
  3. 不要与陌生人交谈

具体表现

  • 对象O的方法M只能调用以下对象的方法:
    1. O本身
    2. M的参数
    3. O直接包含的对象
    4. M中创建的对象

代码示例

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
// 违反迪米特法则
class Customer {
Wallet wallet;

Wallet getWallet() {
return wallet;
}
}

// 客户端代码
float money = customer.getWallet().getMoney(); // 与Wallet这个"陌生人"交谈

// 遵循迪米特法则
class Customer {
private Wallet wallet;

public float getPayment(float amount) {
if (wallet != null) {
return wallet.withdraw(amount);
}
return 0;
}
}

// 客户端代码
float money = customer.getPayment(100); // 只与Customer交谈

优点

  • 减少类之间的耦合
  • 提高模块化程度
  • 更易于维护和修改

三、保持类和方法的小型化

类的大小原则

  • 单一职责原则:一个类应该只有一个引起它变化的原因
  • 经验法则:
    • 行数:通常不超过200-300行
    • 方法数:通常不超过10-20个
    • 属性数:通常不超过10个

方法的大小原则

  • 一个方法应该只做一件事
  • 经验法则:
    • 行数:通常不超过屏幕高度(20-30行)
    • 嵌套层级:不超过2-3层
    • 参数个数:不超过3-4个

重构技巧

  1. 提取方法:将大方法中的代码块提取为独立方法
  2. 提取类:将大类中的相关功能提取到新类中
  3. 使用策略模式:将复杂逻辑委托给专门的策略类

四、使用有意义的名字

命名原则

  1. 类名:名词或名词短语,明确表示职责

    • 好例子:Customer, OrderProcessor
    • 坏例子:Data, Manager, Processor
  2. 方法名:动词或动词短语,描述操作

    • 好例子:calculateTotal(), saveToDatabase()
    • 坏例子:process(), doWork()
  3. 变量名:名词,描述存储内容

    • 好例子:customerCount, isValid
    • 坏例子:temp, data, flag

命名技巧

  • 避免缩写(除非是广泛接受的)
  • 使用领域术语
  • 保持一致性(如全部使用get或全部使用retrieve
  • 长度与作用域成正比(局部变量可以短,成员变量应该更详细)

五、合理使用访问修饰符

访问控制原则

  1. 私有(private)优先:除非有充分理由,否则字段和方法都应该是private的
  2. 逐步放宽:从最严格开始,只在必要时放宽限制
  3. 封装不变性:确保对象的不变量不被破坏

各语言访问修饰符对比

修饰符 Java/C#/C++ Python JavaScript
私有 private __prefix 无(约定用_)
保护 protected
包私有 (default)
公开 public 无前缀

最佳实践

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class BankAccount {
// 字段私有
private float balance;
private String owner;

// 通过公开方法访问
public float getBalance() {
return balance;
}

// 修改方法可以添加验证
public void deposit(float amount) {
if (amount > 0) {
balance += amount;
}
}
}

六、避免上帝对象(God Object)

上帝对象的特征

  1. 知道太多信息
  2. 做太多事情
  3. 与系统中太多其他对象耦合
  4. 通常有数百甚至上千行代码

危害

  • 难以理解和维护
  • 难以测试
  • 修改影响范围大
  • 阻碍并行开发

解决方案

  1. 职责分解:识别可以分离的职责
  2. 领域驱动设计:按照业务领域划分模块
  3. 设计模式应用:如策略模式、外观模式等

示例重构

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
// 上帝对象
class OrderProcessor {
void process(Order order) {
// 验证订单
// 计算价格
// 检查库存
// 创建发票
// 发送通知
// 更新数据库
// ... 1000行代码
}
}

// 重构后
class OrderValidator { /*...*/ }
class PriceCalculator { /*...*/ }
class InventoryChecker { /*...*/ }
class InvoiceGenerator { /*...*/ }
class NotificationSender { /*...*/ }
class OrderRepository { /*...*/ }

class OrderProcessor {
private OrderValidator validator;
private PriceCalculator calculator;
// 其他依赖...

void process(Order order) {
validator.validate(order);
calculator.calculate(order);
// ...
}
}

七、合理使用接口和抽象类

接口 vs 抽象类

特性 接口(Interface) 抽象类(Abstract Class)
实例化 不能 不能
方法实现 Java8+可以有默认方法 可以有具体方法
字段 只能有常量 可以有各种字段
多继承 一个类可实现多个接口 只能继承一个抽象类
设计目的 定义契约 提供部分实现

使用原则

  1. 接口用于

    • 定义跨继承层次的能力
    • 需要多重继承时
    • 定义服务契约
  2. 抽象类用于

    • 提供部分实现
    • 有共同基类逻辑时
    • 希望控制子类如何扩展时

代码示例

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
// 接口定义能力
interface Flyable {
void fly();
}

interface Swimmable {
void swim();
}

// 抽象类提供部分实现
abstract class Bird {
abstract void makeSound();

void breathe() {
System.out.println("Breathing...");
}
}

// 具体类
class Duck extends Bird implements Flyable, Swimmable {
void makeSound() {
System.out.println("Quack");
}

public void fly() {
System.out.println("Duck flying");
}

public void swim() {
System.out.println("Duck swimming");
}
}

接口隔离原则(ISP)

  • 客户端不应该被迫依赖它们不使用的接口
  • 应该将大接口拆分为更小、更具体的接口
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 违反ISP
interface Worker {
void work();
void eat();
void sleep();
}

// 遵循ISP
interface Workable {
void work();
}

interface Eatable {
void eat();
}

interface Sleepable {
void sleep();
}

通过遵循这些面向对象的最佳实践,可以创建出更健壮、更灵活、更易于维护的软件系统。记住,这些原则不是铁律,而是指导方针,在实际应用中需要根据具体情况进行权衡。