MemCache++ 是一个轻量级、类型安全、易于使用且功能齐全的Memcache客户端。
它由Dean Michael Berris开发,他是一位C++狂热爱好者,喜欢从事网络库(cpp-netlib.github.com)的工作,目前在Google澳大利亚工作。他也是Google派往ISO C++委员会的代表之一。您可以在deanberris.github.com上阅读他发表的作品,并在www.cplusplus-soup.com上阅读他的C++博客。
建议学习设计良好的库来掌握C++概念,本文的目标是探讨memcache++的一些设计选择,使其易于理解和使用。
让我们通过CppDepend来分析memcache++的设计。
命名空间模块化
命名空间是模块化应用程序的良好解决方案,但不幸的是,C++项目中使用得不够充分,随便看看开源C++项目就能发现这一事实。更重要的是,当我们搜索C++命名空间定义时,常见的定义是这样的:
A namespace defines a new scope. They provide a way to avoid name collisions.
许多时候,命名冲突被视为首要动机,而不是模块化,这与C#和Java不同,在后两者中,命名空间在模块化应用程序方面被引用得更多。
然而,一些现代C++库,如boost,使用命名空间来很好地组织库并鼓励开发人员使用它们。
memcache++的命名空间模块化如何?
这是memcache++命名空间之间的依赖关系图。
命名空间有两个主要原因:
- 模块化库。
- 隐藏细节,如“memcache::detail”命名空间。如果我们想告知库用户他们不需要直接使用该命名空间内的类型,这种方法会非常有趣。对于C#,“internal”关键字完成了这项工作,但在C++中,无法向库用户隐藏公共类型。
memcache++优雅地利用了命名空间概念,然而,memcache和memcache::detail之间存在依赖循环。我们可以通过在memcache中查找memcache::detail使用的类型来移除这个依赖循环。
为此,我们可以执行以下CQlinq请求:
from t in Types where t.IsUsedBy("memcache.detail")
&& t.ParentNamespace.Name=="memcache"
select new { t,t.TypesUsingMe }
执行请求后的结果如下:
为了移除依赖循环,我们可以将pool_directive和server_pool_test移到memcache中。
泛型还是面向对象?
在C++世界中,有两个非常流行的学派:面向对象编程和泛型编程,每种方法都有其拥护者。这篇
文章解释了它们之间的张力。
memcache++使用了什么方法?
为了回答这个问题,让我们先搜索泛型类型。
from t in Types where t.IsGeneric && !t.IsThirdParty select t
那些非泛型的类型呢?
from t in Types where !t.IsGeneric && !t.IsGlobal && !t.IsNested
&& !t.IsEnumeration && t.ParentProject.Name=="memcache"
select t
几乎所有非泛型类型都与异常类有关,为了更好地了解它们的用途比例,树状图视图非常有用。
蓝色矩形代表CQLinq查询的结果,正如我们所见,只有一小部分库与非泛型类型有关。
最后,我们可以搜索泛型方法。
from m in Methods where m.IsGeneric && !m.IsThirdParty select m
正如我们所观察到的,memcache++主要使用泛型,但这不足以确认它遵循C++泛型方法。要检查这一点,一个好的指标是使用继承和动态多态性。确实,面向对象编程主要使用它们,然而,对于泛型方法,继承的使用非常有限,并且会避免动态多态性。
让我们搜索具有基类的类型。
from t in Types where t.BaseClasses.Count()>0 && !t.IsThirdParty
&& t.ParentProject.Name=="memcache"
select t
异常类使用继承是很正常的,但其他类呢?它们是否使用继承来实现动态多态性?为了回答这个问题,让我们搜索所有虚方法。
from m in Methods where m.IsVirtual select m
只有异常类有一个虚方法,对于使用继承的其他少数类,主要动机是重用基类的代码。
如果未使用动态多态性,那么在需要为特定类实现其他行为时,会采用什么解决方案?
泛型方法常用的解决方案是使用策略。以下是维基百科的简要定义:
“策略导向设计中的核心惯用法是一个类模板(称为宿主类),它接受多个类型参数作为输入,这些类型参数通过用户选择的类型(称为策略类)进行实例化,每个类型都实现一个特定的隐式接口(称为策略)。”
memcache++在memcache.policies命名空间中有许多策略。
让我们来看一个memcache++的例子,以便更好地理解基于策略的设计。
memcache++使用basic_handle类型来实现所有命令,如add、set、get和delete缓存。该类定义如下:
template <
class threading_policy = policies::default_threading,
class data_interchange_policy = policies::binary_interchange,
class hash_policy = policies::default_hash
>
struct basic_handle
memcache++是线程安全的,并且在多线程环境中需要管理同步。默认情况下,threading_policy是“default_threading”,不需要特殊的处理。然而,对于多线程,使用的策略是“boost_threading”。
让我们看一下connect方法的实现。
void connect(boost::uint64_t timeout = MEMCACHE_TIMEOUT) {
typename threading_policy::lock scoped_lock(*this);
for_each(servers.begin(), servers.end(), connect_impl(service_, timeout));
};
如果threading_policy是“default_threading”,第一行就没有影响,因为lock构造函数什么也没做。然而,如果是boost_threading,lock则使用boost在线程之间进行同步。
使用策略为我们提供了更多的灵活性来实现不同的行为,而且理解和使用它们并不困难。
泛型仿函数
memcache++实现了许多与缓存交互的命令,如add、get、set和delete。在这种情况下,命令模式是一个不错的选择。
memcache++通过使用泛型仿函数来实现此模式。以下是获取所有仿函数的CQLinq查询:
from t in Types where t.Methods.Where(a=>a.IsOperator
&& a.Name.Contains("()")).Count()>0
select t
仿函数封装了一个函数调用及其状态,可以用于延迟调用,并充当回调。泛型仿函数比普通仿函数提供了更多的灵活性。
接口暴露
库如何暴露其功能非常重要,它会影响其灵活性和易用性。
为了了解这一点,让我们搜索测试项目与memcache++库之间的通信。
from m in Methods where m.IsUsedBy ("test")
select m
测试项目主要使用泛型方法来调用memcache++的功能。使用模板方法的优点是什么?为什么不使用类或函数?
对于面向对象方法,库接口由类和函数组成,对于设计良好的接口,抽象类被用作契约来强制低耦合。这个解决方案非常有趣,但有一些缺点:
- 接口变得更加复杂,并且可能经常更改:为了解释这一点,让我们以memcache++暴露的add方法为例。如果我们不使用泛型方法,则必须添加许多方法,每种方法都针对特定类型int、double、string……
然而,泛型add方法声明为add<T>,其中T是类型。在这种情况下,我们只需要一个方法,即使我们想添加另一种类型,接口也不需要改变。
- 接口的灵活性较低:例如,如果我们暴露一个这样的方法
calculate(IAlgo* algo)。
用户必须提供一个继承自IAlgo的类。然而,如果我们使用泛型并将其定义为calculate<T>,用户只需要提供一个具有所需方法的类,而不必继承自IAlgo。如果IAlgo因添加某些方法而更改为IAlgo2,则此库的用户不会受到影响。
理想情况下,库暴露的接口不应有任何破坏性更改,并且当库中引入更改时,用户不应受到影响。泛型方法最适合此类约束,因为它在需要更改时非常宽容。
使用的外部API
以下是memcache++使用的外部类型:
memcache++主要使用boost和STL来实现其需求。以下是一些使用的boost特性:
- 多线程。
- 算法。
- spirit。
- asio。
- 单元测试。
当然,还有众所周知的shared_ptr用于RIIA惯用法。
并且在STL中,主要使用容器。
那么,最终使用泛型方法的优点是什么?
- memcache++设计选择效率的第一个指标是代码行数(LOC),只有大约600行代码,这个结果主要归因于两个原因:
- 使用泛型方法消除了样板代码。
- 利用了boost和stl的丰富性。
- 第二个优点是其灵活性,任何更改只会影响一小部分代码。
使用此类方法的缺点
许多开发人员发现泛型方法非常复杂,并且理解代码变得非常困难。
该怎么做?使用还是不使用泛型方法?
如果使用这种方法设计应用程序非常困难,但使用这种方法的库却更容易,就像使用STL或boost一样。
因此,即使我们想避免使用现代方法设计应用程序的风险,使用STL或boost等库也是一个好主意。
附件:[dependency1[1].png] [externals1[1].png] [functors[1].png] [generic[1].png] [genericmethods[1].png] [inheritence[1].png] [interface[1].png] [namespaces[1].png] [notgeneric[1].png] [notgenerictreemap[1].png] [policies2[1].png] [virtual[1].png]