英文缩略语概览:

  • 面向对象编程:OOP Object Oriented Programming
  • 面向对象编程语言:OOPL Object Oriented Programming Language
  • 面向对象分析:OOA Object Oriented Analysis
  • 面向对象设计:OOD Object Oriented Design

OOA、OOD、OOP 三个连在一起就是面向对象分析、设计、编程(实现),正好是面向对象软件开发要经历的三个阶段。

四大特性

面向对象的四大特性:继承、多态、封装、抽象。但有些人认为抽象不算特性之一。

封装

信息隐藏或者数据访问保护,类通过暴露有限的访问接口,授权外部仅能通过类提供的方式(函数)来访问内部信息或数据,保证数据的一致性。

获取类的信息时,我们尽量只能通过对象变量来访问这个对象的变量或方法,不通过引用变量就无法访问其中的变量或方法。对于访问者而言,这个对象是封装成一个整体的,这正体现了面向对象的程序设计的「封装性」。

对于封装的特性,编程语言需要一定的语法机制支持。Java 中的修饰符 publicprivateprotected 可实现封装。

封装的意义在于保护数据不被修改,提高代码的可维护性。仅暴露有限的必要接口,提供更少的承诺,提高类的易用性。这样的封装并不是在自己防自己,只要多一个人开发就要设防,防止傻瓜操作影响到自己的类。

抽象

隐藏方法的具体实现,让使用者只需要关心方法提供了哪些功能,不需要知道这些功能是如何实现的。

抽象可以通过接口类或者抽象类来实现,但其实也并不需要特殊的语法机制来支持,比如一般编程语言中的函数机制也可理解为抽象。Java 中使用 interfaceabstract 关键字实现。

意义在于提高代码的可扩展性,可维护性,修改实现不需要改变定义,减少代码的改动范围;它也是处理复杂系统的有效手段,能够有效地过滤不必要关注的信息。

面向对象中,有时并不愿意把抽象当做特性,是因为抽象是一个非常通用的设计思想,并不需要编程语言提供特殊的语法机制支持,没有很强的“特异性”。

继承

继承表示类之间的 is-a 关系,分两种模式:单继承和多继承。

  • 单继承:一个子类只继承一个父类。
  • 多继承:一个子类可以继承多个父类。

编程语言中的单继承与多重继承:

  • 只支持单继承,不支持多重继承,比如 Java、PHP、C#、Ruby 等
  • 既支持单重继承,也支持多重继承,比如 C++、Python、Perl 等。

C++ 有同名覆盖原则。子类方法与父类方法同名(不需要参数类型都相同),父类方法会被屏蔽。

继承的作用在于代码的复用,但是过度使用继承,如继承层次过深过复杂,会导致代码可读性变差。在继承使用过程中开发子类的人员需要阅读父类的代码,这属于耦合了。

多态

多态即子类可以替换父类,它的基础是继承。

在实际代码运行过程中,将父类的指针(或引用)指向子类的实例。父类的引用指向不同的子类实例时,根据子类实例中方法的更新版本去调用。父类尽可能要少说,定义父类时可以不实现方法。Java 中所有的非 final/static/private 方法都可以实现多态。

多态的特性需要编程语言提供特殊的语法机制来实现,主要实现方式:

  • 继承 + 方法重写。Java 中为 extends@Override
  • 利用接口类语法。比如 Java 中的 interfaceimplements 关键字
  • duck-typing 鸭子类型:它是动态类型的一种风格。在这种风格中,一个对象有效的语义,不是由继承自特定的类或实现特定的接口,而是由「当前方法和属性的集合」决定。「当看到一只鸟走起来像鸭子、游泳起来像鸭子、叫起来也像鸭子,那么这只鸟就可以被称为鸭子。」使用该方式的编程语言有 Python、JavaScript。

Java 语言允许某个类型的引用变量引用子类的实例,而且可以对这个引用变量进行类型转换。

1
2
3
4
5
6
Animal animal=new Dog();
Dog dog=(Dog)animal;
//向下转型,把Animal类型转换为Dog类型

Creature creature=animal;//自动类型转换
//向上转型,把Animal类型转换成Creature类型

