抽象类、接口和契约式编程
抽象类
抽象类(abstract class)机制中总是要定义一个公共的基类(base class),而将特定的细节留给继承者来实现。具体地说,当需要创建一个基础的对象,而创建所需的某些方法并没有完整地定义出来时,就需要用到抽象类。通过使用抽象类概念,可以在开发项目中创建扩展性很好的架构。
例如,文本格式解析功能的实现就非常适合抽象方式。实现这一功能时,我们知道,为了与其他类交互,需要一系列方法,如getData()或getCreatedData()。然而,我们希望将解析文件格式的方法留给为某种特定文件格式而设计的继承类实现。通过使用抽象类,我们可以定义一个必须存在的parse()方法,而不需要明确这个方法是如何实现的。当然,为了实现起来更容易,我们也可以将这一抽象的需求和完整定义的方法在同一个类中。
由于抽象类没有为它所声明的所有方法定义实现的内容,大家可能会将抽象类看作是分部类。抽象类可以不实现所有方法,它具有定义抽象方法的特殊能力,这些抽象方法只是缺少方法的方法原型。当抽象类被继承时,这些方法将会被实现。然而,抽象类不一定只包含抽象方法,我们也可以在其中定义具有完整实现体的方法。
由于抽象类没有为它所声明的所有方法都定义实现,所以使用new操作符是不可以直接创建它的实例的。要创建实例,就必须创建另一个扩展抽象类的类,并重写所有之前声明的抽象方法原型。通过扩展类,我们就能创建特殊的对象,而且它们同样能够保证提供一套公共的功能。
要充分发挥抽象类的特点,就必须牢记一下规则。
- 某个类只要包含至少一个抽象方法就必须声明为抽象类。
- 声明为抽象的方法,在实现的时候必须包含相同或更低的访问级别。例如,如果某个方法在抽象类中的访问级别是受保护的,在继承类中它就必须是受保护的或者公共的,而不能是私有的。
- 不能使用new关键字创建抽象类的实例。
- 被声明为抽象的方法不能包含函数体。
- 如果将扩展的类也声明为抽象的,在扩展抽象类时,就可以不实现所有的抽象方法。在创建具有层次的对象时,准则中做法是很有用的。
在类的声明中使用abstract修饰符就可以将某个类声明为抽象的。
/* * 定义一个抽象基类 */ abstract class Car{ //定义抽象基类抽象方法 abstract function getMaximumSpeed(); }
由于这个类时抽象的,不能实例化,它本身起不到什么作用。要让这个类起作用并且获得一个实例,首先必须扩展它。例如,可以创建一个从Car类继承的名为FastCar的类,并且它定义一个getMaximumSpeed()方法。
/* * 继承抽象 */ class FastCar extends Car{ function getMaximumSpeed(){ return 150; } }
现在有了一个可以实例化的类FastCar。下一步,我们可以创建另外一个类Street,它将用到抽象类提供的公共功能。
/* * 使用抽象类提供的公共功能 */ class Street{ protected $speedLimit; protected $cars; public function __construct($speedLimit = 200){ $this->cars = array(); $this->speedLimit = $speedLimit; } protected function isStreetLegal($car){ if($car->getMaximumSpeed() < $this->speedLimit){ return true; }else{ return false; } } public function addCar($car){ if($this->isStreetLegal($car)){ echo 'The Car was allowed on the road.'; $this->cars = $car; }else{ echo 'The Car is too fast and was not allowed on the road.'; } } }
Street类包含了一个addCar()方法,它的作用是获得一个派生的Car类的实例。现在,我们可以使用Street类,并且向addCar()方法传入一个FastCar类的实例。
/* * 使用抽象类 */ $street = new Street(); $street->addCar(new FastCar());
使用抽象类,我们就可以确定所有继承自Car的对象都会实现getMaximumSpeed()方法,从而共享这个公共功能。如果继承自Car的类没有定义这个方法,就会导致语法错误,程序也就不会运行。这种限制确保了在实例化层次上的兼容性,这样我们就不需要再发生错误之后调试代码找出某个对象也没包含特定方法的原因了。
然而,抽象类也有一些限制。PHP只支持从一个基类继承,而不支持从两个或者更多的抽象类继承。从两个或更多的基类继承的能力被称为多重继承。PHP在设计上是禁止这种功能的。原因在于,当两个或更多的类定义了具有相同原型且具有完成时限的方法时,从多个类继承会导致不必要的复杂性。当发现需要从两个或更多的抽象类继承时,可以将基类的方法拆开,然后使用接口达到相同的目的。
接口
接口是一种类似于类的结构,可用于声明实现类所必须声明的方法。例如,接口通常用于来声明API,而不用定义如何实现这个API。
虽然接口与抽象类很相似,但是接口只能包含方法原型,而不能包含任何完整定义的方法。这可以防止使用抽象类时可能出现的方法冲突,从而能在给定的实现类上使用多个接口。然而,既然接口不能定义完整实现的方法,因此如果我们希望为继承者提供默认功能,就必须单独提供一个非抽象的基类。
为了生命接口,需要使用关键词interface。
interface IExampleInterface{ }
和继承抽象类使用extends关键词不同的是,实现接口使用的是implements关键词。
class ExampleClass implements IExampleInterface{ }
为了形成一个完整的类,我们必须实现接口中的所有方法,这样其他类才能依赖于接口定义的所有方法。只要有一个接口方法没有实现,就会破坏定义公共接口的作用。
前面提到过,接口优于抽象类的一点是每个类可以使用多个接口。当希望在一个类中实现呢两个或者两个以上接口时,我们可以用逗号将它们分割。例如,如果有一个具有数组分格的对象,希望他同时具有迭代和计数的功能,那么我们可以定义这样的一个类。
class MyArrayLikeObject implements Iterator,Countable{ }
使用接口完全有可能实现与抽象类相同的操作。通常,在子类和父类之间存在有逻辑上的层次结构时,应该使用抽象类。而在希望支持差别较大的两个或者更多对象之间的特定交互行为时,使用抽象类会显得不合理,此时应该使用接口。
interface ISpeedInfo{ function getMaximumSpeed(); } class Car{ } class FastCar extends Car implements ISpeedInfo{ function getMaximumSpeed(){ return 150; } }
这段代码会实现与之前使用抽象类方法几乎一模一样的操作。唯一一个重要的区别在于,在使用抽象类的方法中,可以确定实现类有一个getMaximumSpeed()方法。而在使用接口的方法中,Car类不必知道继承者实现了ISpeedInfo接口。
instanceof操作符
instanceof操作符是PHP中的一个比较操作符。它接受左右两边的参数,并返回一个Boolean类型的值。这个操作符是用来确定对象的某个实例是否为特定的类型,或者是否从某个类型继承,又或者是否实现了某个特定的接口。
class Street{ protected $speedLimit; protected $cars; public function __construct($speedLimit = 200){ $this->cars = array(); $this->speedLimit = $speedLimit; } protected function isStreetLegal($car){ if($car instanceof ISpeedInfo){ if($car->getMaximumSpeed() < $this->speedLimit){ return true; }else{ return false; } }else{ //扩展类必须实现了ISpeedInfo才能使Street合法 return false; } } public function addCar($car){ if($this->isStreetLegal($car)){ echo 'The Car was allowed on the road.'; $this->cars = $car; }else{ echo 'The Car is too fast and was not allowed on the road.'; } } }
契约式编程
简单地讲,契约式编程是指在编写类之前事先声明接口的一种编程实践。这种方法在保证类的封装性方面非常有用。
使用契约式编程技术,我们可以在创建应用程序之前定义出试图实现的功能,这和建筑师在修建大楼之前先画好蓝图的做法非常相似。
开发团队经常会使用契约式编程技术,这是因为这种技术会显著改善流程。在实现类之前定义好类之间的交互行为,是团队成员明确知道对象必须实现什么行为,然后再实现它们就比较轻松了。当借口被完整实现之后,类的测试就只需要使用定义在借口上的规则来进行了。
在之前的示例中,ISpeedInfo接口可以被看作是一种契约,这是因为这个接口是Car类和Street类交互式唯一一个API。Street类在接受交互的对象之前会测试这一契约,然后就可以通过创建Car类的任务分配给某个开发人员,而将创建Street类的任务分配给另外一个开发人员,而且不需要他们在超出ISpeedInfo接口的范围之外进行合作。
小结
抽象类是使用abstract关键字声明的类。通过将某个类标记为抽象类,我们可以推迟实现所有声明的方法。要将某个方法声明为抽象方法,只需要省略掉包含所有大括号的方法实现体,将方法声明的代码行用分号结束即可。
抽象类不能直接被实例化,它们必须被继承。如果某个类从抽象类继承,当它没有实现基类中所有抽象方法时,它就必须也被声明为抽象的。
在接口中,我们也可以声明没有方法体的方法原型,这点与抽象类很类似。它们之间的区别在于,接口不能声明任何具有方法体的方法;并且它们使用的语法也不一样。为了将接口规则强加到某个类上,我们需要使用implements关键字,而不是使用extends关键字。
在某些情况下,我们希望确定某个类是否是特定的类型,或者是否实现了特定的接口。intanceof操作符非常适合完成这个任务。instanceof操作符检查三件事情:实例是否是某个特定的类型,实例是否从某个特定的类型继承,实例或者它的任何祖先是否实现了特定的接口。
某些语言具有从多个基类继承的能力,这称为多重继承。PHP不支持多重继承。相反,它提供了为一个类声明多个接口的功能。
接口在声明类必须遵循的规则时非常有用。契约式编程技术使用这一功能来增强封装性,优化工作流。