單例模式
定義:確保一個類只有一個實例,併爲其提供一個全局的訪問入口。c++
那麼什麼狀況下使用單例?最多見的狀況就是一個類須要與一個維持自身狀態的外部系統進行交互,好比說打印機。大多數狀況下都是多人共用一個打印機,這意味着可能由多我的同時向這個打印機發送打印任務,這個時候管理打印機的類就必須熟悉打印機的當前狀態並協調這些任務的執行。這個時候就不容許存在多個打印機的實例,由於實例沒法知道其餘的實例所作的操做,也就沒法進行總體的管理。編程
咱們先看看最多見的單例的實現方式:服務器
class FileSystem { public: static FileSystem& instance_() { if(instance_ == nullptr) { instance_ = new FileSystem(); } return instance_; } private: FileSystem(){} static FileSystem* instance_; };
c++11保證一個局部靜態變量初始化只進行一次,哪怕實在多線程的狀況下也是如此,因此c++11中這樣寫更優雅。多線程
class FileSystem { public: static FileSystem& instance() { static FileSystem& instance_ = new FileSystem(); return instance_; } private: FileSystem(){} };
特性
從代碼實現上看,單例模式由如下幾個特性:併發
- 若是咱們不使用它,就不會建立實例。
- 它在運行時初始化。還有一種方法是使用靜態類,但靜態類有個侷限就是:自動初始化。並且它是在main函數以前初始化,這也就意味了它不能使用運行時才能知道的信息,而且不能相互依賴——編譯器並不能保證靜態函數間初始化的順序。
- 你能夠繼承單例,這可讓咱們更好的控制咱們的代碼,好比對於多平臺的文件系統,咱們定義兩個子類繼承FileSystem的接口,經過一個編譯指令控制文件系統類型的綁定,程序的其餘代碼能夠與文件系統解耦(由於其餘代碼只是用FileSystem::instance())。
後悔使用單例模式的緣由
(1)它是個全局變量框架
根據前人的經驗,全局變量時有害的,咱們應該遠離全局變量。爲何了?函數
- 它令代碼晦澀難懂。原本在一個函數中,咱們只須要關注函數段的局部代碼便可,但若是在函數中使用了全局變量,則咱們就須要追蹤全部能改變全局變量狀態的代碼,若是這樣的代碼由成百上千行,你就會痛恨全局變量了。
- 全局變量促進了耦合。由於全局變量的特性,你只須要包含相應的頭文件,就可使用這個變量,這就增長了代碼的耦合程度。
- 它對併發並不友好,這個顯而易見。
(2)它是個多此一舉的方案佈局
從定義上看出,單例模式實際上是解決了兩個問題:第一保證一個實例,第二提供已訪問入口。保證一個單例是頗有用的,但誰說咱們但願誰都能操做它?而第二個問題,便利的訪問一般是咱們使用單例的主要緣由。但這同時也會引出新的問題,好比一個日誌類,一開始你們都使用這個單例的日誌類時很方便,但隨着項目的深刻,對於日誌的需求也複雜了起來,好比要求分類寫入多個日誌文件,這個時候由於你是單例,因此爲了支持多個實例,你就要修改每一個你調用這個類的地方,結果便利的訪問也就不那麼便利了。性能
(3)延遲初始化剝離了你的控制spa
延遲初始化也就是在第一次調用的時候初始化,這樣也就不能保證你初始化的時機。這一般在對性能要求很是高的遊戲中時不被容許的,設想一個音頻單例單例,初始化須要幾百毫秒,並且伴隨着內存的分配,若是你的遊戲進行中忽然調用這個單例,則會進行初始化操做,這件帶來不可接受的遊戲掉幀和卡頓。並且也不利於內存佈局的控制。
在遊戲中一般使用這樣的方式來實現單例模式:
class FileSystem { public: static FileSystem& instance() { return instance_; } private: FileSystem(){} static FileSystem instance_; };
咱們應該使用單例嗎?
(1)首先看看你需不需類
在遊戲中,我看見了太多的「manager」類了,它們的初衷時爲了管理其它對象,雖然有時確實有用,但我更多的時看到它被濫用。好比下面的一個例子:
class Bullet { public: int getX() const {return x_;} int getY() const {return y_;} void setX(int x) {x_=x;} void setY(int y) {y_=y;} private: int x_; int y_; }; class BulletManager { public: Bullet* create(int x,int y) { Bullet* bullet = new Bullet(); bullet->setX(x); bullet->setY(y); return bullet; } bool isOnScreen(Bullet& bullet) { return bullet.getX() >=0 && bullet.getY >=0 && bullet.getX() <= SCREEN_WIDTH && bullet.getY() <= SCREEN_HEIGHT; } void move(Bullet& bullet) { bullet.setX(bullet.getX() + 5); } };
這個例子有點極端,但現實中不少manager類簡化後就是這樣的一個邏輯。咱們經過一個單例來管理Bullet,感受上好像合理,但仔細分析後,發現這個manager根本就沒有存在的必要,設計出這樣一個類的人應該對OOP不太熟悉。首先咱們分析這三個方法:
- create建立一個Bullet,若是咱們想要更好的管理Bullet的建立,那咱們應該使用工廠模式,它會使咱們的代碼可維護性更高,或者直接在Bullet中提供一個靜態函數來建立一個新的對象,從設計上來講更顯得合理;
- isOnScreen判斷是否在屏幕中,這個方法既能夠放在業務層代碼中也能夠放入Bullet中(由於能夠理解我爲這是bullet的一個狀態),把它放在這個Manager類中顯得不三不四;
- move是移動bullet,這個就好設計了,move原本就是bullet的行爲,因此若是沒有特別的需求,move應該放入Bullet類中。
因此,修改後,咱們只須要一個Bullet類:
class Bullet { public: Bullet(int x,int y):x_(x),y_(y) { } bool isOnScreen() { return x_ >=0 && y_ >=0 && x_ <= SCREEN_WIDTH && y_ <= SCREEN_HEIGHT; } void move() { x_ += 5; } };
這樣修改後,類的設計顯得更合理,更天然。咱們徹底不須要一個額外的manager單例來幫助咱們管理,因此,在咱們設計單例時,首先就要分析咱們是否真的須要這個單例。
(2)將類限制爲單一實例
咱們使用單例模式,不少時候只是要限制該類只有一個實例,但這並不意味着咱們要提供一個全局訪問,咱們可能只是想在某一部分代碼中訪問這個實例,這個時候若是使用單例模式提供一個全局的訪問接口,將會削弱總體的框架。咱們能夠有幾種方式避免這種狀況的出現。好比:
class FileSystem { public: FileSystem() { assert(!instantiated); instantiated = true; } ~FileSystem() { instantiated = false; } private: static bool instantiated; }; bool FileSystem::instantiated = false;
經過一個斷言,保證FileSystem只有一個實例。
(3)爲實例提供便捷的訪問方式
使用單例模式的另外一個需求就是便利的訪問,它能讓咱們隨時隨地的獲取這個惟一的實例。但這與咱們通用的編程準則不符,咱們一般是在保證功能的狀況下儘可能限制變量使用的一個範圍,這樣咱們就只須要記住它的地方機會少不少(想一想全局變量帶來的問題)。那在不適用單例模式的時候,咱們還有什麼其它的途徑訪問一個對象了?一般咱們會有這麼幾種方式:
- 做爲參數傳遞進去。這個是最簡單,一般也是最好的方法。但有時咱們會碰到這樣的狀況,即這個對象與函數的內容沒什麼必然的聯繫,好比咱們執行一個渲染函數時要記錄日誌,若是把日誌對象加入到函數的參數列表中,將會很是的奇怪,對於這種狀況,咱們須要一些其它的辦法。
- 在基類中獲取它。這須要設計一個良好的繼承體系,既然全部的子類都要訪問這個對象,咱們能夠把這個對象放到父類中讓全部的子類都能訪問到它。
- 使用其它全局對象訪問它。現實中咱們不太可能把全部的全局變量都移除,好比在大部分的遊戲代碼中咱們都會定義一個表明整個遊戲狀態的Game或者World對象,咱們能夠把全局對象放入這些已有的全局變量中來減小它們的數量。
- 使用服務定位其來訪問。這是一種專門設計一個類來給對象作全局訪問的,將會在服務器定位模式一節講解。
結語
因此,咱們應該在何時使用單例了?老師說,單例並無你想象的那樣重要,若是你要確保類只被實例化一次,能夠簡單的使用一個靜態類,若是還不知足要求,可使用一個靜態的標識符在運行時檢查是否只有一個實例被建立。不過使用與否仍是要視你本身需求來定,但必定要防止單例模式的濫用,這不會給你帶來任何的好處。