深入浅出面向对象

2018/07/20

305

前言

什么是面向对象?

在1960年,程序设计领域正面临着一种危机:在软硬件环境逐渐复杂的情况下,软件如何得到良好的维护?面向对象程序设计在某种程度上通过强调可复用性解决了这一问题。

为什么要面向对象

public main() : void {
    while (!isEnd) {
        // 黑方走()
        // 重绘()
        // 白方走()
        // 重绘()
    }
}

面向对象是从另外的思路来解决的

可以看出,面向对象是按照功能来划分,而不是步骤。

面向对象的好处

面向对象

封装

将一些重复的代码或大部分重复的代码提取出来,放入一个函数/方法中,通过参数传递变量。

把代码封装成一个方法,很好理解,也很容易实现。

注意:

命名规范

  1. 根据函数名,调用方就知道是否需要调用此方法,甚至能猜测出需要传递什么参数和返回值。
  2. 程序员的基本素养
  3. 参考成熟框架的命名

松耦合

  1. 有利于复用
  2. 有利于测试

单一职责原则

如果一个类承担的职责过多,就等于把这些职责耦合在一起,一个职责的变化可能会削弱或者抑制这个类完成其他职责的能力。这种耦合会导致脆弱的设计,当变化发生时,涉及会遭受到意想不到的破坏

方法也需要职责单一

继承

继承,简单来说,就是子类具有父类publicprotected成员

两大概念

单根性

只能继承一个父类,但是可以实现多个接口。

传递性

子类不仅拥有父类的publicprotected成员,还拥有祖类的publicprotected成员

有这些概念后,你如何看待下列代码?

class Laptop {
    public name: string;

    public boot(): void {
        console.log(`笔记本电脑:${this.name}`);
        console.log(`开机了...`);
    }
}

class Desktop {
    public name: string;

    public boot(): void {
        console.log(`台式电脑:${this.name}`);
        console.log(`开机了...`);
    }
}

将来出现平板电脑,是否又要 ctrl c v ?
这些电脑将来都会增加操作系统属性,是否需要为每种电脑添加这一属性?

每当我们发现有代码相似之处时,我们应该思考下,是否将来会有更多的相似代码出现?能否抽象,使代码更健壮?

class Computer {
    public name: string;

    public boot(): void {
        console.log(`开机...`);
    }
}

class Laptop extends Computer {
    // 派生自Computer
    // 所以拥有 name 和 boot()
}

class Desktop extends Computer {
    // 派生自Computer
    // 所以拥有 name 和 boot()
}

将来,更多的形式电脑,只需要继承Computer就可以了

多态

多态将面向对象发挥的淋漓尽致,如果把面向对象比作计算机,多态则是操作系统,正因为有了操作系统,计算机对于人来说生产力得到了显著提高。(比喻不太确切,想表达的是多态的重要性)

什么是多态

维基百科

在编程语言和类型论中,多型(英语:polymorphism)指为不同数据类型的实体提供统一的接口。 多态类型(英语:polymorphic type)可以将自身所支持的操作套用到其它类型的值上。

上述不接地气,如何使用一句话通俗易懂的描述,什么是多态?

接地气

通过继承,实现的不同对象,调用相同方法,表现出不同行为,称之为多态(只是从多态类型角度形容)

画图

:离开地面,并在空中持续移动一段距离。

根据飞的定义,我们不需要管会飞行的动物怎么飞,而只需要在意一个东西是否具备飞的特征。

所以:鸟通过翅膀飞行,会飞。

那么:人会飞吗?
根据飞的定义,至少在飞机被发明前,人不会飞。

打仗了,我方进行空中管制,连一只苍蝇都不能飞入我方基地。

那么如何做到不让能飞的物体进入?

不能通过物体进入的方式来判断,飞机不飞,采用轮胎方式进入。你怎么说?

所以,我们要从飞的定义来看待。

