国产探花免费观看_亚洲丰满少妇自慰呻吟_97日韩有码在线_资源在线日韩欧美_一区二区精品毛片,辰东完美世界有声小说,欢乐颂第一季,yy玄幻小说排行榜完本

首頁 > 學院 > 開發設計 > 正文

烏托邦式的接口和實現分離技術

2019-11-17 05:27:18
字體:
來源:轉載
供稿:網友

  《Imperfect C++》中展示了一種叫“螺栓”的技術,然而,這本書中的討論并不足夠深入。當然,我也相信Matthew是故意的,從而讓我們這些“三道販子”(Matthew自稱是二道販子)也能夠獲得一點點成就感。

  考慮這樣一個接口設計:

strUCt IRefCount;
struct IReader : public IRefCount;
  在Reader中實現接口:

<!--[if !supportEmptyParas]--> class Reader : public IReader;

  在上述的繼續結構中,IRefCount是一個結構性的類,用來實現引用計數,實際上它和領域邏輯部分IReader沒有什么關系。我們打算在IRefCount的基礎上,建立了一套工具來治理對象生命周期和幫助實現異常安全的代碼 (例如,smart pointer) 。現在來考慮Reader的實現,Reader除了需要實現IReader的接口,還必須實現IRefCount的接口。這一切看起來似乎順理成章,讓我們繼續看下面的設計<!--[if !supportEmptyParas]-->:

struct IWriter : public IRefCount;
<!--[if !supportEmptyParas]--> class Writer : public IWriter;
  現在來考慮Writer的實現,和Reader一樣,Writer除了要實現IWriter的接口外,同時還需要實現IRefCount的接口。現在,我們來看看IRefCount是如何定義的:

struct IRefCount {
virtual void add() = 0;
virtual void release() = 0;
virtual int count() const = 0;
virtual void dispose() = 0;
virtual ~IRefCount(){}
};
  在Reader中的IRefCount的實現:

virtual void add() { ++m_ref_count;}
virtual void release() {--m_ref_count;}
virtual int count() const{return m_ref_count;}
virtual void dispose() { delete this;}

int m_ref_count;
  同樣,在Writer的實現中,也包含了一模一樣的代碼,這違反了DRY原則(Don’t Repeat Yourself)。況且,隨著系統中的類增加,大家都意識到,需要將這部分代碼復用。一個能夠工作的做法是把IRefCount的實現代碼直接放到IRefCount中去實現,通過繼續,派生類就不必再次實現IRefCount了。我們來看一下dispose的實現:

virtual void dispose() { delete this;}
  這里,采用了delete來銷毀對象,這就意味著Reader必須在堆上分配,才可能透過IRefCount正確治理對象的生命周期,沒關系,我們還可以override dispose方法,在Reader如下實現dispose:

virtual void dispose() { }
  但是,這樣又帶來一個問題,Reader不能被分配在堆上了!假如你夠狠,當然,你也可以這么解決問題:

class HeaPReader : IReader;
class StackReader : HeapReader{ virtual void dispose() { } };
  問題是,StackReader 是一個HeapReader嗎?為了代碼復用,我們完全不管什么概念了。當然,假如你和我一樣,看重維護概念,那么這么實現吧:

class HeapReader : IReader;
class StackReader : IReader;
  這樣一來,IReader的實現將被重復,又違反了DRY原則,等著被將來維護的工程師詛咒吧!或許,那個維護工程師就是3個月后的你自己。假如這樣真的能夠解決問題,那么也還是可以接受的,很快,我們有了一個新的接口:

struct IRWiter : IReader, IWriter;
class RWiter : public IRWiter;
  考慮一下IRefCount的語義:它用來記錄對所在對象的引用計數。很顯然,我從IReader和IWriter中的任意一個分支獲得的IRefCount應該都是獲得一樣的引用計數效果。但是現在,這個繼續樹存在兩個IRefCount的實例,我們不得不在RWiter當中重新重載一遍。這樣,從IReader和IWriter繼續來的兩個實例就作廢了,而且,我們可能還浪費了8個字節。為了解決這個問題,我們還可以在另一條危險的道路上繼續前進,那就是虛擬繼續:

