面向对象设计建议

15

这是我的代码:

class Soldier {
public:
   Soldier(const string &name, const Gun &gun);
   string getName();
private:
   Gun gun;
   string name;
};

class Gun {
public:
   void fire();
   void load(int bullets);
   int getBullets();
private:
   int bullets;
}

我需要在 Soldier 对象上调用 Gun 的所有成员函数。类似这样:

soldier.gun.fire();
或者
soldier.getGun().load(15);

那么哪种设计更好呢?将枪对象隐藏作为私有成员,并使用getGun()函数访问它。还是将其设为公共成员?或者我可以封装所有这些函数,但这会使实现更加困难:

soldier.loadGun(15); // calls Gun.load()
soldier.fire(); // calls Gun.fire()

你认为哪一个是最好的呢?


3
结合Stephen和Frustrated的回答。编写一个私有函数get_gun(),该函数执行士兵获取枪支所需的工作,就像Stephen展示的那样,但是像Frustrated展示的那样,告诉士兵该做什么,而不是枪支。 - GManNickG
@GMan - 谢谢,我同意。然而,与其像@Frustrated那样告诉士兵该做什么,不如采用@Austin的方法?例如,使用soldier.Attack()而不是“soldier.loadGun()”。 - Stephen
@Stephen:是的,你可以封装得更深。我的问题只是关于这一部分。当然,你可以说soldier.Attack(),并在这个函数内部检查if (!gun.getBullets()) return或者其他类似的操作。 - pocoa
@Stephen:soldier.Attack() 可能 会调用 soldier.loadGun() - 这取决于士兵的“攻击”行为如何定义。但是,装填枪支可能是应该与攻击分离的东西 - 毕竟,有时您会意识到弹药不足,并希望在下一次攻击之前重新加载。有时候你会凭直觉射击,没有时间先检查弹药。 - FrustratedWithFormsDesigner
谢谢大家,你们的所有回答对我都很有用。Frustrated、bryanjonker、Beta和Matt都给出了类似的答案。现在,我认为封装是在这种情况下更好的选择。再次感谢。 - pocoa
9个回答

21

我认为你应该选择第二个选项:

soldier.loadGun(15); // calls Gun.load()
soldier.fire(); // calls Gun.fire()

起初它需要更多的工作,但随着系统变得更加复杂,您可能会发现士兵在开火之前和之后想做其他事情(比如检查他们是否有足够的弹药,然后喊“死了吸盘!!”然后开火,说“那一定很疼”之后,检查他们是否需要重新装填)。此外,它还隐藏了 Soldier 类的用户不必要的细节,如何精确地开火。


2
此外,不要使用loadGun这样的词汇,而应该使用prepareWeapon。这样当你的士兵在坦克中时,他就不会在旋转坦克炮时摸索手枪了。 - jmucchiello

11

首先,从Soldier类外部访问Gun将违反 迪米特法则

我会考虑使用这样的方法:

soldier.ArmWeapon(...);
soldier.Attack(...);

使用这种方法,您还可以实现您的拳头、刀、手榴弹、棒球棒、激光猫等武器。


7

