深入PHP面向对象、模式与实践(第5版)-马特.赞德斯彻著-第一部分对象-第3章对象基础

第一部分 对象

第3章 对象基础

3.1.类和对象

3.1.1.第一个类

class ShopProduct
{
    // 类体
}

3.1.2.一个(或两个)对象

$product1 = new ShopProduct();
$product2 = new ShopProduct();
var_dump($product1);
var_dump($product2);

// 输出
object(ShopProduct)#1 (0) {
}
object(ShopProduct)#2 (0) {
}

注意:老版本的 PHP(PHP 5.1 之前)可以直接打印一个对象,这会将对象转换为一个包含对象 ID 的字符串。但是自 PHP 5.2 开始,PHP 就不再支持这个功能了。除非在这个对象的类中定义有 __toString() 方法,否则任何时候试图将一个对象当作字符串处理都会导致错误。

3.2.设置类中的属性

我们可以在类中定义称为 "属性" 的特殊变量。属性也被称为 "成员变量",用于保存在各个对象中不同的数据。

除了再声明它们时必须可见性关键字,类中的属性与普通变量看起来非常相似。可见性关键字可以是 pubic、protected 或 private,它确定了属性能够被访问的作用域。

class ShopProduct
{
    public $title = "default product";
    public $producerMainName = "main name";
    public $producerFirstName = "first name";
    public $price = 0;
}

我们看到,我定义了四个属性,并分别给它们赋予了默认值。现在,任何从 ShopProduct 类中实例化出的对象都会被这些默认值初始化。各个属性都是以 public 关键字声明的,确保可以从对象外部访问这些属性。

可以使用字符 ->(对象运算符)连接对象变量和属性名来访问属性变量。

$product1 = new ShopProduct();
print $product1->title;

# 输出
default product

由于属性被定义为 public,因此既可以赋值给它们,覆盖类中设置的默认值,也可以读取它们的值。

$product1 = new ShopProduct();
$product2 = new ShopProduct();
$product1->title = "My Antonia";
$product2->title = "Catch 22";

不过,尽管能够初始化这些值,但之后各个 ShopProduct 对象中的这个属性的值可能会不同。

注意:调用类、函数或方法的代码通常都被称为该类、函数或方法的客户端或客户端代码。

事实上,PHP 并不强制要求在类中声明所有属性。可以动态地向一个对象添加属性。

$product1->aritraryAddition = "treehouse";

不过,在面向对象编程中向对象赋予属性并非一种好的实践。

为什么动态地设置属性是一种糟糕的实践呢?创建类就是定义了一种类型,是告诉其他开发人员这个类(以及任何实例化自它的对象)包含一组字段和函数。如果 ShopProduct 类定义了一个 $title 属性,那么任何与 ShopProduct 对象一起工作的代码都是以 $title 属性的可用性为工作前提的,但动态设置属性就失去了这种保证。

在目前这个阶段,这个对象仍然不太好用。这是因为,当需要使用某个对象属性时,必须从对象外进行操作,进入对象内部设置和获取属性的值。随着对象数量和属性数量的增加,设置属性很快就会变得非常麻烦。

$product1 = new ShopProduct();
$product1->title = "My Antonia";
$product1->producerMainName = "Cather";
$product1->producerFirstName = "Willa";
$product1->price = 5.99;
print "author: {$product1->producerFirstName} " . "{$product1->producerMainName}\n";


// 输出
author: Willa Cather

这种设置属性值的方法有不少问题。PHP 允许动态设置属性,因此就算拼错属性名或是忘了属性名,PHP 也不会给出任何警告。假设我打算编写这行代码:

$product1->producerMainName = "Cather";

但不幸的是,我误写为如下代码:

$product1->producerFirstName = "Cather";  

就 PHP 引擎来说,这句代码是合法的,它不会给出任何警告。但当我试图打印制作者信息时,就得不到所期待的结果。

另一个问题是类的限制太宽松了。PHP 不会强制要求我们设置产品的名字、价格或制造商的名字。因此,尽管客户端代码知道这些属性肯定存在,但碰到的值可能是默认值也可能不是。理想情况下,我们希望实例化 ShopProduct 对象的人能将属性设置为有意义的值。

最后一个问题是,我们不得不重复一些需要频繁进行的处理。正如之前看到的,输出完整的制作者姓名就是一个烦人的过程。

