设计一个更好的API接口,将结构从一个类传递到另一个类

时间:2012-04-30 15:07:51

标签: c++ api

我坚信以下设计理念:

1>服务应尽可能在存储数据的地方实施。

2 - ; Getter和Setter是邪恶的,应该小心使用。

我宁愿不在这里争论两个论点,并假设他们有自己的优势。

这是我目前面临的挑战。我有两个类(即AComputerA),其中AComputer为A提供一些服务,A保存所有基本数据成员。

事实:由于系统设计,我不允许在AComputer内合并A。我知道,它打破了我的观点1>计算应该与数据保持一致。

将数据从A传递到AComputer时,我们必须传递10个(大约)个别参数,因此最好设计一个结构来执行此操作,否则构造函数列表会变得疯狂。存储在AComputer中的大多数数据都是A中存储的数据的直接副本。我们选择将这些数据存储在AComputer中,因为AComputer中的其他函数也需要这些变量。

这是一个问题(我要求考虑API维护和修改的最佳实践):

1>我们应该在哪里定义传递结构PassData

2 - ;我们应该为struct PassData提供getter / setter吗?

我提供了一个示例代码,详细说明了我的问题。我最好能找到一个真正的工作开源API来解决同一个问题,以便我可以从中学习。

如果您查看课程PassData m_data;中定义的私有AComputer,我会这样做。换句话说,如果我们更改AComputer的基础实现,我们可以用单个变量或其他内容替换PassData m_data;,但不要破坏PassData的接口。 所以在这个设计中,我没有为结构PassData提供getter / setter。

谢谢

class AComputer
{
public:
    struct PassData
    {   // int type just used as an illustration. Real data has different types,
        // such as double, data, string, enum, etc.
        // Note: they are not exact copies of variables from A but derived from them
        int m_v1;
        // from m_v1 to m_v10
        //...
        int m_v10;
    };

    // it is better to store the passed-in data since other functions also need it.
    AComputer(const PassData& pd) : m_data(pd) {}

    int GetCombinedValue() const
    { /* This function returns a value based the passed-in struct of pd */ }

private:
    PassData m_data;    
};

class A
{
private:
    int m_i1;
    // from m_i1 to m_i10
    // ...
    int m_i10;
    // from m_i11 to m_i20
    // ...
    int m_i20;

    boost::shared_ptr<AComputer> m_pAComputer;

public:
    A()
    {
        AComputer::PassData aData;
        // populate aData ...
        m_pAComputer = boost::shared_ptr<AComputer>(new AComputer(aData));
    }

    int GetCombinedValue() const
    {
        return m_pAComputer->GetCombinedValue();
    }
};

4 个答案:

答案 0 :(得分:11)

我认为在开始之前澄清几点是更好的,你说:

  

如果你看私人PassData m_data;我在电脑类中定义的   这样做是为了目的。换句话说,如果我们改变基础   执行电脑,我们可以替换PassData m_data;同   个别变量或其他东西但不破坏界面   PassData。

事实并非如此,PassData是您界面的一部分!您不能在不破坏客户端代码的情况下替换PassData,因为您需要在AComputer的构造函数中使用PassData。 PassData不是实现细节,但它是纯接口。

需要澄清的第二点:

  

2 - ; Getter和Setter是邪恶的,应该小心使用。

正确!但是你应该知道POD(Plain-Old-Data结构)甚至是最差的。使用POD而不是使用getter和setter类的唯一优点是可以省去编写函数的麻烦。但真正的问题仍然是开放的,你班级的界面太麻烦,维护起来非常困难。

设计始终是不同要求之间的权衡:

虚假的灵活感

您的图书馆已经发布,很多代码都在使用您的课程。在这种情况下,PassData的变化将是戏剧性的。如果您可以在运行时支付一小笔费用,那么您可以灵活地使用界面。例如,AComputer的构造函数将是:

AComputer(const std::map<std::string,boost::any>& PassData);

看一下boost :: any here。 您还可以为地图提供factory,以帮助用户轻松创建地图。

  • 如果您不再需要字段,则代码不会更改。

缺点

  • 运行时间价格较小。
  • 丢失编译器类型安全检查。
  • 如果您的功能需要另一个必填字段,您仍然遇到麻烦。客户端代码将编译但行为不正确。

总的来说,这个解决方案并不好,最后它只是原版的一个奇特版本。

策略模式

struct CalculateCombinedValueInterface
{
   int GetCombinedValue()=0;
   virtual ~CalculateCombinedValueInterface(){}
};

class CalculateCombinedValueFirst : CalculateCombinedValueInterface
{
   public:
       CalculateCombinedValueFirst(int first):first_(first){}
       int GetCombinedValue(); //your implementation here
   private:
       //I used one field but you get the idea
       int first_;
};

客户端代码为:

CalculateCombinedValueFirst* values = new CalculateCombinedValueFirst(42);

