假设我的项目使用一个库,LibFoo
,它通过许多类提供其功能,例如FooA
和FooB
。现在,有许多类似的库(例如,提供BarA
和BarB
的LibBar
(提供与LibFoo
相同的功能,我希望我的项目用户能够选择使用哪个库,最好是在运行时。
创建了一个"包装层",用于定义我期望从库中获得的接口。在我的示例中,此层包含两个接口:IfaceA
和 IfaceB
。然后,对于我想要支持的每个库,我创建一个"实现层",使用其中一个库实现接口。
我现在的问题在于如何很好地实现实现层。为了演示我的问题,请考虑我们有以下接口(如C++所示,但应该适用于类似的语言(:
class IfaceA
{
public:
virtual void doSomethingWith(IfaceB& b) = 0;
...
};
class IfaceB
{
...
};
LibFoo
的实现层中的类将保存来自相应 LibFoo
类的对象。接口中的操作应使用这些对象实现。因此(请原谅我可怕的名字(:
class ConcreteAForFoo : public IfaceA
{
public:
void doSomethingWith(IfaceB& b) override
{
// This should be implemented using FooA::doSomethingWith(FooB&)
}
private:
FooA a;
};
class ConcreteBForFoo : public IfaceB
{
public:
FooB& getFooB() const
{
return b;
}
private:
FooB b;
};
问题在于实现ConcreteAForFoo::doSomethingWith
:它的参数具有类型 IfaceB&
,但我需要访问ConcreteBForFoo
的实现详细信息才能正确实现该方法。我发现这样做的唯一方法是到处使用丑陋的下垂:
void doSomethingWith(IfaceB& b) override
{
assert(dynamic_cast<ConcreteBForFoo*>(&b) != nullptr);
a.doSomethingWith(static_cast<ConcreteBForFoo&>(b).getFooB());
}
由于不得不向下投射通常被认为是一种代码气味,我不禁认为应该有更好的方法来做到这一点。还是我一开始就设计错了?
博士
给定一个相互依赖的接口层(在一个接口中的方法接收对其他接口的引用(。这些接口的实现如何共享实现细节,而不会在接口中向下转换或公开这些细节?
看起来像一个经典的双重调度用例。
class IfaceA
{
public:
virtual void doSomethingWith(IfaceB& b) = 0;
...
};
class IfaceB
{
virtual void doSomethingWith(IfaceA & a) = 0;
...
};
struct ConcreteAForFoo : public IfaceA {
virtual void doSomethingWith (IfaceB &b) override {
b.doSomethingWith(*this);
}
};
struct ConcreteBForFoo : public IfaceB
{
virtual void doSomethingWith(IfaceA & a) override {
//b field is accessible here
}
private:
FooB b;
};
IfaceB::d oSomethingWith 签名可以改变,以获得耦合和访问级别之间的最佳平衡(例如,ConcreteAForFoo 可以用作参数,以紧密耦合为代价访问其内部(。
正确答案是:
- 使您的界面真正可互操作和互换,或者
- 不要改变任何东西。继续做动态转换。
- 向类型系统添加其他内容,使用户不可能做错误的事情并混合不兼容的具体实现。
您现在的基本问题是接口/继承是一个"过度简化的谎言",我的意思是您的接口实际上并没有遵循 LSP。
如果你想按照 LSP 修复这个问题并使库可混合,你需要做 1:修复你的代码以不使用偷偷摸摸的实现细节,并真正遵循 LSP。但是这个选项基本上被问题语句排除了,其中明确表示类的实现是不兼容的,我们可能应该假设这种情况总是如此。
假设库永远不会是可混合的,正确答案是 2。继续使用动态转换。让我们想想为什么:
OP 的接口定义字面意思是ConcreteAForFoo
可以成功地doSomethingWith
任何IFaceB
对象。但是OP知道这不是真的 - 它真的必须是一个ConcreteBForFoo,因为需要使用某些在IFaceB接口中找不到的实现细节。
在这种特定描述的场景中,向下投射是最好的做法。
请记住:向下转换适用于您知道对象的类型,但编译器不知道的情况。编译器只知道方法签名。您比编译器更了解运行时类型,因为您知道一次只加载一个库。
您希望对编译器隐藏真实类型,因为您希望将库抽象到接口中,并对库的用户隐藏基础类型,我认为这是一个很好的设计决策。向下转换是抽象的实现部分,你告诉编译器"相信我,我知道这会起作用,这只是一个抽象"。
(使用dynamic_cast而不是static_cast说"哦,是的,因为我有点偏执,如果我碰巧错了,请让运行时抛出错误",例如,如果有人通过尝试混合不兼容的 ConcreteImplementations 来滥用库。
选项 3 被扔在那里,即使我认为这不是 OP 真正想要的,因为它意味着打破"接口兼容性"约束,这是停止违反 LSP 并满足强类型粉丝的另一种方式。
首先,我要感谢您,因为您的问题问得很好。
我的第一感觉是,如果你想传递一个IfaceB&
但需要使用具体类型,你就会遇到架构问题。
但是,我理解您想要做的事情的复杂性,因此我将尝试为您提供比沮丧更好的解决方案。
体系结构的开头是一个不错的选择,因为这是一种适配器模式(链接是 C# 中的,但即使我暂时不使用这种语言,它仍然是我在该主题上找到的最好的语言!而这正是你想要做的。
在以下解决方案中,您将添加另一个对象c
负责a
和b
之间的交互:
class ConcreteAForFoo : public IfaceA
{
private:
FooA a;
};
class ConcreteBForFoo : public IfaceB
{
private:
FooB b;
};
class ConcreteCForFoo : public IfaceC
{
private:
ConcreteAForFoo &a;
ConcreteBForFoo &b;
public:
ConcreteCForFoo(ConcreteAForFoo &a, ConcreteBForFoo &b) : a(a), b(b)
{
}
void doSomething() override
{
a.doSomethingWith(b);
}
};
class IfaceC
{
public:
virtual void doSomething() = 0;
};
你应该使用工厂来获取你的对象:
class IFactory
{
public:
virtual IfaceA* getA() = 0;
virtual IfaceB* getB() = 0;
virtual IfaceC* getC() = 0;
};
class IFactory
{
public:
virtual IfaceA* getA() = 0;
virtual IfaceB* getB() = 0;
virtual IfaceC* getC() = 0;
};
class FooFactory : public IFactory
{
private:
IfaceA* a;
IfaceB* b;
IfaceC* c;
public:
IfaceA* getA()
{
if (!a) a = new ConcreteAForFoo();
return a;
}
IfaceB* getB()
{
if (!b) b = new ConcreteBForFoo();
return b;
}
IfaceC* getC()
{
if (!c)
{
c = new ConcreteCForFoo(getA(), getB());
}
return c;
}
};
当然,您肯定必须调整此代码,因为您可能有很多 b 或 a。
此时,您将能够像这样处理方法doSomething
:
factory = FooFactory();
c = factory.getC();
c->doSomething();
也许有更好的解决方案,但我需要一个真正的代码来告诉你。我希望这对你有帮助。
最后,我想为我的C++(和我的英语(道歉,我已经很长时间没有用C++编码了(我知道我至少犯了一些错误(什么意思是 null == this.a 没有指针??
编辑:
避免提供传递接口的可能性而需要具体类型的歧义的另一种可能性是使用一种与命名注册表关联的中介器(负责A
和B
实例之间的交互(:
class FooFactory : public IFactory
{
private:
IMediator* mediator;
public:
IfaceA* getNewA(name)
{
a = new ConcreteAForFoo();
mediator = getMediator();
mediator->registerA(name, *a);
return a;
}
IfaceB* getNewB(name)
{
b = new ConcreteBForFoo();
mediator = getMediator();
mediator->registerB(name, *b);
return b;
}
IMediator* getMediator()
{
if (!mediator) mediator = new ConcreteMediatorForFoo();
return mediator;
}
};
class ConcreteMediatorForFoo : public IMediator
{
private:
std::map<std::string, ConcreteAForFoo> aList;
std::map<std::string, ConcreteBForFoo> bList;
public:
void registerA(const std::string& name, IfaceA& a)
{
aList.insert(std::make_pair(name, a));
}
void registerB(const std::string& name, IfaceB& b)
{
bList.insert(std::make_pair(name, b));
}
void doSomething(const std::string& aName, const std::string& bName)
{
a = aList.at(aName);
b = bList.at(bName);
// ...
}
}
然后,您可以像这样处理A
和B
实例的交互:
factory = FooFactory();
mediator = factory.getMediator();
a = factory.getNewA('plop');
bPlap = factory.getNewB('plap');
bPlip = factory.getNewB('plip');
// initialize a, bPlap and bPlip.
mediator->doSomething('plop', 'plip');
这不是一件容易的事。关闭C++的类型系统不太合适。原则上,没有什么可以(静态地(阻止您的用户实例化一个库中的IFaceA
和另一个库中的IFaceB
,然后根据需要混合和匹配它们。您的选择是:
- 不要
使库动态可选,即不要让它们实现相同的接口。相反,让他们实现一系列接口的实例。
template <typename tag> class IfaceA; template <typename tag> class IfaceB; template <typename tag> class IfaceA { virtual void doSomethingWith(IfaceB<tag>& b) = 0; };
每个库都实现具有不同标记的接口。用户可以在编译时轻松选择标记,但在运行时则不然。
- 使接口真正可互换,以便用户可以混合和匹配由不同库实现的接口。
- 将演员表隐藏在一些漂亮的界面后面。
- 使用访客模式(双重派单(。它消除了强制转换,但存在一个众所周知的循环依赖问题。非循环访问者通过引入一些动态转换来消除问题,因此这是 #3 的变体。
以下是双重派单的基本示例:
//=================
class IFaceBDispatcher;
class IFaceB
{
IFaceBDispatcher* get() = 0;
};
class IfaceA
{
public:
virtual void doSomethingWith(IfaceB& b) = 0;
...
};
// IFaceBDispatcher is incomplete up until this point
//=================================================
// IFaceBDispatcher is defined in these modules
class IFaceBDispatcher
{
virtual void DispatchWithConcreteAForFoo(ConcreteAForFoo*) { throw("unimplemented"); }
virtual void DispatchWithConcreteAForBar(ConcreteAForBar*) { throw("unimplemented"); }
};
class ConcreteAForFoo : public IfaceA
{
virtual void doSomethingWith(IfaceB& b) { b.DispatchWithConcreteAForFoo(this); }
}
class IFaceBDispatcherForFoo : public IFaceBDispatcher
{
ConcreteBForFoo* b;
void DispatchWithConcreteAForFoo(ConcreteAForFoo* a) { a->doSomethingWith(*b); }
};
class IFaceBDispatcherForBar : public IFaceBDispatcher
{
ConcreteBForBar* b;
void DispatchWithConcreteAForBar(ConcreteAForBar* a) { a->doSomethingWith(*b); }
};
class ConcreteBForFoo : public IFaceB
{
IFaceBDispatcher* get() { return new IFaceBDispatcherForFoo{this}; }
};
class ConcreteBForBar : public IFaceB
{
IFaceBDispatcher* get() { return new IFaceBDispatcherForBar{this}; }
};
在你的设计中,可以同时实例化libFoo和libBar。也可以将 IFaceB 从 libBar 传递到 libFoo 的 IFaceA::d oSomethingWith((。
因此,您被迫dynamic_cast下来以确保libFoo对象不会传递给libBar对象,反之亦然。需要验证用户没有搞砸。
我只看到你能做的两件事:
- 接受dynamic_cast以验证每个函数基础知识的接口
- 重新设计接口,使所有函数参数都不是接口
您可能只能完成第二个任务,只允许从其他对象内部创建对象,并且每个创建的对象都保留对创建它的对象的引用。
例如,对于libFoo:
IFaceA* libFooFactory (void)
{
return new libFoo_IFaceA ();
}
IFaceB* libFoo_IFaceA::CreateIFaceB (void)
{
return new libFoo_IFaceB (this);
}
.
.
.
libFoo_IFaceB::libFoo_IFaceB (IFaceA* owner)
{
m_pOwner = owner;
}
libFoo_IFaceB::doSomething (void)
{
// Now you can guarentee that you have libFoo_IFaceA and libFoo_IFaceB
m_pOwner-> ... // libFoo_IFaceA
}
用法如下所示:
IFaceA* libFooA = libFooFactory ();
IFaceB* libFooB = libFooA->CreateIFaceB();
libFooB->doSomething();
如果目标是从接口中删除对具体实现的向下转换,则需要向接口公开具体类型(而不是其实现(。下面是一个示例:
class LibFoo;
class LibBar;
class IfaceA;
class IfaceB;
template <typename LIB> class BaseA;
template <typename LIB> class BaseB;
struct IfaceA
{
virtual ~IfaceA () {}
virtual operator BaseA<LibFoo> * () { return 0; }
virtual operator BaseA<LibBar> * () { return 0; }
virtual void doSomethingWith (IfaceB &) = 0;
};
struct IfaceB {
virtual ~IfaceB () {}
virtual operator BaseB<LibFoo> * () { return 0; }
virtual operator BaseB<LibBar> * () { return 0; }
};
BaseA
和 BaseB
的实现将覆盖相应的转换运算符。他们还了解它将与之交互的类型。 BaseA
依靠转换运算符使IfaceB
达到匹配BaseB
,然后调度到适当匹配的方法。
template <typename LIB>
struct BaseA : IfaceA {
operator BaseA * () override { return this; }
void doSomethingWith (IfaceB &b) override {
doSomethingWithB(b);
}
void doSomethingWithB (BaseB<LIB> *b) {
assert(b);
doSomethingWithB(*b);
}
virtual void doSomethingWithB (BaseB<LIB> &b) = 0;
};
template <typename LIB>
struct BaseB : IfaceB {
operator BaseB * () override { return this; }
};
然后,具体实现可以做需要做的事情。
struct LibFoo {
class FooA {};
class FooB {};
};
struct ConcreteFooA : BaseA<LibFoo> {
void doSomethingWithB (BaseB<LibFoo> &) override {
//...
}
LibFoo::FooA a_;
};
struct ConcreteFooB : BaseB<LibFoo> {
LibFoo::FooB b_;
};
将另一个库添加到组合中时,除非适当扩展Iface
,否则将导致编译时错误(但Base
不一定需要任何扩展(。您可能会认为该结果是有用的功能,而不是有害的功能。
struct ConcreteBazA : BaseA<LibBaz> { // X - compilation error without adding
// conversion operator to `IfaceA`
工作示例。
如果更新所有接口对象不是一种选择,那么阻力最小的路径是利用动态向下投射,因为它旨在解决此问题。
struct IfaceA
{
virtual ~IfaceA () {}
template <typename LIB> operator BaseA<LIB> * () {
return dynamic_cast<BaseA<LIB> *>(this);
}
virtual void doSomethingWith (IfaceB &) = 0;
};
struct IfaceB {
virtual ~IfaceB () {}
template <typename LIB> operator BaseB<LIB> * () {
return dynamic_cast<BaseB<LIB> *>(this);
}
};
由于转换不再是虚拟的,因此BaseA
和BaseB
模板不再需要覆盖转换方法。
template <typename LIB>
struct BaseA : IfaceA {
void doSomethingWith (IfaceB &b) override {
doSomethingWithB(b);
}
void doSomethingWithB (BaseB<LIB> *b) {
assert(b);
doSomethingWithB(*b);
}
virtual void doSomethingWithB (BaseB<LIB> &b) = 0;
};
template <typename LIB>
struct BaseB : IfaceB {
};
这个答案可以被认为是n.m建议的访客模式的例证。
@Job,我不能把它放在评论中,但它应该在那里。您尝试使用哪些技术来寻找此问题的解决方案?为什么这些技术不起作用?了解这一点将有助于其他人解释为什么一种技术实际上可能有效或避免重复工作。
我认为您的问题面临的最大挑战是您希望IfaceA
的实现需要调用IfaceB
实现的未知函数。这将始终导致您必须强制转换传递的对象才能获得所需的方法,除非您正在调用的函数位于 IfaceB
接口中。
class IfaceB
{
public:
virtual void GetB() = 0; // Added function so I no longer have to cast.
}
现在,我想说你的初始实现类似于短语"如果你所拥有的只是一把锤子,那么一切看起来都像钉子。但是,我们已经知道您有木螺钉、金属螺钉、螺栓、滞后螺栓等。因此,您需要为"锤子"提供一种通用方法,使其并不总是锤子,而是钻头,扳手,棘轮等。 这就是为什么我提出通过强制实现通用函数来扩展您的界面的观点。
我也注意到你的评论:
我不确定这是否真的重要,但它是一个库,开发人员应该能够选择使用哪个后端。
这实际上对你的设计产生了巨大的影响。我看到你在这里发生了两件事:
- 尝试高内聚,但通过允许实现相互调用来强制耦合。
- 看来您正在做的是构建数据抽象层(DAL(。
我最近实现了一个DAL,用于处理切换创建图像的对象(JPEG,PNG,BMP等(。我使用了一个接口来实现基本功能,并使用了一个工厂来处理对象的创建。效果很好。我永远不必更新任何东西,只需添加一个新类来处理不同的图像类型。您可以使用模板化工厂对Iface#
实现执行相同的操作。它将允许您在工厂中注册一个类本身,并且调用代码根据注册名称获取对象。
从接口中删除 doSomethingWith(( 方法。提供免费功能:
void doSomethingWith(ConcreteAForFoo & a, ConcreteBForFoo & b);
必须使类型表示它可以做什么和不能做什么。您的接口无法处理接口的任意子类型。
这是使用泛型的Java解决方案的一个镜头。 我对C++不是很熟悉,所以我不知道是否有类似的解决方案。
public interface IfaceA<K extends IfaceB> {
void doSomething(K b);
}
public interface IfaceB {
}
public class ConcreteAForFoo implements IfaceA<ConcreteBForFoo> {
private FooA fooA;
@Override
public void doSomething(ConcreteBForFoo b) {
fooA.fooBar(b.getFooB());
}
}
public class ConcreteBForFoo implements IfaceB {
private FooB fooB;
public FooB getFooB() {
return fooB;
}
}
public class FooA {
public void fooBar(FooB fooB) {
}
}
public class FooB {
}