Объектно-ориентированное дизайнерское предложение

Вот мой код:

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()

Итак, какой из них вы считаете лучшим?

Ответы

Ответ 1

Я бы сказал, идите со вторым вариантом:

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

Изначально это больше работает, но по мере того, как система становится более сложной, вы можете обнаружить, что солдат захочет делать другие вещи до и после стрельбы из своего пистолета (возможно, проверьте, хватит ли у них боеприпасов, а затем кричит "Усыновители!!" перед стрельбой и пробормотанием "после этого", и проверьте, нужна ли им перезагрузка). Он также скрывает от пользователей класса "Солдат" ненужные детали того, как именно стрелять из пушки.

Ответ 2

Во-первых, вы нарушите Закон Demeter, обратившись к Gun извне класса Soldier.

Вместо этого я бы рассмотрел такие методы:

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

Таким образом, вы могли бы также реализовать свой кулак, нож, гранату, бейсбольную биту, лазерную кошку и т.д.

Ответ 3

Закон Деметры сказал бы, чтобы инкапсулировать функции.

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

Таким образом, если вам нужен какой-то тип взаимодействия между солдатом и орудием, у вас есть место для вставки кода.

Изменить: найдена соответствующая статья из Википедии: http://www.ccs.neu.edu/research/demeter/demeter-method/LawOfDemeter/paper-boy/demeter.pdf Пример paperboy очень, очень похож на пример солдата, который вы публикуете.

Ответ 4

В действительности, это зависит от того, какой контроль вы хотите иметь.

Чтобы смоделировать реальный мир, вы даже можете полностью инкапсулировать объект оружия и просто иметь метод soldier.attack(). Тогда метод soldier.attack() будет видеть, носил ли солдат пистолет и какое состояние пистолета было, и стрелял или перезаряжал его по мере необходимости. Или, возможно, выбросить пистолет у цели и убежать, если для любой операции присутствуют недостаточные боеприпасы...

Ответ 5

Если вы раскрываете оружие, вы допускаете вещи за пределами функций-членов Gun, что, вероятно, не очень хорошая идея:

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

Если вы используете getGun(), вызовы выглядят немного уродливо, но вы можете добавлять функции в Gun без изменения Солдата.

Если вы инкапсулируете функции (которые я рекомендую), вы можете изменить Gun или ввести другие (производные) классы Gun без изменения интерфейса для Солдата.

Ответ 6

Обычно мое решение основано на характере класса контейнера (в данном случае Солдата). Либо это полностью POD, либо нет. Если это не POD, я делаю все члены данных конфиденциальными и предоставляю методы доступа. Класс является POD только в том случае, если он не имеет инвариантов (т.е. Внешний субъект не может сделать свое состояние непоследовательным, изменяя его члены). Класс вашего солдата больше похож на не-POD, поэтому я перейду к опции метода доступа. Если он вернет ссылку на константу или регулярная ссылка - ваше собственное решение, основанное на поведении fire() и других методов (если они изменяют состояние оружия или нет).

BTW, Bjarne Stroustrup немного рассказывает об этой проблеме на своем сайте: http://www.artima.com/intv/goldilocks3.html

Ответ: Я знаю, что не совсем то, что вы просили, но я бы посоветовал вам также рассмотреть многие упоминания, сделанные в других ответах на закон Деметры: разоблачить методы действий (которые действуют на оружие) вместо весь объект оружия через метод геттера. Поскольку у солдата "есть" пистолет (он у него в руке и он нажимает спусковой крючок), мне кажется более естественным, что другие актеры "просят" солдата стрелять. Я знаю, что это может быть утомительным, если у пушки есть много способов действовать, но, возможно, они также могут быть сгруппированы в более высокоуровневые действия, которые демонстрирует солдат.

Ответ 7

Нет золотого правила, которое применяется в 100% случаев. Это действительно вызов в зависимости от ваших потребностей.

Это зависит от того, сколько функций вы хотите скрыть/запретить для пушки от доступа к Solider.

Если вы хотите иметь только доступ только для чтения к Gun, вы можете вернуть ссылку const на свой собственный член.

Если вы хотите выявить только определенные функции, вы можете сделать функции-обертки. Если вы не хотите, чтобы пользователь попытался изменить настройки оружия с помощью Солдата, тогда создайте функции обертки.

В общем, хотя я вижу Gun как свой собственный объект, и если вы не возражаете разоблачить всю функциональность Gun и не возражаете, чтобы вещи могли быть изменены через объект Soldier, просто сделайте его общедоступным.

Вероятно, вы не хотите копировать пистолет, поэтому, если вы сделаете метод GetGun(), убедитесь, что вы не возвращаете копию оружия.

Если вы хотите, чтобы ваш код был простым, попросите солдата ответить за пистолет. Вам нужен другой код для работы с пистолетом? Или солдат может всегда знать, как работать/перезагружать свое оружие?

Ответ 8

Предоставьте "getGun()" или просто "gun()".

Представьте, что в один прекрасный день вам может понадобиться сделать этот метод более сложным:

Gun* getGun() {
  if (!out_of_bullets_) {
    return &gun_;
  } else {
    PullPieceFromAnkle();
    return &secret_gun_;
  }
}

Кроме того, вы можете захотеть предоставить конструктор состязания, чтобы люди могли использовать консольный пистолет для солдат-солдат:

const Gun &getGun() const { return gun_; }

Ответ 9

Инкапсулируйте функции для обеспечения согласованного пользовательского интерфейса, даже если вы позже измените логику. Соглашения об именах зависят от вас, но я обычно не использую "getFoo()", а просто "foo()" в качестве аксессоров и "setFoo()" в качестве сеттеров.

  • return reference-to-const, когда вы можете (Эффективный С++ Item # 3).
  • Предпочитают константы, перечисления и строки для использования жестко закодированных чисел (элемент № 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;
}