C ++: Добавление методов в иерархию полиморфных классов без нарушения SRP?

У меня есть проблема с дизайном, с которой я постоянно сталкиваюсь.

Для иллюстрации предположим, что у меня есть иерархия полиморфных классов.

class A { public: virtual ~A() {} ... };
class B: public A { ... };
class C: public B { ... };
class D: public A { ... };
...

Я хочу иметь возможность печатать экземпляры этих классов полиморфным способом, т.е. каждый класс имеет свой собственный способ печати. Очевидный способ добиться этого - добавить

virtual void print(OutputStream &os) = 0;

в базовый класс и переопределите этот метод в каждом подклассе. Однако, если первоначальная ответственность классов не связана с печатью, это добавит к ним еще одну ответственность, тем самым нарушив SRP.

Мой вопрос: как правильно достичь желаемого поведения без нарушения SRP?

В этом сообщении решение, основанное на Предлагается шаблон оформления посетителей. Однако тогда мне нужно будет создать класс, который должен знать о каждом подклассе A. Я хотел бы иметь возможность добавлять и удалять подклассы без необходимости всегда изменять посетителя.

Есть ли другой способ сохранения SRP, кроме двух описанных выше?


person s3rvac    schedule 25.07.2013    source источник
comment
Что делает ваш метод print ()? Содержит ли его вывод какую-либо информацию о внутреннем устройстве класса? В этом случае я бы поместил его определение в класс, потому что единственный другой вариант - нарушить инкапсуляцию, а это еще менее гибко. Все другие известные мне решения связаны с реализацией динамической диспетчеризации аргументов функций в C ++, и это может быть некрасиво.   -  person Markus Mayr    schedule 25.07.2013
comment
@Markus Mayr: Предположим, что передаваемая информация может быть получена из открытого интерфейса каждого подкласса, но не обязательно с использованием только интерфейса базового класса. В противном случае, как вы уже упомянули, экапсуляцию придется прервать.   -  person s3rvac    schedule 25.07.2013


Ответы (4)


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

person Juraj Blaho    schedule 25.07.2013
comment
Спасибо, я не знала эту выкройку! Это может быть то, что мне нужно, хотя ответ n.m. тоже имеет смысл. Но ваш ответ применим и к другим случаям, где есть явное нарушение. - person s3rvac; 26.07.2013

Нет ничего плохого в самой печати классов. Это не нарушает SRP, потому что печать не несет ответственности.

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

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

person n. 1.8e9-where's-my-share m.    schedule 25.07.2013

Для этого вам понадобится какое-то решение для двойной отправки. Подход с двойной отправкой немного более легкий, так что как насчет чего-то вроде этого:

In A:

class Processor
{
public:
  virtual void Process(const A &a)const {}
  virtual void Process(const B &b)const {}
  virtual void Process(const C &c)const {}
  virtual void Process(const D &d)const {}
  virtual void Process(const E &e)const {}
};

In A:

class A
{
public:
  virtual void Process(const Processor &processor) 
  {
    processor.Process(*this);
  }
};

Затем в каждом производном классе переопределите Process с идентичным определением:

virtual void Process(const Processor &processor) 
{
  processor.Process(*this);
}

Это обеспечит вызов правильной перегрузки в Process.

Теперь создадим потоковый процессор:

class StreamProcessor : public Processor
{
private:
 OutputStream &m_OS;

public:
  StreamProcessor(OutputStream &os) : m_OS(os)
  {
  }

  virtual void Processor(const A &a)const
  {
   m_os << "got a A";
  }

  virtual void Processor(const B &b)const
  {
   m_os << "got a B";
  }

  virtual void Processor(const C &c)const
  {
   m_os << "got a C";
  }

  // etc
};

А потом:

 OutputStream &operator<<(OutputStream &os, A &a)
 {
   PrintProcessor(os);
   a.Process(PrintProcessor);
   return os;
 }
person Sean    schedule 25.07.2013
comment
Спасибо за ваш ответ. Однако, как я предложил в вопросе, я хотел бы иметь возможность добавлять и удалять подклассы без необходимости всегда изменять посетителя (Processor в вашем примере). - person s3rvac; 25.07.2013
comment
Достаточно справедливо, но вам придется изменять что-то при добавлении или удалении классов. В таких языках, как Java или C #, вы можете использовать relfection, но это не вариант для вас в C ++. - person Sean; 25.07.2013

Вы можете предоставить интерфейс для печати ответственности и сохранить общие обязанности в иерархии классов. Пример:

class Printer { public: virtual void print(OutputStream &os) = 0; }
class A { public: virtual ~A() {} ... };
class B: public A, public Printer { ... }; // this needs print function, use interface.
class C: public B { ... };
class D: public A { ... };
person Cengiz Kandemir    schedule 25.07.2013
comment
Спасибо за ответ, но я хочу иметь возможность распечатать любой подкласс A, а не только B и его подклассы. Это может быть решено путем наследования от Printer в A. Однако мне все еще интересно, нарушает ли этот подход SRP или нет. - person s3rvac; 25.07.2013
comment
Я не уверен, в чем проблема, если вы хотите иметь возможность печатать все классы A. Если печать необходима для подклассов A, то они должны быть у вас. Оценка того, необходима ли такая потребность для этих классов или не зависит от вас (невозможно для нас оценить с ограниченной информацией), но насколько я понимаю, вы хотите распечатать подклассы A, и реализация функции печати не является нарушением SRP, если это необходимо унаследованным классам . - person Cengiz Kandemir; 25.07.2013