3
请记住要避免陷入“低点计数法则”(http://haacked.com/archive/2009/07/14/law-of-demeter-dot-counting.aspx)。 - R. Martinho Fernandes
在这种情况下,考虑到“Gun”是构造函数参数,我认为这并不适用。这不会破坏封装性,因为客户端类需要首先实例化枪支。这并不意味着Soldier :: Attack(Target * t)方法不是更好的方法。 - Stephen
我认为很大程度上取决于士兵的行动--如果士兵只是一个包含枪支和一些统计数据的容器,那么你只是在数点。但是,如果士兵是一个需要被操作的实际对象,那么我认为LOD是有用的。 - bryanjonker
@pocoa:是的,在这种情况下,我会选择“soldier.fire()” // 调用Gun.fire()。如果你让控制类说“soldier.getGun().fire()”,你基本上是在告诉对象从士兵那里拿枪,开火,然后把枪还回去。士兵对象并没有真正控制枪支,这可能不是你的意图。 - bryanjonker
@Martinho:太棒了,我要开始称它为“Demeter的偶尔有用建议”了。 - rmeador
显示剩余2条评论

5

实际上,这取决于您想要多少控制。

为了模拟现实世界,您甚至可能希望完全封装枪支对象,只有一个soldier.attack()方法。然后,soldier.attack()方法将查看士兵是否携带枪支以及枪支的状态,并根据需要开火或重新装弹。或者,如果没有足够的弹药执行任何操作,可能会将枪扔向目标并逃跑...


3

如果您公开枪支,那么您允许超出枪支成员函数范围的事物,这可能不是一个好主意:

soldier.gun = anotherGun; // where did you drop your old gun?

如果你使用getGun(),调用看起来有点丑陋,但是你可以在不修改Soldier的情况下向Gun添加函数。

如果你封装这些函数(我建议这么做),你可以修改Gun或引入其他(派生)Gun类而不改变对Soldier的接口。


2
通常,我的决定基于容器类的性质(在本例中为Soldier)。它要么完全是POD,要么不是。如果它不是POD,我会将所有数据成员设为私有并提供访问器方法。只有当它没有不变量时(即,外部参与者无法通过修改其成员使其状态不一致),该类才是POD。你的士兵类对我来说更像是非POD,所以我会选择访问器方法选项。它返回一个const引用或常规引用是你自己的决定,取决于fire()和其他方法的行为(它们是否修改gun的状态)。
顺便说一句,Bjarne Stroustrup在他的网站上谈到了这个问题: http://www.artima.com/intv/goldilocks3.html 一个旁注:我知道这不是你准确要求的,但我建议你也考虑其他答案中提到的德米特法则:通过暴露操作方法(对枪执行操作)而不是整个枪对象来考虑。由于士兵“拥有”枪(他手中拿着并扣动扳机),所以我认为其他角色“请求”士兵开火更为自然。我知道如果枪有许多要执行的方法可能会很繁琐,但也许这些方法可以分组成更高级别的操作,由士兵公开。

不,Soldier并不仅仅是一个容器类。感谢提供链接。 - pocoa

1
提供一个“getGun()”或者简单的“gun()”。
想象一天你可能需要让这个方法变得更加复杂:
Gun* getGun() {
  if (!out_of_bullets_) {
    return &gun_;
  } else {
    PullPieceFromAnkle();
    return &secret_gun_;
  }
}

此外,您可能希望提供一个const访问器,以便人们可以在const士兵上使用const枪:
const Gun &getGun() const { return gun_; }

1

没有适用于100%时间的黄金法则。这取决于您的需求。

这取决于您希望从士兵访问枪支时隐藏/禁止多少功能。

如果您只想读取对枪支的访问权限,可以返回对自己成员的const引用。

如果您只想公开某些功能,则可以创建包装器函数。如果您不希望用户尝试通过士兵更改枪支设置,则创建包装器函数。

通常情况下,我将枪支视为其自身的对象,如果您不介意公开所有枪支功能,并且不介意通过士兵对象允许更改事物,请将其设置为public。

您可能不想复制枪支,因此如果您创建一个GetGun()方法,请确保您未返回枪支的副本。

如果您希望使代码简单化,则应让士兵负责处理枪支。您的其他代码是否需要直接使用枪支?或者士兵总是知道如何操作/重新加载自己的枪支?


在上面的例子中,gun是私有的,它需要被改为公有或者编写一个访问器方法,比如getGun()。 - Fermin
@Brian:我需要从士兵访问枪对象的所有公共成员。 - pocoa
@pocoa:然后通过GetGun()返回一个引用,或者直接将枪设置为public。这可能是最简单的方法。当您以后想要更改Gun接口时,您不需要再更改士兵类的接口。 - Brian R. Bondy

0

封装函数以提供一致的UI,即使您稍后更改逻辑。命名约定由您决定,但我通常不使用“getFoo()”,而只是使用访问器“foo()”和设置器“setFoo()”。

  • 如果可以,请返回对const的引用(Effective C++ Item#3)。
  • 优先使用常量,枚举和内联函数,而不是使用硬编码数字(Item#4)
  • 为私有成员提供唯一的命名约定,以将它们与参数区分开来
  • 在合适的情况下使用无符号值,以将错误移至编译时
  • 当const值(如最大值)适用于整个类时。将它们设为静态。
  • 如果计划继承,请确保您的析构函数是虚拟的
  • 将所有成员初始化为合理的默认值

这是经过以上修改后的类的样子。CodePad

#include <iostream>
#include <string>
#include <stdint.h>

using namespace std;

class Gun 
{
public:
   Gun() : _bullets(0) {}
   virtual ~Gun() {}
   void fire() {cout << "bang bang" << endl; _bullets--;}
   void load(const uint16_t bullets) {_bullets = bullets;}
   const int bullets() const {return _bullets;}

   static const uint16_t MAX_BULLETS = 17;

protected:
   int _bullets;
 };

class Soldier 
{
public:
   Soldier(const string &name, const Gun &gun) : _name(name), _gun(gun) {}
   virtual ~Soldier() {}
   const string& name() const;
   Gun& gun() {return _gun;}

protected:
   string _name;
   Gun _gun;
};


int main (int argc, char const *argv[])
{
   Gun gun; // initialize
   string name("Foo");
   Soldier soldier(name, gun);

   soldier.gun().load(Gun::MAX_BULLETS);

   for(size_t i = 0; i < Gun::MAX_BULLETS; ++i)
   {
     soldier.gun().fire();
     cout << "I have " << soldier.gun().bullets() << " left!" << endl;
   }
  return 0;
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接