【简介】设计模式不只是简单地描述了问题的解决方案,而且很重视解决方案的可重用性和灵活性。
组合:如何通过聚合对象来获得比只是用继承更好的灵活性。
解耦:如何降低系统中元素的依赖性。
8 模式原则
8.1 组合与继承
继承是应对变化的环境及上下文设计的有效方式,然而它会限制灵活性,尤其当类承担多重责任的时候。
8.1.1 问题
UML 描述了这个一个场景。有一个 Lesson 抽象类用来描述课程,它定义抽象的 cost() 方法和 chargeType() 方法用来描述收费类型和具体收费。同时,定义了两个实现类(FixedPriceLesson 和 TimedPriceLesson)用来实现具体的收费机制。
这样一个系统能够很好地在两种课程下的收费情况。但是,如果我们的课程需求改成了需要处理演讲和研讨会呢?两种课程类型下都有 Fixed 和 Timed 两种收费模式,如果我们只是用继承的方式来实现的话:
就会发现不得不出现重复的问题,定价策略在 Lecture 和 Seminar 类的子类中被重复实现。
如果考虑在父类 Lesson 中使用条件语句来实现不同情况下的收费机制,从而移除这些重复。
Lesson 类里面的判断逻辑代码:
abstract class Lesson {
protected $duration;
const FIXED = 1;
const TIMED = 2;
private $costtype;
function __construct($duration, $costtype=1) {
$this->duration = $duration;
$this->costtype = $costtype;
}
function cost() {
switch ($this->costtype) {
case self::TIMED :
return (5*$this->duration);
break;
case self::FIXED:
return 30;
break;
default:
$this->costtype = self::FIXED;
return 30;
}
}
function chargeType() {
switch ($this->costtype) {
case self::TIMED:
return 'hourly rate';
break;
case self::FIXED:
return 'fixed rate';
break;
default:
$thsi->costtype = self::FIXED;
return 'fixed rate';
}
}
// Lesson的更多方法
}
class Lecture extends Lesson {
// Lecture特定的实现
}
class Seminar extends Lesson {
// Seminar特定的实现
}
以及测试代码:
$lecture = new Lecture(5, Lesson::FIXED);
echo $lecture->cost().$lecture->chargeType();
$seminar = new Seminar(3, Lesson::TIMED);
echo $seminar->cost().$seminar->chargeType();
虽然很好地解决了重复的问题,但是父类中使用了条件语句,这与我们通常用多态替换条件的重构思想背道而驰,是一种倒退。
8.1.2 使用组合
我们可以使用策略模式来解决这个问题。策略模式适用于将一组算法移入到一个独立的类型中。通过移走费用计算的代码,可以简化 Lesson 类型。
创建一个 CostStrategy 的抽象类,它定义了抽象方法 cost() 和 chargeType() 。cost() 方法需要一个 Lesson 实例,用于生成费用数据。并提供两个 CostStrategy 的两个实现类,用于明确两个情况下的费用算法。
有关 Lesson 类的代码:
abstract class Lesson {
private $duration;
private $costStrategy;
function __construct($duration, CostStrategy $strategy) {
$this->duration = $duration;
$this->strategy = $strategy;
}
function cost() {
return $this->costStrategy->cost($this);
}
function chargeType() {
return $this->costStrategy->chargeType();
}
function getDuration() {
return $this->duration;
}
// Lesson的更多方法
}
class Lecture extends Lesson {
// Lecture特定的实现
}
class Seminar extends Lesson {
// Seminar特定的实现
}
$lecture = new Lecture(5, Lesson::FIXED);
echo $lecture->cost().$lecture->chargeType();
$seminar = new Seminar(3, Lesson::TIMED);
echo $seminar->cost().$seminar->chargeType();
Lesson 类需要一个作为属性的 CostStrategy 对象。Lesson::cost() 方法只调用 CostStrategy::cost()。同样,Lesson::chargeType() 方法只调用 CostStrategy::chargeType()。这种显式调用另一个对象的方法来执行一个请求的方式便是「委托」。在示例代码中,CostStrategy 对象是 Lesson 的委托方。Lesson 类不再负责计费,而是把计费任务传给 CostStrategy 类,而具体计费的实现则是在 CostStrategy 的两个子类 TimedCostStrategy 和 FixedCostStrategy 中完成。
下面代码执行了委托操作:
1)Lesson 类的调用:
function cost() {
return $this->costStrategy->cost($this);
}
2) CostStrategy 类及其实现子类:
abstract class CostStrategy {
abstract function cost(Lesson $lesson);
abstract function chargeType();
}
class TimedCostStrategy extends CostStrategy {
function cost(Lesson $lesson) {
return $lesson->getDuration()*5;
}
function chargeType() {
return 'hourly rate';
}
}
class FixedCostStrategy extends CostStrategy {
function cost(Lesson $lesson) {
return 30;
}
function chargeType() {
return 'fixed rate';
}
}
通过传递不同 CostStrategy 对象,可以在代码运行时改变 Lesson 对象计算费用的方式。这种方式有助于产生具有高度灵活性的代码。
实际调用:
$lesson[] = new Seminar(4, new TimedCostStrategy());
$lesson[] = new Lecture(4, new FixedCostStrategy());
foreach ($lessons as $lesson) {
echo "lesson charge ".$lesson->cost();
echo "Charge type: ".$lesson->chargeType();
}
组合使用对象比使用继承体系更灵活,但同时也会导致代码的可读性下降,因为组合需要更多的对象类型,而这些类型的关系并不像继承关系那般有固定的可预见性,所以要理解系统中类和对象的关系会有些困难。
8.2 解耦
系统中一个组件的改变迫使系统其他许多地方也发生改变的时候,这种称之为紧耦合,这是我们需要避免的,重用性设计是面向对象设计的主要目标之一。
8.3 针对接口编程,而不是针对实现编程
把不同的实现隐藏在父类所定义的共同接口下。