类和对象:面向对象

上一节我们学习了类的结构,大致清楚了一个类是如何创建的,其属性和方法是怎么进行访问的,并将图书类作为示例进行了详细的分析。

这一节我们将学习面向对象的基本概念。

面向对象并不是一种语法或者结构,而是一种编程范式(一类典型的编程风格),当然还有其他的编程范式,例如面向过程式编程、函数式编程等。不同语言对不同编程范式的支持度不同,这里讲解PHP支持的面向对象式编程。

在前两节我们了解了类和对象的基本概念,那么面向对象就是将对象作为基本单元,对数据进行封装,同时各个对象保持独立又可相互调用的编程思想

提到面向对象,就不得不说起它的三大特性:继承封装多态

继承

继承(Inheritance)具象到我们实际生活中,很容易联想到“继承家业”(虽然笔者并没有家业要继承),面向对象中的继承也有类似的意思。继承发生在父类(也叫基类)和子类之间,父类概念与定义比子类更抽象、简单,而子类则会更加具体,除了可以继承父类的特性之外,还可以定义自己独特的属性和方法,也叫做派生类。

打个比方,在上一节的图书馆案例中,管理员拥有读者所有的功能,又比读者拥有更多的权限,那么我们可以将读者类作为基类,管理员类作为子类。读者类有一个属性“读者号”和一个方法“借书”,其子类(管理员类)可以继承这个方法和属性,就像下面的代码:

<?php

// 读者类

class Reader
{
    public $rdnum = '000000';// 读者号

    public function __construct()
    {
        // 构造函数
    }

    public function borrow()
    {
        // 方法:借书
        echo '你调用了', __CLASS__, '类的方法:', __FUNCTION__, PHP_EOL;
    }
}

// 管理员类

class Admin extends Reader
{
    public $adnum = '000';// 管理员号

    public function __construct()
    {
        // 构造函数
    }

    public function book_storage()
    {
        // 图书入库
        echo '你调用了', __CLASS__, '类的方法:', __FUNCTION__, PHP_EOL;
    }    
}

// 测试:

// 创建子类 Admin 类的一个实例
$admin = new Admin();
// 子类实例调用父类 Reader 的方法 borrow()
$admin->borrow();// 输出:你调用了Reader类的方法:borrow

// 创建父类 Reader 的一个实例
$reader = new Reader();
// 父类实例试图调用子类方法
$reader->book_storage();// 输出:Fatal error: Uncaught Error: Call to undefined method Reader::book_storage()

从示例代码我们可以看到,子类可以调用父类的方法及属性(访问控制为public或者protected时),而父类不能调用子类的方法及属性。即:父类允许继承的一切,子类都可以使用。

在 PHP 中,一个子类只允许继承一个父类,不支持多重继承。

在 PHP 中使用关键字extends来表示继承,其后跟当前类所要继承的父类名称。

封装

顾名思义,封装是将一系列数据及行为“封”起来,不让其他人知道其细节,通过设置数据的访问权限(publicprotectedprivate)以避免类成员变量被随意更改。

例如在上述的图书馆借阅系统,读者一次最多可借阅十本书,读者可查询自己还可以借阅图书的数目,但不能更改这个数目,示例代码如下:

<?php

// 读者类

class Reader
{
    public $rdnum = '000000';// 读者号
    private $brnum = 10;// 剩余可借阅数量:初始值为10

    public function __construct()
    {
        // 构造函数
    }

    public function borrow()
    {
        // 方法:借书
        // echo '你调用了', __CLASS__, '类的方法:', __FUNCTION__, PHP_EOL;
        // 借书成功后,剩余可借阅图书数量 - 1
        $this->brnum--;
    }

    public function getBrnum()
    {
        // 方法:返回可借阅书籍数量
        return $this->brnum;
    }
}

// 测试:

$reader = new Reader();
// 查看目前可借阅书籍数量
echo '剩余可借阅书籍数量:', $reader->getBrnum(), PHP_EOL;// 输出:剩余可借阅书籍数量:10
// 借书
$reader->borrow();
// 再次查看目前可借阅书籍数量
echo '剩余可借阅书籍数量:', $reader->getBrnum(), PHP_EOL;// 输出:剩余可借阅书籍数量:9

上述代码中可借阅书籍数量为私有属性,外部无法直接看到,只能通过接口getBrnum()方法进行访问,并且在没有公开修改其值的方法时,外部无法修改它。这就是封装。

通过封装,合理的隐藏部分细节代码,可使代码更加容易理解与维护,同时也增强了安全性。

多态

由继承产生的相关而又不相同的类中,相同的方法有着不同的表现形式,这就叫多态。PHP 中由于其弱类型特性,此特性多通过接口来表现。

比如说在数学图形中,不同图形如矩形、三角形和圆形,其都具有求周长的方法,但各种图形求周长的方法都不一样。表现在程序设计中,就是:

  • 接口:求周长
  • 矩形类、圆形类:继承接口,实现“求周长”的方法

示例代码如下:

<?php

interface iPerimeter
{// 接口
    public function perimeter();
}

class Rectangle implements iPerimeter
{// 矩形类
    public $length;
    public $width;

    public function __construct($width, $length)
    {
        $this->width = $width;
        $this->length = $length;
    }

    public function perimeter()
    {
        // 实现 求周长 的接口
        return ($this->length + $this->width) * 2;
    }
}

class Circle implements iPerimeter
{// 圆形类
    public $radius;

    public function __construct($radius)
    {
        $this->radius = $radius;
    }

    public function perimeter()
    {
        // 实现 求周长 的接口
        return $this->radius * M_PI * 2;
    }
}

// 测试
$rect = new Rectangle(3, 4);
echo '矩形周长:', $rect->perimeter(), PHP_EOL;// 输出:矩形周长:14
$cir = new Circle(3);
echo '圆形周长:', $cir->perimeter(), PHP_EOL;// 输出:圆形周长:18.849555921539

都是求周长,但求的方式不同,这就是多态。


在本节中我们了解了面向对象的三个特性:继承封装多态,并用不同的案例分别具象化了这三个特性,理解面向对象编程可帮助我们更加简单地设计并维护程序,也更加便于设计、分析代码。

由于本节示例代码中使用了部分前面未仔细讲解的内容(继承、接口),接下来的几节中将详细讲解三种特殊的类接口Trait