飞机被发明前 -> 飞机被发明后 -> 空中管制

用最小的代价应对不断的变化

手段一:重写父类方法

class Computer {
    public name: string;

    public boot(): void {
        console.log(`开机...`);
    }
}

class Laptop extends Computer {
    public boot(): void {
        super.boot();
        console.log(`笔记本电脑:${this.name}`)
    }
}

class Desktop extends Computer {
    public boot(): void {
        super.boot();
        console.log(`台式电脑:${this.name}`)
    }
}

const laptop = new Laptop();
laptop.name = "联想";
laptop.boot();

console.log("---------------")

const desktop = new Desktop();
desktop.name = "戴尔";
desktop.boot();

输出结果如下:

开机...
笔记本电脑:联想
---------------
开机...
台式电脑:戴尔

这就是多态,但是上述例子并没有将多态的优势展现出来。没关系,接着来。

手段二:抽象类

我们知道,对于电脑来说,OS是抽象的。我告诉你,这里有一台电脑,你不看操作系统,你知道这台电脑是什么操作系统吗?

所以,我们需要对代码进行一些修改。

abstract class Computer {
    public abstract OS: string;
    public name: string;

    public boot(): void {
        console.log(`开机...`);
    }
}

class Laptop extends Computer {
    // 子类必须实现父类的抽象方法,除非子类也是抽象类
    public OS: string = "我也不知道是什么操作系统";
}

abstract class Desktop extends Computer {
    // 可以不实现
}

子类必须实现父类的抽象方法,除非子类也是抽象类。这是语法层面的,意味着,不遵守,编译将不会通过。

抽象方法必须位于抽象类种,否则编译不通过。意味着,如果方法有abstrace,类也必须有。

其实手段二也属于重写,应该归结到手段一中,但是想讲讲抽象类。

抽象类不允许实例化

有时候不希望父类实例化,或者父类就不应该被实例化,考虑使用抽象类

abstract class Computer {
    public abstract OS: string;
    public name: string;

    public boot(): void {
        console.log(`开机...`);
        console.log(`操作系统:${this.OS}`)
    }
}

class Laptop extends Computer {
    public boot(): void {
        super.boot();
        console.log(`笔记本电脑:${this.name}`)
    }
}

class WindowsLaptop extends Laptop {
    public OS: string = "Windows 10";
}

class MacLaptop extends Laptop {
    public OS: string = "OSX";
}

const win10 = new WindowsLaptop();
win10.name = "小米";
win10.boot();

console.log("---------------");

const mac = new MacLaptop();
mac.name = "Mac";
mac.boot();

输出结果如下:

开机...
笔记本电脑:小米
操作系统:Windows 10
---------------
开机...
笔记本电脑:Mac
操作系统:OSX

新入职的同时,需要一台Windows笔记本,但是公司只有一台Mac Book Pro笔记本,怎么办?

对于新员工来说,他想要的只是Windows笔记本,不在乎是什么品牌的。

所以,给Mac安装上Windows操作系统即可。

class MacBookProLaptop extends MacLaptop {
    constructor() {
        super();
        this.name = "Mac Book Pro";
    }
}

MacBookProLaptop继承自MacLaptop,由于继承具有单根性,所以它不能再继承WindowsLaptop

手段三:接口

电脑上都有USB接口:

  1. 供电
  2. 读取数据
  3. 写入数据
  4. 暴露了更多的功能:外接键盘,USB转网线……

早期,MP3和MP4通过USB接口与电脑交互,后来所有的新设备,只要你遵守USB接口规范,实现USB接口功能。此设备就可以与电脑交互。

由于类的单根性,在遇到多态问题时,显得无能为力,这时候,接口的作用就体现出来了。

一种规范
可扩展
可控制

接口,表示一种行为,一种规范。

为什么说是一种行为?
I开头,如果可以,末尾追加able,从命名规范上可以看出接口表示一种行为。(不是TypeScript的规范)