如果能让对象代替我们做这件苦差事就好了。

其实,只要在 ShopProduct 对象内部编写一组函数,让它们在对象内部操作数据,这些问题就可以迎刃而解。

3.3.使用方法

就像属性允许对象存储数据一样,方法也允许对象执行任务。方法是在类中定义的特殊函数。

public function myMethod($argument, $anther)
{
    //......
}

如果在方法声明中省略了可见性关键字,那么该方法会被隐式地声明为 public。不过,为所有方法都显式地设置可见性是一个种优秀的实践。

class ShopProduct
{
    public $title = "default product";
    public $producerMainName = "main name";
    public $producerFirstName = "first name";
    public $price = 0;
    
    public function getProduct()
    {
        return $this->producerFirstName . " " . $this->producerMainName;
    }
}

多数情况下,我们都会通过使用->运算符连接对象变量和方法名来调用方法。与调用函数一样,即使不传递任何参数给方法,调用方法时也必须加上一对圆括号。

$product1 = new ShopProduct();
$product1->title = "My Antonia";
$product1->producerMainName = "Cather";
$product1->producerFirstName = "Willa";
$product1->price = 5.99;

print "author:{$product1->getProduct()}\n";


# 输出
author:Willa Cather

我在这个方法体内使用了 "伪变量" 特性。$this 伪变量是一种让类引用自己的对象实例的方式。如果你觉得这个概念难以理解,那么请试着用 "当前实例" 这个短语替换 $this,

即把语句:$this->productFirstName

理解为:当前实例的 $producerFirstName 属性

这样 getProduct() 方法会连接 $productFirstName 和 $productMainName 属性并返回链接后的值,省去了每次用到制作者全名都要进行处理的麻烦。

这么做只是稍微改善了一下这个类,其中仍然有一些不受欢迎的不确定性。这个类依赖客户端设置对象的属性值,这种方式有两个问题:首先,初始化一个 ShopProduct 对象需要五行代码,所有人都会觉得非常麻烦;其次,ShopProduct 对象被初始化时,无法保证所有属性都被设置了值。我们需要一个在实例化类的对象时会被自动调用的方法。

编写一个构造方法

构造方法会在对象被创建时调用。我们可以使用它继续进行初始化工作,确保所有必要的属性都被设置了值,所有必需的准备工作都已完成。

注意:在 PHP5 之前,构造方法的名字与类名相同,也就是说 ShopProduct 类的构造方法就是 ShopProduct()。现在这条约定不再适用于所有情况,而且已经被 PHP7 废除了。在 PHP7 中,构造方法的名称是 __construct()。

请注意方法名的开头有两条下划线,我们还会在 PHP 类的其他特殊方法中看到这种命名约定。

class ShopProduct
{
    public $title;
    public $producerMainName;
    public $producerFirstName;
    public $price = 0;

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

    public function getProduct()
    {
        return $this->producerFirstName . " " . $this->producerMainName;
    }
}

$product1 = new ShopProduct(
    "My Antonia",
    "Willa",
    "Cather",
    5.99
);
print "author: {$product1->getProduct()}\n";


# 输出
author: Cather Willa

这里的所有参数都会被传递给构造方法。构造方法会通过伪变量 $this 为对象的各个属性赋值。

注意:现在,ShopProduct 对象更易于实例化,使用起来也更安全,实例化和初始化只需要一句话即可。而且可以确信,任何使用 ShopProduct 对象的代码中的属性都已经被初始化了。

这种可预测性是面向对象编程的一个重要方面。我们所设计的类应当能让对象的调用者知道它有哪些特性。一种能确保安全使用类的方法是,确保类中属性的数据类型是可预测的,例如确保 $name 属性的值都是由字符数据组成的。但如果这个数据可能会被传递到类外部,该如何确保这一点呢?

3.4.参数和类型

类型决定了 PHP 脚本管理数据的方式,例如,使用字符串显示字符数据或使用字符串函数操作字符数据、在数学表达式中使用整数、在条件表达式中使用布尔类型等。这些类型被称为基本类型。不过站在一个更高的角度看,一个类其实也定义了一种类型。因此 ShopProduct 对象不仅是 object 基本类型,同是也是 ShopProduct 类类型。