struct IReader : virtual public IRefCount;
struct IWriter : virtual public IRefCount;
  還記得大師們給予的忠告嗎--“不要在虛基類中存放數據成員”。“這樣有什么問題嗎,我們不必對大師盲目崇拜”,你一定也聽過這樣的建議。假如大師們不能說服這些人,那么我也不能。于是,我們進一步在所有的接口中提供默認實現,包括IReader和IWriter.

  現在的問題是:

struct IRWiter : IReader, IWriter;
  還是

struct IRWiter : virtual IReader, virtual IWriter ?
  假如你沒有選擇virtual,那么IRWiter被派生后,那么派生類的繼續樹中可能存在多個IReader實現,假如這個派生類要求只能提供一份IReader的語義怎么辦?除了重新實現接口還能怎樣?反過來,假如我們選擇了virtual繼續,那么派生類需要多個實現怎么辦?真是個麻煩事。“這是典型的過度設計,我們為什么要考慮這么多?”你可以這么說,但事實上,即使是一個數百文件的小型系統,也完全可能迫使你作出選擇。雖然,我們仍然有辦法作出拯救措施,但是也只是茍延殘喘而已。正如我前面所說,這是一個危險的道路,聰明如你,是斷然不會讓自己陷入這樣的泥潭的。

  讓我們離開虛擬繼續,先回到重復代碼的問題上來。有沒有更好的解決辦法呢?還好,在C++的世界里,我們有神奇的template,讓我們來消除重復的代碼:


template<typename Base>
class ImpReader : public Base{
constraint(is_base_derive(IReader, Base))
Implementation IReader
<!--[if !supportEmptyParas]--> };
class HeapReader : ImpReader<IReader>{};
class StackReader : ImpReader <IReader>{
virtual void dispose() {};
<!--[if !supportEmptyParas]--> };
  請注重,我們還是假設IRefCount已經提供了一個默認實現。現在,情況好了很多,所有的代碼都只有一份,而且,概念也沒有被破壞。假設,Writer也同樣需要類似的能力,那么,我們又多了StackWriter和HeapWriter.事實上,真的有人用到了StackWriter嗎?我不知道,只是,提供了StackReader,沒有理由不提供StackWriter啊。讓我們繼續。

  現在,我們發現,需要改進內存分配的性能問題,于是,我們希望通過內存池來分配對象,相應的dispose也需要修改:

virtual void dispose(){ distory(this);}
  于是,我們又多出兩個類,PoolReader和PoolWriter。這真是糟糕,組合爆炸可不是什么好兆頭。

  從我們前述的變化來看,都是IRefCount在變化,為什么不把這種變化分離出來呢?不必為IRefCount提供默認實現,借鑒ImpReader的手法:

template<typename Base>
class ImpHeapRefCount : public Base{
constraint(is_base_derive(IRefCount, Base));
..};
  類似的:

template<typename Base> class ImpStackRefCount : public Base;
<!--[if !supportEmptyParas]--> template<typename Base> class ImpPoolRefCount : public Base; <!--[endif]-->
  再看看,我們如何實現所有的Reader.

typedef ImpReader< ImpHeapRefCount<IReader> > HeapReader;
typedef ImpReader< ImpStackRefCount<IReader> > StackReader;
typedef ImpReader< ImpPoolRefCount<IReader> > PoolReader;
  以HeapReader為例,實際的繼續關系是這樣的:

ImpReaderàImpHeapRefCountàIReaderàIRefCount;
  對于Writer,我們完全可以采取同樣的手法來實現。對于上述的typedef可以預先定義,也完全可以不定義,交給最終用戶去組裝吧。現在,類的設計者再也不必為選擇實現而痛苦了,你只要提供不同的磚頭,客戶程序員可以輕而易舉的建立起大廈。還有比這更讓一個設計師幸福的嗎?

  繼續深入,考察ImpHeapRefCount和ImpStackRefCount的實現,我們提到,dispose方法的實現是不一樣的,但是,其他部分:add,releasee和count的實現完全可以相同。然而我們現在又分別實現了一遍,為了不違反DRY原則,我們如下處理:

template<typename Base>
class ImpPartialRefCount : public Base{
//實現add, release和count.
};

template<typename Base>
class ImpHeapRefCount : public Base{
virtual void dispose() { delete this;}
};