下面给出鸭子类型实现多态的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Logger:
def record(self):
print("1")

class DB:
def record(self):
print("2")

def test(recorder):
recorder.record()

logger = Logger()
db = DB()
test(logger)
test(db)

上面的代码中,Logger 和 DB 没有任何关系。只要都实现了 record 方法,都可以传到 test 中。

多态特性能提高代码的可扩展性和复用性,是很多设计模式、设计原则、编程技巧的代码实现基础。

编程范式/编程风格

常见的编程范式/编程风格有三种:面向过程编程,面向对象编程以及函数式编程。

面向过程编程的特点是数据和操纵数据的方法是分离的。

面向对象的编程和面向对象编程语言的区别:

  • 面向对象的编程是一种编程范式或编程风格,以类或对象作为组织代码的基本单元,并将封装、抽象、继承、多态四个特性,作为代码设计和实现的基石。
  • 面向对象编程语言是支持类或对象的语法机制,并有现成的语法机制,能方便地实现面向对象编程的四大特性的编程语言。
  • 面向对象的编程是一种思想,和具体语言没关系。用 OOPL 不一定就使用 OOP 思想,例子详看下文;使用非 OOPL 也可以运用 OOP 思想,只不过代码成本比较高。

下面列出违反面向对象编程风格的典型代码设计的案例,有一些是错误需要避免,有一些是有意的:

  1. 滥用 gettersetter 方法
    • setter 暴露了直接修改数据的方法,可能破坏数据的一致性。我们必须在使用 setter 时可以保证数据的一致。非必要的情况下尽量不要给属性定义 setter 方法。
    • 如果数据公开,getter 也要限制!因为可能返回给用户的引用(一个地址),用户可以通过地址修改。
      • 如果变量是一个对象,getter 应该返回这个对象的副本。
      • getter 可返回 Collections.unmodifiableList() 方法防止数据被篡改。但这样集合容器中存储的引用地址还是可以被非法使用。
  2. 滥用全局变量和全局方法,静态变量和静态方法。
    • 不允许修改,只可以只读,常见于 Constants 类和 Utils
    • Constants 类拆解为功能更加单一的多个类,比如跟 MySQL 配置相关的常量,我们放到 MysqlConstants 类中;跟 Redis 配置相关的常量,我们放到 RedisConstants 类中。
    • 不单独地设计 Constants 常量类,而是哪个类用到了某个常量,我们就把这个常量定义到这个类中。比如,RedisConfig 类用到了 Redis 配置相关的常量,那我们就直接将这些常量定义在 RedisConfig 中。如果能将这些类中的属性和方法,划分归并到其他业务类中,能极大地提高类的内聚性和代码的可复用性。
    • 设计 Utils 类的时候,最好也能细化一下,针对不同的功能,设计不同的 Utils 类,比如 FileUtilsIOUtilsStringUtilsUrlUtils 等,不要设计一个过于大而全的 Utils 类。
  3. 定义数据和方法分离的类
    • 传统的 MVC 结构分为 Model 层、Controller 层、View 层这三层。
    • 在做前后端分离之后,三层结构在后端开发中,会稍微有些调整,被分为 Controller 层、Service 层、Repository 层。
    • 一般情况下,VO、BO、Entity 中只会定义数据,不会定义方法,所有操作这些数据的业务逻辑都定义在对应的 Controller 类、Service 类、Repository 类中。
    • 这就是典型的面向过程的编程风格:贫血模型

interface 接口 vs 抽象类

抽象类:

  • 抽象类不允许被实例化,只能被继承
  • 它可以包含属性和方法。方法既可以包含代码的实现,也可以不包含代码实现。不包含代码实现的方法叫做抽象方法。
  • 子类(具体类)继承抽象类,必须实现抽象类中的所有抽象方法。

接口(指的是狭义接口,即 Java interface):

  • 不包含属性
  • 接口和抽象类都可以叫接口
  • 接口只能声明方法,方法不能包含代码实现
  • 类实现接口的时候,必须实现接口中声明的所有方法

