抽象类、接口、Trait
抽象类
抽象类的概念
抽象类提供了一个规范,继承该抽象类的所有的类, 都要实现抽象类中定义的方法,以此保证所有的子类都有相似的行为
当我们项目中,假设我们需要两种数据库的操作类(mysqli、PDO),我们就可以使用抽象类,在抽象类中定义抽象方法(规范)
在了解php的抽象类之前,需要先了解什么是抽象方法,在面向对象的编程语言中,一个类可以有很多子类,而每一个类中至少有一个公共方法作为外部访问它的(父类)接口,为了方便类继承就引入了抽象方法
什么是抽象方法?
抽象方法是没有方法体的方法,没有方法体是指方法声明时只有方法名和参数列表,没有方法体(也就是没有具体的实现),然后分号结尾,声明抽象方法要使用abstract关键字修饰。声明抽象方法格式:abstract function($a,$b);
什么是抽象类?
任何一个类中只要声明了抽象方法的类就必须声明为抽象类,抽象类也要使用abstract关键字修饰,抽象类中可以有不是抽象方法的方法和成员属性,但访问权限不能是私有
抽象类的特点
抽象类需要使用abstract
一个类中只要有一个抽象方法,那么这个类必须被声明为抽象类
抽象类不能被实例化,只能实例化继承了该抽象类的非抽象类
抽象方法不能有方法体,且访问类型不能为private
继承该抽象类的类,必须实现抽象类中的所有抽象方法。且在子类中实现的抽象方法的访问级别不能比在抽象类中的严(比如抽象类中的某个抽象方法访问类型为protected,那么继承该抽象类的类中,实现该抽象方法时的访问类型为protected或pubic,不能为private)
抽象类中可以有构造函数
子类在实现抽象类中的抽象方法时,参数必须和抽象方法中的一致,但是子类在实现抽象方法时可以添加可选参数
这个比较不容易注意到,抽象类继承另外一个抽象类时,抽象类中,不能重写抽象父类的抽象方法。这样的用法,可以理解为对抽象类的扩展
举例:
get('Hello');//输出:Hello Workd.
二、接口
为什么要使用接口?
我们都知道PHP是单继承的,要想实现多继承,就可以借助接口来实现。因为,一个类可以实现多个接口
接口有哪些特点?
定义接口的关键字,interface
接口中的所有方法的访问类型都必须是public
跟抽象方法一样,接口中的方法,也只能有方法名和参数列表,不能有方法体
实现接口的类,必须实现接口中的所有方法
实现一个接口,使用关键字:implements
接口之间也可以继承,使用extends,且可以继承多个
一个类可以同时实现多个接口
示例:
三、Trait
什么是Trait?
其实可以将Trait看做类的部分实现,一个Trait可以在一个或多个类中使用,一个类也可以使用多个Trait。Trait有两个作用:
表明类可以做什么
帮助类提供模块化实现
它是一种代码复用的技术,为PHP提供了灵活的代码复用机制。
Trait的使用场景有哪些?
其实laravel的底层有很多地方都用到了Trait,比如Http处理内核
假设有这样一种场景,一个方法在很多类中都需要使用,会怎样处理?
通常的做法是,写一个基础类,在该类中实现该方法,所有类都继承这个基类。
其实这不是最好的处理方法,我们使用继承,一般是在有几个类有很多相似的地方,比如动物作为基类,小猫、小狗这些类去继承它。
这个时候,Trait就发挥作用了
边举例,边熟悉用法(来源:https://www.php.net/traits(PHP手册)):
sayHello();
//输出:Hello World!
/**
* 再看一个调用优先级的例子
*/
trait HelloWorld
{
public function sayHello()
{
echo 'Hello World!';
}
}
class TheWorldIsNotEnough
{
use HelloWorld;
public function sayHello()
{
echo 'Hello Universe!';
}
}
$o = new TheWorldIsNotEnough();
$o->sayHello();
//输出:Hello Universe!
/**
* 2、通过逗号分隔,在 use 声明列出多个 trait,可以都插入到一个类中。
*/
trait Hello
{
public function sayHello()
{
echo 'Hello ';
}
}
trait World
{
public function sayWorld()
{
echo 'World';
}
}
class MyHelloWorld
{
use Hello, World;
public function sayExclamationMark()
{
echo '!';
}
}
$o = new MyHelloWorld();
$o->sayHello();
$o->sayWorld();
$o->sayExclamationMark();
//输出:Hello World!
/**
* 3、如果两个 trait 都插入了一个同名的方法,如果没有明确解决冲突将会产生一个致命错误。
* 为了解决多个 trait 在同一个类中的命名冲突,需要使用 insteadof 操作符来明确指定使用冲突方法中的哪一个。
* 以上方式仅允许排除掉其它方法,as 操作符可以 为某个方法引入别名。 注意,as 操作符不会对方法进行重命名,也不会影响其方法。
* 在本例中 Talker 使用了 trait A 和 B。由于 A 和 B 有冲突的方法,其定义了使用 trait B 中的 smallTalk 以及 trait A 中的 bigTalk。
* Aliased_Talker 使用了 as 操作符来定义了 talk 来作为 B 的 bigTalk 的别名。
*/
rait A {
public
function smallTalk()
{
echo 'a';
}
public
function bigTalk()
{
echo 'A';
}
}
trait B
{
public function smallTalk()
{
echo 'b';
}
public function bigTalk()
{
echo 'B';
}
}
class Talker
{
use A, B {
B::smallTalk insteadof A;
A::bigTalk insteadof B;
}
}
class Aliased_Talker
{
use A, B {
B::smallTalk insteadof A;
A::bigTalk insteadof B;
B::bigTalk as talk;
}
}
/**
* 4、使用 as 语法还可以用来调整方法的访问控制。
*/
trait HelloWorld
{
public function sayHello()
{
echo 'Hello World!';
}
}
// 修改 sayHello 的访问控制
class MyClass1
{
use HelloWorld {
sayHello as protected;
}
}
// 给方法一个改变了访问控制的别名
// 原版 sayHello 的访问控制则没有发生变化
class MyClass2
{
use HelloWorld {
sayHello as private myPrivateHello;
}
}
/**
* 5、正如 class 能够使用 trait 一样,其它 trait 也能够使用 trait。在 trait 定义时通过使用一个或多个 trait,能够组合其它 trait 中的部分或全部成员。
*/
trait Hello
{
public function sayHello()
{
echo 'Hello ';
}
}
trait World
{
public function sayWorld()
{
echo 'World!';
}
}
trait HelloWorld
{
use Hello, World;
}
class MyHelloWorld
{
use HelloWorld;
}
$o = new MyHelloWorld();
$o->sayHello();
$o->sayWorld();
//输出:Hello World!
/**
* 6、为了对使用的类施加强制要求,trait 支持抽象方法的使用。
*/
trait Hello
{
public function sayHelloWorld()
{
echo 'Hello' . $this->getWorld();
}
abstract public function getWorld();
}
class MyHelloWorld
{
private $world;
use Hello;
public function getWorld()
{
return $this->world;
}
public function setWorld($val)
{
$this->world = $val;
}
}
/**
* 7、Trait中也是可以定义属性的,包括静态属性和静态方法
*/
//静态方法
trait Counter
{
public function inc()
{
static $c = 0;
$c = $c + 1;
echo "$c\n";
}
public static function doSomething()
{
return 'Doing something';
}
}
class C1
{
use Counter;
}
class C2
{
use Counter;
}
echo C1::doSomething();// Doing something
$o = new C1(); $o->inc(); // echo 1
$p = new C2(); $p->inc(); // echo 1
//trait中定义属性
trait PropertiesTrait
{
public $x = 1;
}
class PropertiesExample
{
use PropertiesTrait;
}
$example = new PropertiesExample;
$example->x;
四、抽象类、接口、Trait之间的区别
抽象类和接口的共同点
两者都不能被直接实例化
抽象类和接口中的方法,在继承他们的类中都必须被实现。(抽象类中的所有抽象方法都必须被实现)
抽象类和接口的不同点
被继承时使用的关键字不同,实现接口,使用implements,继承抽象类,使用extends
一个类可以实现多个接口,但一个类只能继承一个抽象类
接口强调的是特定功能的实现,抽象类强调的是所属关系
其实接口中的每一个方法都可以看做是抽象方法,实现接口的类,必须实现接口中的所有方法。而抽象类中,只有抽象方法必须被实现(抽象类中并非所有的方法都是抽象方法,至少会有一个)
接口中的方法默认是public的,也只能是public 的,不能用 private,protected修饰符修饰而抽象类中的抽象方法则可以用public,protected来修饰,但不能用private
Trait与抽象类和接口的异同
相同点
一个类中,可以同时实现多个Trait、接口(抽象类只能继承一个)
trait中的方法和抽象类中的非抽象方法有方法体
不同点
他们的访问控制不同,抽象类中只能是protected和public,接口中只能是public,trait中的方法可以是任意的访问类型,因为使用了trait其实就是将trait中的方法,加到类中了
trait中的方法在子类中可以不被都调用,而抽象类中的抽象方法和接口中的所有方法都必须在子类中实现
trait中的方法的访问控制不受限制,trait中的方法会覆盖该类继承的类中的同名方法
五、抽象类、接口、Trait的使用场景
抽象类使用场景
一句话,在既需要统一的接口,又需要实例变量或缺省的方法的情况下,就可以使用它。最常见的有:
(1)定义了一组接口,但又不想强迫每个实现类都必须实现所有的接口。此时我们就可以用抽象类,定义一组方法体,甚至可以是空方法体,然后由子类选择自己所感兴趣的方法来覆盖
(2)某些场合下,只靠纯粹的接口不能满足类与类之间的协调,还必需类中表示状态的变量来区别不同的关系。 abstract的中介作用可以很好地满足这一点
(3)规范了一组相互协调的方法,其中一些方法是共同的,与状态无关的,可以共享的,无需子类分别实现;而另一些方法却需要各个子类根据自己特定的状态来实现特定的功能
接口的使用场景
类与类之间需要特定的接口进行协调,而不在乎其如何实现
作为能够实现特定功能的标识存在,也可以是什么接口方法都没有的纯粹标识
需要将一组类视为单一的类,而调用者只通过接口来与这组类发生联系
需要实现特定的多项功能,而这些功能之间可能完全没有任何联系
Trait的使用场景
Trait就像实现某类功能的组件,可以更灵活的复用自定义功能,而不必局限与继承关系上。它可以混入不同结构独立的类中复用方法集