方法和函数定义并不要求明确指定参数的类型。这既是一种便利,也是一种麻烦。"参数可以是任何类型" 为代码带来了灵活性。我们可以编写一个响应不同数据类型的方法,根据不同的情况让其进行不同的处理。然而,当方法体期待接收一种类型的参数,但实际接收到了另外一种类型的参数时,这种灵活性也会引起歧义。

3.4.1.基本类型

PHP 是一门弱类型编程语言,这表示没有必要将变量声明为特定的数据类型。例如,变量 $number 可以在同一个作用域内存储数值 2 和字符串 two。而在 C 和 Java 等强类型编程语言中,在将值赋给某个变量前,必须声明该变量的类型,而且也只能赋给它该类型的值。

但这并不意味着 PHP 没有类型的概念,每个被赋给变量的值都有一个类型。可以使用 PHP 的类型检查函数确定变量值的类型。

类型检查函数 类型 说明
is_bool() Boolean true 或 false 两个特殊值之一
is_integer() Integer 整数值,等价于 is_int() 和 is_long()
is_double() Double 浮点类型数值(一个带小数点的数值),等价于 is_float()
is_string() String 字符数据
is_object Object 对象
is_array Array 数组
is_resource Resource 识别和使用外部资源(数据库和文件等)的句柄
is_null Null 一个未被赋值的值

在于方法参数和函数参数打交道时,检查参数类型格外重要。

基本类型错误:一个示例

我们需要时刻留意代码中数据的类型。下面是一个可能会遇到的与类型相关的错误。

假设需要从 XML 文件中提取配置的值,其中 XML 节点<resolvedomains>指定应当解析 IP 地址还是域名。这是一个非常重要但性能开销很大的处理过程。

<settings>
    <resolvedomains>false</resolvedomains>    
</settings>

应用程序需要将字符串 false 提取出来,并将其作为一个标识位传递给显示 IP 地址的方法 outputAddresses()。

class AddressManager
{
    private $addresses = ["209.131.36.159", "216.58.213.174"];
    
    public function outputAddresses($resolve)
    {
        foreach ($this->addresses as $address)
        {
            print $address;
            if ($resolve)
            {
                print "(" . gethostbyaddr($address) . ")";
            }
            print "\n";
        }
    }
}

当然,这个 AddressManager 类还有改善的余地。例如,尽管 outputAddresses() 方法会循环 $addresses 数组属性,打印出其中的所有元素,但将 IP 地址硬编码在类中并不好。当 $resolve 参数变量本身就是 true 时,这个方法会输出域名以及 IP 地址。

下面是一段使用 AddressManager 类处理配置文件中的 XML 元素的示例带啊吗。请试着找出其中的问题。

$settings = simplexml_load_file(__DIR__."/resolve.xml");
$manager = new AddressManager();
$manager->outputAddresses((string)$settings->resolvedomains);

# 输出
209.131.36.159(UNKNOWN-209-131-36-X.yahoo.com)
216.58.213.174(par21s04-in-f14.1e100.net)

这段代码使用 SimpleXML API 去获取 resolvedomains 元素的值。在本例中,我知道这个值是文本元素 "false",而且如 SimpleXML 文档建议的那样,我将它转换为了一个字符串。

然而,这段代码并不会如我们所期待的那样正常工作。在将字符串 "false" 传递给 outputAddresses() 方法时,我误解了该方法都参数的隐式假设——该方法期待传入的是一个布尔类型的值(也就是 true 或 false)。而字符串 "false" 其实会在测试参数类型时被转换为布尔值 true。

if ("false")
{
    //...    
}

实际上,它等价于

if (true)
{
    //...
}

有很多方法可以解决这个问题。

一种方法是让 outputAddresses() 的限制更加宽松,即让它在识别到参数是字符串后,根据一些基本规则将参数转换为对应的布尔值。

class AddressManager
{
    private $addresses = ["209.131.36.159", "216.58.213.174"];

    public function outputAddresses($resolve)
    {
        if (is_string($resolve))
        {
            $resolve = (preg_match("/^(false|no|off)$/i", $resolve)) ? false : true;
        }

        foreach ($this->addresses as $address)
        {
            print $address;
            if ($resolve)
            {
                print "(" . gethostbyaddr($address) . ")";
            }
            print "\n";
        }
    }
}

