我们使用使用 VC6 编译器用 C++ 实现的非常旧的遗留系统。现在我们正在重构代码。我们还切换到VC9编译器。
我们使用外部专有框架,它也是遗留代码并且不可进行单元测试。为了使我们的代码单元可测试,我们引入了框架类的接口和包装器(提示:请参阅 Martin Fowler 的“使用遗留代码”):
现在我们依赖于接口。包装器调用框架方法,我们可以愉快地在单元测试中使用模拟。
这里我们遇到了我们的问题......
框架类包含许多需要包装和模拟的方法。为了实现这一目标,我们的供应商团队编写了一个 API,它使用 C++ 宏生成接口、包装器和模拟实现。
包装器头文件示例:
class PlanWrapper : public IPlan
{
// ...
WRP_DECLARE_DEFAULTS(FrameworkPlan); // macro
WRP_DECLARE_CSTR_ATTR(FrameworkPlanLabel); // macro
// ...
};
宏 WRP_DECLARE_CSTR_ATTR 定义如下:
#define WRP_DECLARE_CSTR_ATTR(AttrName) \
virtual bool set##AttrName (LPCTSTR Value_in); \
virtual bool get##AttrName (CString& Value_out); \
virtual bool unset##AttrName (); \
virtual bool isSet##AttrName ()
包装器 cpp 文件示例:
#include "StdAfx.h"
using namespace SomeNamespace;
WRP_IMPLEMENT_MODDICOM_DEFAULTS(FrameworkPlan)
WRP_IMPLEMENT_W_CSTR_ATTR (FrameworkPlan,FrameworkType1, FrameworkPlanLabel)
// ...
宏 WRP_IMPLMENT_W_CSTR_ATTR 定义如下:
#define WRP_IMPLEMENT_W_CSTR_ATTR(ClassName,AtrTypeObj,AttrName) \
bool ClassName##Wrapper::set##AttrName (LPCTSTR Value_in) { \
AtrTypeObj aValue = Value_in; \
FrameworkLink<ClassName> convertedObj = NULL_LINK; \
framework_cast(convertedObj, m_Object); \
return convertedObj != NULL_LINK ? \
convertedObj->set##AttrName (aValue) : false; \
}
// ...
我们有一堆更复杂的东西,但我想您已经明白了。
API 的问题在于它极其复杂、不可读、不可调试、不可测试。
我们希望提出一个更好的机制来实现同样的目标。我们的想法是,我们使用新编译器附带的一些高级功能,如高级模板、类型列表、特征等。
使用模板,我们几乎可以实现我们的目标,但我们受困于方法名称。我们可以概括类型,但是我们如何处理属性名称?
我们还考虑创建一个自动生成包装器+接口+模拟代码的工具。然而,我们外部框架的API极其复杂,编写这样的工具成本非常高。
您认为解决此类问题的最佳方法是什么?也许您已经处理过类似的事情并且可以提供很好的提示?我们期待看到您的答案!
We work with very old legacy system implemented in C++ with VC6 compiler. Now we are in the process of refactoring the code. We also switched to VC9 compiler.
We use an external proprietary framework, which is also legacy code and not unit testable. In order to make our code unit testable, we introduced interfaces and wrappers for the framework classes (hint: see “Working With Legacy Code” by Martin Fowler):
Now we depend on interfaces. The wrappers call the framework methods and we may happily use mocks in our unit tests.
And here we come to our problem...
The framework classes contain many methods that need to be wrapped and mocked. In order to achieve this goal, our supplier team wrote an API which generates interfaces, wrappers and mocks implementations with use of C++ Macros.
Example of wrapper header file:
class PlanWrapper : public IPlan
{
// ...
WRP_DECLARE_DEFAULTS(FrameworkPlan); // macro
WRP_DECLARE_CSTR_ATTR(FrameworkPlanLabel); // macro
// ...
};
The Macro WRP_DECLARE_CSTR_ATTR is defined like this:
#define WRP_DECLARE_CSTR_ATTR(AttrName) \
virtual bool set##AttrName (LPCTSTR Value_in); \
virtual bool get##AttrName (CString& Value_out); \
virtual bool unset##AttrName (); \
virtual bool isSet##AttrName ()
Example of wrapper cpp file:
#include "StdAfx.h"
using namespace SomeNamespace;
WRP_IMPLEMENT_MODDICOM_DEFAULTS(FrameworkPlan)
WRP_IMPLEMENT_W_CSTR_ATTR (FrameworkPlan,FrameworkType1, FrameworkPlanLabel)
// ...
The Macro WRP_IMPLEMENT_W_CSTR_ATTR is defined like this:
#define WRP_IMPLEMENT_W_CSTR_ATTR(ClassName,AtrTypeObj,AttrName) \
bool ClassName##Wrapper::set##AttrName (LPCTSTR Value_in) { \
AtrTypeObj aValue = Value_in; \
FrameworkLink<ClassName> convertedObj = NULL_LINK; \
framework_cast(convertedObj, m_Object); \
return convertedObj != NULL_LINK ? \
convertedObj->set##AttrName (aValue) : false; \
}
// ...
We have a bunch of even more complicated stuff, but I think you get the idea.
The problem with the API is that it is extremely complicated, not readable, not debuggable and not testable.
We would like to come up with a better mechanism for achieving the same goal. The idea was that we use some of the advanced features that came with new compiler like advanced templates, typelists, traits etc.
With templates we can almost achieve our goal, but we are stuck with the method names. We can generalize for types, but how do we deal with attribute names?
We also thought about creating a tool for automatically generating the wrapper + interfaces + mocks code. However, the API of our external framework is extremely complicated and writing such a tool would be very costly.
What do you think is the best way to solve such a problem? Maybe you already dealt with something like that and can provide good hints? We're looking forward to see your answers!
发布评论
评论(6)
我想我会使用代码生成工具。我可能会制作一些简单的实用程序:一个用于生成与遗留框架的类相对应的接口,一个用于生成包装器,一个用于生成模拟对象(或至少一个骨架)。
这意味着有某种方法来解析遗留框架的代码。我会看看 Clang,或者简单地运行 ctags 在源文件上并处理生成的标签。
I think I would go with a code generation tool. I would probably make a few simple utility programs: one for generating an interface corresponding to a class of your legacy framework, one for generating the wrapper and one for generating the mock object (or at least a skeleton).
This implies having some way to parse the code of your legacy framework. I would have a look at Clang, or perhaps simply run ctags on the source file and treat the resulting tags.
使用抽象工厂代替宏来解决这个问题。
在为您的 Normal api 和 moc api 实现此接口并将工厂实例传递到您的系统后,如下所示:
或
您的系统必须声明为:
在您的工厂中,您将返回正常 api 实现或 moc 对象。
当然,您可以从根工厂返回工厂“这将返回其他工厂或对象”。
Use Abstract factory instead macros to solve this problem.
After implement this interface for your Normal api and moc api and pass the instance of your factory to your system like:
or
Your system must be declared as:
In your factory you will return the normal api implementation or moc objects.
Of course you could return factories "which will return other factories or objects" from your root factory.
如果代码库足够大(即数十万行 C++),并且可以使用 GCC(最近的版本,即4.6)您可以也许考虑制作一个特定的GCC插件,或者MELT 扩展。 (MELT 是扩展 GCC 的高级领域特定语言)。然而,这种GCC 定制需要付出大量的努力(数周而不是数小时的工作)。
If the code base is large enough (i.e. several hundred thousands lines of C++), and if you can compile it with GCC (a recent version, i.e. 4.6) you could perhaps consider making a specific GCC plugin, or a MELT extension. (MELT is a high-level domain specific language to extend GCC). However such GCC customization takes a significant amount of efforts (weeks, not hours, of work).
我喜欢代码生成器。您可能会创建一个包含所有要包装的内容的字典,这需要时间,然后使用一个简单的工具来生成代码。除了代码之外,您还可以获得遗留系统的清晰参考,以及稍后替换它的指南。
I like the code generator. You may create a dictionary of all things to be wrapped, it takes time, then use a simple tool to generate the code. Beside the code, you also get a clear reference of the legacy system, a guide to replace it some later.
我们的DMS软件重组工具包是一个程序转换工具,它读取源代码并执行对其进行任意变换。
DMS 及其 C++ 前端 (支持 VC6 和更现代的 Visual Studio),它可能用于读取要模拟的组件的头文件,并生成模拟。
DMS 已用于对 C++ 代码进行大规模转换,特别是涉及接口更改和以各种方式重新排列接口。请参阅我的一篇技术论文,
通过自动程序转换重新设计 C++ 组件模型。
Our DMS Software Reengineering Toolkit is a program transformation tool that reads source code and carries out arbtitrary transformations on it.
DMS, with its C++ Front End (VC6 and more modern Visual Studio capable), it could probably be used to read the header files for the components to be mocked, and generate the mocks.
DMS has been used to carry out massive transformations on C++ code, especially involving interface changes and shuffling interfaces around in various ways. See one of my technical papers,
Re-engineering C++ Component Models Via Automatic Program Transformation.
有一种方法可以以完全受控的方式构建代码生成,并且仍然保持生成您需要的困难 API 内容的所有自由。
如果您使用 Visual Studio(至少 2005,但最好是 2008 或 2010),您可以使用 T4 团队提到的结构化代码生成方式 - 仅基于 T4 和 XML:
http://blogs.msdn.com/b/t4/archive/2011/11/30/some-nice-new-getting-started-with-t4-videos.aspx
我我是该 ADM 方法的创新者和首席架构师,也是该博客 http://abstractiondev.wordpress.com/
不幸的是,我没有具体的案例可以链接来匹配您的案例,但我很高兴为您提供解决方案(如果您希望造福社区,发布一个案例)。
There is a way to structurize your code generation in completely controlled fashion and still maintain all the freedom of generate what ever difficult API stuff you need.
If you use Visual Studio (2005 at least, but preferably 2008 or 2010) you can use this T4 Team's mentioned way of structurized code generation - that is based on T4 and XML only:
http://blogs.msdn.com/b/t4/archive/2011/11/30/some-nice-new-getting-started-with-t4-videos.aspx
I am the innovator and lead architect of that ADM methodology and author of that blog http://abstractiondev.wordpress.com/
Unfortunately I don't have concrete case to link to match yours, but I'm happy to work the solution for you (and in case you want to benefit the community, publish a case from that).