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

Job*_*Job 19 c++ oop interface

假设我的项目使用了一个库,LibFoo它通过许多类来提供它的功能,比如说FooAFooB.现在,有许多类似的库(例如,LibBar提供BarA和提供BarB)提供相同的功能LibFoo,我希望我的项目用户能够选择使用哪个库,最好是在运行时.

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

我现在的问题在于如何很好地实现实现层.为了演示我的问题,请考虑我们有以下接口(以C++显示,但应适用于类似语言):

class IfaceA
{
public:
    virtual void doSomethingWith(IfaceB& b) = 0;
    ...
};

class IfaceB
{
    ...
};
Run Code Online (Sandbox Code Playgroud)

实现层中的类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;
};
Run Code Online (Sandbox Code Playgroud)

问题在于实现ConcreteAForFoo::doSomethingWith:它的参数有类型IfaceB&但我需要访问实现细节ConcreteBForFoo才能正确实现该方法.我发现这样做的唯一方法就是在整个地方使用丑陋的垂头丧气:

void doSomethingWith(IfaceB& b) override
{
    assert(dynamic_cast<ConcreteBForFoo*>(&b) != nullptr);
    a.doSomethingWith(static_cast<ConcreteBForFoo&>(b).getFooB());
}
Run Code Online (Sandbox Code Playgroud)

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

TL; DR

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

Tim*_*ith 7

正确答案是:

  1. 使您的界面真正可互操作和可互换,或
  2. 不要改变任何东西.继续进行动态演员表演.
  3. 在您的类型系统中添加其他内容,这使得用户无法做错事并混合不兼容的具体实现.

你现在的基本问题是接口/继承是一个"过度简化谎言",我真正的意思是你的接口实际上并没有跟踪LSP.

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

假设库永远不会混合,正确答案是2. 继续使用动态强制转换.让我们考虑一下原因:

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

在这个特定的描述场景中,向下转换是最好的事情.

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

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

(并且使用dynamic_cast而不是static_cast说'并且哦,是的,因为我有点偏执,请让运行时如果我碰巧错误的话请抛出错误',例如,如果有人因为试图混合而误用库不兼容的ConcreteImplementations.)

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


Bas*_*evs 7

看起来像一个经典的双重调度用例.

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;
};
Run Code Online (Sandbox Code Playgroud)

IfaceB :: doSomethingWith签名可以变化,以获得耦合和访问级别之间的最佳平衡(例如,ConcreteAForFoo可以用作参数,以紧密耦合为代价访问其内部).


Gnu*_*cki 6

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

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

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

你的架构的开始是一个很好的选择,因为这是一个适配器模式(链接在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;
};
Run Code Online (Sandbox Code Playgroud)

您应该使用工厂来获取对象:

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;
        }
};
Run Code Online (Sandbox Code Playgroud)

当然,您肯定必须调整此代码,因为您可能有很多b或a.

此时,您将能够doSomething像这样处理方法:

factory = FooFactory();
c = factory.getC();

c->doSomething();
Run Code Online (Sandbox Code Playgroud)

可能有更好的解决方案,但我需要一个真实的代码来告诉你.我希望这能帮到您.

最后,我想为我的C++(以及我的英语)道歉,很长一段时间我没有用C++编写代码(而且我知道我至少犯过一些错误(这意味着null == this.a没有指针??)).

编辑:

另一种可能性,以避免提供可能的不确定性,而你想一个具体类型是使用一种调解员(负责实例之间的相互作用来传递的接口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);

            // ...
        }
}
Run Code Online (Sandbox Code Playgroud)

然后,您可以处理的情况下的相互作用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');
Run Code Online (Sandbox Code Playgroud)


n. *_* m. 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;
    };
    
    Run Code Online (Sandbox Code Playgroud)

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

  2. 使接口真正可互换,以便用户可以混合和匹配由不同库实现的接口.
  3. 隐藏一些漂亮的界面背后的演员表.
  4. 使用访客模式(双重调度).它消除了演员表,但是有一个众所周知的循环依赖问题.非循环访问者通过引入一些动态转换消除了这个问题,因此这是#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}; }
};
Run Code Online (Sandbox Code Playgroud)