У меня есть проблема дизайна, с которой я постоянно сталкиваюсь.
Для иллюстрации, давайте предположим, что у меня есть полиморфная иерархия классов
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, кроме двух описанных выше?
Есть ациклический посетитель шаблон, который устраняет необходимость знать о каждом подклассе. Опирается на dynamic_cast
, но может быть то, что вам нужно.
В самой классной печати нет ничего плохого. Это не нарушает SRP, потому что печать не является ответственностью.
Помните, что ответственность определяется как причина для изменения. Вы не меняете класс, потому что ваши требования к печати меняются. Класс должен отправлять только пары имя-значение объекту, ответственному за печать, называемому форматер. Эта процедура отправки пар имя-значение никогда не меняется сама по себе. Любые изменения в нем вызваны только другими изменениями, не связанными с печатью (когда вы, например, добавляете поле, вы также добавляете его представление в процедуру печати).
Модуль форматирования должен ничего не знать о классах, которые он печатает, а просто представлять пары имя-значение в соответствии с некоторым набором требований. Форматер изменяется при изменении требований к печати. Поэтому печать будет исключительной ответственностью форматера.
Вам нужно будет обратиться к какому-то посетителю решения двойной отправки, чтобы сделать это. Подход с двойной диспетчеризацией является немного более легким, так что примерно так:
В:
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 {}
};
В:
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;
}
Вы можете предоставить интерфейс для печати ответственности и сохранить общие обязанности в соответствии с иерархией вашего класса. Пример:
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 { ... };