template<typename Base>
class ImpStackRefCount : public Base{
virtual void dispose() { }
};
  然后,我們可以這樣定義Reader:

typedef ImpReader<ImpHeapRefCount<ImpPartialRefCount<Ireader> > > HeapReader;
  請注重,我們在這里展示了一種能力,不必在一個實現當中完整的實現整個接口,可以把一個接口的實現分拆到多個實現當中。這個能力是非凡的,借助于此,我們可以提供更小粒度的實現單位,給最終用戶來組裝。具體拆分到什么樣的程度完全取決于代碼復用的需求,以及概念維護的需要。

  我們提供了高度的復用能力,同時避免了繼續帶來的強耦合,以及對推遲設計決策的支持,這些能力對于軟件設計師而言,正如Matthew在《Imperfect C++》中所說的,這簡直就是現實中的烏托邦!

  現在我們把這種手法首先針對單繼續做一個小結。對于任意的接口IInterface,我們提供如下的實現:

template<typename Base>
class ImpInterface : public Base{
constraint(is_base_derive(IInterface, Base));
};
  請注重,一個接口可以有任意多個實現,并且可以是任意的部分實現。<!--[if !supportEmptyParas]--> <!--[endif]-->

  假設我們有如下接口繼續樹:

InterfaceN àInterfaceN_1àInterfaceN_2à…àInterface0
  并且提供了實現類ImpInterface0 ~ ImpInterfaceN.

  那么,InterfaceN的實例類型就是:


typedef ImpInterfaceN<
ImpInterfaceN_1<
ImpInterfaceN_2<

ImpInterface0<InterfaceN> …> > > ConcreteClassN;
  我們注重到,定義ConcreteClassN的時候,我們的ImpInterface是按照順序來的,我認為這是合適的做法。當然了,最后組裝的權力已經交給客戶了,客戶愛怎么組裝就怎么組裝吧。然而我們還是有一些需要注重的問題。

  1.假定,我需要在ImpInterfaceI中引用基類的方法,記住,不要使用這樣的手法:

ImpInterfaceI_K::SomeMethod();
  這樣調用不具有多態性,而應該這樣:

this-> SomeMethod();
  2.不要在自己的ImpInterfaceI實現中覆蓋基類接口的其他已經實現的方法,假如你一定要這么做,那么務必在文檔中說明,因為在組裝的時候,順序將是極其要害的了。

  3.這個方法和設計模式中的Template Pattern目的是不一樣的。Template Pattern是在基類中定義了一個算法,讓派生類定制算法的某些步驟。這里的方法針對的是接口模型的概念,提供接口和實現分離的技術。

  關于第二條,應該盡量避免發生。這里說的覆蓋是指基類實現已經實現了該方法,而后繼實現又覆蓋該方法。基類實現可以是一個部分實現,對于沒有實現的那些方法,在派生接口的實現類中實現則是常見的。一方面,我們盡量合理分解層次之間的功能,另一個方面,可以通過定制實現模板類,來保證順序。盡可能的讓語言本身來保證正確性,而不是依靠文檔。我們可以像這樣預先裝配一些東西:

template<typename Base>
class SomeComponent : public ImpPartA < ImpPartB <Base> >{};
  可惜,C++暫時還不支持模板的不完全typedef,否則,我們還可以如下定以:

template<typename Base>
typedef ImpPartA< ImpPartB<Base> > SomeComponent;
  不過,C0x很可能會支持類似的語法。這樣,我們使用SomeComponent來作為一個預制品,來保證一些安全性,而不必完全依靠文檔了。 <!--[endif]-->

  看看ConcreteClassN的定義,也許你和我一樣,并不喜歡這種嵌套的、遞歸的定義方式,太難看了。讓世界稍微美好一點吧!于是我們提供一個輔助類:

<!--[if !supportEmptyParas]--> template<typename T>struct Empty{};

template<typename I, typename template<class> class B>
struct Merge{ <!--[if !supportEmptyParas]-->typedef B<I> type;};

template<typename I >
struct Merge<I, Empty >{
typedef I type;
<!--[if !supportEmptyParas]--> }; <!--[endif]-->

template

