深入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) 收藏 举报
浙公网安备 33010602011771号