boost::shared_ptr<CalculateCombinedValueInterface> data(values);

现在,如果您要修改代码,则不应触及已部署的界面。面向对象的解决方案是提供一个从抽象类继承的新类。

class CalculateCombinedValueSecond : CalculateCombinedValueInterface
{
   public:
       CalculateCombinedValueFirst(int first,double second)
           :first_(first),second_(second){}
       int GetCombinedValue(); //your implementation here
   private:
       int first_;
       double second_;
};

客户将决定升级到新班级还是继续使用现有版本。

  • 在不破坏客户端代码的情况下改进您的界面。
  • 您没有触及现有代码,但在新文件中引入了新功能。
  • 如果您想要更小的粒度控制,可能需要使用template method design pattern

缺点

  • 使用虚拟功能的开销(基本上只有几皮秒!)
  • 您不能破坏现有代码。您必须保持现有界面不变,并添加一个新类来模拟不同的行为。

参数数量

如果在一个函数中输入了一组十个参数,则这些值很可能与逻辑相关。您可以在课程中收集其中一些值。这些类可以组合在另一个类中,它将作为函数的输入。你在一个班级中拥有10个(或更多!)数据成员的事实应该响起。

single responsibility principle说:

改变课程的原因绝不应该是一个原因。

这个原则的必然结果是:你的班级必须很小。如果你的班级有20个数据成员,你很可能会发现有很多理由要改变它。

<强>结论

在向客户端提供接口(任何类型的接口)之后,您无法对其进行更改(一个很好的示例是C ++中编译器需要实现多年的所有弃用功能)。请注意您提供的界面甚至是隐式界面。在您的示例中,PassData不是实现细节,但它是类接口的一部分。

参数的数量是需要检查设计的信号。改变一个大班很难。你的类应该很小,只能通过接口(C ++俚语中的抽象类)依赖于其他类。

如果你的班级是:

1)小而且只有一个原因需要改变

2)派生自抽象类

3)其他类使用指向抽象类的指针

来引用它

您的代码可以轻松更改(但必须保留已提供的界面)。

如果您不满足所有这些要求,您将遇到麻烦。

注意:要求2)和3)如果不是提供动态多态,而是设计使用静态多态,则可以改变。

答案 1 :(得分:0)

您可以考虑重构以使用模式对象 - 此对象的唯一目的是包含方法调用的参数。有关详细信息:http://sourcemaking.com/refactoring/introduce-parameter-object

答案 2 :(得分:0)

在普通的类设计中,所有成员函数都将this指针作为隐式参数传递,以便它们可以访问数据成员:

// Regular class
class SomeClass
{
public:
    // will be name-mangled by the compiler as something like: 
    // void SomeClass_getValue(const SomeClass*) const;
    void getValue() const 
    {
        return value_; // actually: return this->value_;
    }

private:
    int value_;
};

你应该尽可能地模仿这个。如果由于某些原因你不允许将AC计算机和A类合并到一个干净的类中,那么下一个最好的事情就是让AComputer将指针指向A作为数据成员。在AComputer的每个成员函数中,您必须明确使用A的getter / setter函数来访问相关的数据成员。

class AComputer
{
public:
    AComputer(A* a): p_(a) {}

    // this will be mangled by the compiler to something like
    // AComputer_GetCombinedValue(const Acomputer*) const;
    int GetCombinedValue() const
    {
         // in a normal class it would be: return m_i1 + m_i2 + ...
         // which would actually be: return this->m_i1 + this->m_i12 + ...
         // the code below actually is: return this->p_->m_i1 + this->p_->m_i2 + ... 
         return p_->get_i1() + p_->get_i2() + ...       
    }

private:
    class A;
    A* p_;
};

class A
{
public:
   // setters and getters

private:
   // data only, NO pointer to AComputer object
}

因此,实际上,您已经创建了一个额外的间接级别,这会给用户带来错觉,即AComputer和A是同一抽象的一部分。

答案 3 :(得分:0)

如果您完全控制所有AComputer客户端,则使用PassData而不是10个参数是很好的。它有两个优点:当您添加要传递的另一个数据时,您需要进行更少的更改,并且您可以使用赋值来调用调用者站点上的成员,以使每个“参数”的含义清晰。

但是,如果其他人打算使用电脑,使用PassData有一个严重的缺点。如果没有它,当您向AComputer构造函数添加第11个参数时,编译器将为未更新实际参数列表的用户检测错误。如果将第11个成员添加到PassData,编译器将默默接受新成员为垃圾的结构,或者在最好的情况下为零。

在我看来,如果你使用PassData,那么拥有getter和setter将是一种矫枉过正。 Sutter和Alexandresku的“C ++编码标准”同意这一点。项目#41的标题是:“使数据成员保密,除了无行为聚合(C风格结构)”(重点是我的)。