面向对象的再思考

1

为什么要再次思考

临近毕业,自己投了很多简历,也参加了好多轮的面试,不论结果如何,自己也从中收获了很多,发现了自己的诸多不足。大多数的面试都会提到面向对象的问题,虽然自己上了很多面向对象编程的课,学习了几门面向对象的语言,但当坐在面试官面前面对面讨论面向对象问题时,自己的无知立马显现出来,所以我觉得有必要再次思考什么是面向对象(Object Oriented)。

什么是对象?

面向对象中的对象(Object)是什么呢?面向对象程序设计(OOP)及时使用对象来进行程序设计,对象就是代表现实世界中可以明确分辨的一个实体,一个人、一辆车、一个圆、一次交易都可以看作是一个对象,不管是真实存在的还是虚拟的。一个对象通常都有自己的特征和行为。

  • 对象的特征(property),也可以称为属性(attribute),是一个数据域
  • 对象的行为(behavior),也可以称为动作(action),只这个对象可以去做什么

面向过程到面向对象的过渡

刚开始接触编程时,还是学的C语言(多么古老而又不可替代的语言啊),慢慢接触到了C++,Java,PHP等面向对象的语言,面向过程到面向对象的转换并非易事,首先就是思考问题的方式,落到实际行动就是面向过程首先关注这个函数怎么写,面向对象首先关注如何把要解决的问题抽象出来,编写父类、子类。

类与对象

面向对象中的类是一个模版、蓝本或者说是合约,用来定义一个对象的数据域是什么以及方法是做什么的。一个对象就是一个类的实例,可以从类中创建多个实例,类和对象之间的关系类似于生产玩具汽车的铸模和玩具汽车。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Car
{
public $branch = "default branch";
public $no = "A123";

function __construct($branch, $no) {
$this->branch = $branch;
$this->no = $no;
}

function run() {
echo "The car is running!";
}
}

$car1 = new Car("奔驰", "B111");
$car2 = new Car("宝马", "C222");

var_dump($car1);
var_dump($car2);

2686

什么是面向对象?

在学习面向对象时,经常会谈到面向对象的三大特点:封装、继承和多态,正是由于这三大特点,面向对象的世界才如此的丰富。对于我来说,尽管接触了这么长时间的面向对象,但如果要我具体说一下这三大特点,我可能还要查资料才说的明白,所以还是有必要把整个学习的过程记录下来。

封装 Encapsulation

在讨论这三大特点时,往往最容易忽视的就是封装,而在开始学习的时候,初学者并没有太在意这个问题,因为刚开始接触的时候,不管是声明属性还是方法,直接public ...,因为这是最简单而且有效的,当然书上包括老师上课也讲了publicprotectedprivate的一些关系,但新手并不会对项目的工程化做太多的思考,代码跑起来就行了,这在阅读别人的代码时一眼就能看出些这个代码的人是新手还是老手,是否真正理解封装,善用访问修饰符。

public可以用啊,为什么还要使用其他的访问修饰符呢?public, protected, private的访问权限:

  • 任何地方都可以访问public属性和方法
  • 当前类才能访问private属性和方法,子类也不能访问
  • 当前类和子类都可以访问protected属性和方法,其他的外部代码都不能访问

先理解什么是封装?简单的说,封装就是通过众所周知的接口将用户与实际应用程序的内部工作原理分离。人都有好奇心,总喜欢朝着未知探索,喜欢把东西拆开,了解里面的小零件如何工作,虽然能得到精神上的满足,但有时深入了解事物内部的工作原理并不是必要的,我们每天都在使用计算机和手机,但很少有人真正了解它的工作原理,但即便不理解原理,也不想我们使用它们,因为接口隐藏了这些细节。

要怎么实现呢?最简单的方式就是给属性和方法添加访问修饰符(public, protected, private)。通过对客户端代码隐藏属性,就可以防止对对象的属性恶意修改了。其次还可以通过制作接口,把细节隐藏起来。

public这里不多讲,先来讲protected吧,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?php
class Animal {
protected $name = "default name";

protected function say() {
echo "Name: " . $this->name . "\n";
echo "animal say\n";
}
}

class Dog extends Animal {

public function f1() {
$this->say();
}
}

$dog = new Dog();
$animal = new Animal();

echo $dog->f1();

echo $animal->name;
?>

5741

可以看到Dog类继承自Animal类,在Dog类中可以获取到name属性,还能执行say()方法,结果中我们还看到,程序执行到23行就报错了,很明显,我们在外面定义了一个Animal变量,并试图访问protected修饰的name属性,导致程序出错。

再来看看private

1
2
3
4
5
6
7
8
9
<?php
class Foo
{
private $key = "This is a private value";
}

$foo = new Foo;
echo $foo->key; // Error 类外不能访问private属性
?>

6863

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php
class Foo
{
private $key = "This is a private value";
}

class Bar extends Foo
{
public function f1() {
echo $this->key . '\n';
}
}

$bar = new Bar;
$bar->f1(); // Error 子类中也不能访问private属性
?>

8391

看到子类Bar并没有从父类Foo继承private属性。

继承 Inheritance

继承是从一个基类得到一个或多个派生类的机制。通过定义一个从其他类继承过来的类,我们可以确保一个类拥有其自由的功能和父类的功能(除private方法或属性),另一种理解继承的思路是“搜索”,当我们调用一个变量或方法时,会先在当前调用的类中查找有没有这个属性或方法,如果没有就查找父类中的默认实现。

