最近在学极客时间上的专栏“设计模式之美”,决定跟随专栏的更新,立一个flag,坚持打卡每节都写一篇笔记,作为对学习过程的巩固,以加深理解和思考。本篇是专栏第5节的学习笔记,第5节是专栏中介绍"面向对象"这一设计原则和设计思想的一节。

在过去我们学习各种面向对象编程语言(比如java)时,一般多会说面向对象有三大特性: 封装、继承和多态。而专栏中加入也将"抽象"作为第四个特性加入其中。

封装(Encapsulation)

封装即信息隐藏或数据保护,“数据结构"通过暴露有限的访问接口,授权外部仅能通过"数据结构"提供的方法(函数)来访问其内部的数据。

几行代码胜千言,下面分别给出python和go中实现封装的示例代码:

 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
33
34
35
36
37
38
39
40
import uuid
import time
import decimal

class Wallet:
    def __init__(self):
        self.__id = uuid.uuid1().__str__()
        self.__create_time = int(time.time())
        self.__balance = decimal.Decimal('0.00')
        self.__balance_last_modified_time = int(time.time())

    @property
    def id(self):
        return self.__id

    @property
    def create_time(self):
        return self.__create_time

    @property
    def blance(self):
        return self.__balance

    @property
    def balance_last_modified_time(self):
        return self.__balance_last_modified_time

    def increase_balance(self, increased_amount):
        if increased_amount < 0:
            raise ValueError('...')
        self.__balance += increased_amount
        self.__balance_last_modified_time = int(time.time())

    def decrease_balance(self, decreased_amount):
        if decreased_amount < 0:
            raise ValueError('...')
        if decreased_amount > self.__balance:
            raise ValueError('...')
        self.__balance -= decreased_amount
        self.__balance_last_modified_time = int(time.time())
 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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
import (
	"errors"
	"time"

	"github.com/shopspring/decimal"
)

type wallet struct {
	id                      string
	createTime              time.Time
	balance                 decimal.Decimal
	balanceLastModifiedTime time.Time
}

func NewWallet() wallet {
	return wallet{
		id:                      "",
		createTime:              time.Now(),
		balance:                 decimal.NewFromFloat(0),
		balanceLastModifiedTime: time.Now(),
	}
}

func (w *wallet) Id() string {
	return w.id
}

func (w *wallet) CreateTime() time.Time {
	return w.createTime
}

func (w *wallet) Blance() decimal.Decimal {
	return w.balance
}

func (w *wallet) BalanceLastModifiedTime() time.Time {
	return w.balanceLastModifiedTime
}

func (w *wallet) IncreaseBalance(increasedAmount decimal.Decimal) error {
	if increasedAmount.LessThan(decimal.NewFromFloat(0)) {
		return errors.New("...")
	}
	w.balance = w.balance.Add(increasedAmount)
	w.balanceLastModifiedTime = time.Now()
	return nil
}

func (w *wallet) DecreasedBalance(decreasedAmount decimal.Decimal) error {
	if decreasedAmount.LessThan(decimal.NewFromFloat(0)) {
		return errors.New("...")
	}
	if decreasedAmount.GreaterThan(w.balance) {
		return errors.New("...")
	}
	w.balance = w.balance.Sub(decreasedAmount)
	w.balanceLastModifiedTime = time.Now()
	return nil
}

封装这个特性需要使用编程语言提供的"访问控制权限"这个语法机制来实现。封装主要是讲如何隐藏信息、保护数据。

抽象(Abstraction)

抽象讲的是如何隐藏方法实现,让方法调用者只关心方法提供了哪些功能,并不需要知道这些功能是如何实现的。 在面向对象编程中经常借助接口(或抽象类)的语法机制来实现抽象。

几行代码胜千言,下面分别给出python和go中实现抽象的示例代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19

from abc import ABC, abstractmethod

class PictureStorage(ABC):
    
    @abstractmethod
    def save_picture(self, picure):
        pass

    @abstractmethod
    def get_picture(self, picture_id):
        pass

class PictureStorageImpl(PictureStorage):
    def save_picture(self, picture):
        print('...')

    def get_picture(self, picture_id):
        print('...')
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
type Picture struct {
	//...
}

type PictureStorage interface {
	SavePicture(picture *Picture) error
	GetPicture(pictureId string) (*Picture, error)
}

type pictureStorageImpl struct {
}

func (p *pictureStorageImpl) SavePicture(picture *Picture) error {
	// ...
	return nil
}

func (p *pictureStorageImpl) GetPicture(pictureId string) (*Picture, error) {
	// ...
	return nil, nil
}

实际中抽象这个特性是很容易实现的,并不一定需要依赖接口和抽象类这类语法机制,例如在非面向对象语言中编写的函数(方法)本身就具有抽象的特性,即函数(方法)的使用者并不需要详细去了解函数(方法)内部具体的实现逻辑,而只要通过函数(方法)的命名、注释或文档即可了解其提供的功能后就可以直接使用了。这也是为什么我们经常说面向对象有"封装”、“继承”、“多态"三大特性,而没有包含"抽象"这第四个特性的原因。

延展一下,在广义上,抽象是一种非常宽泛的设计思想,很多的设计原则都体现了抽象这种思想,如基于接口而非实现编程、开闭原则(对扩展开放、对修改关闭)、代码解耦等等。

继承(Inheritance)

继承用来表示is-a的关系。从继承关系来说有单继承多重继承两种模式。 为了实现继承特性,编程语言需要提供特殊的语法机制来支持。例如: java使用extends关键字,python中使用()来实现继承。 有的语言只支持单继承,如java;有的语言支持多重继承,如python。

继承的好处是可以实现代码复用,但继承这个特性有很大争议,不应过度使用。例如如果继承的层次过深就会导致代码可读性和可维护性变差。 因此我们平时要少用继承而多用组合。这也是go语言不支持继承特性而提倡使用组合的原因

多态(polymorphism)

多态是指子类可以替换父类,在实际代码运行过程中调用子类的方法实现。 多态这种特性也需要编程语言提供特殊的语法机制来实现。

多态可以通过"子类继承父类+子类重写父类方法+父类引用指向子类对象"的方式实现,另外还可以通过"接口语法"的方式实现,这两种方式在Java中都支持。 多态另外还可以通过第三种方式即duck-typing的语法实现,这种实现方式一般只有一些动态语言如Python才支持,另外go语言中的"隐藏式接口"也算是duck-typing

下面示例给出duck-typing实现多态在python和go中的示例代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17

class Logger:
    def record(self):
        print('I write a log into file.')
        
class DB:
    def record(self):
        print('I insert data into db. ')
        
def test(recorder):
    recorder.record()

def demo():
    logger = Logger()
    db = DB()
    test(logger)
    test(db)

从上面的python代码中可以看出,Logger和DB两个类没有任何关系,既不是继承关系,也不是接口和实现的关系,但在运行时它们的对象却都可以传递到test()方法中,并执行各自的record()方法。

 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
package main

type Recorder interface {
	Record() error
}

type Logger struct {
}

func (l *Logger) Record() error {
	return nil
}

type DB struct {
}

func (db *DB) Record() error {
	return nil
}

func Test(recorder Recorder) {
	recorder.Record()
}

func main() {
	logger := &Logger{}
	db := &DB{}
	Test(logger)
	Test(db)
}

多态可以提高代码的可扩展性和可复用性。多态可以说是面向对象中最重要的一个特性,是很多设计模式、设计原则、编程技巧的代码实现基础,例如策略模式、基于接口而非实现编程、依赖倒置原则、里氏替换原则等等。

参考