Принцип единой ответственности vs принцип открытого закрытия

Я пишу программу, чтобы показать пользователю набор вопросов, собрать его ответы и распечатать их.

У меня есть разные типы вопросов в зависимости от того, какой ответ они требуют: целочисленные, логические или текстовые.

Я начал писать этот код:

abstract class Question
{
    string text;
}

class IntegerQuestion : Question
{
    int response;
}

class TextQuestion : Question
{
    string response;
}

class BooleanQuestion : Question
{
    bool response;
}

Что ж, теперь нам нужно распечатать вопросы и ответы.

Моим первым подходом было определение новой абстрактной функции Print в классе Question, чтобы подклассы определяли метод Print, а затем класс Printer:

abstract class Question
{
    string text;
    abstract string Print();
}

class Printer
{
    string PrintQuestions(List<Question> questions)
    {
        string result = "";

        foreach(var question in Questions)
            result += question.Print() + "\r\n";

        return result;
    }
}

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

class Printer
{
    string PrintQuestions(List<Question> questions)
    {
        string result = "";

        foreach(var question in Questions)
        {
            if(question is IntegerQuestion)
            {
                var integerQuestion = (IntegerQuestion)question;
                result += integerQuestion.text + integerQuestion.response;
            }
            if(question is TextQuestion)
            {
                ...
            }
            ...
        }

        return result;
    }
}

Очевидно, что второй подход не соответствует классу OCP for Printer, а первый использует его.

Но как насчет SRP?

Если тогда мне нужно написать вопросы и ответы в HTML:

abstract class Question
{
    string text;
    abstract string Print();
    abstract string PrintHTML();
}

class HTMLPrinter { ... }

¿Разве подклассы вопросов не нарушают SRP, потому что они умеют печатать их в виде обычного текста и HTML?


person Lamelas84    schedule 07.06.2018    source источник


Ответы (1)


Подклассы не подвергаются сомнению, нарушая SRP, потому что они знают, как распечатать их в виде обычного текста и HTML.

Вы совершенно правы.

Во-первых, о вашем соглашении об именах и вашем дизайне, если я понимаю вашу демонстрацию, почему ответы расширяются Question? Наследование - это отношение между объектами.

Должны ли мы сказать, что ответ - это вопрос? Похоже, в вашем бизнесе есть две разные концепции:

  • Вопрос, в котором содержится вопрос
  • Ответ, содержащий ответ пользователя на вопрос

Я, наверное, сделаю что-то вроде: (извините за синтаксис, это какой-то псевдокод)

interface IAnswer{
    string toString();
}
class IntegerAnswer implements IAnswer{
    int answer;
    string toString(){
        return (string)this.answer;
    }
}
....
class Question{
    string text;
    IAnswer answer; //or List<IAnswer> answers if you can old more than one answer by Question
    string toString(){
        return this.text;
    }
}

Затем вы можете определить принтер:

interface IQuestionPrinter{
    string print(List<Question> questions);
}
class Printer implements IQuestionPrinter{
     string print(List<Question> questions){
          string res = '';
          foreach(question in questions){
              res+=question.toString() + " : " + question.answer.toString();
          }
          return res;
     }
}
class HTMLPrinter implements IQuestionPrinter{
    string print(List<Question> questions){
          string res = "<ul>";
          foreach(question in questions){
              res+="<li>";
              res+= "<span>" + question.toString() + "</span>";
              res+="<span>" + question.answer.toString()+"</span>;
              res+="</li>";
          }
          return res+"</ul>";
     }
}

Или что-то подобное.

Тогда все ваши вопросы и ответы знают о том, что они должны расширять метод toString (), и мы делегируем работу по печати специальному IQuestionPrinter.

Создание интерфейса ответа - это хорошо, поскольку принтеру не нужно знать, является ли ответ целым, логическим, строковым или чем-то еще. И если у вас есть другие «типы» вопросов, вы должны определить интерфейс IQuestion:

interface IQuestion{
    IAnswer answer; // or List<IAnswer> answers
    string toString();
}

И тогда IQuestionPrinter должен принять это во внимание:

interface IQuestionPrinter{
    string print(List<IQuestion> questions);
}
person Ariart    schedule 07.06.2018
comment
Мне это нравится. Я не думал о разделении ответа и ответа на два отдельных класса. Спасибо! - person Lamelas84; 07.06.2018