$settings = simplexml_load_file(__DIR__."/resolve.xml");
$manager = new AddressManager();
$manager->outputAddresses((string)$settings->resolvedomains);  

不过,我们应当避免采取这种设计方式。通常,为方法或函数提供一个清晰、严格的接口,比模糊、宽松的接口更好。如果函数和方法的接口过于模糊、宽松,程序就可能混乱,续而产生 bug。

我们还可以修改 outputAddresses(),只是加入一段注释,清晰地说明 $resolve 参数应当是一个布尔值,这种方法要求程序员必须阅读这段注释,否则在编程时就会犯错。

class AddressManager
{
    private $addresses = ["209.131.36.159", "216.58.213.174"];

    /**
     * 输出 IP 地址列表
     * 如果 $resolve 为 true,那么解析所有的 IP 地址
     * @param $resolve boolean 是否解析 IP 地址? 
     */
    public function outputAddresses($resolve)
    {
        foreach ($this->addresses as $address)
        {
            print $address;
            if ($resolve)
            {
                print "(" . gethostbyaddr($address) . ")";
            }
            print "\n";
        }
    }
}

$settings = simplexml_load_file(__DIR__."/resolve.xml");
$manager = new AddressManager();
$manager->outputAddresses((string)$settings->resolvedomains);

如果编写客户端代码的程序员总是会勤勉地阅读注释,那么这也是一种合理的方法。

最后一种方法是让 outputAddresses() 严格地检查参数 $resolve 的数据类型。在 PHP7 发布之前,确实只有一种检查 Boolean 等基本类型的方法。我们需要编写一些代码检查接收的数据的类型,并在它不符合期待时采取一些措施。

class AddressManager
{
    private $addresses = ["209.131.36.159", "216.58.213.174"];
    
    public function outputAddresses($resolve)
    {
        if (! is_bool($resolve))
        {
            // 发出警告
        }
        
        foreach ($this->addresses as $address)
        {
            print $address;
            if ($resolve)
            {
                print "(" . gethostbyaddr($address) . ")";
            }
            print "\n";
        }
    }
}

$settings = simplexml_load_file(__DIR__."/resolve.xml");
$manager = new AddressManager();
$manager->outputAddresses((string)$settings->resolvedomains);

这种方法可以强制客户端代码确保 $resolve 的参数类型是正确的,否则程序就会报出警告。

注意:3.4.2 节介绍一种更好的方式来约束传递给方法或函数的参数类型。代替客户端将字符串参数转换为 Boolean 类型虽然更加友好,但也可能会带来其他问题。在进行转换时,我们需要猜测客户端代码的环境和意图。另一方面,通过强制规定 "参数必须为 Boolean 类型",可以让客户端决定是否将字符串转换为 Boolean 数据类型,以及将哪个单词转换为 true 或 false。这样的话,outputAddresses() 方法可以更专注于它自己应当执行的任务。在面向对象设计中,这种强调 "专注于特定的任务,忽视外部的上下文" 是一种重要的设计原则。

事实上,处理参数类型的策略取决于两点——潜在 bug 的严重程度,以及灵活性能带来多少好处。PHP 会根据上下文进行许多基本类型的转换。在数学表达式中,数字字符串会被转换为等值的整数或浮点类型。因此,我们的代码就会对类型错误非常宽松。不过,如果期待得到的方法参数是一个数组,那么就得更加小心了。将一个非数组值传递给 PHP 中接收数组作为参数的函数,不仅不会得到正确的结果,还可能引发一连串错误。

因此,我们得在检查类型、转换类型、以及依赖良好、清晰的文档(无论决定采取哪种方法都应当提供文档)这三者之间做出权衡。

无论采取哪种方式解决这类问题,我们都可以确定:类型很重要。鉴于 PHP 是一门类型宽松的编程语言,这一点特别重要。我们无法指望编译器来阻止类型相关的 bug,所以必须仔细思考,当类型不正确的参数传递给方法时会有哪些潜在的问题。我们不应当指望客户端程序员来理解我们的设计方式,而是要思考如何用自己的方法应付这些传进来的错误类型的数据。

3.4.2.接受提示:对象类型

3.5.继承

3.5.1.继承问题

3.5.2.使用继承

3.5.3.public、private 和 protected:管理类的访问

3.6.小结

posted on 2020-02-05 20:14  herisson_pan  阅读(17)  评论(0)    收藏  举报

导航