設計模式(筆記)優先使用物件組合而不是類繼承
優先使用物件組合而不是類繼承
文章內容參考自:
- http://www.hautelooktech.com/2013/02/05/design-principle-favor-composition-over-inheritance/
- 《agiledevelopment principles patterns and practices》
概述
繼承和組合都能達到一個程式碼複用的效果,但是類的繼承通常是白箱複用,物件組合通常為黑箱複用。我們在使用繼承的時候同時也就擁有了父物件中的保護成員,增加了耦合度。而物件組合就只需要在使用的時候介面穩定,耦合度低。
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
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原則。