现在有CDProductBookProduct两种商品,我么分别用继承和不继承来对比下:

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
61
<?php
class CDProduct {
public $playLength;
public $title;
public $mainName;
public $firstName;
public $price;

public function __construct($title, $mainName, $firstName, $price, $playLength) {
$this->title = $title;
$this->mainName = $mainName;
$this->firstName = $firstName;
$this->price = $price;
$this->playLength = $playLength;
}

public function getPlayLength() {
return $this->playLength;
}

public function getSummaryLine() {
$base = "{$this->title} ( {$this->mainName}, {$this->firstName})";
$base .= " playing time = {$this->playLength}";
return $base;
}

public function getProucer() {
return "{$this->firstName} {$this->mainName}";
}
}

class BookProduct {
public $numPages;
public $title;
public $mainName;
public $firstName;
public $price;

public function __construct($title, $mainName, $firstName, $price, $numPages) {
$this->title = $title;
$this->mainName = $mainName;
$this->firstName = $firstName;
$this->price = $price;
$this->numPages = $numPages;
}

public function getNumberOfPages() {
return $this->numPages;
}

public function getSummaryLine() {
$base = "{$this->title} ( {$this->mainName}, {$this->firstName})";
$base .= " page count = {$this->numPages}";
return $base;
}

public function getProucer() {
return "{$this->firstName} {$this->mainName}";
}
}
?>

可以看到,在没有使用继承的情况下,我们做了很多重复的劳动,每个产品都有getSummaryLine()方法,每个类中的getProducer()方法完全相同,他们的构造方法中用同样的方法设置了许多相同的属性,这样的代码看起来一点都不干净,即使我们我可以不停地复制、粘贴,仍然存在问题,不利于后期代码的重构。我们现在使用继承来优化它:

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
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
<?php
class Product
{
private $title;
private $mainName;
private $firstName;
protected $price;

public function __construct($title, $firstName, $mainName, $price) {
$this->title = $title;
$this->firstName = $firstName;
$this->mainName = $mainName;
$this->price = $price;
}

public function getFirstName() {
return $this->firstName;
}

public function getMainName() {
return $this->mainName;
}

public function getTitle() {
return $this->title;
}

public function getPrice() {
return $this->price;
}

public function getProucer() {
return "{$this->firstName} {$this->mainName}";
}

public function getSummaryLine() {
$base = "{$this->title} ( {$this->mainName}, {$this->firstName})";
return $base;
}
}

class CDProduct extends Product {
private $playLength = 0;

public function __construct($title, $firstName, $mainName, $price, $playLength) {
parent::__construct($title, $firstName, $mainName, $price);
$this->playLength = $playLength;
}

public function getPlayLength() {
return $this->playLength;
}

public function getSummaryLine() {
$base = parent::getSummaryLine();
$base .= " : playing time = {$this->playLength}";
return $base;
}
}

class BookProduct extends Product {
private $numPages = 0;

public function __construct($title, $firstName, $mainName, $price, $numPages) {
parent::__construct($title, $firstName, $mainName, $price);
$this->numPages = $numPages;
}

public function getNumberOfPages() {
return $this->numPages;
}

public function getSummaryLine() {
$base = parent::getSummaryLine();
$base .= " : plage count = {$this->numPages}";
return $base;
}
}

多态 Polymorphism

多态是来自希腊语的一个术语,原来的意思是“有多种形态”。多态的三大特征:

  • 子类继承父类
  • 子类重写父类
  • 父类指向子类

多态实现的前提:必须是类与类之间要有关系,要么继承,要么实现,存在重写(override),其实就是抽象函数或接口。
多态的应用:父类对象的引用指向子类对象,其实本质上就是一个向上转型。
多态的好处:大大提高程序的扩展,增强系统的灵活性,降低模块间的耦合。

举个模型例子,一家公司有员工类(Employee),还有其子类:销售(Sales)、市场(Market)、工程师(Engineer)等。某一天老板招待所有的员工开了个短会,完了之后对所有的员工(Employee)说,大家回去工作吧。在这里我们可以认为老板调用了所有员工对象的continueToWork()方法,而不是对一个个员工细说做什么工作,比如对销售部说你去制定销售计划(调用makeSalePlan();),对市场部说你去制定产品的价格(调用makeProductPrice();)….这种逐个细说的方式并不是面向对象,而是面向个体。可以确定的是,员工类应该有一个方法continueToWork()。而员工如何实现他们工作的方法却只有精确到子类才能确定,因为不同的员工的工作方式是不一样的。因此,我们很多时候只需要关心对象的父类型,而忽略更精确的子类型,比如上面老板叫大家回去工作时,他对全体员工说的,主要指的是全体员工类型。
上述的UML图:

22520

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
<?php
abstract class Employee{
abstract function continueToWork();
}
class Sales extends Employee{
private function makeSalePlan(){
echo "make sale plan";
}

public function continueToWork(){
$this->makeSalePlan();
}
}

class Market extends Employee{
private function makeProductPrice(){
echo "make product price";
}

public function continueToWork(){
$this->makeProductPrice();
}
}

class Engineer extends Employee{
private function makeNewProduct(){
echo "make new product";
}

public function continueToWork(){
$this->makeNewProduct();
}
}

class Demo{
public function Work($employeeObj){
$employeeObj->continueToWork();
}
}
//调用
$obj = new Demo();
$obj->Work(new Sales());
$obj->Work(new Market());
$obj->Work(new Engineer());
?>

总结

面向对象能将相同的属性封装在一起,同时又能复用很多代码,同时针对不同情况,还能实现多种形态,这就是面向对象的强大之处,有了它我们可以更方便地写出易于管理的代码,但作为初学者,要深刻理解它的三大特点,灵活运用也并非易事。
上课时,老师稍微对一个知识多讲了几遍就觉得不耐烦,觉得自己已经懂了,但自己真正遇到的时候,又觉得自己什么都不懂,书到用时方恨少,学习不能浅尝辄止,蜻蜓点水,要静下心来学习。