深入PHP面向对象、模式与实践——对象

PHP与对象

  • 最初:php/FI

    PHP于1994年由Rasmus Lerdorf创建,刚刚开始是Rasmus Lerdorf 为了要维护个人网页而制作的一个简单的用Perl语言编写的程序。PHP原始为Personal Home Page的缩写,意为“个人主页工具”,而FI即Form Interpreter,意为表单解释器,现在已经正式更名为 “PHP: Hypertext Preprocessor”的缩写。这两个工具用C语言重写后组成了PHP/FI 2.0,这时还没有对象。

  • 语法糖:PHP 3

    PHP3是由PHP/FI 2.0完全重写成的,但是对象并不是新语法中必需的部分。在这个时期对象这是个边缘话题,PHP手册只用了一句话和一段代码示例来介绍对象,而且该示例并未介绍对象的继承和属性等。

  • 一场静悄悄的革命:PHP 4

    PHP 4可算是夯实PHP基础的另一个阶段,因为此时PHP的大部分核心代码都被重写了。Zend引擎开始为PHP提供强大动力。在PHP 4中,我们可以在子类中覆盖并访问父类的方法。然而主要的不足在于把对象赋值给变量,传递对象到函数,或从方法返回对象,结果都会创建对象的副本。当然,你可以使用使用&来按引用赋值,例如:other = &my_obj;// other my_obj指向相同对象。这厮PHP手册中不仅包含语法,还特别增加了对象方面的内容,并且面向对象的代码开始成为主流。

  • 拥抱改变:PHP 5

    PHP 5明确表示支持对象和面向对象的程序设计,对象已经从事后的想法变成PHP语言的驱动力。可能最重要的改变是默认按引用传递的行为代替了问题多多的对象复制。但这只是一个开始,我们将会遇到更多扩展和增强PHP对象支持的更多变化,包括参数指示、私有、保护类型的方法及属性、static关键字、命名空间、异常等。

  • 拥护和疑虑:关于对象的争辩

    对象和面向对象设计引起了两方阵营的热烈争辩。我们一开始就应该记住那个著名的Perl格言:做一件事有很多种方法(There’s more than one way to do it)。写代码的技巧在于:搞清楚什么时候你一时的想法会成为另一个更大的开发的源头,在确定设计方案之前暂停大量的代码编写工作。

对象基础

  • 类和对象

简单地说,类是用于生成对象的代码模板。我们使用class关键字和任意的类名来声明类:

class ShopProduct
{
    //类体
}

如果类是生成对象的模板,那么对象是根据类中定义的模板所构造的数据。对象可以被说成是类的“实例”,它是由类定义的数据类型。new操作符和类名一起使用:

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

在PHP脚本中创建的每个对象也有唯一的身份,PHP会在一个进程内重复使用这些身份(或标识符,identifier)来访问这些对象。

var_dump($product1);
var_dump($product2);
//输出object(ShopProduct)#1 (0) { } object(ShopProduct)#2 (0) { }
  • 设置类中的属性

类可以定义被称为属性的特定变量。属性也成为成员变量,用来存放对象之间互不相同的数据。类的属性和标准的变量很相似,不过必须在声明和赋值前加一个代表可见性的关键字(keyword)。这个关键字可以是public、protected或private,它决定了属性的作用域。

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

//我们可以使用->字符连接对象变量和属性名来访问属性变量,如下所示:
$product1 = new ShopProduct();
print $product1->title;
//输出default product
//因为属性被定义为public,所有可以给属性赋值(也可以读取属性)。
$product1->title = "My Antonia";

事实上,PHP并没有强制所有的属性都要在类中声明。我们可以动态地增加属性到对象,如下所示:

$product1->arbitraryAddition = "treehouse";
//这种给对象赋属性的方法在面向对象编程中并不是一个良好的做法,我们基本不使用。

