PHP的继承
继承就是从基类中派生出一个或多个类的机制。如果一个类继承自另外一个类,那么就说前者是后者的子类。这种关系通常用父子关系来形容。子类派生自父类并继承了父类的特性,这些特性既包括属性也包括方法。通常,子类都会在父类(也被称为超类)所提供功能的基础上加入一些新功能。因此,也可以说子类扩展了父类。
在讲如何声明继承前,先来看看继承可以解决哪些问题。
继承问题
回顾一下ShopProduct类。目前它非常通用,适用于各类商品。
$product1 = new ShopProduct(
'My Antonia',
'Willa',
'Cather',
5.99
);
$product2 = new ShopProduct(
'Exile on Coldharbour Lane',
'The',
'Alabama 3',
10.99
);
print "author: {$product1->getProducer()}<br/>";
print "author: {$product2->getProducer()}<br/>";结果
author: Willa Cather
author: The Alabama 3将制作者名分为两部分对图书和CD都没有问题,目前无须担心将ShopProduct类用于多种商品。不过,只要稍微在本例中加入一些新的需求,事情立即就会变得复杂起来。假设现在需要展示图书和CD特有的数据。对于CD,我们必须存储整张CD的播放时间;而对于图书,则是整本书的页数。当然,两者之间还有其他差别,但现在这个足够讲解继承问题了。
我们应当如何扩展ShopProduct类来应对这个问题呢?目前想到两种方法:第一种,将所有的数据和方法都放到ShopProduct类中;第二,将ShopProduct分为两个单独的类。
先看看第一种方法。将CD和图书相关的数据放在同一个类种。
class ShopProduct
{
public $numPages;
public $playLength;
public $title;
public $producerMainName;
public $producerFirstName;
public $price = 0;
public function __construct(
string $title,
string $firstName,
string $mainName,
float $price,
int $numPages = 0,
int $playLength = 0
)
{
$this->title = $title;
$this->producerFirstName = $firstName;
$this->producerMainName = $mainName;
$this->price = $price;
$this->numPages = $numPages;
$this->playLength = $playLength;
}
public function getNumberOfPages()
{
return $this->numPages;
}
public function getPlayLength()
{
return $this->playLength;
}
public function getProducer()
{
return $this->producerFirstName . ' ' . $this->producerMainName;
}
}通过同时提供访问$numPages属性和$playLength属性的方法,来满足CD和图书这两种商品的需求。现在,实例化自这个类的对象会包含一些冗余的方法。此外,这种做法还存在实例化CD时必须向构造方法传递多余参数的问题。也就是说,CD中有与图书页数相关的信息和功能,图书种也有与播放时间相关的信息和功能。现在也许还可以忍受,但如果想添加更多商品类型,而且它们都有自己特有的方法,难道还得在ShopProduct类中为每个类型添加方法吗?如果那么做了,那么这个类会变得非常复杂且难以管理。
可以看到,强制在某类中加入原本不属于该类的字段,会产生具有冗余属性和方法的庞大对象。
不仅仅是数据上存在问题,功能上也存在问题。请考虑输出商品摘要信息的方法。假设销售部门要求在发票上打印出清晰的商品摘要信息:是CD的话,要包含CD播放时间;是图书的话,要包含图书的页数。因此,不得不为每种类型的商品实现不同的输出摘要信息的方法。可以使用一个标识位来确定对象的类型。
public function getSummaryLine()
{
$base = "{$this->title} ({$this->producerMainName}, ";
$base .= "{$this->producerFirstName} )";
if($this->type == 'book')
{
$base .= ": page count - {$this->numPages}";
}
elseif($this->type == 'cd')
{
$base .= ": playing time - {$this->playLength}";
}
return $base;
}可以先检查传递给构造方法的参数$numPages,从而决定设置给$type属性什么值。不过与之前一样,ShopProduct类没有必要变得如此复杂。随着在图书和CD这两种商品间加入更多的区别,抑或是加入新的商品,这些功能上的不同将会变得越来越难以管理。也许应该换一种思路来解决这个问题。
随着ShopProduct越来越像两个类挤在一个类中,应该接受现实————创建两个类比创建一个类好。
class CdProduct
{
public $playLength;
public $title;
public $producerMainName;
public $producerFirstName;
public $price;
public function __construct(
string $title,
string $firstName,
string $mainName,
string $price,
int $playLength
)
{
$this->title = $title;
$this->producerFirstName = $firstName;
$this->producerMainName = $mainName;
$this->price = $price;
$this->playLength = $playLength;
}
public function getPlayLength()
{
return $this->playLength;
}
public function getSummaryLine()
{
$base = "{$this->title} ({$this->producerMainName}, ";
$base .= "{$this->producerFirstName} )";
$base .= ": playing time - {$this->playLength}";
return $base;
}
public function getProducer()
{
return $this->producerFirstName . ' ' . $this->producerMainName;
}
}class BookProduct
{
public $numPages;
public $title;
public $producerMainName;
public $producerFirstName;
public $price;
public function __construct(
string $title,
string $firstName,
string $mainName,
string $price,
int $numPages
)
{
$this->title = $title;
$this->producerFirstName = $firstName;
$this->producerMainName = $mainName;
$this->price = $price;
$this->numPages = $numPages;
}
public function getNumberOfPages()
{
return $this->numPages;
}
public function getSummaryLine()
{
$base = "{$this->title} ({$this->producerMainName}, ";
$base .= "{$this->producerFirstName} )";
$base .= ": page count - {$this->numPages}";
return $base;
}
public function getProducer()
{
return $this->producerFirstName . ' ' . $this->producerMainName;
}
}复杂性问题得到了解决,但是也为此付出了代价。现在可以为每种类型的商品都创建一个getSummaryLine()方法,而且无须再检查商品类型。各个类也不用再维护与它们自身无关的字段和方法。而代价则是代码重复。在这两个类中,getProducer()方法的实现完全相同,构造方法也以相同的方式设置了多个相同的属性。这是一种令人不愉快的代码异味。
如果要确保所有类中getProducer()方法的实现都完全相同,那么对于任何一个实现所做的修改都必须反映到其他类中,稍不小心就会导致不同步。
即便有信心维护好该方法的多个副本,担心也是无法避免的,因为现在有两种类型,而不是一种。
还记得ShopProductWriter类吗?它的write()方法是只为ShopProduct这一种类型设计的。应当怎么修改,才能让它像之前那样工作呢?可以从方法签名中删除参数的类型声明,但这样就只能寄希望于调用放将正确类型的对象传递给write()方法了。另一种方法是在方法中添加一段检查参数类型的代码。
class ShopProductWriter
{
public function write($shopProduct)
{
if(
!($shopProduct instanceof CdProduct) && !($shopProduct instanceof BookProduct)
)
{
exit('Wrong type supplied');
}
$str = $shopProduct->title . ': '
. $shopProduct->getProducer()
. '(' . $shopProduct->price . ')';
print $str;
}
}请注意代码中的instanceof运算符。如果左侧操作数的对象是右侧操作数所表示的类型,那么instanceof运算结果为true。
不过这再次无谓地增加了代码地复杂性,因为我们不仅要在write()方法中检查$shopProduct参数的类型是否是我们所期待的两种类型之一,还要祈祷以后这两种类型会继续支持相同的字段和方法。如果能够只使用一种类型,代码就会简洁许多,因为可以继续在方法签名中指定参数的类型,而且可以确信ShopProduct类支持某个特定的方法。
让ShopProduct类同时适用于CD和图书不太好,但编写两个分别对应CD和图书的类也不太好。既希望让图书和CD分别作为一种类型,又希望能够为每种类型提供不同的实现。为了避免代码重复,我们希望能在一个类中提供共通功能,但允许其他类在处理一些方法调用时可以有所不同。这时就需要继承。
使用继承
创建继承树的第一步是找出基类中不适合放在一起或需要进行不同处理的元素。通过上面的讲解,可以看到getPlayLength()和getNumberOfPages()方法不适合放在一起。同时,还注意到必须为getSummaryLine()方法提供不同的实现。以这些区别为基础,编写两个派生类吧。
// 类是用于生成一个或多个对象的代码模板
// 使用 class 关键字和任意类名来声明
// 类名可以是任意字母和数字 但是不能以数学开头
class ShopProduct
{
public $numPages;
public $playLength;
public $title;
public $producerMainName;
public $producerFirstName;
public $price;
public function __construct(
string $title,
string $firstName,
string $mainName,
float $price,
int $numPages = 0,
int $playLength = 0
)
{
$this->title = $title;
$this->producerFirstName = $firstName;
$this->producerMainName = $mainName;
$this->price = $price;
$this->numPages = $numPages;
$this->playLength = $playLength;
}
public function getProducer()
{
return $this->producerFirstName . ' ' . $this->producerMainName;
}
public function getSummaryLine()
{
$base = "{$this->title} ({$this->producerMainName}, ";
$base .= "{$this->producerFirstName} )";
return $base;
}
}
// 继承 ShopProduct 类
class CdProduct extends ShopProduct
{
public function getPlayLength()
{
return $this->playLength;
}
public function getSummaryLine()
{
$base = "{$this->title} ({$this->producerMainName}, ";
$base .= "{$this->producerFirstName} )";
$base .= ":playing time - {$this->playLength}";
return $base;
}
}
class BookProduct extends ShopProduct
{
public function getNumberOfPages()
{
return $this->numPages;
}
public function getSummaryLine()
{
$base = "{$this->title} ({$this->producerMainName}, ";
$base .= "{$this->producerFirstName} )";
$base .= ":page count - {$this->numPages}";S
return $base;
}
}可以通过在类声明中使用extends关键字来创建一个子类。在本例中,创建了两个继承于ShopProduct的新类:BookProduct和CdPorduct。
因为派生出的类中没有定义构造方法,所以实例化它们时会自动调用父类的构造方法。子类会继承父类的public和protected方法(但不会继承private的方法和属性)。这意味着,尽管getProducer方法定义在ShopProduct中,但可以在实例化自CdProduct的对象上调用它。
$product2 = new CdProduct(
'Exile on Coldharbour Lane',
'The',
'Alabama 3',
10.99,
0,
60.33
);
print "author: {$product2->getProducer()}<br/>";这样的话,两个子类就都继承了同一个父类的行为。可以将BookProduct对象当作ShopProduct对象,将BookProduct对象或CdProduct对象传递给ShopProductWrite类的write()方法,一切依然会正常运转。
请注意,CdProduct类和BookProduct类都重写了getSummaryLine()方法,提供了它们各自的实现。派生类可以扩展和修改父类的功能。
getSummaryLine()方法在超类(父类)中的实现看起来有些多余,因为它的两个子类都重写了这个方法。但是,它不仅能够为新的子类提供直接可用的基本功能,还可以确保所有的ShopProduct对象都有getSummaryLine()方法。稍后将介绍另一种即使不提供实现方法也能提供同样保证的方式。现在,每个ShopProduct类的子类都会继承父类的属性,BookProduct和CdProduct会在它们各自的getSummaryLine()方法中访问$title属性。
乍一看,继承似乎是一个难以理解的概念。通过定义一个类继承自另外一个类,可以确保实例化后的对象既有自身的特性,也有其父类的特性。另一种帮助我们理解继承的思考方式是“追溯”。当$product2->getProducer()被调用时,由于在CdProduct类找不到这个方法,程序会调用ShopProduct类中该方法的默认实现。另一方面,当$product2->getSummaryLine()被调用时,由于CdProduct中有getSummaryLine()方法,它会被调用。
属性访问也是如此。当在BookProduct类的getSummaryLine()方法中访问$title时,由于BookProduct类中找不到该属性,程序会从父类ShopProduct中得到$title属性的值。$title属性同时适用于这两个子类,因此它应当属于超类。
然而,浏览ShopProduct的构造方法就会发现,仍然在基类中管理着那些应当由其子类处理的数据————BookProduct类应该处理$numPages参数和属性,CdProduct类应该处理$playLength参数和属性。可以通过在每个子类中定义构造方法来实现这一点。
构造方法和继承
当在子类中定义构造方法时,必须将参数传递给父类,否则构造出的就是不完整的对象。要调用父类中的方法,必须先找到一种引用父类自身的方法:一个句柄(handle)。PHP为此提供了parent关键字。要引用一个类的方法而不是对象的方法,可以使用::而不是->,如:parent::__construct(),这句代码的意思是“调用父类的__construct()方法”。下面修改代码,让每个类都只处理应当由它处理的数据。
class ShopProduct
{
public $title;
public $producerMainName;
public $producerFirstName;
public $price;
public function __construct(
$title,
$firstName,
$mainName,
$price
)
{
$this->title = $title;
$this->producerFirstName = $firstName;
$this->producerMainName = $mainName;
$this->price = $price;
}
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
{
public $playLength;
public function __construct(
string $title,
string $firstName,
string $mainName,
float $price,
int $playLength
)
{
parent::__construct(
$title,
$firstName,
$mainName,
$price
);
$this->playLength = $playLength;
}
public function getPlayLength()
{
return $this->playLength;
}
public function getSummaryLine()
{
$base = "{$this->title} ({$this->producerMainName}, ";
$base .= "{$this->producerFirstName} )";
$base .= ":playing time - {$this->playLength}";
return $base;
}
}
class BookProduct extends ShopProduct
{
public $numPages;
public function __construct(
string $title,
string $firstName,
string $mainName,
float $price,
int $numPages
)
{
parent::__construct(
$title,
$firstName,
$mainName,
$price
);
$this->numPages = $numPages;
}
public function getNumberOfPages()
{
return $this->numPages;
}
public function getSummaryLine()
{
$base = "{$this->title} ({$this->producerMainName}, ";
$base .= "{$this->producerFirstName} )";
$base .= ":page count - {$this->numPages}";
return $base;
}
}每个子类都会在设置自己的属性前调用父类的构造方法。基类现在只知道自己有哪些数据,子类通常都是父类的特化。有一条设计规则是,应当避免告诉父类任何与子类有关的信息。
调用被重写的方法
现在可以在所有重写父类方法的方法中使用parent关键字。在重写方法时,我们可能希望扩展父类中的功能,而不是去掉它。这时,可以通过在当前对象的上下文中调用父类的方法来实现此需求。回顾下getSummaryLine()方法中的实现,可以看到其中有大量重复代码。直接复用ShopProduct类中已经实现的功能比复制它更好。
// ShopProduct类...
function getSummaryLine()
{
$base = "{$this->title} ({$this->producerMainName}, ";
$base .= "{$this->producerFirstName} )";
return $base;
}
// BookProduct类...
function getSummaryLine()
{
$base = parent::getSummaryLine();
$base .= ":page count - {$this->numPages}";
return $base;
}上面的代码中,首先在基类ShopProduct中实现getSummaryLine()方法的核心功能;接着,在向摘要信息加入更多数据之前,只调用父类的这个方法,而不是将这段代码复制到CdProduct子类和BookProduct子类中。
在掌握了“继承”的基础知识后,再来看看属性和方法的“可见性”
public、private和protected:管理类的访问
到目前为止,所有属性都被声明为public。如果在属性声明时使用了以前的var关键字,那么方法和属性都会被默认设置为public。
类中的元素都可以被声明为public、private和protected
public的属性和方法可以在程序中的任何上下文中被访问private的方法或属性只能在类内部被访问,即使时子类也无法访问父类中的private的方法或属性。protected的方法或属性只能在类内部或是子类中被访问,其他外部代码无法访问。
那么,这到底有什么用呢?可见性关键字允许我们只将类中客户端所需的部分暴露给客户端,为对象设置了一个清晰的接口。
通过阻止客户端代码访问特定的属性,访问权限控制还有助于防止bug的产生。假设ShopProduct对象支持打折功能,那么可以在ShopProduct类中加入$discount属性和setDiscount()方法。
// ShopProduct类
public $discount = 0;
// ...
public function setDiscount(int $num)
{
$this->discount = $num;
}有了设置折扣的手段后,就可以创建一个负责打折的getPrice()方法
public function getPrice()
{
return ($this->price - $this->discount);
}不过这里有一个问题。我们只想将打折后的价格暴露给调用方,但现在调用方可以轻松地绕过getPrice()访问$price属性
print "The price is {$product1->price} <br/>"这会输出原始价格,而不是我们希望展示的折后价格。可以通过将$price属性设置为private,从而防止客户端直接访问该属性,强制要求客户端必须调用getPrice()方法。任何从ShopProduct类外部访问$price属性的企图都不会得逞。对类外部而言,这个属性就仿佛不存在一样。
将属性设置为private有时可能过于严格,因为子类是无法访问私有属性的。假设业务规则规定,只有图书不参与打折,那我们本可以重写getPrice()方法让它不返回折后价格,直接返回$price属性
// BookProduct
public function getPrice()
{
return $this->price;
}但是,由于$price属性并非声明在BookProduct类中,而是声明在ShopProduct类中,因此在试图访问$price属性时会出错。解决方法是将$price属性声明为protected,赋予子类访问该属性的权限。记住,我们无法从类层次外部访问protected的属性或方法,只能在声明它的类或是子类中访问。
通常,我们倾向于严格控制可访问性,即应当先将属性或方法声明为private或protected,然后在需要时逐渐放宽访问限制。尽管类中的许多方法(即便不是大多数方法)都会是public,但要再一次强调,不确定时就应当严格控制它们的访问权限。有些方法只在类内部为其他方法服务,请将它们设置为private或protected。
访问方法
如果客户端程序员需要使用保存在我们的类中的值,最好不要让他们直接访问属性,而是提供方法让他们获取所需的值。这些方法被称为访问方法,也可以叫作getter和setter。
前面已经讲过提供访问方法的一个优点了。正如getPrice()方法那样,访问方法可以根据不同的情况返回不同的值。
访问方法的另一个优点是可以限制属性类型。类型声明可以限制方法参数的类型,但属性可以持有任意类型的数据。还记得那个使用ShopProduct对象输出商品清单数据的ShopProductWriter类吗?我们可以改进它,让它一次输出多个ShopProduct对象的信息。
class ShopProductWriter
{
public $products =[];
public function addProduct(ShopProduct $shopProduct)
{
$this->products[] = $shopProduct;
}
public function write($shopProduct)
{
$str = "";
foreach($this->products as $shopProduct)
{
$str .= "{$shopProduct->title}:";
$str .= $shopProduct->getProducer();
$str .= "({$shopProduct->getPrice()}) <br/>";
}
print $str;
}
}现在,ShopProductWriter类的功能更加强大了。它可以保存许多ShopProduct对象并一次输出所有这些对象的信息。但我们必须相信,客户端程序员会尊重我设计该类的意图。因为尽管我提供了addProduct()方法,但我并没有阻止客户端程序员直接访问$products属性。他们不仅可以将类型错误的对象加入$products数组属性,还可以用基本类型的值替换整个数组。可以通过将$products属性设置为private来防止这种情况。
class ShopProductWriter
{
private $products =[];
// ...现在外部代码就无法破坏$products属性了。所有的访问都必须经由addProduct()方法,而且用于方法声明的类类型声明可以确保,只有ShopProduct对象才能被加入$products数组属性中。
现在,来通过修改ShopProduct类及其子类的访问权限来为本章画上句号吧。
class ShopProduct
{
private $title;
private $producerMainName;
private $producerFirstName;
protected $price;
private $discount = 0;
public function __construct(
string $title,
string $firstName,
string $mainName,
float $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;
public function __construct(
string $title,
string $firstName,
string $mainName,
float $price,
int $playLength
)
{
parent::__construct(
$title,
$firstName,
$mainName,
$price
);
$this->playLength = $playLength;
}
public function getPlayLength()
{
return $this->playLength;
}
public function getSummaryLine()
{
$base = "{$this->title} ({$this->producerMainName}, ";
$base .= "{$this->producerFirstName} )";
$base .= ":playing time - {$this->playLength}";
return $base;
}
}
class BookProduct extends ShopProduct
{
private $numPages;
public function __construct(
string $title,
string $firstName,
string $mainName,
float $price,
int $numPages
)
{
parent::__construct(
$title,
$firstName,
$mainName,
$price
);
$this->numPages = $numPages;
}
public function getNumberOfPages()
{
return $this->numPages;
}
public function getSummaryLine()
{
$base = "{$this->title} ({$this->producerMainName}, ";
$base .= "{$this->producerFirstName} )";
$base .= ":page count - {$this->numPages}";
return $base;
}
public function getPrice()
{
return $this->price;
}
}这个新版本的ShopProduct类家族其实并没有增加新功能,只是将所有的属性都设置了private或protected。此外,还加入了一些访问方法供客户端代码访问这些属性。
至此,PHP对象基础都结束了。这篇文章和之前的几篇文章都出自《深入PHP·面向对象、模式与实践》(第5版)
【完】
评论已关闭