【注意】最后更新于 November 13, 2019,文中内容可能已过时,请谨慎使用。
最近在学极客时间上的专栏“设计模式之美”,决定跟随专栏的更新,立一个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
|
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
|
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)
}
|
多态可以提高代码的可扩展性和可复用性。多态可以说是面向对象中最重要的一个特性,是很多设计模式、设计原则、编程技巧的代码实现基础,例如策略模式、基于接口而非实现编程、依赖倒置原则、里氏替换原则等等。
参考