使用接口包装库而无需向下转发

时间:2014-12-01 08:57:31

标签: c++ oop interface

让我们说我的项目使用了一个库LibFoo,它通过许多类提供其功能,例如FooAFooB。现在,有许多类似的库(例如,LibBar提供BarABarB)提供与LibFoo相同的功能,我希望我的项目的用户能够能够选择使用哪个库,最好是在运行时。

为了实现这一点,我创建了一个"包装层"它定义了我期望从库中获得的接口。在我的示例中,该层包含两个接口:IfaceAIfaceB。然后,对于我想要支持的每个库,我创建一个"实现层"使用其中一个库实现接口。

我现在的问题在于如何很好地实现实现层。为了演示我的问题,请考虑我们有以下接口(以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());
}

由于不得不贬低通常被认为是一种代码气味,所以我认为应该有更好的方法来做到这一点。或者我开始错误地设计这个?

TL; DR

给定一层相互依赖的接口(在一个接口中的方法接收对其他接口的引用)。这些接口的实现如何共享实现细节而无需在接口中向下转换或公开这些细节?

9 个答案:

答案 0 :(得分:7)

正确的答案是:

  1. 使您的界面真正可互操作且可互换,或
  2. 不要改变任何事情。继续进行动态演员表。
  3. 在您的类型系统中添加其他内容,这使得用户无法做错事并混合不兼容的具体实现。
  4. 你现在的基本问题是接口/继承是一个过度简化的谎言。我的意思是你的接口实际上并不是LSP

    如果你想修复这个跟随LSP并使库可以混合,你需要做1:修复你的代码不使用偷偷摸摸的实现细节,并且真正遵循LSP。但是这个选项基本上被问题陈述所排除,其中显式类的实现是不兼容的,我们应该假设情况总是如此。

    假设库从不将是可混合的,正确的答案是2. 继续使用动态强制转换。让我们思考一下原因:

    OP的界面定义字面上说ConcreteAForFoo可以成功doSomethingWith 任何 IFaceB对象。但OP知道这不是真的 - 它确实必须是ConcreteBForFoo,因为需要使用某些实现细节,而这些细节在IFaceB的界面中找不到。

    在这个特定的情景中,向下倾斜是最好的事情

    请记住:向下转换是指你知道对象的类型,但编译器没有。编译器只知道方法签名。您知道运行时类型更好比编译器更好,因为您知道一次只加载一个库

    您希望隐藏编译器中的真实类型,因为您希望将库抽象到接口中并隐藏库中用户的基础类型,我认为这是一个很好的设计决策。向下转换是你抽象的实现部分,你告诉编译器相信我,我知道这将起作用,这只是一个抽象概念。

    (使用dynamic_cast代替static_cast说&#39;哦,是的,因为我有点偏执,如果我碰巧错了,请让运行时抛出错误&#39;,例如如果有人通过尝试混合不兼容的ConcreteImplementations而误用库。)

    选项3被抛入其中,尽管我认为它不是OP真正想要的,因为它意味着打破了界面兼容性&#39;约束,它是阻止违反LSP的另一种方式,并且满足了强类型的粉丝。

答案 1 :(得分:7)

看起来像一个经典的double dispatch用例。

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 :: doSomethingWith签名可以改变以获得耦合和访问级别之间的最佳平衡(例如,ConcreteAForFoo可以用作参数来以紧密耦合为代价访问其内部结构)。

答案 2 :(得分:6)

首先,我要感谢你,因为你的问题很好。

我的第一感觉是,如果您想传递IfaceB&但需要使用具体类型,则会遇到架构问题。

但是,我理解你想做的事情的复杂性,所以我会尝试给你一个比垂头丧气更好的解决方案。

你的架构的开始是一个很好的选择,因为这是一个adapter pattern(链接在C#中,但即使我暂时不使用这个语言,它仍然是我在这个主题上找到的最好的! )。这正是你想要做的。

在以下解决方案中,您添加了另一个对象c,负责ab之间的互动:

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;
};

您应该使用factory来获取对象:

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 ==这个。没有指针??))。

修改

避免提供传递接口的可能性的另一种可能性,而你想要一个具体的类型是使用一种中介(负责AB的实例之间的交互)与命名注册表相关联:

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);

            // ...
        }
}

