面向对象的设计建议

时间:2010-04-06 14:52:11

标签: c++ oop visibility class-members

这是我的代码:

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

我需要调用Gun的所有成员函数而不是Soldier对象。类似的东西:

soldier.gun.fire();

soldier.getGun().load(15);

那么哪一个是更好的设计?将枪对象隐藏为私有成员并使用getGun()函数访问它。或者让它成为公共会员?或者我可以封装所有这些函数会使实现更难:

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

那么你认为哪一个是最好的?

9 个答案:

答案 0 :(得分:21)

我想说你的第二个选择:

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

最初它的工作量更大,但随着系统变得越来越复杂,你可能会发现一名士兵在开枪之前和之后都想做其他事情(可能会检查他们是否有足够的弹药然后尖叫“吸盘! “在射击之前,然后嘀咕”“之后必须受伤”并检查他们是否需要重装。它还从士兵类的用户那里隐藏了枪的准确性的不必要细节。

答案 1 :(得分:11)

首先,您可以通过Gun课程外部访问Soldier来违反Law of Demeter

我会考虑这些方法:

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

这样你也可以实现你的拳头,刀,手榴弹,棒球棒,激光猫等。

答案 2 :(得分:7)

Demeter法则会说要封装这些功能。

http://en.wikipedia.org/wiki/Law_of_Demeter

这样,如果你想在士兵和枪之间进行某种类型的交互,你就有了插入代码的空间。

编辑:找到维基百科链接中的相关文章: http://www.ccs.neu.edu/research/demeter/demeter-method/LawOfDemeter/paper-boy/demeter.pdf paperboy示例与您发布的士兵示例非常相似。

答案 3 :(得分:5)

事实上,这很大程度上取决于你想要控制多少。

要模拟真实世界,您甚至可能想要完全封装枪对象,并且只需要一个soldier.attack()方法。士兵。攻击()方法然后会看到士兵是否携带枪,以及枪的状态是什么,并在必要时开火或重新加载。或者如果两种操作都没有足够的弹药,可能会将枪射向目标并逃跑......

答案 4 :(得分:3)

如果你暴露枪,你允许超出枪的成员功能的东西,这可能不是一个好主意:

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

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

如果您封装了这些功能(我推荐),您可以修改枪支或引入其他(派生)枪类,而无需更改士兵的界面。

答案 5 :(得分:2)

通常我的决定是基于容器类的性质(在本例中为Soldier)。要么它完全是POD,要么不是。如果它不是POD,我将所有数据成员设为私有,并提供访问方法。只有当没有不变量时,该类才是POD(即外部actor无法通过修改其成员使其状态不一致)。你的士兵课看起来更像是非POD,所以我会去访问者方法选项。如果它会返回一个const引用或者一个常规引用​​是你自己的决定,基于fire()和其他方法的行为(如果他们修改了枪的状态)。

BTW,Bjarne Stroustrup在他的网站上谈了这个问题: http://www.artima.com/intv/goldilocks3.html

旁注:我知道这并不是你提出的要求,但是我建议你也要考虑在得墨忒耳法律的其他答案中提到的许多提及:揭露行动方法(对枪支采取行动)而不是整个枪支物体通过吸气剂方法。由于士兵“拥有”枪(它在他的手中并且他扣动扳机),我觉得其他演员“要求”士兵开枪似乎更自然。我知道如果枪有许多方法可以采取行动,这可能会很乏味,但也许这些可以归入士兵暴露的更高级别的行动中。

答案 6 :(得分:1)

没有黄金法则适用于100%的时间。根据您的需要,这真的是一个判断。

这取决于您想要隐藏/禁止枪支访问Solider的功能。

如果您只想对Gun进行只读访问,则可以向自己的成员返回const引用。

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

一般来说,我认为枪是它自己的对象,如果你不介意暴露Gun的所有功能,并且不介意允许通过Soldier对象改变事物,那就把它公之于众。

你可能不想复制枪,所以如果你制作一个GetGun()方法,请确保你没有返回枪的副本。

如果你想保持你的代码简单,那么让士兵负责处理枪支。您的其他代码是否需要直接使用枪支?或者士兵是否总能知道如何工作/重装自己的枪?

答案 7 :(得分: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_; }

答案 8 :(得分:0)

封装函数以提供一致的UI,即使您稍后更改逻辑也是如此。命名约定取决于你,但我通常不使用“getFoo()”,而只使用“foo()”作为访问器,将“setFoo()”作为setter。

  • 尽可能返回reference-to-const(Effective C ++ Item#3)。
  • 首选使用硬编码数字(第4项)的consts,enums和inlines
  • 为您的私有成员提供唯一的命名约定,以区别于参数
  • 使用无符号值将错误转移到编译时
  • 当const值(如maximums)适用于整个类时。让它们变得静止。
  • 如果您打算继承,请确保您的析构函数是虚拟的
  • 将所有成员初始化为合理的默认值

这是课程照顾的方式。 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;
}