面向對象之兩大要領

本文同步發在: http://cpper.info/2016/01/05/Two-Points-Of-Oriented-Object.htmlhtml

總覽java

在工做初期,咱們可能會常常會有這樣的感受,本身的代碼接口設計混亂、代碼耦合較爲嚴重、一個類的代碼過多等等,本身回頭看的時候都以爲汗顏。再看那些知名的開源庫,它們大多有着整潔的代碼、清晰簡單的接口、職責單一的類,這個時候咱們一般會捶胸頓足而感嘆:何時老夫才能寫出這樣的代碼!mysql

做爲新手,咱們寫的東西不規範或者說不夠清晰的緣由是缺少一些指導原則。咱們手中揮舞着面向對象的大旗,寫出來的東西卻充斥着面向過程的氣味。也許是咱們不知道有這些原則,也許是咱們知道可是不能很好運用到實際代碼中,亦或是咱們沒有在實戰項目中體會到這些原則可以帶來的優勢,以致於咱們對這些原則並無足夠的重視。linux

在這裏,咱們將一塊學習一些面向對象設計的要點、原則及模式。sql

在此以前,有一點須要你們知道,熟悉這些原則不會讓你寫出優秀的代碼,只是爲你的優秀代碼之路鋪上了一層柵欄,在這些原則的指導下你才能避免陷入一些常見的代碼泥沼,從而讓你專心寫出優秀的東西。數據庫

本文將逐漸介紹一下內容:編程

  1. 面向對象之2大要點
  2. 面向對象之6大原則
  3. 面向對象之23種設計模式

本節先介紹面向對象之2大要點。設計模式

面向對象之兩大要點

面向接口編程而不是面向實現編程

簡述api

面向接口和麪向實現都基於面向對象的模式,也就是說面向接口並不能稱爲比面向對象的更高的一種編程模式,而是在面向對象中大的背景下的一種更加合理的軟件設計模式,這裏的接口並非具體語言中的實現機制(好比java中的interface),而是軟件設計層面的一種模式。面向接口編程它加強了類與類之間,模塊與模塊的之間的低耦合性,使軟件系統更容易維護、擴展。oracle

舉個簡單的例子,咱們常常用的操做數據庫的方法,在jdk中定義的都是接口,由不一樣的數據庫廠商實現,好比mysql的驅動,oracle的驅動,都是實現了jdk中定義接口標準。jdk中數據庫驅動的設計就是面向接口的,而不一樣的數據庫廠商就是面向實現的。面向接口的好處就是,定義好接口標準,不論是誰只要按定義好的標準來實現,均可以無縫的切換,因此不論是用mysql也好,仍是用oracle也都,從用戶層面來講都是在使用jdk的api。

面向實現編程主要缺點是高耦合,不支持擴展,而面向接口編程的主要優勢是低耦合,便於擴展。

優勢

  • 客戶端不知道他們所使用對象的具體類型
  • 一個對象能夠被另外一個對象輕易地替換
  • 對象不須要硬鏈接到一個特殊類的對象,所以增長了靈活性
  • 鬆耦合
  • 增長了重用的機會
  • 增長了組合的機會,由於被包含的對象能夠被實現了特定接口的其餘對象替換

缺點

  • 某種程度上增長了設計的複雜性

例子

假設咱們要封裝一個IO複用的類,一般有select、poll、epoll模型,且容許用戶自行選擇某一模型, 所以能夠經過繼承體系來實現。

/// I/O MultiPlexing 抽象接口
class Poller
{
public:
    typedef std::vector<Channel *>          ChannelList;
    typedef std::map<ZL_SOCKET, Channel *>  ChannelMap;

public:
    explicit Poller(EventLoop *loop);
    virtual ~Poller();

    /// 根據各類宏定義及操做系統區分建立可用的backends
    static Poller *createPoller(EventLoop *loop);

public:
    /// 添加/更新Channel所綁定socket的I/O events, 必須在主循環中調用
    virtual bool updateChannel(Channel *channel) = 0;

    /// 刪除Channel所綁定socket的I/O events, 必須在主循環中調用
    virtual bool removeChannel(Channel *channel) = 0;

    /// 獲得可響應讀寫事件的全部鏈接, 必須在主循環中調用
    virtual Timestamp poll_once(int timeoutMs, ChannelList &activeChannels) = 0;

    /// 得到當前所使用的IO複用backends的描述
    virtual const char* ioMultiplexerName() const = 0;
protected:
    ChannelMap  channelMap_;
    EventLoop   *loop_;
};

/*static*/ Poller* Poller::createPoller(EventLoop *loop)
{
#if defined(USE_POLLER_EPOLL)
    return new EpollPoller(loop);
#elif defined(USE_POLLER_SELECT)
    return new SelectPoller(loop);
#elif defined(USE_POLLER_POLL)
    return new PollPoller(loop);
#else
    return NULL;
#endif
}

class SelectPoller : public Poller
{
public:
    explicit SelectPoller(EventLoop *loop);
    ~SelectPoller();
    
    virtual bool updateChannel(Channel *channel);
    virtual bool removeChannel(Channel *channel);
    virtual Timestamp poll_once(int timeoutMs, ChannelList& activeChannels);
    virtual const char* ioMultiplexerName() const { return "select"; }
private:
    fd_set readfds_;           /// select返回的全部可讀事件
    fd_set writefds_;          /// select返回的全部可寫事件
    fd_set exceptfds_;         /// select返回的全部錯誤事件

