我坚信以下设计理念:
1>服务应尽可能在存储数据的地方实施。
2 - ; Getter和Setter是邪恶的,应该小心使用。
我宁愿不在这里争论两个论点,并假设他们有自己的优势。
这是我目前面临的挑战。我有两个类(即AComputer
和A
),其中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();
}
};
答案 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_;
};
客户将决定升级到新班级还是继续使用现有版本。
临
缺点
参数数量
如果在一个函数中输入了一组十个参数,则这些值很可能与逻辑相关。您可以在课程中收集其中一些值。这些类可以组合在另一个类中,它将作为函数的输入。你在一个班级中拥有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风格结构)”(重点是我的)。