优先使用对象组合而不是类继承
文章内容参考自:
继承和组合都能达到一个代码复用的效果,但是类的继承通常是白箱复用,对象组合通常为黑箱复用。我们在使用继承的时候同时也就拥有了父对象中的保护成员,增加了耦合度。而对象组合就只需要在使用的时候接口稳定,耦合度低。
Is a和has a
我们怎么来判断是用继承还是组合呢?只有在对象之间关系具有很强的is a关系的时候才使用继承。
那么继承为什么有的时候反而不好呢。下面举个简单的例子。
假设我们需要设计音乐播放器。现在需要设计连个。一个是record player,另外一个是8轨道的播放器。所以现在我们先设计一个基类。
abstract class AbstractPlayer
{
public functionplay()
{
echo"I'm playing music through my speakers!";
}
public functionstop()
{
echo"I'm not playing music anymore.";
}
}
看起来好像可以了。然后我们设计两个类RecordPlayer和EightTrackPlayer,都继承自AbstractPlayer。
class RecordPlayer extends AbstractPlayer
{
}
class EightTrackPlayer extendsAbstractPlayer
{
}
现在,新的需求来了,我们需要完成一个随身听播放器。他和上面两个播放器很像,但是需要从耳机来播放音乐:
class PortableCassettePlayer extendsAbstractPlayer
{
public functionplay()
{
echo"I'm playing music through headphones!";
}
}
看起来没问题哈。我们的PortableCassettePlayer继承自AbstractPlayer,并且修改了一点Play的方法。更进一步,我们需要实现一个MPS播放器。
class Mp3Player extends AbstractPlayer { public function play() { echo "I'm playing music through headphones!"; } } |
我们发现我们实际上是直接复制了PortableCassettePlayer类的play函数。我怕我们不能直接使用基类的play函数因为MP3需要使用耳塞来播放音乐。我们可能想,我们可以继承PortableCassettePlayer啊,但是这样我们也就拥有了随身听播放器的其他功能,比如插入磁带,但是其实MP3是不需要的。我们发现我们本来想用继承来复用代码,但是实际上我们的代码确更加的重复了。
然后我们看使用组合怎么来做:
interface PlayBehaviorInterface { public function play(); }
class PlayBehaviorSpeakers implements PlayBehaviorInterface { public function play() { echo "I'm playing music through my speakers!"; } }
class PlayBehaviorHeadphones implements PlayBehaviorInterface { public function play() { echo "I'm playing music through headphones!"; } } |
现在我们的播放更能被抽离出来了。
abstract class AbstractPlayer
{
protected$play_behavior;
abstract protectedfunction _createPlayBehavior();
public function__construct()
{
$this->setPlayBehavior($this->_createPlayBehavior());
}
public functionplay()
{
$this->play_behavior->play();
}
public functionsetPlayBehavior(PlayBehaviorInterface $play_behavior)
{
$this->play_behavior= $play_behavior;
}
public functionstop()
{
echo"I'm not playing music anymore.";
}
}
我们看到AbstractPlayer有一个创建Player的方法,也由一个setPlayBehavior的方法。这就允许我们在运行的时候动态的改变play的行为。我们看看Mp3播放器:
class Mp3Player extends AbstractPlayer
{
protected function_createPlayBehavior()
{
returnnew PlayBehaviorHeadphones;
}
}
试想以后,假设我们的M篇
播放器需要支持蓝牙播放,很方便的就可以修改了:
class PlayBehaviorBluetooth implementsPlayBehaviorInterface
{
public functionplay()
{
echo"I'm playing music wirelessly through Bluetooth!";
}
}
新的蓝牙功能可以再运行的时候动态的设置给播放器:
$mp3_player = new Mp3Player;
$mp3_player->play(); //echoes "I'mplaying music through headphones!"
$mp3_player->setPlayBehavior(newPlayBehaviorBluetooth);
$mp3_player->play(); //echoes "I'mplaying music wirelessly through Bluetooth!"
抽象这些变化的行为到另外一个接口类中让我们可以更好的扩展他的功能。回顾上面的代码,我们其实用到了策略模式,不违背OCP原则,等。
Is a是就行为而言的。比如经典的长方形和正方形而言:
Class Rectangle {
Privatedouble mWidth;
Privatedouble mHight;
Publicvoid setWidth(double width) {
mWidth= width;
}
publicvoid setHigh(tdouble hight) {
mHight= hight;
}
publicdouble area() {
returnmWidth * mHight;
}
}
class Square extends Rectangle {
Public void setWidth(double width) {
Super.setWidth(width);
Super.setHight(hight);
}
publicvoid setHigh(tdouble hight) {
Super.setWidth(width);
Super.setHight(hight);
}
}
void adjust(Rectangle r) {
r.setWidth(5);
r.setHeight(4);
asset(r.area()== 20);
}
从设计正方形类的人角度来看,正方形就是一个特殊的长方形。但是从编写者角度来看,他是基于假设width和heigh独立变化来判定结果的。所以导致断言错误。在这个地方Square就不能替换Rectangle了,因为Square在adjust中的行为方式和Rectangle的行为方式已经不一样了,他们违反了LSP原则。
我们可以添加if else来确定对象的类型啊,来判定是rectangle或者Square啊,那么以后新增子类都会在这个地方来修改代码,使代码的各个部分充斥着if else,违背OCP原则。