IEnumerable, IDisposable, IList

为什么说是一种规范?
实现了一个接口,就必须实现接口中定义的所有成员。

类只能继承一个父类,但是接口可以有多个实现。

interface IWindows {
    hiCortana(): void;
}

class MacBookProLaptop extends MacLaptop implements IWindows {
    constructor() {
        super();
        this.name = "Mac Book Pro";
    }

    public hiCortana(): void {
        console.log("Hi, I'm Cortana")
    }
}

一台运行Windows10MacBookPro诞生了。

如果这时候,有一台Mac Air也需要安装Windows,怎么办?

interface IWindows {
    hiCortana(): void;
}

class MacBookProLaptop extends MacLaptop implements IWindows {
    constructor() {
        super();
        this.name = "Mac Book Pro";
    }

    public hiCortana(): void {
        console.log("Hi, I'm Cortana")
    }
}

class MacAirLaptop extends MacLaptop implements IWindows {
    constructor() {
        super();
        this.name = "Mac Book Air";
    }

    public hiCortana(): void {
        console.log("Hi, I'm Cortana")
    }
}

这时候,又有重复的代码,忍不了。

abstract class MacWindowsLaptop extends MacLaptop implements IWindows {
    public OS: string = "Windows 10";

    public hiCortana(): void {
        console.log("Hi, I'm Cortana")
    }
}

class MacBookProLaptop extends MacWindowsLaptop {
    constructor() {
        super();
        this.name = "Mac Book Pro";
    }
}

class MacAirLaptop extends MacWindowsLaptop {
    constructor() {
        super();
        this.name = "Mac Book Air";
    }
}

公司有苹果开发者(微软开发者)

class Employee {
    public name: string;

    // 苹果程序员使用Mac
    // 微软程序员使用Windows
    // 这里的Computer属性类型应该是什么?
    //    1 如果非必要,请不要使用`any`
    //    2 也不应该是Computer类型
    //        2.1 取值时,每次都要类型转换
    //        2.2 赋值时,可能给苹果开发者电脑赋值成Windows电脑
    public computer: any;

    public work(): void {
        // use computer
        console.log(`working ...`);
    }
}

静态类型编程语言的特性可以让我们设计出非常严谨的程序。

既然不能是anyComputer类型

interface IComputer {
    // ...
}

interface IOSX extends IComputer {
    hiSiri(): void;
}

interface IWindows extends IComputer {
    hiCortana(): void;
}

class Employee<T extends IComputer> {
    public name: string;

    // 苹果程序员使用Mac
    // 微软程序员使用Windows
    public computer: T;

    public work(): void {
        // use computer
        console.log(`working ...`);
    }
}

class AppleDeveloper extends Employee<IOSX> {

}

class MicrosoftDeveloper extends Employee<IWindows> {

}

const appleDeveloper = new AppleDeveloper();
const msDeveloper = new MicrosoftDeveloper();

// appleDeveloper 只有 hiSiri(),没有 hiCortana()
appleDeveloper.computer.hiSiri();

// msDeveloper 只有 hiCortana(),没有 hiSiri()
msDeveloper.computer.hiCortana();

总结

面向对象

封装、继承、多态

面向对象的好处

继承

单根性,传递性

抽象类

  1. 子类必须实现父类的抽象方法,除非子类也是抽象类。
  2. 抽象方法必须位于抽象类种。
  3. 抽象类不允许实例化

接口

  1. 弥补类在多态特性上的不足
  2. 不允许实例化
  3. 实现接口的类中,必须拥有与接口中定义的成员与之匹配签名,成员可以来自继承树上的。

Ts中的重写父类方法,不够严谨,似乎父类所有的publicprotected都可以被子类重写,可能是因为JS的缘故,选择的妥协。具体不清楚。
但是在实际架构设计中,有些方法需要让子类访问,却不想被子类重写的。

评论