typename I,
typename template<class> class B1,
typename template<class> class B2 = Empty,

typename template<class> class Bn = Empty,

struct Reform{
typedef typename Merge<
typename Merge<
typename Merge<I, B1>::type
, B2>::type , …,Bn>::type type;
};
  現在,我們可以這樣定義ConcreteClassN了:

Typedef Reform<InterfaceN, ImpInterface0, ImpInterface1,
…ImpInterfaceN>::type ConcreteClassN;
  是不是清爽了很多?

  在繼續下面內容以前,請回味一下這個不是問題的問題:

  假設IReader有3種實現,IRefCount有3種實現,我們將如何漂亮地解決掉他們。 <!--[endif]-->

  現實世界總是要復雜得多,讓我們進入真實的世界。回顧這個接口:

struct IRWiter : IReader, IWriter;
  假設我們確實需要IReader, IWriter,但是并不需要IRWrite,可不可以讓一個對象同時支持這兩個接口呢,就像COM一樣?當然可以,我們借助于這樣一個輔助模版:

template<typename B1, typename B2>
struct Combine : B1, B2{
typedef B1 type1;
typedef B1 type2;
<!--[if !supportEmptyParas]--> }; <!--[endif]-->

typedef Reform< Combine<IReader, IWriter>, ImpRefCount, ImpWriter, ImpReader >::type ConcreteRWiter
  為了現實需要,我們可以提供Combine的多個特化版本以支持任意數量的接口組合。假如僅僅是為了去掉一個IRWiter就引入一個Combine,雖有好處,但是意義也不大。那么,考慮這樣一個例子。

struct IHttpReader : IReader;
struct IFileReader : IReader;
  我們需要一個對象,同時支持從網絡和從文件讀取的能力。先看不引入Combine的做法:

struct IFileHttpReader : IFileReader , IHttpReader;
typedef Reform<IFileHttpReader, ImpRefCount, ImpHttpReader,
ImpFileReader>::type ConcreteRWiter;
  覺得有什么問題嗎?ImpReader同時實現了IFileReader分支和IHttpReader分支中的IReader,但是,和IRefCount不同的是,我們完全有理由相信,這兩個分支其實需要不同的IReader的實現。即使IReader確實可以是同樣的實現,另一個嚴重的問題是,ImpReader是一個不完整的實現,ImpFileReader和ImpHttpReader都分別重載了IReader中的一部分方法,例如,兩者都實現了如下方法:


virtual bool open(const char* url);
  如何解決這個問題?讓我們回顧一下IFileHttpReader,首先這個接口就是個問題產物:

  open到底open什么?文件,還是HTTP連接,還是兩個都打開?也就是說,從概念上來講,IFileHttpReader就存在矛盾,這樣的概念很顯然是難以維護的。其次,我們完全沒有辦法為兩個分支提供不同的實現,當然,其根源是IFileHttpReader的錯誤設計導致的,不采用我們這里提到的技術,問題依然存在。現在引入一個結論:假如某個接口的基類樹中多次出現同一個接口,我們的技術無法為這些接口分別提供不同的實現。這里的解決方案是拋棄IFileHttpReader,引入Combine, 我們可以這樣解決問題:

typedef Reform<
Combine< ImpFileReader <IFileReader>, ImpHttpReader <IHttpReader> >,
ImpRefCount, ImpReader
>::type ConcreteFileHttpReader;
  假設,ImpReader不能同時滿足兩個分支的要求,我們可以這么做:

typedef Reform <
Combine< ImpFileReader < ImpReaderA<IFileReader> >,
ImpHttpReader < ImpReaderB <IHttpReader> >
>,
ImpRefCount
>::type ConcreteFileHttpReader;
  利用Combine,我們可以充分發揮多重繼續的組合能力,我們既享受了接口設計和實現分離的好處—更輕易維護概念了,也充分享有代碼復用的能力。并且,將設計決策充分推遲:甚至客戶程序員完全可以定制自己的接口實現從而和現有系統結合,這是一個完美的Open-Close的設計手段。