    fd_set select_readfds_;    /// 加入到select中的感興趣的全部可讀事件
    fd_set select_writefds_;   /// 加入到select中的感興趣的全部可寫事件
    fd_set select_exceptfds_;  /// 加入到select中的感興趣的全部錯誤事件
    std::set< int, std::greater<int> >  fdlist_;
};

class EpollPoller : public Poller
{
public:
    explicit EpollPoller(EventLoop *loop, bool enableET = false);
    ~EpollPoller();
    
    virtual bool updateChannel(Channel *channel);
    virtual bool removeChannel(Channel *channel);
    virtual Timestamp poll_once(int timeoutMs, ChannelList& activeChannels);
    virtual const char* ioMultiplexerName() const { return "linux_epoll"; }

private:
    typedef std::vector<struct epoll_event> EpollEventList;
    int  epollfd_;
    bool enableET_;
    EpollEventList events_;
};

以上經過多態實現了一個IO複用的繼承體系。在純虛基類(Poller)中定義了全部要實現IO複用的接口,而由其餘子類(SelectPoller、EpollPoller)針對接口分別實現相應功能。對於使用者來講,並不須要考慮其內容實現細節,只需根據Poller類的接口便可使用。

優先使用組合而不是繼承(CARP)

簡述

面向對象系統中功能複用的兩種最經常使用技術是類繼承和對象組合(object composition)。

類繼承容許你根據其餘類的實現來定義一個類的實現。這種經過生成子類的複用一般被稱爲白箱複用(white-box reuse)。術語「白箱」是相對可視性而言:在繼承方式中,父類的內部細節對子類可見,能夠說是「破壞了封裝性」,父類實現中的任何變化必然會致使子類發生變化,彼此間的依賴程度高。

對象組合是類繼承以外的另外一種複用選擇。新的更復雜的功能能夠經過組裝或組合對象來得到。對象組合要求被組合的對象具備良好定義的接口。這種複用風格被稱爲黑箱複用(black-box reuse),由於對象的內部細節是不可見的。對象只以「黑箱」的形式出現,只須要關心提供的接口,彼此間的依賴低。

優先使用組合而不是繼承要求咱們在複用對象的時候,要優先考慮使用組合,而不是繼承,這是由於在使用繼承時,父類的任何改變均可能影響子類的行爲,而在使用組合時,是經過得到對其餘對象的組合而得到更強功能,且有助於保持每一個類的單一職責原則。

繼承和組合各有優缺點。類繼承是在編譯時刻靜態定義的,且可直接使用,由於程序設計語言直接支持類繼承。類繼承能夠較方便地改變被複用的實現。當一個子類重定義一些而不是所有操做時,它也能影響它所繼承的操做,只要在這些操做中調用了被重定義的操做。

優先使用對象組合有助於你保持每一個類被封裝,並被集中在單個任務上。這樣類和類繼承層次會保持較小規模,而且不太可能增加爲不可控制的龐然大物。另外一方面,基於對象組合的設計會有更多的對象 (而有較少的類),且系統的行爲將依賴於對象間的關係而不是被定義在某個類中。

例子

Echo協議是指服務端收到客戶端的任何數據後都原封不動的發送回去。若是要實現一個EchoServer,按照傳統的作法,通常是先實現一個TcpServer抽象類,設置幾個虛函數,好比鏈接到來、數據到來,鏈接關閉等等,這也是面向對象中多態機制的經常使用實現方式。而這裏咱們演示一個基於函數回調的實現。

假設咱們已經有了一個封裝了IO複用的EventLoop類和一個能夠直接使用但不作任何數據處理的TcpServer類。
TcpServer的接口以下:

class TcpServer : zl::NonCopy
{
public:
    TcpServer(EventLoop *loop, const InetAddress& listenAddr);  
    void start();   
public:
    EventLoop* getLoop() const
    { return loop_; }

    void setConnectionCallback(const ConnectionCallback& cb)
    { connectionCallback_ = cb; }

    void setMessageCallback(const MessageCallback& cb)
    { messageCallback_ = cb; }

    void setWriteCompleteCallback(const WriteCompleteCallback& cb)
    { writeCompleteCallback_ = cb; }
};

那麼一個EchoServer實現起來就很簡單了:

class EchoServer
{
public:
    EchoServer(EventLoop *loop, const InetAddress& listenAddr);
    void start();
private:
    /// 客戶端鏈接創建或關閉時的回調
    void onConnection(const TcpConnectionPtr& conn);
    /// 客戶端有數據發送過來時的回調
    void onMessage(const TcpConnectionPtr& conn, ByteBuffer *buf, const Timestamp& time);
private:
    EventLoop *loop_;
    TcpServer *server_;
};

使用方式也很簡單:

EventLoop loop;
InetAddress listenAddr(port);
EchoServer server(&loop, listenAddr);
server.start();
loop.loop();

這樣就完成了一個EchoServer。

與此相似,還有簡單的daytime協議,客戶端鏈接服務端後,服務端返回給客戶端一個當前時間。一個DaytimeServer徹底能夠按照上面的思路來實現。

這也是面向對象和基於對象實現的差別,關於以函數回調方式替代面向對象中純虛函數方式的更多介紹請參考:以boost::function和boost:bind取代虛函數

相關文章
相關標籤/搜索