此时我们的对象依然很笨拙。当需要使用对象的属性时,必须在对象之外才能完成。用这种方式设置属性值有很多问题。因为PHP允许动态设置属性,所以如果拼错或忘记属性名时并不会得到警告。另外一个问题是类过于松散。我们没有被强制设置标题、价格或产品名,客户端代码可以确定的是这些属性的存在,但是面对的可能是默认值也可能不是。我们希望实例化ShopProduct对象时设置有意义的属性值。

  • 使用方法

    属性可以让对象存储数据,类方法则可以让对象执行任务。方法是类中声明的特殊函数。方法声明类似于函数声明。function关键字在方法名之前,方法名之后圆括号中的是可选的参数列表。方法体用大括号括起来:

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

方法也可以接受限定词,包括可见性关键字,public、protected或private。将方法声明为public,就可以在当前对象之外调用该方法。如果省略关键字,那么将被隐式声明为public。在大多数情况下,我们可以使用->连接对象变量和方法名来调用方法。调用方法时必须使用一对圆括号。

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

    public function getProducer()
    {
        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->getProducer()}\n";
//输出author:Willa Cather

我们使用$this伪变量(pseudo-variable)把类指向一个对象实例。

创建构造方法:
创建对象时,构造方法[constructor method,也称为构造器(constructor)]会被自动调用,确保必要的属性被设置,并完成任何需要准备的工作。在PHP5中,构造方法命名为__construct()。

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

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

    function getProducer()
    {
        return "{$this->ProducerFirstName}" . "{$this->producerMainName}";
    }
}

当使用new操作符创建对象时,构造方法会被调用。任何给定的参数都将被传递给构造方法。

$product1 = new ShopProduct("My Antonia", "Willa", "Cather", 5.99);
print "author: {$product1->getProducer()}\n";
//输出author:Willa Cather
  • 参数和类型

PHP是一种弱类型语言,即变量不需要声明为特定的数据类型。这并不是说PHP没有类型概念,每个赋给变量的值都有一种类型。可以使用PHP的类型检查函数来确定变量的值的类型。处理方法和函数的参数时,检查变量的类型尤为重要。下面看一个可能遇到的问题。

\\settings.xml
<settings>
    <resolvedomains>false</resolvedomains>
</settings>
<?php
/**
 * 应用程序提取字符串“false”作为特征值传递给outputAddresses()方法,这个方法用于显示ip地址数据。
 */
class AddressManager
{
    private $addresses = array("209.131.36.159", "74.125.19.106");

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

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

代码不会按照我们期望那样执行。事实上,字符串“false”在测试中解析为true。这是因为php在测试变量时会转化一个非空字符串值为布尔值true。有许多方法可以纠正这个错误。
你可以使outputAddresses()方法更宽松,让它可以识别字符串并用一些基本规则将字符串转换为相等的布尔值。

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

你也可以添加注释,明确说明参数应该是一个布尔值。

/**
*输出地址列表
*如果$resolve为true,则每个地址都会被解析成域名
*@param $resolve Boolean是否解析地址?
*/
function outputAddresses($resolve){
    //...
}

最后,你也可以让方法一个检查参数中的数据类型

function outputAddresses($resolve){
    if (!is_bool($resolve)) {
        die("outputAddresses() requires a Boolean argument\n");
    }
    //...
}

在面向对象开发中,“专注特定任务,忽略外界上下文”是一个重要的设计原则。采用何种策略取决于潜在bug严重程度,你应该仔细权衡。无论你如何解决这类问题,你都要认真思考一件事:类型处理。
获得提示:对象类型。

class ShopProductWriter
{