<!--[if !supportLists]--><!--[if !supportLists]--><!--[if !supportLists]--><!--[if !supportLists]--> 現在,總結一下在多重繼續中的注重事項。

  1.接口盡量是單繼續的。

  2.多重繼續的接口必須意識到,所有繼續樹的相同接口只能共享同一份實現。

  3.嚴苛地去維護接口的概念,不要為了實現問題定義中間的接口(就象那個IFileHttpReader)

  4.合理地利用多重繼續的組合能力。<!--[if !supportEmptyParas]-->

  關于最后一條,您可以做一些有趣的探索。給出一個空基類:

struct Over{};
  當然,也可以是其它非模板類。把所有的類都實現成模版形式:ImpClassA<T>, ImpClassB<T>,借助于Combine,我們可能給出這樣的定義:

typedef Combine<ImpClassA< Combine<ImpClassB< Over >, ImpClassC< Over > > >,Combine<ImpClassF<Over>, ImpClassB<ImpClassD< Over > >>,ImpClassE<Over>>::type ConcreteSomeClass;
  我們注重于將這些ImpClasses拆成盡可能小的正交模塊。那么借助組合技術,可能獲得很高的復用性。但是,有句老話,不要為了復用而復用,反正,這里的探索我也是淺嘗輒止,出了什么事情和我無關。非凡提醒一下,上面代碼中Combine里面出現了一個type,你可以嘗試在上面施加你喜歡的TMP手法嘛。

  把那些有趣的探索先放在一邊。現在,我已經把這種技術完整地呈現出來了。然而,沒有一項技術是完美的,這里也不例外。這個技術有兩個小小的缺陷。

  第一個缺陷則是構造函數的問題。回顧Combine的實現,我們無法為Combine額外提供合適的構造函數。不過,這個問題并不是非凡嚴重,我們完全可以定制自己的Combine。并且,這些不同的Combine可以混合使用。另外,在組裝的時候需要小心的維護構造函數的調用鏈,這可能傷害到復用性。Assignment中也存在類似的問題。運算符重載也可能導致混亂,不過,我一直認為,在值語義之外的類當中重載運算符可是要非常謹慎的,很顯然,我們這里展示的技術并不適合值語義的類型設計。

  另一個缺陷是工程上的。因為上述的實現都是模板類,所以,我們通常需要將實現在頭文件里面提供,這個可能是有些人不愿意的。我將展現一種解決的方法,這就是另一個利器:Pimpl慣用法。
以IReader為例,假設如下接口:

struct IReader : IRefCount
{
virtual bool open(const char* url) = 0;
virtual void read(Buffer& buf, size_t size) = 0;
};
  現在,我們只實現read方法:

class ConcreteImpReader;//前置申明
template<typename Base>
class ImpPartialReader : public Base
{
Pimpl<ConcreteImpReader> m_reader;
public:
ImpPartialReader() : m_reader(this), Base(){}
virtual void read(Buffer& buf, size_t size) { m_reader->read(buf, size);}
};
  現在,給出一個原始的Pimpl實現:

template<typename T>
struct Pimpl
{
T* imp;
template<typename Host>
eXPlicit Pimpl(Host* host) : imp(new T(host)){}
T* Operator->() const{return imp;}
~Pimpl(){delete imp;}

};
  在單獨的文件中實現:


class ConcreteImpReader
{
ConcreteImpReader(IReader * host) : m_host(host){}
void read(Buffer& buf, size_t size) { …}

};
  ConcreteImpReader中可以引用所在的host對象的其他方法,但是自己實現的方法除外。假如我們愿意,也可以把接口的實現分拆到多個具體的實現類當中,只是我們無法獲得象多重繼續那樣強大的組合能力。

發表評論 共有條評論
用戶名: 密碼:
驗證碼: 匿名發表
主站蜘蛛池模板: 左云县| 崇文区| 龙门县| 大田县| 贡觉县| 乌拉特后旗| 汉阴县| 广西| 确山县| 高尔夫| 连城县| 曲水县| 汶川县| 定南县| 方城县| 新泰市| 饶平县| 灵璧县| 响水县| 民乐县| 商水县| 长春市| 曲松县| 达日县| 连云港市| 兰考县| 咸阳市| 平凉市| 大英县| 旅游| 古浪县| 洪江市| 唐河县| 桦甸市| 会昌县| 河源市| 饶平县| 银川市| 开阳县| 银川市| 防城港市|