Nonsense

今天有位非计算机相关专业的朋友问我什么是面向对象程序设计,我向他解释了很久,顺带给他写了这篇 Blog,旨在尽量脱离具体语言来通俗而不过分失真地解释 OOP 的相关概念和编程思想。

和自我介绍中写的一样,作者水平有限,请多指正。

为什么要 OOP

面向对象程序设计可以看作一种在程序中包含各种独立而又互相调用的对象的思想,这与传统的思想刚好相反:传统的程序设计主张将程序看作一系列函数的集合,或者直接就是一系列对计算机下达的指令。面向对象程序设计中的每一个对象都应该能够接受数据、处理数据并将数据传达给其它对象,因此它们都可以被看作一个小型的“机器”,即对象。当前已经被证实的是,面向对象程序设计推广了程序的灵活性和可维护性,并且在大型项目设计中广为应用。

简单说,面向对象程序设计是对世界的抽象,将所需的程序组件抽象为自治的个体,并通过类这种蓝图批量进行生产,借由对象之间的互相通讯和协作完成目标。

基本概念

类 (Class)、属性 (Property)、方法 (Method) 和 实例 (Instance)

class Dog {
    //property
    color dog_color;

    //method
    bark() {
        /* bark here */
    }
};

// instance
new first_dog = Dog();
new second_dog = Dog();

类是创建对象的蓝图,描述了所创建的实例的共同的属性(成员变量)和方法(成员函数)。

在这个例子里我们用 Dog 类定义了狗具有属性 color,有方法 bark()。

实例是类实例化的结果,即依照类创建出的 对象 (Object)

类存在的意义是用同一块代码批量创建不同对象。

继承 (Inheritance)、接口 (Interface) 和 多态 (polymorphism)

class Pub extends Dog {
    size pug_size;
    pubRollOver() {
        /* pub roll over here */
    }
}

new pub = Pub();

pub.bark();
pub.pubRollOver();

顾名思义,由一个类派生出另外一个类即为继承。Dog 是 Pub 的父类或称超类 (supertype),反之 Pub 是 Dog 的子类或称派生类 (subtype)。子类从父类继承了父类所有的属性和方法,也可以添加自己的属性和方法,或者重载 (override) 父类实现的方法。

继承的意义是复用父类的代码。

interface Leg {
    length leg_length;
}

class Human implements Leg {
    length human_leg_length;
}

接口是一种契约或者协议,在不同语言中接口所能描述的范围各不相同,但都是为了使被描述的对象符合统一规范,声明实现了接口就要具体完成接口内需要实现的细节。

为不同数据类型的实体提供统一接口即为多态,简单来说,相同的消息给予不同的对象会引发不同的动作。

接口和多态的意义是为了方便地使用对象实现的接口部分,而不需要知道其具体类型和其他特征。

封装 (Encapsulation)

封装即包装隐藏内部实现的细节。

class DepositBox {
public:
    inputPassword() {
        /* input password */
    };

private:
    passwd password;
    unlock() {
        /* open the door */
    };
}

通常 public 表示成员在整个程序中可以被访问, private 表示可以被该类的成员函数访问,不能被使用该类的代码访问,即被隐藏。

封装有利于在外部无感知的状态下修改内部实现,一些语言的封装还具有数据访问保护作用。

设计原则

尽量将违反规则的做法孤立于易于控制的地方

开闭原则

Software entities should be open for extension,but closed for modification.

即在设计一个模块的时候应该使这个模块可以在不被修改的前提下被扩展。

换句话说,通过扩展已有的软件系统,可以提供新的行为,不必修改已有的软件模块,特别是最重要的抽象层。

这需要对抽象层的设计足够严谨,并找到系统中的可变因素进行封装,并且不将不同的可变性进行混合。

里氏替换原则 (LSP)

严格表达是:

如果对每一个类型为 T1 的对象 o1,都有类型为 T2 的对象 o2,使得以 T1 定义的所有程序 P 在所有的对象 o1 都替换成 o2 时,程序 P 的行为没有变化,那么类型 T2 是类型 T1 的子类型。

换言之,一个软件实体如果使用的是一个父类的话,那么一定适用于其子类,而且它根本不能察觉出父类对象和子类对象的区别。

具体讲:

  • 子类可以实现父类的抽象方法,但不能覆盖父类的非抽象方法。
  • 子类中可以增加自己特有的方法。
  • 当子类的方法重载父类的方法时,即方法的形参要比父类方法的输入参数更宽松。
  • 当子类的方法实现父类的抽象方法时,方法的返回值要比父类更严格。

聚合复用原则 (CARP)

要尽量使用组合,尽量不要使用继承。

组合

将已有的对象纳入到新对象中,使之成为新对象的一部分,因此新对象可以调用已有对象的功能

优点:

  • 新对象存取成分对象的唯一方法是通过成分对象的接口
  • 黑箱复用,即成分对象的内部细节是新对象所看不见的
  • 所需要的依赖较少
  • 可以在运行时间动态进行,新对象可以动态的引用与成分对象类型相同的对象

缺点: - 系统会有较多的对象需要管理

继承

优点:

  • 易于实现

缺点:

  • 破坏了封装,父类的实现细节暴露给子类,是白箱复用
  • 父类改变,子类也改变,不易于维护
  • 不可能在运行时间内发生改变

依赖倒置原则 (DIP)

即抽象不应当依赖于细节;细节应当依赖于抽象。

换句话说,要针对接口编程,不要针对实现编程。

因为抽象是战略的,实现是战术的,不应该让战术决定战略。

实际中业务模块应该依赖抽象类型而不是它的具体类型。

End

以上仅为基础中的基础,不同语言和应用环境各有不同,尤其是 C++ 这种复杂的且时髦的多范式编程语言。

藏点私货:Ruby 是真的面向对象编程语言。