面向对象
面向对象思想
面向对象到底是什么?这是每一个编程人员都要明确的一个问题。
首先,面向对象是一种思想,它要求每个开发人员先认识客观的现实世界,然后在自己的头脑中构建出一个完整的系统。在这个系统中,包含众多角色,他们之间有不同的关系,不同角色、关系之间还可以互相交互,而通过不断交互最终实现系统目标的过程。
将面向对象思想应用到计算机世界中,就会引出面向对象编程和面向对象编程语言。
面向对象编程
面向对象编程,就是基于面向对象思想进行编程的过程。前面说了,面向对象思想首先要求程序设计人员在自己的头脑中构建出一个系统,这个系统中又有不同角色,不同角色之间又有关系,也可以交互。在编程时,为了高效实现系统目标,需要程序设计人员先对这些角色进行归类,得到能够代表一类角色的普遍形式,这就是“类”。接着,根据这个“类”泛化出多个实例——对象,通过这些实例之间的交互,最终实现系统目标。
概括来说,面向对象编程就是一种编程范式或风格,它以“类”和“对象”作为组织代码的基本单元,通过对象和对象之间的不断交互,实现系统目标的过程。
要真正地实现面向对象编程,还需要一种基础设施提供支持,它就是面向对象编程语言。面向对象编程语言要有现成的语法机制,主要能够支持生成“类”和“对象”这两类基本单元。并且,在进行面向对象编程的过程中,人们逐渐总结出一系列方法(特性),最主要的是下面这几个:
- 抽象
- 继承
- 封装
- 多态
这些特性让面向对象编程变得更加高效实用。作为基础设施,几乎所有面向对象编程语言支持这四大特性。
四大特性
抽象
“抽象”这个词本身就比较“抽象”,和哲学有着千丝万缕的关系,它的对立面是具体、细节。在科学范畴内,它的定义是“从众多的事物中抽取出共同的、本质性的特征,而舍弃其非本质的特征的过程”。
将抽象的这些意思放进编程这一具体行为中,同样也是适用的。前面说过,面向对象编程思想需要将众多角色归类,这其实就是一种抽象,其结果是得到能够代表一类角色的“类”。在具体的实践中,有时候还需要设计“抽象类”,从名字上就可以知道,这也是一种抽象,其结果是得到多个类中共有的部分,形成一个基础类。
另外,在很多OOPL中,还有“接口类”,比如Java中的interface,Objective-C中的protocol等。它们的实质都是一样的,即对某种行为过程的抽象,结果是得到一个能够实现某种具体行为的普遍表示。这样,调用者就不用关心具体的实现细节,只管调用即可,真正的实现由其他角色“秘密”完成。
比如,Java中的interface:
public interface IPictureStorage {
void savePicture(Picture picture);
Image getPicture(String pictureId);
void deletePicture(String pictureId);
void modifyMetaInfo(String pictureId, PictureMetaInfo metaInfo);
}
Objective-C中的protocol,下面是为UITableView提供数据的protocol:
@protocol UITableViewDataSource<NSObject>
@required
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section;
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath;
@optional
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView;
// ...
@end
除此之外,所有编程语言都支持的语法,函数function,同样也是一种抽象。因为在函数内必然包裹了一段具体的代码细节,调用者只管调用,就可以得到稳定的功能实现。比如数学函数sin、cos、log,C语言中的内存操作函数malloc和free,它们对所有调用者而言,都是平等且透明的。
封装
封装也叫信息隐藏或者数据访问保护,类通过暴露有限的访问接口,授权访问者通过类提供的方法(函数)来正确地访问内部数据。
比如,网络支付作为一个基础服务,是很多应用的必备功能。对此,我们就可以用一个类Wallet来抽象。
我们都知道,钱财是每个人最重要的资产之一,因此我们必须保证数据的正确性,比如肯定不能让账户余额随便被修改,至多提供一个增减余额的方法,再加上一些基本信息的获取即可。代码如下:
public class Wallet {
private String id;
private long createTime;
private BigDecimal balance;
private long balanceLastModifiedTime; // ...省略其他属性...
public Wallet() {
this.id = IdGenerator.getInstance().generate();
this.createTime = System.currentTimeMillis();
this.balance = BigDecimal.ZERO;
this.balanceLastModifiedTime = System.currentTimeMillis();
}
public String getId() { return this.id; }
public long getCreateTime() { return this.createTime; }
public BigDecimal getBalance() { return this.balance; }
public long getBalanceLastModifiedTime() { return this.balanceLastModifiedTime;
public void increaseBalance(BigDecimal increasedAmount) {
if (increasedAmount.compareTo(BigDecimal.ZERO) < 0) {
throw new InvalidAmountException("...");
}
this.balance.add(increasedAmount);
this.balanceLastModifiedTime = System.currentTimeMillis();
}
public void decreaseBalance(BigDecimal decreasedAmount) {
if (decreasedAmount.compareTo(BigDecimal.ZERO) < 0) {
throw new InvalidAmountException("...");
}
if (decreasedAmount.compareTo(this.balance) > 0) {
throw new InsufficientAmountException("...");
}
this.balance.subtract(decreasedAmount);
this.balanceLastModifiedTime = System.currentTimeMillis();
}
}
所以,通过封装,数据的正确性得到了有效保证,这是所有系统最基本的要求。同时,还能够增加易用性。因为在一个系统中,经常是多个数据之间有关联,一处修改必然带动其他地方的修改。所以,将这些行为都放在一起,封装在一个方法中,作为一个整体暴露给访问者,可以有效降低使用难度。
继承
继承主要用来表示类和类之间的is-a关系,比如钢琴是一种乐器。几乎所有编程语言都提供了现成的语法支持,比如Java的extends,C++的冒号:,Python的括号(),JavaScript在ES6中也提供了extends和class关键字等,甚至有的语言还支持多继承。
比如JavaScript的新语法:
class Animal {
constructor(name) {
this.name = name;
}
speak() {
console.log(this.name + ' makes a noise.');
}
}
class Dog extends Animal {
constructor(name) {
super(name);
}
speak() {
console.log(this.name + ' barks.');
}
}
let d = new Dog('Mitzie');
d.speak(); // Mitzie barks.
继承最大的作用就是解决了代码复用这一问题,通过将共有数据和行为“抽象”到父类中,减少重复代码,提高开发效率。
但是,成也萧何败也萧何,一个特点在正面看是优点,在反面看往往就会成为缺点。对继承的过度使用,会让类的继承关系变得非常冗长,这让代码的可读性、可维护性严重受损。对此,有些语言甚至抛弃了继承特性,比如Go,转而建议开发者使用组合,而非继承。
多态
多态对初学者来说,可能并不好理解。我们先从字面理解,“多态”就是多个状态或行为的意思,这很容易理解,而让人费解的是还要加一个前置条件:同一件对象、方法。也就是说,同一个对象或者方法可以表现出多个不同状态,这就是它之所以神奇和难以理解的地方。
具体而言,多态指的是,子类可以替换父类,在实际的代码运行过程中,调用子类的方法实现,从而达到不同的行为状态。这需要三种语法的支持:
- 支持继承,实现父类和子类;
- 支持父类对象引用子类对象;
- 支持重写,子类可以重新父类中的方法。
只有支持了这三种语法,才能实现多态特性。实际上,在不同的编程语言中,实现多态的语法各不相同。
常规实现
在Objective-C中,用常规语法实现:
@interface Player : NSObject
@property (nonatomic, assign) CGFloat duration;
@property (nonatomic, copy) NSString * name;
- (void)play;
@end
@interface VideoPlayer : Player
@end
@interface AudioPlayer : Player
@end
@implementation Player
- (void)play {
NSLog(@"player(%@) starts playing...", self.name);
}
@end
@implementation VideoPlayer
- (void)play {
NSLog(@"video player(%@) starts playing...", self.name);
}
@end
@implementation AudioPlayer
- (void)play {
NSLog(@"audio player(%@) starts playing...", self.name);
}
@end
基于接口实现
在Java中,用接口类实现多态:
public interface Iterator {
String hasNext();
String next();
String remove();
}
public class Array implements Iterator {
private String[] data;
public String hasNext() { ... }
public String next() { ... }
public String remove() { ... }
//...省略其他方法...
}
public class LinkedList implements Iterator {
private LinkedListNode head;
public String hasNext() { ... }
public String next() { ... }
public String remove() { ... }
//...省略其他方法...
}
public class Demo {
private static void print(Iterator iterator) {
while (iterator.hasNext()) {
System.out.println(iterator.next());
}
}
public static void main(String[] args) {
Iterator arrayIterator = new Array();
print(arrayIterator);
Iterator linkedListIterator = new LinkedList();
print(linkedListIterator);
}
}
duck-typing实现
再用Python的duck-typing实现多态:
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)
所以,实现多态的语法各不相同,但是它们的实质都是一样的:为了提高代码的复用性和可扩展性。因为在多态中,既通过继承提高了代码的复用性,又通过重新增加了代码的扩展性,让子类更加符合具体需求。
总结
面向对象编程之所以强大,是因为基于面向对象编程思想,能够对一切复杂的客观存在进行建模,构建出一个完整的系统,借助抽象、封装、继承、多态四大特性高效实现系统目标。
抽象,通过去除一切细微的具体实现细节,暴露一个简单的接口或者抽象类,甚至一个方法名,让调用者只管调用而无需关心细节。抽闲提高了效率,增加了代码的可读性、可维护性等。
封装,也叫信息隐藏或者数据访问保护,通过暴露有限的访问接口,授权外部通过类提供的方法来访问数据,能够保证数据的正确性,也能够提高易用性。
继承,用来表示类和类之间的is-a关系,主要解决代码复用问题,分为单继承和多继承。在实际工作中,过度使用继承造成一个严重的后果,太深的继承关系严重影响了代码的可读性和可维护性。对此,有些编程语言甚至放弃了继承特性,转而建议使用组合,而非继承。
多态,是指用子类替代父类,在实际代码运行时,调用子类实现以满足更加细致的需求。多态需要特殊的语法支持,主要解决的是代码复用和可扩展性问题。









网友评论