抽象类和接口存在的意义:

  • 抽象类是对成员变量和方法的抽象是一种 is-a 关系。是为了解决代码复用问题。
  • 接口仅仅是对方法的抽象,是一种 has-a 关系,表示具有某一组行为特性,是为了解决解耦问题,隔离接口和具体的实现,提高代码的扩展性。

抽象类和接口选用规则:

  • 如果要表示一种 is-a 的关系,并且是为了解决代码复用问题,就用抽象类;
  • 如果要表示一种 has-a 关系,并且是为了解决抽象而非代码复用问题,那就用接口。

C++ 中只有抽象类,没有接口,我们可以进行用抽象类模拟:

1
2
3
4
5
6
7
class Strategy{
public:
~Strategy();
virtual void algorithm()=0;
protected:
Strategy();
}

基于接口而非实现编程

“接口”就是一组“协议”或者“约定”,是功能提供者提供给使用者的一个“功能列表”。

这条原则可以将接口和实现分离,封装不稳定的实现,暴露稳定的接口。上游系统面向接口而非实现编程,不依赖不稳定的实现细节,这样当实现发生变化的时候,上游系统的代码基本上不需要改动,以此来降低耦合性,提高扩展性。

另一个表述方式:基于抽象而非实现编程。设计得越抽象就越能够适应变化。具体实践中,函数的命名使用抽象的命名方式不暴露细节(当然内部代码也应当没有特异化的实现,或直接不实现)是体现这一思想的案例。

如果在我们的业务场景中,某个功能只有一种实现方式,未来也不可能被其他实现方式替换,那我们就没有必要为其设计接口。越是不稳定的系统,我们越是要在代码的扩展性、维护性上下功夫。

多用组合少用继承

组合优于继承,多用组合少用继承。继承表示类之间的 is-a,支持多态特性,代码复用。但继承层次过深、过复杂,也会影响到代码的可维护性。

继承的一些作用可以被其他技术代替:

  • is-a 关系:用组合和接口的 has-a 关系替代
  • 支持多态:利用接口实现
  • 代码复用。用组合和委托来实现

下面通过例子展示继承缺点以及替代继承的方法。这是一个关于鸟的类:

1
2
3
4
5
6
7
8
9
public class AbstractBird{
public void fly(){}
}

public class Ostrich extends AbstractBird{ // 鸵鸟
public void fly(){
throw new UnSupportedMethodException("我不会飞");
}
}

由于并不是所有的鸟都会飞,所以并不是所有的鸟都应该实现 fly() 方法,所以我们把抽象的鸟再继续分类,分成会飞的和不会飞的,然后对应的子类继承正确的父类:

image.png

如果考虑到鸟会不会叫,会不会下蛋等因素,问题变得越来越复杂了…

image.png

对此,我们可以使用接口来优化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Ostrich implements Tweetable, EggLayable{ // 鸵鸟
@Override
public void tweet(){/* ... */};
@Override
public void layEgg(){/* ... */};
}

public class Sparrow implements Flyable, Tweetable, EggLayable{ // 鸵鸟
@Override
public void fly(){/* ... */};
@Override
public void tweet(){/* ... */};
@Override
public void layEgg(){/* ... */};
}

但即使通过接口优化,我们还需要再每个类实现对应的方法。对此,我们可以通过组合 + 委托复用代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public interface Flyable{
void fly();
}

public class FlyAbility implements Flyable {
@Override
public void fly(){/* ... */}; // 编写具体方法实现
}

public class Ostrich implements Tweetable, EggLayable{ // 鸵鸟
// 组合
private TweetAbility tweetAbility = new TweetAbility();
private EggLaytAbility eggLayAbility = new EggLayAbility();
@Override
public void tweet(){
tweetAbility.tweet(); // 委托
};
@Override
public void layEgg(){
eggLayAbility.layEgg(); // 委托
};
}

如何判断该用组合还是继承?

  • 层次潜浅的使用继承;复杂的用组合代替。
  • 除此之外,还有一些设计模式、特殊的应用场景,会固定使用继承或者组合。

本文参考

  • 本科生课程《设计模式&程序设计中级实践》课程笔记。教师:TJU - 🍐⚱️。
  • 极客时间专栏 - 王争 - 设计模式之美