框架是一U特D的软gQ它q不能提供完整无~的解决Ҏ(gu)Q而是Z构徏解决Ҏ(gu)提供良好的基。框架是半成品。典型地Q框架是pȝ或子pȝ的半成品Q框架中的服务可以被最l应用直接调用,而框架中的扩展点是供应用开发h员定制的“可变化点”?/p>
软g架构不是软gQ而是关于软g如何设计的重要决{。Y件架构决{涉及到如何Y件系l分解成不同的部分、各部分之间的静态结构关pd动态交互关pȝ。经 q完整的开发过E之后,q些架构决策体现在最l开发出的Y件系l中Q当Ӟ引入软g框架之后Q整个开发过E变成了“分两步走”Q而架构决{往往会体现在 框架之中。或许,Z常把架构和框架Z谈的原因在于此?
“请写一个Singleton?#8221;面试官微W着和我说?/p>
“q可真简单?#8221;我心里想着Qƈ在白板上写下了下面的Singleton实现Q?/p>
“那请你讲解一下该实现的各l成?#8221;面试官的怸仍然带着微笑?/p>
“首先要说的就是Singleton的构造函数。由于Singleton限制其类型实例有且只能有一个,因此我们应通过构造函数设|ؓ非公?来保证其不会被用户代码随意创建。而在cd实例讉K函数中,我们通过局部静态变量达到实例仅有一个的要求。另外,通过该静态变量,我们可以该实例的创?延迟到实例访问函数被调用时才执行Q以提高E序的启动速度?#8221;
保护
“说得不错Q而且更可늚是你能注意到Ҏ(gu)造函数进行保护。毕竟中间g代码需要非怸谨才能防止用户代码的误用。那么,除了构造函C外,我们q需要对哪些l成q行保护Q?#8221;
“q需要保护的有拷贝构造函敎ͼ析构函数以及赋D符。或许,我们q需要考虑取址q算W。这是因为编译器会在需要的时候ؓq些成员创徏一个默认的实现?#8221;
“那你能详l说一下编译器会在什么情况下创徏默认实现Q以及创些默认实现的原因吗?”面试官l问道?/p>
“在这些成员没有被声明的情况下Q编译器用一pd默认行ؓQ对实例的构造就是分配一部分内存Q而不对该部分内存做Q何事情;对实例的拯?仅仅是将原实例中的内存按位拷贝到新实例中Q而赋D符也是对类型实例所拥有的各信息q行拯。而在某些情况下,q些默认行ؓ不再满条gQ那么编译器 尝试根据已有信息创些成员的默认实现。这些媄响因素可以分为几U:cd所提供的相应成员,cd中的虚函C及类型的虚基cR?#8221;
“׃构造函Cؓ例,如果当前cd的成员或基类提供了由用户定义的构造函敎ͼ那么仅进行内存拷贝可能已l不是正的行ؓ。这是因成员的构?函数可能包含了成员初始化Q成员函数调用等众多执行逻辑。此时编译器需要ؓq个cd生成一个默认构造函敎ͼ以执行对成员或基cL造函数的调用。另外,?果一个类型声明了一个虚函数Q那么编译器仍需要生成一个构造函敎ͼ以初始化指向该虚函数表的指针。如果一个类型的各个zcM拥有一个虚基类Q那么编译器 同样需要生成构造函敎ͼ以初始化该虚基类的位|。这些情况同样需要在拯构造函C考虑Q如果一个类型的成员变量拥有一个拷贝构造函敎ͼ或者其基类拥有一 个拷贝构造函敎ͼ位拷贝就不再满要求了,因ؓ拯构造函数内可能执行了某些ƈ不是位拷贝的逻辑。同时如果一个类型声明了虚函敎ͼ拯构造函数需要根据目 标类型初始化虚函数表指针。如基类实例l过拯后,其虚函数表指针不应指向派生类的虚函数表。同理,如果一个类型的各个zcM拥有一个虚zQ那么编?器也应ؓ其生成拷贝构造函敎ͼ以正设|各个虚基类的偏UR?#8221;
“当然Q析构函数的情况则略为简单一些:只需要调用其成员的析构函C及基cȝ析构函数卛_Q而不需要再考虑对虚基类偏移的设|及虚函数表指针的设|?#8221;
“在这些默认实CQ类型实例的各个原生cd成员q没有得到初始化的机会。但是这一般被认ؓ是Y件开发h员的责QQ而不是编译器的责仅R?#8221;说完q些Q我长出一口气Q心里也暗自庆幸曄研究q该部分内容?/p>
“你刚才提到需要考虑保护取址q算W,是吗Q我想知道?#8221;
“好的。首先要声明的是Q几乎所有的人都会认为对取址q算W的重蝲是邪恶的。甚臌QboostZ防止该行为所产生的错误更是提供了 addressof()函数。而另一斚wQ我们需要讨论用户ؓ什么要用取址q算W。Singleton所q回的常常是一个引用,对引用进行取址得到相?cd的指针。而从语法上来_引用和指针的最大区别在于是否可以被delete关键字删除以及是否可以ؓNULL。但是Singletonq回一个引用也 pC其生存期由非用户代码所理。因此用取址q算W获得指针后又用delete关键字删除Singleton所q回的实例明显是一个用户错误。综上所 qͼ通过取址q算W设|ؓU有没有多少意义?#8221;
重用
“好的Q现在我们换个话题。如果我现在有几个类型都需要实CؓSingletonQ那我应怎样使用你所~写的这D代码呢Q?#8221;
刚刚q在z洋自得的我恍然大?zhn)Q这个Singleton实现是无法重用的。没办法Q只好一Ҏ(gu)一边说Q?#8220;一般来_较ؓ行的重用方法一共有?U:l合、派生以及模ѝ首先可以想到的是,对Singleton的重用仅仅是对Instance()函数的重用,因此通过从Singletonz以 承该函数的实现是一个很好的选择。而Instance()函数如果能根据实际类型更改返回类型则更好了。因此奇异递归模板QCRTPQThe Curiously Recurring Template PatternQ模式则是一个非常好的选择?#8221;于是我在白板上飞快地写下了下面的代码Q?/p>
1 template <typename T> 2 class Singleton 3 { 4 public: 5 static T& Instance() 6 { 7 static T s_Instance; 8 return s_Instance; 9 } 10 11 protected: 12 Singleton(void) {} 13 ~Singleton(void) {} 14 15 private: 16 Singleton(const Singleton& rhs) {} 17 Singleton& operator = (const Singleton& rhs) {} 18 };
同时我也在白板上写下了对该Singleton实现q行重用的方法:
1 class SingletonInstance : public Singleton<SingletonInstance>…
“在需要重用该Singleton实现Ӟ我们仅仅需要从Singletonzq将Singleton的泛型参数设|ؓ该类型即可?#8221;
生存期管?/strong>
“我看你在实现中用了静态变量,那你是否能介l一下上面Singleton实现中有关生存期的一些特征吗Q毕竟生存期理也是~程中的一个重要话题?#8221;面试官提Z下一个问题?/p>
“嗯,让我想一惟뀂我认ؓ对Singleton的生存期Ҏ(gu)的讨论需要分Z个方面:Singleton内用的静态变量的生存期以?Singleton外在用户代码中所表现的生存期。Singleton内用的静态变量是一个局部静态变量,因此只有在Singleton?Instance()函数被调用时其才会被创徏Q从而拥有了延迟初始化(LazyQ的效果Q提高了E序的启动性能。同时该实例生存至E序执行完毕。而就 Singleton的用户代码而言Q其生存期诏I于整个E序生命周期Q从E序启动开始直到程序执行完毕。当ӞSingleton在生存期上的一个缺陷就 是创建和析构时的不确定性。由于Singleton实例会在Instance()函数被访问时被创建,因此在某处新d的一处对Singleton的访?可能导致Singleton的生存期发生变化。如果其依赖于其它组成,如另一个SingletonQ那么对它们的生存期q行理成Z个灾难。甚臛_ 以说Q还不如不用SingletonQ而用明的实例生存期管理?#8221;
“很好Q你能提到程序初始化及关闭时单g的构造及析构序的不定可能D致命的错误这一情况。可以说Q这是通过局部静态变量实?Singleton的一个重要缺炏V而对于你所提到的多个Singleton之间怺兌所D的生存期理问题Q你是否有解册问题的方法呢Q?#8221;
我突焉意识到自q自己Z一个难题:“有,我们可以Singleton的实现更改ؓ使用全局静态变量,q将q些全局静态变量在文g中按照特定顺序排序即可?#8221;
“但是q样的话Q静态变量将使用eager initialization的方式完成初始化Q可能会Ҏ(gu)能影响较大。其实,我想听你说的是,对于h兌的两个SingletonQ对它们q行使用?代码常常局限在同一区域内。该问题的一个解x法常常是对它们q行使用的管理逻辑实现为SingletonQ而在内部逻辑中对它们q行明确的生存期?理。但不用担心Q因个答案也q于l验之谈。那么下一个问题,你既然提C全局静态变量能解决q个问题Q那是否可以讲解一下全局静态变量的生命周期是?L呢?”
“~译器会在程序的main()函数执行之前插入一D代码,用来初始化全局变量。当Ӟ静态变量也包含在内。该q程被称为静态初始化?#8221;
“嗯,很好。用全局静态变量实现Singleton的确会对性能造成一定媄响。但是你是否注意到它也有一定的优点呢?”
见我怹没有回答Q面试官d帮我解了_“是线E安全性。由于在静态初始化时用户代码还没有来得及执行,因此其常常处于单U程环境下,从而保 证了Singleton真的只有一个实例。当Ӟqƈ不是一个好的解x法。所以,我们来谈谈Singleton的多U程实现吧?#8221;
多线E?/strong>
“首先请你写一个线E安全的Singleton实现?#8221;
我拿LQ在白板上写下早已烂熟于心的多线E安全实玎ͼ
1 template <typename T> 2 class Singleton 3 { 4 public: 5 static T& Instance() 6 { 7 if (m_pInstance == NULL) 8 { 9 Lock lock; 10 if (m_pInstance == NULL) 11 { 12 m_pInstance = new T(); 13 atexit(Destroy); 14 } 15 return *m_pInstance; 16 } 17 return *m_pInstance; 18 } 19 20 protected: 21 Singleton(void) {} 22 ~Singleton(void) {} 23 24 private: 25 Singleton(const Singleton& rhs) {} 26 Singleton& operator = (const Singleton& rhs) {} 27 28 void Destroy() 29 { 30 if (m_pInstance != NULL) 31 delete m_pInstance; 32 m_pInstance = NULL; 33 } 34 35 static T* volatile m_pInstance; 36 }; 37 38 template <typename T> 39 T* Singleton<T>::m_pInstance = NULL;
“写得很精彩。那你是否能逐行讲解一下你写的q个Singleton实现呢?”
“好的。首先,我用了一个指针记录创建的Singleton实例Q而不再是局部静态变量。这是因为局部静态变量可能在多线E环境下出现问题?#8221;
“我想插一句话Qؓ什么局部静态变量会在多U程环境下出现问题?”
“q是由局部静态变量的实际实现所军_的。ؓ了能满局部静态变量只被初始化一ơ的需求,很多~译器会通过一个全局的标志位记录该静态变量是否已l被初始化的信息。那么,寚w态变量进行初始化的伪码就变成下面q个样子Q?#8221;?/p>
1 bool flag = false; 2 if (!flag) 3 { 4 flag = true; 5 staticVar = initStatic(); 6 }
“那么在第一个线E执行完对flag的检查ƈq入if分支后,W二个线E将可能被启动,从而也q入if分支。这P两个U程都将执行寚w态变?的初始化。因此在q里Q我使用了指针,q在Ҏ(gu)针进行赋g前用锁保证在同一旉内只能有一个线E对指针q行初始化。同时基于性能的考虑Q我们需要在?ơ访问实例之前检查指针是否已l经q初始化Q以避免每次对Singleton的访问都需要请求对锁的控制权?#8221;
“同时Q?#8221;我咽了口口水l箋_“因ؓnewq算W的调用分ؓ分配内存、调用构造函C及ؓ指针赋g步,像下面的构造函数调用:”
1 SingletonInstance pInstance = new SingletonInstance();
“q行代码会{化ؓ以下形式Q?#8221;
1 SingletonInstance pHeap = __new(sizeof(SingletonInstance)); 2 pHeap->SingletonInstance::SingletonInstance(); 3 SingletonInstance pInstance = pHeap;
“q样转换是因为在C++标准中规定,如果内存分配p|Q或者构造函数没有成功执行, newq算W所q回的将是空。一般情况下Q编译器不会L调整q三步的执行序Q但是在满特定条gӞ如构造函C会抛出异常等Q编译器可能Z优化?目的第一步和W三步合qؓ同一步:”
1 SingletonInstance pInstance = __new(sizeof(SingletonInstance)); 2 pInstance->SingletonInstance::SingletonInstance();
“q样可能导致其中一个线E在完成了内存分配后p切换到另一U程Q而另一U程对Singleton的再ơ访问将׃pInstance已经 赋D越qif分支Q从而返回一个不完整的对象。因此,我在q个实现中ؓ静态成员指针添加了volatile关键字。该关键字的实际意义是由其修饰的变量 可能会被意想不到地改变,因此每次对其所修饰的变量进行操作都需要从内存中取得它的实际倹{它可以用来L~译器对指o序的调整。只是由于该关键字所?供的止重排代码是假定在单线E环境下的,因此q不能禁止多U程环境下的指o重排?#8221;
“最后来说说我对atexit()关键字的使用。在通过new关键字创建类型实例的时候,我们同时通过atexit()函数注册了释放该实例?函数Q从而保证了q些实例能够在程序退出前正确地析构。该函数的特性也能保证后被创建的实例首先被析构。其实,寚w态类型实例进行析构的q程与前面所提到 的在main()函数执行之前插入静态初始化逻辑相对应?#8221;
引用q是指针
“既然你在实现中用了指针Qؓ什么仍然在Instance()函数中返回引用呢Q?#8221;面试官又抛出了新的问题?/p>
“q是因ؓSingletonq回的实例的生存期是由Singleton本n所军_的,而不是用户代码。我们知道,指针和引用在语法上的最大区 别就是指针可以ؓNULLQƈ可以通过deleteq算W删除指针所指的实例Q而引用则不可以。由该语法区别引甛_的语义区别之一是q些实例的生存期?义:通过引用所q回的实例,生存期由非用户代码管理,而通过指针q回的实例,其可能在某个旉Ҏ(gu)有被创徏Q或是可以被删除的。但是这两条 Singleton都不满Q因此在q里Q我使用指针Q而不是引用?#8221;
“指针和引用除了你提到的这些之外,q有其它的区别吗Q?#8221;
“有的。指针和引用的区别主要存在于几个斚w。从低层ơ向高层ơ上来说Q分为编译器实现上的Q语法上的以及语义上的区别。就~译器的实现来说Q?声明一个引用ƈ没有为引用分配内存,而仅仅是变量赋予了一个别名。而声明一个指针则分配了内存。这U实C的差异就D了语法上的众多区别:对引用进 行更改将D其原本指向的实例被赋|而对指针q行更改导致其指向另一个实例;引用永q指向一个类型实例,从而导致其不能为NULLQƈ׃该限制?D了众多语法上的区别,如dynamic_cast对引用和指针在无法成功进行{化时的行Z一致。而就语义而言Q前面所提到的生存期语义是一个区别, 同时一个返回引用的函数常常保证其返回结果有效。一般来_语义区别的根源常常是语法上的区别Q因此上面的语义区别仅仅是列举了一些例子,而真正语义上?差别常常需要考虑它们的语境?#8221;
“你在前面说到了你的多U程内部实现使用了指针,而返回类型是引用。在~写q程中,你是否考虑了实例构造不成功的情况,如newq算W运行失败?”
“是的。在和其它hq行讨论的过E中Q大家对于这U问题有各自的理解。首先,对一个实例的构造将可能在两处抛出异常:newq算W的执行以及?造函数抛出的异常。对于newq算W,我想说的是几炏V对于某些操作系l,例如WindowsQ其常常使用虚拟地址Q因此其q行常常不受物理内存实际大小 的限制。而对于构造函C抛出的异常,我们有两U策略可以选择Q在构造函数内对异常进行处理,以及在构造函C外对异常q行处理。在构造函数内对异常进?处理可以保证cd实例处于一个有效的状态,但一般不是我们想要的实例状态。这样一个实例会D后面对它的用更为繁琐,例如需要更多的处理逻辑或再ơ导?E序执行异常。反q来Q在构造函C外对异常q行处理常常是更好的选择Q因Y件开发h员可以根据生异常时所构造的实例的状态将一定范围内的各个变量更 改ؓ合法的状态。D例来_我们在一个函C试创徏一对相互关联的cd实例Q那么在一个实例的构造函数抛Z异常Ӟ我们不应该在构造函数里对该实例?状态进行维护,因ؓ前一个实例的构造是按照后一个实例会正常创徏来进行的。相Ҏ(gu)_攑ּ后一个实例,q将前一个实例删除是一个比较好的选择?#8221;
我在白板上比划了一下,l箋说到Q?#8220;我们知道Q异常有两个非常明显的缺P效率Q以及对代码的污染。在太小的粒度中使用异常Q就会导致异怋用次数的增加Q对于效率以及代码的整洁型都是伤実뀂同样地Q对拯构造函数等l成常常需要用类似的原则?#8221;
“反过来说QSingleton的用也可以保持着q种原则。Singleton仅仅是一个包装好的全局实例Q对其的创徏如果一旦不成功Q在较高层次上保持正常状态同h一个较好的选择?#8221;
Anti-Patten
“既然你提CSingleton仅仅是一个包装好的全局变量Q那你能说说它和全局变量的相同与不同么?”
“单g可以说是全局变量的替代品。其拥有全局变量的众多特点:全局可见且诏I应用程序的整个生命周期。除此之外,单g模式q拥有一些全局变量所 不具有的性质Q同一cd的对象实例只能有一个,同时适当的实现还拥有延迟初始化(LazyQ的功能Q可以避免耗时的全局变量初始化所D的启动速度不佳{?问题。要说明的是QSingleton的最主要目的q不是作Z个全局变量使用Q而是保证cd实例有且仅有一个。它所h的全局讉KҎ(gu)仅仅是它的一个副 作用。但正是q个副作用它更cM于包装好的全局变量Q从而允许各部分代码对其直接q行操作。Y件开发h员需要通过仔细地阅d部分对其q行操作的代码才 能了解其真正的用方式,而不能通过接口得到lg依赖性等信息。如果Singleton记录了程序的q行状态,那么该状态将是一个全局状态。各个组件对?q行操作的调用时序将变得十分重要Q从而各个lg之间存在着一U隐式的依赖?#8221;
“从语法上来讲Q首先Singleton模式实际上将cd功能与类型实例个数限制的代码混合在了一Pq反了SRP。其ơSingleton模式在Instance()函数中将创徏一个确定的cdQ从而禁止了通过多态提供另一U实现的可能?#8221;
“但是从系l的角度来讲Q对Singleton的用则是无法避免的Q假设一个系l拥有成百上千个服务Q那么对它们的传递将会成为系l的一个灾 难。从微Y所提供的众多类库上来看Q其常常提供一U方式获得服务的函数Q如GetService(){。另外一个可以减轻Singleton模式所带来?良媄响的Ҏ(gu)则是为Singleton模式提供无状态或状态关联很的实现?#8221;
“也就是说QSingleton本nq不是一个非常差的模式,对其使用的关键在于何时用它q正的使用它?#8221;
面试官抬h腕看了看旉Q?#8220;好了Q时间已l到了。你的C++功底已经很好了。我怿Q我们会在不久的来成ؓ同事?#8221;
W者注Q这本是Writing Patterns Line by Line的一文章,但最后想惻I写模式的人太多了Q我q是省省吧。。?/p>
下一回归WPFQ环境刚好。可能中间穿插些别的内容Q比如HTML5QJSQ安全等{?/p>
头一ơ写品文,不知道效果是不是好。因U文章的特点是知识点分散Q而且隐藏在文章的每一句话中。。。好处就是写hLQ呵c。?/p>
Flyweight定义:
避免大量拥有相同内容的小cȝ开销(如耗费内存),使大家共享一个类(元类).
Z么?
面向对象语言?原则是一切都是对?但是如果真正使用h,有时对象数可能显得很庞大,比如,字处理Y?如果以每个文字都作ؓ一个对?几千个字,对象数就是几? 无疑耗费内存,那么我们q是?求同存异",扑ևq些对象的共同?设计一个元c?装可以被共享的c?另外,q有一些特性是取决于应?(context),是不可共享的,q也Flyweight中两个重要概念内部状态intrinsic和外部状态extrinsic之分.
说白?span>,是先捏一个的原始模型,然后随着不同场合和环?再生各L征的具体模型,很显?在这里需?产生不同的新对象,所以Flyweight模式中常出现Factory模式.Flyweight的内部状态是用来׃n?Flyweight factory负责l护一个Flyweight pool(模式?来存攑ֆ部状态的对象.
Flyweight模式是一个提高程序效率和性能的模?会大大加快程序的q行速度.应用场合很多:比如你要从一个数据库中读取一pd字符?q些字符串中有许多是重复?那么我们可以这些字W串储存在Flyweight?pool)?
如何使用?
我们先从Flyweight抽象接口开?
public interface Flyweight //用于本模式的抽象数据cd(自行设计) |
下面是接口的具体实现(ConcreteFlyweight) ,qؓ内部状态增加内存空? ConcreteFlyweight必须是可׃n?它保存的M状态都必须是内?intrinsic),也就是说,ConcreteFlyweight必须和它的应用环境场合无?
public class ConcreteFlyweight implements Flyweight { } |
当然,q不是所有的Flyweight具体实现子类都需要被׃n?所以还有另外一U不׃n的ConcreteFlyweight:
public class UnsharedConcreteFlyweight implements Flyweight { public void operation( ExtrinsicState state ) { } } |
Flyweight factory负责l护一个Flyweight?存放内部状?,当客Lh一个共享Flyweight?q个factory首先搜烦池中是否已经 有可适用?如果?factory只是单返回送出q个对象,否则,创徏一个新的对?加入到池?再返回送出q个对象.?/span>
public class FlyweightFactory { Flyweight flyweight = (Flyweight) flyweights.get(key); if( flyweight == null ) { return flyweight; |
x,Flyweight模式的基本框架已l就l?我们看看如何调用:
FlyweightFactory factory = new FlyweightFactory();
Flyweight fly1 = factory.getFlyweight( "Fred" );
Flyweight fly2 = factory.getFlyweight( "Wilma" );
......
从调用上?好象是个Ua的Factory使用,但奥妙就在于Factory的内部设计上.
Flyweight模式在XML{数据源中应?/strong>
我们上面已经提到,当大量从数据源中d字符?其中肯定有重复的,那么我们使用Flyweight模式可以提高效率,以唱片CDZ,在一个XML文g?存放了多个CD的资?
每个CD有三个字D?
1.出片日期(year)
2.歌唱者姓名等信息(artist)
3.q曲目 (title)
其中,歌唱者姓名有可能重复,也就是说,可能有同一个演p的多个不同时期 不同曲目的CD.我们?歌唱者姓?作ؓ可共享的ConcreteFlyweight.其他两个字段作ؓUnsharedConcreteFlyweight.
首先看看数据源XML文g的内?
<cd> <cd> <cd> ....... </collection> |
虽然上面举例CD只有3?CD可看成是大量重复的小c?因ؓ其中成分只有三个字段,而且有重复的(歌唱者姓?.
CD是cM上面接口 Flyweight:
private String title; |
?歌唱者姓?作ؓ可共享的ConcreteFlyweight:
public class Artist { //内部状?br /> private String name; // note that Artist is immutable. Artist(String n){ |
再看看Flyweight factory,专门用来刉上面的可共享的ConcreteFlyweight:Artist
public class ArtistFactory { Hashtable pool = new Hashtable(); Artist result; |
当你有几千张甚至更多CD?Flyweight模式节省更多空?׃n的flyweight多,I间节省也就大.