5

我坚信以下设计理念:

1> 服务应该在尽可能靠近数据存储的地方实现。

2> Getter 和 Setter 是邪恶的,应该谨慎使用。

我宁愿不在这里争论以上两个论点,并假设它们有其优势。

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

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

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

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

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

2> 我们应该为 struct 提供 getter/setterPassData吗?

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

如果您查看PassData m_data;在 class 中定义的private AComputer,我是故意这样做的。换句话说,如果我们更改 的底层实现AComputer,我们可以PassData m_data;用单个变量或其他东西替换,但不会破坏PassData. 所以在这个设计中,我没有为 struct 提供 getter/setter PassData

谢谢

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

4 回答 4

11

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

如果您查看私有 PassData m_data; 在 AComputer 类中定义,我是故意这样做的。也就是说,如果我们改变AComputer的底层实现,就可以替换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。您还可以为地图提供工厂,以帮助用户轻松创建地图。

  • 如果您不再需要某个字段,则代码将保持不变。

缺点

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

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

策略模式

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

客户将决定是升级到新类还是保留现有版本。

  • 在不破坏客户端代码的情况下改进您的界面。
  • 您没有触及现有代码,而是在新文件中引入了新功能。
  • 如果您想要更小的粒度控制, 您可能想要使用模板方法设计模式。

缺点

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

参数数量

如果您在一个函数中输入了一组十个参数,那么这些值很可能在逻辑上是相关的。您可以在类中收集其中一些值。这些类可以组合在另一个类中,它将成为您的函数的输入。一个类中有 10 个(或更多!)数据成员的事实应该敲响警钟。

单一责任原则说:

改变班级的理由不应该不止一个。

这个原则的推论是:你的班级必须很小。如果你的类有 20 个数据成员,你很可能会找到很多理由来改变它。

结论

向客户端提供接口(任何类型的接口)后,您将无法更改它(一个很好的例子是编译器需要多年实现的 C++ 中所有过时的特性)。注意你提供的接口,甚至是隐式接口。在您的示例中, PassData 不是实现细节,而是类接口的一部分。

参数的数量是您的设计需要审查的信号。换一个大班是很困难的。您的类应该很小并且仅通过接口(C++ 俚语中的抽象类)依赖于其他类。

如果您的班级是:

1) 小而且只有一个需要改变的理由

2) 派生自抽象类

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

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

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

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

于 2012-05-04T11:42:01.963 回答
0

如果您可以完全控制所有 AComputer 客户端,则使用 PassData 而不是 10 个参数会很好。它有两个优点:当您添加另一条要传递的数据时,您需要做的更改更少,您可以使用分配给调用者站点上的结构成员来明确每个“参数”的含义。

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

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

于 2012-05-08T14:36:13.907 回答
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_;
};

你应该尽可能地模仿这个。如果由于某些原因不允许将 AComputer 和 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 是同一个抽象的一部分的错觉。

于 2012-05-04T11:44:34.490 回答
0

您可能会考虑重构以使用模式对象——该对象的唯一目的是包含方法调用的参数。更多细节:http: //sourcemaking.com/refactoring/introduce-parameter-object

于 2012-04-30T18:14:22.610 回答