本文是 《基于 Qt 的组件合成框架》的其中一节,建议全章阅读。
前面我们提到组件合成的各个语言平台实现时,说到了 C# 中的 MEF 框架,它提供了很好的架构思路,并且核心部分很简洁,所以我们选择将 MEF 框架移植到 Qt 中来。
Managed Extensibility Framework (MEF) 托管扩展框架或MEF是用于创建轻量级和可扩展应用程序的库。它使应用程序开发人员无需配置即可发现和使用扩展。它还使扩展开发人员可以轻松地封装代码并避免脆弱的硬依赖性。MEF不仅允许扩展在应用程序内重用,而且还可以跨应用程序重用。
模型概述
MEF的主要模型包括下列元素,我们在迁移中将基础常用的部分进行了对比实现,舍弃了不常用或者难以在C++中实现的部分。
- 导入导出基础(Import and Export Basics)
- 导入类型(Types of Imports)
避免被发现(Avoiding Discovery)- 扩展信息(Metadata and Metadata Views)
- 导入导出继承(Import and Export Inheritance)
自定义导出属性(Custom Export Attributes)- 创建策略(Creation Policies)
- 生命期及释放(Life Cycle and Disposing)
MEF的定义是基于 C# 语言的,映射到 Qt 上应该是这样的:
模型元素 | Qt 映射 | 备注 |
类型 Type | QObject 及派生类 由 QMetaObject 对象代表 | 在 MEF 中,可以是任意类型 |
导出(Exports) | 通过全局 QExport 对象声明 | 在 MEF 中,可作用于:类,字段,属性或方法 在 Qt 中,仅可作用于 QObject 类 |
导入(Imports) | 提供全局 QImport 对象声明 | 在 MEF 中,可作用于:字段,属性或参数构造 在 Qt 中,仅可作用于 QObject 类的属性 Q_PROPERTY |
扩展信息(Metadata) | 通过 QObject 类的 Q_CLASSINFO 定义扩展信息 | |
创建策略 | 通过 Q_INVOKABALE 构造方法创建对象 | |
导出继承(Export Inheritance) | 利用 QObject 类的Q_CLASSINFO 以及继承关系 | 实现一些与继承关系相关的策略 |
接下来看看如何用 Qt 实现模型的各个部分。
导出、导入(Exports、Imports)
假如我们实现了一个功能(形式上是一个类),然后要告诉别人有这样一个类可以使用,就需要通过一个导出声明来发布我们的功能组件。
又假如我们需要使用一个功能,但是不想操心它的创建、管理,那就发布一个导入声明,让组件合成框架来满足我们的需求。
导出声明本质上是在组件合成框架中注册一个类型,需要执行注册方法。导入声明也是注册一些需求信息,以便将来被满足。
在 C++ 中,要让一块代码没有被调用也能执行,好像只有全局对象的构造方法了。因此我们提供了 QExport、QImport 类,他们都是继承 QPart 类,让他们的构造方法完成相关的注册工作。
这些类的实现,都比较简单。但是在使用这些类定义对象时,需要注意使用规则。
你必须通过在全局作用域定义这些类的对象,以便在模块加载的时候,他们的构造函数就被执行。另外,在某些情况下,整个 C++编译单元(cpp文件)会被链接阶段丢弃,必须保证该编译单元的符号有被引用。
好在 C++ 11 引了的匿名名字空间,它能够保证匿名名字空间的全局对象一定会被创建。
这里也用到了一些 Qt 相关的技术:
首先,注册的类型必须是 QObject 派生类,通过 Q_OBJECT 创建了元数据信息,并且通过 Q_INVOKABLE 在元数据中导出了其构造方法。举个例子:
class RobotPenService: public DeviceService
{
Q_OBJECT
public:
Q_INVOKABLE RobotPenService();
};
其次,必须通过 Qt 的属性 (Q_PROPERTY)导入外部组件,这个属性的类型是 QObject 继承体系的类,也可以是数组(用来导入一组组件对象)。比如:
class DeviceManager : public QObject
{
Q_OBJECT
Q_PROPERTY(QList<DeviceService*> services READ services WRITE setServices)
public:
QList<DeviceService*> services() const;
void setServices(QList<DeviceService*> const & services);
};
从这里,你应该可以猜出来,我们是通过 Qt 元数据(QMetaObject)来创建组件对象的,正如所见:
QObject * QComponentContainer::getExportValue(QMetaObject const & meta, bool share)
{
return getExportValue(meta, share, [](auto meta) {
return meta.newInstance();
});
}
QMetaObject::newInstance 也可以附带参数,建议阅读 Qt 文档了解更多。
而通过属性导入组件时,框架将组件的实例对象,用 setProperty 的方式,注入到使用该组件的对象中,这个过程是这样实现的:
void QImportBase::compose(QObject * obj, QVector<QObject *> const & targets) const
{
if (typeRegister_)
typeRegister_();
obj->setProperty(prop_, QVariant::fromValue(targets));
}
当设置值的实际类型与属性类型不一样时,setProperty 会进行自动转换,值类型转换是 QVariant/QMetaType 提供的功能。其中 QObject 与其派生类指针类型转换是默认支持的,但是数组导入时,需要将 QVector<QObject*> 转换为目标属性的类型(比如可能 QList<U*>>),这就需要在 Qt 类型系统(QMetaType)里面注册相应的转换器(Converter)。上面的 typeRegister_ 是对应该导入声明的相关类型转换器的注册函数,它通常是这么实现的:
template<typename U, typename List>
inline static bool registerImportManyConverter()
{
qRegisterMetaType<List>();
return QMetaType::registerConverter<QVector<QObject*>, List>([](QVector<QObject*> const & f) {
List list;
for (auto l : f)
list.push_back(qobject_cast<U*>(l));
return list;
});
}
使用 Qt 元数据的另一个例子是:自定义可选的注入完成后处理。在类中声明一个名称为 onComposition() 的槽方法,那么组件合成框架在完成该对象的属性注入后,会调用该方法,让对象可以做一些自己的后期处理工作。在框架中,通过 QMetaMethod 去调用该方法:
int index = meta.indexOfMethod("onComposition()");
if (index >= 0)
meta.method(index).invoke(obj);
上面这种调用方式实现了弱契约模式,是可选的弱依赖性。使用 Qt 元数据可以很方便的实现这种模式。
除了使用 QMetaMethod 外,在 Qt 中,还有一个更直接的方法,可以用来实现上面的调用,它是 QMetaObject::invokeMethod,所以上面的代码可以改为:
QMetaObject::invokeMethod(obj, "onComposition")
但是使用这种方式,如果方法不存在或者调用参数不匹配,会有警告信息。
另外需要注意的是,这两种方式都会有一种线程亲缘性,方法必定在对象关联的线程中执行,如果调用线程不是对象关联的线程,那么就会通过向该对象发送事件来异步调用相关的方法。建议参考 Qt 文档了解如何传递参数和接收返回值。
扩展信息(Metadata)
利用 Qt 的类型信息(Q_CLASSINFO),可以为导出声明添加额外的扩展信息。形式是这样的:
class Control : public QObject
{
Q_OBJECT
Q_CLASSINFO("version", "1.0")
};
在组件合成框架中,通过 QMetaClassInfo 来读取这些信息:
void QExportBase::collectClassInfo()
{
QMap<char const *, char const *> attrs;
for (int i = 0; i < meta_->classInfoCount(); ++i) {
QMetaClassInfo const ci = meta_->classInfo(i);
attrs.insert(ci.name(), ci.value());
}
};
上面的方法收集了一个类的所有 Qt 类型信息,包括信息的名称和值。但是有时候我们需要知道一个类型是否自己具有某个名称的类型信息(不包括从基类继承来的),后面就会有这样的例子,这里就不展开了。
导出继承(Export Inheritance)
在导出里面,我们还会提供一些与类型派生关系相关策略。比如,基类可以声明导出继承,那么在声明导出派生类时,也会用基类类型作为导出类型,注册一个新的导出项。
具体方案是,在基类中增加一个特定名称("InheritedExport")的类型信息,举个例子:
// qpart.h
#define Q_INHERITED_EXPORT Q_CLASSINFO("InheritedExport", "true")
// deviceservice.h
class INTERACTBASE_EXPORT DeviceService : public QObject
{
Q_OBJECT
Q_INHERITED_EXPORT
};
在处理导出注册信息时,需要通过 QMetaObject 读取 InheritedExport 信息,并且要确定是哪个类型直接定义了 InheritedExport,而不是继承基类中的定义。
void QComponentRegistry::inheritedExport(QComponentRegistry::Meta &m, QExportBase *e)
{
QMetaObject const * type = e->type_->superClass();
while (type && type != &QObject::staticMetaObject) {
int index = type->indexOfClassInfo("InheritedExport");
if (index >= type->superClass()->classInfoCount()) {
if (QByteArray("true") == type->classInfo(index).value()) {
m.exports.push_back(new QExportBase(*e, type));
}
}
type = type->superClass();
}
}
这里,QMetaObject 的 superClass()->classInfoCount(),就是自己的直接 CLASS_INFO 的第一项的索引位置,当 index 不小于改索引值时,说明这个 InheritedExport 信息是属于 type 这个类型的。
当然,更底层的基类还可能定义了 InheritedExport 信息,所以遍历检查所有基类,直到触达最终基类 QObject::staticMetaObject。
通过 QMetaObject::superClass 可以遍历所有基类,在基础框架中,会经常用到这一点。