然后,您可以像这样处理AB的实例的互动:

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');

答案 3 :(得分:5)

这不是一件容易的事。关于C ++的类型系统还不够。原则上没有任何东西可以(静态地)阻止用户从一个库中实例化IFaceA并从另一个库中IFaceB实例化,然后根据需要混合和匹配它们。您的选择是:

  1. 不要让库动态选择,即不要让它们实现相同的接口。相反,让他们实现一系列接口的实例。

    template <typename tag>
    class IfaceA;
    template <typename tag>
    class IfaceB;
    
    template <typename tag>
    class IfaceA
    {
       virtual void doSomethingWith(IfaceB<tag>& b) = 0;
    };
    

    每个库都使用不同的标记实现接口。用户可以在编译时轻松选择标记,但不能在运行时选择。

  2. 使接口真正可以互换,以便用户可以混合和匹配不同库实现的接口。
  3. 隐藏一些不错的界面背后的演员阵容。
  4. 使用访客模式(双重调度)。它消除了演员表,但是有一个众所周知的循环依赖问题。非循环访问者通过引入一些动态强制转换消除了这个问题,因此这是#3的变种。
  5. 以下是双重调度的基本示例:

    //=================
    
    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}; }
    };
    

答案 4 :(得分:3)

如果目标是从接口中删除具体实现的向下转换,则需要将具体类型公开给接口(但不是它的实现)。这是一个例子:

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; }
};

BaseABaseB的实现会覆盖相应的转换运算符。他们还了解与之互动的类型。 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`

Working example.


如果不能更新所有接口对象,那么阻力最小的路径就是利用动态向下转换,因为它旨在解决这个问题。

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);
    }
};

由于转化不再是虚拟转化,BaseABaseB模板不再需要覆盖转换方法。

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建议的访客模式的例证。

答案 5 :(得分:3)

在您的设计中,可以同时实例化libFoo和libBar。也可以将IFaceB从libBar传递给libFoo的IFaceA :: doSomethingWith()。

因此,您强制到dynamic_cast,以确保libFoo对象不会传递给libBar对象和visaversa。需要验证用户没有搞砸。

我只看到你能做的两件事:

  • 接受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();

答案 6 :(得分:2)

@Job,我不能把它放在评论中,但它应该在那里。您尝试使用哪些技术来找到此问题的解决方案?为什么这些技术不起作用?了解这将有助于其他人解释为什么技术可能实际工作或避免重复工作。

我认为对您的问题的最大挑战是,您期望IfaceA的实现需要调用IfaceB的实现尚未知功能。这将导致您必须转换传入的对象以获取所需的方法,除非您调用的函数位于IfaceB接口中。

class IfaceB
{
public:
    virtual void GetB() = 0; // Added function so I no longer have to cast.
}

现在,我想说你的初始实现类似于短语&#34;如果你所拥有的只是一把锤子,那么一切看起来都像钉子一样。&#34;但是,我们已经知道你有木螺钉,金属螺钉,螺栓,方头螺栓等。所以你需要为锤子提供通用的方法。并不总是锤子,而是钻头,扳手,棘轮等。这就是为什么我提出扩展你的界面的强迫实施常用功能。

我也在你的评论中注意到了:

  

我不确定它是否真的重要但它是一个开发人员应该能够选择使用哪个后端的库。

这实际上对您的设计产生了巨大影响。我看到你在这里发生了两件事:

  1. 尝试高凝聚力,但通过允许实现相互调用来强制耦合。
  2. 您正在做的是构建数据抽象层(DAL)。
  3. 我最近实施了一个DAL来处理切换创建图像的对象(JPEG,PNG,BMP等)。我使用了一个用于基本功能的接口,以及一个用于处理创建对象的工厂。它效果很好。我永远不需要更新任何东西,只需添加一个新类来处理不同的图像类型。您可以使用Templated FactoryIface#实施执行相同的操作。它允许你让一个类在工厂注册,调用代码根据注册名称获取一个对象。

答案 7 :(得分:0)

从接口中删除doSomethingWith()方法。提供免费功能:

void doSomethingWith(ConcreteAForFoo & a, ConcreteBForFoo & b);

必须使类型代表它能做什么以及它不能做什么。你的无法处理接口的任意子类型。

答案 8 :(得分:0)

以下是使用泛型的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 {

}