最近我一直在努力学习PHP,我发现自己被trait缠住了。我理解横向代码重用的概念,并且不希望必然地继承抽象类。我不明白的是:使用特征和使用界面之间的关键区别是什么?
我曾试着搜索过一篇像样的博客文章或文章,解释什么时候使用其中一种或另一种,但到目前为止,我找到的例子似乎非常相似,甚至完全相同。
最近我一直在努力学习PHP,我发现自己被trait缠住了。我理解横向代码重用的概念,并且不希望必然地继承抽象类。我不明白的是:使用特征和使用界面之间的关键区别是什么?
我曾试着搜索过一篇像样的博客文章或文章,解释什么时候使用其中一种或另一种,但到目前为止,我找到的例子似乎非常相似,甚至完全相同。
当前回答
公共服务公告:
我想要声明的是,我相信trait几乎总是一种代码气味,应该避免使用组合。在我看来,单继承经常被滥用到反模式的地步,而多重继承只会加剧这个问题。在大多数情况下,使用组合而不是继承(无论是单个还是多个)会更好。如果您仍然对特征及其与界面的关系感兴趣,请继续阅读…
让我们这样开始:
面向对象编程(OOP)可能是一个难以掌握的范例。 仅仅因为你在使用类并不意味着你的代码就是 面向对象(OO)。
要编写OO代码,你需要理解OOP实际上是关于对象的功能。你必须考虑类可以做什么,而不是它们实际做什么。这与传统的过程式编程形成了鲜明的对比,在传统的过程式编程中,重点是让一小段代码“做一些事情”。
如果面向对象编程是关于规划和设计的,那么接口就是蓝图,对象就是完全建成的房子。与此同时,特征只是一种帮助建造蓝图(界面)所布置的房子的方法。
接口
那么,我们为什么要使用接口呢?很简单,接口使我们的代码不那么脆弱。如果您怀疑这种说法,可以问问那些被迫维护遗留代码的人,这些代码不是针对接口编写的。
接口是程序员和他/她的代码之间的契约。这个界面说:“只要你遵守我的规则,你可以随心所欲地实现我,我保证不会破坏你的其他代码。”
举个例子,考虑一个现实世界的场景(没有汽车或小部件):
你想为一个web应用程序实现一个缓存系统 服务器负载下降
你开始写一个类来缓存请求响应使用APC:
class ApcCacher
{
public function fetch($key) {
return apc_fetch($key);
}
public function store($key, $data) {
return apc_store($key, $data);
}
public function delete($key) {
return apc_delete($key);
}
}
然后,在HTTP响应对象中,在执行生成实际响应的所有工作之前检查缓存命中:
class Controller
{
protected $req;
protected $resp;
protected $cacher;
public function __construct(Request $req, Response $resp, ApcCacher $cacher=NULL) {
$this->req = $req;
$this->resp = $resp;
$this->cacher = $cacher;
$this->buildResponse();
}
public function buildResponse() {
if (NULL !== $this->cacher && $response = $this->cacher->fetch($this->req->uri()) {
$this->resp = $response;
} else {
// Build the response manually
}
}
public function getResponse() {
return $this->resp;
}
}
这种方法非常有效。但也许几周后你决定使用基于文件的缓存系统而不是APC。现在你必须改变你的控制器代码,因为你已经把你的控制器编程为使用ApcCacher类的功能,而不是一个表达ApcCacher类功能的接口。让我们假设你让Controller类依赖于CacherInterface而不是具体的ApcCacher,就像这样:
// Your controller's constructor using the interface as a dependency
public function __construct(Request $req, Response $resp, CacherInterface $cacher=NULL)
你可以这样定义你的界面:
interface CacherInterface
{
public function fetch($key);
public function store($key, $data);
public function delete($key);
}
反过来,你让你的ApcCacher和你的新FileCacher类实现CacherInterface,你编程你的Controller类来使用接口所需的功能。
这个示例(希望)演示了如何通过接口编程来更改类的内部实现,而不用担心这些更改是否会破坏其他代码。
特征
另一方面,trait只是一种重用代码的方法。界面不应该被认为是与特征相排斥的选择。事实上,创建满足接口所需功能的特征是最理想的用例。
只有当多个类共享相同的功能(可能由相同的接口决定)时,才应该使用trait。使用trait为单个类提供功能是没有意义的:这只会混淆类的功能,更好的设计应该将trait的功能移到相关的类中。
考虑下面的trait实现:
interface Person
{
public function greet();
public function eat($food);
}
trait EatingTrait
{
public function eat($food)
{
$this->putInMouth($food);
}
private function putInMouth($food)
{
// Digest delicious food
}
}
class NicePerson implements Person
{
use EatingTrait;
public function greet()
{
echo 'Good day, good sir!';
}
}
class MeanPerson implements Person
{
use EatingTrait;
public function greet()
{
echo 'Your mother was a hamster!';
}
}
一个更具体的例子:假设接口讨论中的FileCacher和ApcCacher都使用相同的方法来确定缓存项是否过时,是否应该删除(显然在现实生活中不是这样的,但还是这样吧)。您可以编写一个trait,并允许两个类使用它来满足公共接口需求。
最后提醒一句:注意不要在性格特征上走极端。当独特的类实现就足够了的时候,特征常常被用作糟糕设计的拐杖。为了获得最佳的代码设计,你应该限制特征以满足界面需求。
其他回答
主要的区别在于,对于接口,您必须在实现上述接口的每个类中定义每个方法的实际实现,因此您可以让许多类实现相同的接口但具有不同的行为,而trait只是注入到类中的代码块;另一个重要的区别是trait方法只能是类方法或静态方法,不像接口方法也可以(通常是)是实例方法。
基本上,您可以将trait视为代码的自动“复制-粘贴”。
使用trait是很危险的,因为在执行前我们无法知道它的作用。
然而,由于缺乏遗传等限制,性状更加灵活。
trait在注入方法时很有用,它可以检查类中是否存在另一个方法或属性。这是一篇不错的文章(但是是法语,抱歉)。
对于能读法语的人来说,GNU/Linux杂志HS 54有一篇关于这个主题的文章。
trait只是为了代码重用。
Interface只是提供了要在类中定义的函数的签名,它可以根据程序员的判断使用。这样就为我们提供了一组类的原型。
供参考, http://www.php.net/manual/en/language.oop5.traits.php
如果你懂英语,知道trait的意思,它就是这个名字的意思。它是一个无类的方法和属性包,您可以通过输入use将其附加到现有类。
基本上,你可以将它与单个变量进行比较。闭包函数可以从作用域外部使用这些变量,这样它们就有了作用域内部的值。他们是强大的,可以在任何地方使用。如果trait被使用,也会发生同样的情况。
公共服务公告:
我想要声明的是,我相信trait几乎总是一种代码气味,应该避免使用组合。在我看来,单继承经常被滥用到反模式的地步,而多重继承只会加剧这个问题。在大多数情况下,使用组合而不是继承(无论是单个还是多个)会更好。如果您仍然对特征及其与界面的关系感兴趣,请继续阅读…
让我们这样开始:
面向对象编程(OOP)可能是一个难以掌握的范例。 仅仅因为你在使用类并不意味着你的代码就是 面向对象(OO)。
要编写OO代码,你需要理解OOP实际上是关于对象的功能。你必须考虑类可以做什么,而不是它们实际做什么。这与传统的过程式编程形成了鲜明的对比,在传统的过程式编程中,重点是让一小段代码“做一些事情”。
如果面向对象编程是关于规划和设计的,那么接口就是蓝图,对象就是完全建成的房子。与此同时,特征只是一种帮助建造蓝图(界面)所布置的房子的方法。
接口
那么,我们为什么要使用接口呢?很简单,接口使我们的代码不那么脆弱。如果您怀疑这种说法,可以问问那些被迫维护遗留代码的人,这些代码不是针对接口编写的。
接口是程序员和他/她的代码之间的契约。这个界面说:“只要你遵守我的规则,你可以随心所欲地实现我,我保证不会破坏你的其他代码。”
举个例子,考虑一个现实世界的场景(没有汽车或小部件):
你想为一个web应用程序实现一个缓存系统 服务器负载下降
你开始写一个类来缓存请求响应使用APC:
class ApcCacher
{
public function fetch($key) {
return apc_fetch($key);
}
public function store($key, $data) {
return apc_store($key, $data);
}
public function delete($key) {
return apc_delete($key);
}
}
然后,在HTTP响应对象中,在执行生成实际响应的所有工作之前检查缓存命中:
class Controller
{
protected $req;
protected $resp;
protected $cacher;
public function __construct(Request $req, Response $resp, ApcCacher $cacher=NULL) {
$this->req = $req;
$this->resp = $resp;
$this->cacher = $cacher;
$this->buildResponse();
}
public function buildResponse() {
if (NULL !== $this->cacher && $response = $this->cacher->fetch($this->req->uri()) {
$this->resp = $response;
} else {
// Build the response manually
}
}
public function getResponse() {
return $this->resp;
}
}
这种方法非常有效。但也许几周后你决定使用基于文件的缓存系统而不是APC。现在你必须改变你的控制器代码,因为你已经把你的控制器编程为使用ApcCacher类的功能,而不是一个表达ApcCacher类功能的接口。让我们假设你让Controller类依赖于CacherInterface而不是具体的ApcCacher,就像这样:
// Your controller's constructor using the interface as a dependency
public function __construct(Request $req, Response $resp, CacherInterface $cacher=NULL)
你可以这样定义你的界面:
interface CacherInterface
{
public function fetch($key);
public function store($key, $data);
public function delete($key);
}
反过来,你让你的ApcCacher和你的新FileCacher类实现CacherInterface,你编程你的Controller类来使用接口所需的功能。
这个示例(希望)演示了如何通过接口编程来更改类的内部实现,而不用担心这些更改是否会破坏其他代码。
特征
另一方面,trait只是一种重用代码的方法。界面不应该被认为是与特征相排斥的选择。事实上,创建满足接口所需功能的特征是最理想的用例。
只有当多个类共享相同的功能(可能由相同的接口决定)时,才应该使用trait。使用trait为单个类提供功能是没有意义的:这只会混淆类的功能,更好的设计应该将trait的功能移到相关的类中。
考虑下面的trait实现:
interface Person
{
public function greet();
public function eat($food);
}
trait EatingTrait
{
public function eat($food)
{
$this->putInMouth($food);
}
private function putInMouth($food)
{
// Digest delicious food
}
}
class NicePerson implements Person
{
use EatingTrait;
public function greet()
{
echo 'Good day, good sir!';
}
}
class MeanPerson implements Person
{
use EatingTrait;
public function greet()
{
echo 'Your mother was a hamster!';
}
}
一个更具体的例子:假设接口讨论中的FileCacher和ApcCacher都使用相同的方法来确定缓存项是否过时,是否应该删除(显然在现实生活中不是这样的,但还是这样吧)。您可以编写一个trait,并允许两个类使用它来满足公共接口需求。
最后提醒一句:注意不要在性格特征上走极端。当独特的类实现就足够了的时候,特征常常被用作糟糕设计的拐杖。为了获得最佳的代码设计,你应该限制特征以满足界面需求。