PHP中Trait详解及其应用

正文开始

<p>PHP的Traits:到底是祸害还是好得飞起?</p><p>http://blog.ihuxu.com/php-traits-good-or-bad/</p><p><br /></p><p><br /></p><p>http://oomusou.io/php/php-trait/<br /></p><p>https://segmentfault.com/a/1190000008009455</p>  <p>Trait是部分class的实现 ,让我们能将部分class注入到其他class。</p><p>trait有两个功能 : </p>提供如interface的合约。提供如class的实作。<p>所以trait是一个看起来像interface,但用起来像class的东西。</p>Version<p>PHP 5.4</p>为什麽要使用Trait?<p>假设现在我们有两个class : RetailStore class与Car class,我们希望这两个class都能提供log功能,纪录class的内部运作。</p><p>我们分别想到了3个方法实现log功能 : </p><p>使用继承</p><p>使用多型 (interface)</p><p>使用trait使用继承</p><p><a href="http://img.li6.cc/content_img/1/article/154/trait000.svg" rel="gallery0"><br /></a></p><p><a href="http://img.li6.cc/content_img/1/article/154/trait000.svg" rel="gallery0"><img src="http://img.li6.cc/content_img/1/article/154/trait000.svg" alt="" width="400" /><br /></a></p><p><a href="http://img.li6.cc/content_img/1/article/154/trait000.svg" rel="gallery0"></a>log.php</p><pre>class Log { function startLog() { ... } function stopLog() { ... } } </pre> RetailStore.php<pre>class RetailStore extends Log { ... } </pre> Car.php<pre>class Car extends Log { ... } </pre> <p>这种写法强迫RetailStore与Car去继承Log,继承后也会连Log的startLog()与stopLog()一起继承。</p><p>以功能面来说,这样是可行的,但问题继承并不是这样用的。</p><p>解释继承最典型的例子就是国中学生物的界门纲目科属种 ,举例来说,人类属于 : </p>界 : 动物界 Animalia门 : 脊索动物门 Chordata纲 : 哺乳纲 Mammalia目 : 灵长目 Primates科 : 人科 Hominidae属 : 人属 Homo种 : 智人 H. sapiens<p>下一层的分类都是上一层的特化,也继承了上一层分类的所有特性。</p><p>也就是智人包含了哺乳纲 的所有特性,所以智人也算是一种哺乳类。</p><p>但因为智人还有其他的特性,是哺乳纲所没有的,所以又再被分类成灵长目。 </p><p>但RetailStore class在理解上与Log class不相关,Car class在理解上也与Log class不相关,也就是说RetailStore根本不算是一种Log类,而Car也根本不算是一种Log类,在这裡使用继承完全是因为Log功能硬兜出来的,而不是因为class天生的特性加以继承所分类。换句话说,Log class根本不适合当RetailStore class与Log class的祖先。</p><p>这是一个典型误用继承的例子,并不是一个好的方法。</p><p>使用多型<a href="http://img.li6.cc/content_img/1/article/154/trait001.svg" rel="gallery0"><img src="http://img.li6.cc/content_img/1/article/154/trait001.svg" alt="" width="400" /></a><br /></p><p>ILog.php</p><pre>interface ILog { public function startLog(); public function stopLog(); } </pre> RetailStore.php<pre>class RetailStore implements ILog { public function startLog() { ... } public function stopLog() { ... } } </pre> Car.php<pre>class Car implements ILog { public function startLog() { ... } public function stopLog() { ... } } </pre> <p>既然继承不能用,那我们改用多型吧,建立一个ILog interface,定义了startLog()与stopLog()两个函式,基于之前强迫继承的错误,我们只要求RetailStore class与Car class实践ILog interface。</p><p>使用interface方式比用继承好多了,但还不是最好,由于RetailStore class与Car class都要实践ILog interface,这导致了startLog()与stopLog()演算法在RetailStore class与Car class都要实作,也就是说,将来万一start()与stop()演算法有bug,两个class都要去修改code,这违反了DRY原则 (Don’t Repeat Yourself)原则,因为相同的code写两分,将来维护会很麻烦。</p><p>目前看来物件导向两大绝招继承与多型 已经阵亡了。</p><p>Trait就是为了解决这个问题所产生的,trait允许我们写一个部分class的TLog trait,然后将Log trait注入到RetailStore class与Car class。</p>使用Trait<a href="http://img.li6.cc/content_img/1/article/154/trait002.svg" rel="gallery0"><img src="http://img.li6.cc/content_img/1/article/154/trait002.svg" alt="" width="500" /></a>TLog.php<pre>trait TLog { public function startLog() { ... } public function stopLog() { ... } } </pre> RetailStore.php<pre>class RetailStore { use TLog; } </pre> Car.php<pre>class Car { use TLog; } </pre> <p>既然多型还不够好,会造成code重複的问题,假如我们能把实践的部分也写在interface就好了。trait就是允许我们直接将实践写在裡面,避免code重複。</p><p>建立一个TLog trait,定义了startLog()与stopLog()两个函式,直接将startLog()与stopLog()演算法写在trait的函式内。</p><p>既然trait已经将startLog()与stopLog()都写好了,其他class就不用implements了,PHP使用了use关键字,让我们将trait的函式直接注入到RetailStore class与Car class。也就是说,虽然RetailStore class与Car class都没有实践startLog()与stopLog(),透过use住入TLog trait之后,就相当于有了startLog()与stopLog()函式了。 </p><p>无论以上3种写法,对于使用RetailStore class与Car class来说都一样。</p><pre>$store = new RetailStore(); $store->startLog(); $store->stopLog(); $car = new Car(); $car->startLog(); $car->stopLog(); </pre> 继承、interface与trait的比较<p>继承的方式虽然可行,但很暴力也不合理;interface方式虽然也可行,但不符合DRY原则,将来不好维护;只有使用trait,一个介于继承与interface中间的方法,巧妙的融合class与interface的优点,对于class间要加入毫不相关的功能提供了另外一种不错的解决方式。</p> PHP内部实践Trait方式<p>对于PHP来说,在使用use关键字时,PHP只是将trait的所有变数与函式「複製」进class内,让class马上拥有trait的所有功能。</p>Trait的作用域<p>因为trait在实作上是使用複製, 所以原本在trait内宣告的public、protected、private变数与函式都会複製到class内,也就是说,class内被trait加进来变数与函式的scope将与原trait完全一样。</p><p>比较一下继承的extends,当使用继承时,只有public与protected的变数与函式会被继承下来,private函式将不会被继承下来。</p><pre>trait PrivateTrait { private privateFunc() { echo "This is private"; } } class PrivateClass { use privateTrait; // This is allowed. public publicFunc() { $this->privateFunc(); } } class ExtendingClass extends PrivateClass { // This is NOT allowed - privateFunc() is only available // to the class which directly utilizes it. public someFunc() { $this->privateFunc(); } } </pre><p>第1行</p><pre>trait PrivateTrait { private privateFunc() { echo "This is private"; } } </pre><p>定义了PrivateTrait,并宣告的一个private函式。</p><p>第9行</p><pre>class PrivateClass { use privateTrait; // This is allowed. public publicFunc() { $this->privateFunc(); } } </pre><p>publicFunc()函式呼叫了在PrivateTrait定义的privateFunc(),虽然privateFunc()是private,但因为没用继承,所以在class内可以使用。</p><p>20行</p><pre>class ExtendingClass extends PrivateClass { // This is NOT allowed - privateFunc() is only available // to the class which directly utilizes it. public someFunc() { $this->privateFunc(); } } </pre><p>ExtendingClass 是继承自PrivateClass,但在someFunc()函式裡呼叫privateFunc()是错误的,因为privateFunc()在PrivateTrait定义是private,而private无法被继承,所以在ExtendingClass是看不到privateFunc()的。</p>Insteadof与As关键字<p>由于PHP使用複製来实作use,所以有可能出现trait所定义的变数与函式已经在其他class已经被定义,PHP提出了insteadof与as关键字来解决。</p><pre>trait A { function someFunc() { ... } function otherFunc() { ... } } trait B { function someFunc() { ... } function otherFunc() { ... } } class MyClass { use A, B { A::someFunc insteadof B; B::otherFunc as differentFunc; } } </pre><p>第1行</p><pre>trait A { function someFunc() { ... } function otherFunc() { ... } } trait B { function someFunc() { ... } function otherFunc() { ... } } </pre><p>两个trait所定义的函式完全相同。</p><p>25行</p><pre>class MyClass { use A, B { A::someFunc insteadof B; B::otherFunc as differentFunc; } } </pre><p>MyClass同时使用了A trait与B trait,但因为这两个trait的函式完全相同,所以造成了函式名称衝突。</p><p>use关键字同时加上了A与B两个trait,后面加上大括号描述该怎麽处理函式名称衝突。</p><p>insteadof关键字告诉PHP我们要使用A trait的someFunc()而不要使用B trait的someFunc()。</p><p>as关键字可以替trait的变数名称与函式名称取别名。</p> 那B::someFunc()怎麽办?<p>以上面的例子,因为没有描述B::someFunc(),所以就被忽略而消失不见了。</p><p>若要使用B::someFunc()可以自行加上as另外取别名。</p>继承 vs. 多型 vs. Trait<p>回到一个更基本的问题,何时该用继承?何时该用多型?何时该用trait?</p>继承<p>想要重複使用既有程式功能。缺点是藕合度高,当你继承了某个class时,已经绑死了某个class,而且继承在编译时期就已经决定,无法在执行时期改变。</p>多型<p>想要解藕合既有程式功能。只要订出interface,未来有新的功能只要实作interface即可。由于多型特性,可以在执行时期动态改变物件。优点是藕合度低,藕合只到interface层级,不会被特定class绑死。</p>Trait<p>想要共享相同程式功能。有一句话说 : </p><p>Inheritance is for extending logic, trait is for sharing behavior.</p><p>也就是所谓的Vertial Inheritance and Horizontal Reuse。继承看起来像是垂直的结构,而trait看起来像是水平的结构。</p><p>实务上多型可以取代继承,但trait却无法取代多型与继承,只要需要共享相同的功能,就要多多使用trait。 </p>Good Practice<p>一个档案只使用一个Trait<br />就语法上,PHP允许一个档案内包含多个trait :</p><pre>trait TFoo { public function getFoo() { ... } } trait TBar { public function getBar() { ... } } </pre><p>但实务上,基于一个class一个档案one class per file原则,一个档案也应该只包含一个trait。</p><p>Namespace的Use与Trait的Use<br />虽然namespace与trait都使用use关键字,但彼此并不会混淆。</p><pre>namespace MyNamespaceSubNamespace; use IlluminateSupportContractsJsonableInterface; use IlluminateSupportContractsRenderableInterface; class MyClass { use MyTrait; } </pre>当use用在namespace时,会写在class外面。当use用在trait时,会写在class裡面。与其他程式语言比较Ruby<pre>module MyTrait def setAddress(address) ... end end class MyClass include MyTrait end class MyClass extend MyTrait end </pre><p>宣告trait : </p>Ruby没提供trait关键字,要用module模拟。<p>注入trait : </p><p>include关键字是instance methodextend关键字是class method(相当于PHP的 static function)。</p><p>PHP</p><pre>trait MyTrait { public function setAddress($address) { ... } } class MyClass { use MyTrait; } </pre><p>宣告trait : </p>使用trait关键字。<p>注入trait : </p>在class内部使用use关键字。Conclusiontrait可以简单的想成部分class,允许我们将trait注入到其他的class内,有些应用在继承与多型都不好用时,可以考虑用trait。insteadof与as关键字可解决trait与class函式与变数名称衝突的问题。实务上多型可以取代继承,但trait却无法取代多型与继承,只要需要共享相同的功能,就要多多使用trait。 namespace与trait都使用了use关键字,唯用在namespace时会写在class外面,而用在trait时会写在class裡面。 <br />

正文结束

PHP接口(interface)和抽象类(abstract) 阿里云oss获取object的潜规则