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# можете да използвате reflection, но това не е опция за вас в 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