    public function write($shopProdut)
    {
        $str = "{$shopProdut->title}:" . $shopProdut->getProducer() . "({$shopProdut->price})\b";
    }
}
$product1 = new ShopProduct("My Antonia", "Willa", "Cather", 5.99);
$writer = new ShopProductWriter();
$writer->write($product1);

我们把参数变量命名为$shopProdut,说明该方法希望接受一个ShopProduct对象,但是我们并没有强制要求这一点,可能接受到非预期的对象或基本类型。为了解决这个问题,PHP5引入了类型提示(type hint)。要增加一个方法参数的类型提示,只需要简单地将类名放在需要约束的方法参数之前。

public function write(ShopProduct $shopProdut){
    //...
}

类型提示不用于强制规定参数为某种基本数据类型,比如字符串和整型。但你可以强制规定使用数组作为参数。这被称为数组提示。

function setArray(array $storearray){
    $this->array = $storearray;
}

还有对null默认值的参数提示,可以指定参数为一个特定类型或null值:

function setWriter(ObjectWriter $objwriter=null){
    $this->writer = $objwriter;
}
  • 继承
    继承是从一个基类得到一个或多个派生类的机制。创建继承树的第一步是找到现有基类元素中不适合放在一起,或者不需要进行特殊处理的类方法。
class ShopProduct
{

    private $title;
    private $producerMainName;
    private $producerFirstName;
    protected $price;
    private $discount = 0;

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

    }

    public function getProducerFirstName()
    {
        return $this->producerFirstName;
    }

    public function getProducerMainName()
    {
        return $this->producerMainName;
    }

    public function setDiscount($num)
    {
        $this->discount = $num;
    }

    public function getDiscount()
    {
        return $this->discount;
    }

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

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

    public function getProducer()
    {
        return "{$this->ProducerFirstName}" . "{$this->producerMainName}";
    }

    public function getSummaryLine()
    {
        $base = "$this->title({$this->producerMainName},";
        $base .= "{$this->ProducerFirstName})";
        return $base;
    }
}

class CdProduct extends ShopProduct
{
    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 ShopProduct
{
    private $numPages = 0;
    public function __construct($title, $firstName, $mainName, $price, $numPages)
    {
        parent::__construct($title, $firstName, $mainName, $price);
        $this->numPages = $numPages;
    }
    public function getNumberPages()
    {
        return $this->numPages;
    }

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

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

如果派生类没有定义构造方法,在实例化时会自动调用父类的构造方法。子类默认继承了父类所有的public和protected方法。上面两个子类都覆写了getSummaryLine()方法,提供了自己独特的实现。派生类可以扩展和修改父类的功能。在子类中定义构造方法时,需要传递参数给父类的构造方法,否则你的得到的可能是一个构造不完整的对象。要调用父类的方法,首先要找到一个引用类本身的途径:句柄(handle)。PHP提供了parent关键字。要引用一个类而不是对象的方法,可以使用::而不是->。每个子类都会在设置自己的属性前调用父类的构造方法。我们应该避免告诉父类任何关于子类的信息,这是一条经验规则。parent可以在任何覆写父类方法中使用,我们并不希望删除父类的功能,而是扩展它,通过在当前对象中调用父类的方法可以达到这个目的。事实上,我们利用ShopProduct类中已经存在的功能,而不应该重复开发。

类中的元素可以被声明为public、private或protected。

  • 在任何地方都可以访问public属性和方法。
  • 只能在当前类中访问private方法或属性。
  • 可以在当前类或子类中访问protected方法或属性。

一般来说,我们倾向于控制可访问性。最好将类属性初始化为private或protected,然后在需要的时候再放松限制条件。类中的许多方法都可以是public,但是如果拿不定主意的话就限制一下。有些类只为类中其他方法提供本地功能,与类外部的代码没有任何联系,应该将其设置为private或protected。

当客户程序员需要使用类中保存的值时,通常比较好的做法是不要允许直接访问属性,而是提供方法来取得属性的值,这样的方法称为访问方法,也可称为getter和setter。

还记得ShopProductWriter类使用ShopProduct对象输出清单数据吗?我们将改进她,使其一次能输出许多ShopProduct对象:

class ShopProductWriter
{
    private $products = array();
    public function addProduct(ShopProduct $shopProduct)
    {
        $this->products[] = $shopProduct;
    }
    public function write()
    {
        $str = "";
        foreach ($this->products as $shopProduct) {
            $str .= "{$shopProduct->title}:";
            $str .= $shopProduct->getProducer();
            $str .= ({ $shopProduct->getPrice()})\n"
        }
        print $str;
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值