先从简单的模拟鸭子应用做起
Joe上班的公司做了一套相当成功的模拟鸭子游戏:SimUDuck。 游戏中出现各种鸭子,一边游泳戏水,一边呱呱叫。此系统 的内部设计使用了标准的OO技术,设计了一个鸭子超类( Superclass),并让各种鸭子继承此超类。
现在我们得让鸭子能飞,Joe怎么想呢?
我只需要在Duck类中加上 fly()方法,然后所有鸭子都会继承fly()。
但是,可怕的问题发生了
Joe忽略了一件事:并非Duck 所有的子类都会飞。当Joe在Duck超类 中加上新的行为,这会使得某些子 类也具有这个不恰当的行为。现在可好了!SimUDuck程序中有一个会飞的非动物。
这么解决橡皮鸭不会飞的问题呢
Joe想到继承,可以把橡皮鸭类 中的fly()方法覆盖掉,就好像覆盖quack()的作法一 样...
可是,如果以后我加入诱饵鸭(DecoyDuck),又会如何?诱饵鸭是假鸭,不会飞也不会叫...
很显然利用继承后覆盖方法不是最佳答案。
分开变化和不会变化的部分
我们知道Duck类内的fly()和quack()会随着鸭子的不同而改变。为了要把这两个行为从Duck类中分开,我们将把它们自Duck类中 取出,建立一组新类代表每个行为。
设计鸭子的行为
如何设计类实现飞行和呱呱叫的行为?
我们希望一切能有弹性,毕竟,正是因为一开始的鸭子行为没 有弹性,才让我们走上现在这条路。我们还想能够「指定」行 为到鸭子的实例,比方说,想要产生绿头鸭实例,并指定特定「类型」的飞行行为给它。干脆顺便让鸭子的行为可以动态地改 变好了。换句话说,我们应该在鸭子类中包含设定行为的方法, 就可以在「运行时」动态地「改变」绿头鸭的飞行行为。
我们利用接口代表每个行为,比方说,FlyBehavior与 QuackBehavior,而行为的每个实现都必须实现这些接口之一。
所以这次鸭子类不会负责实现Flying与Quacking接口,反而是由其他类专门实现FlyBehavior与QuackBehavior,这就称为 「行为」类。由行为类实现行为接口,而不是由Duck类实现行为接口。
这样的作法迥异于以往,以前的作法是:行为是继承Duck超类 的具体实现而来,或是继承某个接口并由子类自行实现而来。 这两种作法都是依赖于「实现」,我们被实现绑得死死的,没 办法更改行为(除非写更多代码)。
在我们的新设计中,鸭子的子类将使用接口(FlyBehavior与 QuackBehavior)所表示的行为,所以实际的「实现」不会被 绑死在鸭子的子类中。(换句话说,特定的实现代码位于实现 FlyBehavior与QuakcBehavior的特定类中)。
实现鸭子的行为
在此,我们有两个接口,FlyBehavior和QuackBehavior,还有它们对应 的类,负责实现具体的行为:
好处:
这样的设计,可以让飞行和呱呱叫的动作被其他 的对象复用,因为这些行为已经与鸭子类无关了。
而我们可以新增一些行为,不会影响到既有的行 为类,也不会影响有「使用」到飞行行为的鸭子类。
这么一来,有了继承 的「复用」好处,却 没有继承所带来的包袱。
整合鸭子的行为
关键在于,鸭子现在会将飞行和呱呱叫的动作,「委托」(delegate)别人 处理,而不是使用定义在自己类(或子类)内的方法。
作法是这样的:
1、首先,在鸭子中「加入两个实例变量」,分别为「flyBehavior」与「 quackBehavior」,声明为接口类型(而不是具体类实现类型),每个变量 会利用多态的方式在运行时引用正确的行为类型(例如:FlyWithWings、 Squeak...等)。
我们也必须将Duck类与其所有子类中的fly()与quack()移除,因为这些行为 已经被搬移到FlyBehavior与Quackehavior类中了。
我们用performFly()和performQuack()取代Duck类中的fly()与quack()。 稍后你就知道为什么。
2、现在,我们来实现performQuack():
abstract class Duck
{
/**
* @var
* 每只鸭子都会引用实现 QuackBehavior接口的对象
*/
public $quackBehavior;
//还有很多
/**
* 不亲自处理呱呱叫行为,而是委托给quackBehavior对象。
*/
public function preformQuack()
{
$this->quackBehavior->quack();
}
}
很容易,是吧?想进行呱呱叫的动作,Duck 对象只要叫quackBehavior 对象 去呱呱叫就可以了。在这部分的代码中,我们不在乎QuackBehavior 接口的对 象到底是什么,我们只关心该对象知道如何进行呱呱叫就够了。
3、好吧!现在来关心「如何设定flyBehavior与 quackBehavior的实例变量」。看看MallardDuck类:
class MallardDuck extends Duck
{
/**
* MallardDuck constructor.
* 绿头鸭使用Quack类处理呱呱叫,
* 所以当performQuack()被调用,
* 就把责任委托给Quack对象进行正的呱呱叫.
* 使用FlyWithWings作为其 FlyBehavior类型。
*/
public function __construct()
{
$this->quackBehavior= new Quack1();
$this->flyBehavior= new FlyWithWings();
}
public function display()
{
echo "绿头鸭子\n";
}
}
所以,绿头鸭会真的『呱呱叫』,而不是『吱吱叫』,或『叫不出 声』。这是怎么办到的?当MallardDuck实例化时,它的构造器会 把继承来的quackBehavior实例变量初始化成Quack类型的新实例 (Quack是QuackBehavior的具体实现类)。
同样的处理方式也可以用在飞行行为上:MallardDuck的构造器 将flyBehavior实例变量初始化成FlyWithWings类型的实例( FlyWithWings是FlyBehavior的具体实现类)。
测试Duck的代码
quackBehavior->quack();
}
/**
* 不亲自处理呱呱叫行为,而是委托给flyBehavior对象。
*/
public function preformFly()
{
$this->flyBehavior->fly();
}
public abstract function display();
}
class MallardDuck extends Duck
{
/**
* MallardDuck constructor.
* 绿头鸭使用Quack类处理呱呱叫,
* 所以当performQuack()被调用,
* 就把责任委托给Quack对象进行正的呱呱叫.
* 使用FlyWithWings作为其 FlyBehavior类型。
*/
public function __construct()
{
$this->quackBehavior= new Quack1();
$this->flyBehavior= new FlyWithWings();
}
public function display()
{
echo "绿头鸭子\n";
}
}
/**
* Interface FlyBehavior
* 这是一个接口,所有飞行类 都实现它,
* 所有新的飞行类 都必须实现fly()方法。
*/
interface FlyBehavior
{
public function fly();
}
/**
* Class FlyWithWings
* 这里实现了所有有翅 膀的鸭子飞行动作
*/
class FlyWithWings implements FlyBehavior
{
public function fly()
{
echo "飞\n";
}
}
/**
* Class FlyNoWay
* 这里实现了所有不会飞的鸭子的动作
*/
class FlyNoWay implements FlyBehavior
{
public function fly()
{
//什么都不做
}
}
/**
* Interface QuackBehavior
* 呱呱叫行为也一样,
* 只包含一 个需要实现的quack()方法。
*/
interface QuackBehavior
{
public function quack();
}
/**
* Class Quack1
* 真的呱呱叫
*/
class Quack1 implements QuackBehavior
{
public function quack()
{
echo "呱呱\n";
}
}
/**
* Class Squeak
* 名为呱呱叫,其实是吱吱叫
*/
class Squeak implements QuackBehavior
{
public function quack()
{
echo "吱吱\n";
}
}
/**
* Class MuteQuack
* 名为呱呱叫,其实不出声
*/
class MuteQuack implements QuackBehavior
{
public function quack()
{
//什么都不做
}
}
$duck =new MallardDuck();
$duck->preformFly();
$duck->preformQuack();
$duck->display();
运行代码
动态设定行为
在鸭子里建立了一堆动态的功能没有用到,就太可惜了!假设我们想在鸭子子 类透过「设定方法(setter method)」设定鸭子的行为,而不是在鸭子的构 造器内实例化。
1、在Duck类中,加入两个新方法:
public abstract function setFlyBehavior(FlyBehavior $flyBehavior);
public abstract function setQuackBehavior(QuackBehavior $quackBehavior);
从此以后,我们可以「随时」调用这两个方法改变鸭子的行为。
改变测试类
class MallardDuck extends Duck
{
public function setFlyBehavior(FlyBehavior $flyBehavior)
{
$this->flyBehavior = $flyBehavior;
}
public function setQuackBehavior(QuackBehavior $quackBehavior)
{
$this->quackBehavior=$quackBehavior;
}
public function display()
{
echo "绿头鸭子\n";
}
}
$duck =new MallardDuck();
$duck->setFlyBehavior(new FlyWithWings());
$duck->setQuackBehavior(new Quack1());
$duck->preformFly();
$duck->preformQuack();
$duck->display();
运行结果一样
封装行为的大局观好,我们已经深入鸭子模拟器的设计,该是将头探出水面,呼吸空气的时候了。现在就来看看整体的格局。
下面是整个重新设计后的类结构,你所期望的一切都有:鸭子继承Duck, 飞行行为实现FlyBehavior接口,呱呱叫行为实现QuackBehavior接口。
也请注意,我们描述事情的方式也稍有改变。不再把鸭子的行为说 成「一组行为」,我们开始把行为想成是「一族算法」。想想看,在 SimUDuck 的设计中,算法代表鸭子能做的事(不同的叫法和飞行法), 这样的作法也能用于用一群类计算不同州的销售税金。
请特别注意类之间的『关系』。拿一枝笔,把下面图形中的每个箭头 标上适当的关系,关系可以是IS -A(是一个)、HAS-A(有一个)、 IMPLEMENTS(实现)。
代码地址:gitHub