Должен ли получатель генерировать исключение, если его объект имеет недопустимое состояние?

55

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

Предположим, что у меня есть класс, который имеет String nameполе и String surnameполе.

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

public void String name;
public void String surname;

public String getCompleteName() {return name + " " + surname;}

public void displayCompleteNameOnInvoice() {
    String completeName = getCompleteName();
    //do something with it....
}

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

Я могу добавить код, вызывающий исключение, в getCompleteNameметод. Но таким образом я нарушаю «неявный» контракт с пользователем класса; в общем случае геттеры не должны генерировать исключения, если их значения не установлены. Хорошо, это не стандартный метод получения, поскольку он не возвращает ни одного поля, но с точки зрения пользователя различие может быть слишком тонким, чтобы об этом думать.

Или я могу выбросить исключение изнутри displayCompleteNameOnInvoice. Но чтобы сделать это, я должен проверить напрямую nameили surnameполя, и, делая это, я бы нарушил абстракцию, представленную getCompleteName. Именно этот метод отвечает за проверку и создание полного имени. На основании других данных он может даже решить, что в некоторых случаях этого достаточно surname.

Таким образом, единственная возможность, по-видимому, состоит в том, чтобы изменить семантику метода getCompleteNameна то composeCompleteName, что предполагает более «активное» поведение и, соответственно, возможность генерировать исключение.

Это лучшее дизайнерское решение? Я всегда ищу лучший баланс между простотой и правильностью. Есть ли ссылка на дизайн для этой проблемы?

AgostinoX
источник
8
«в общем случае геттеры не должны генерировать исключения, если их значения не установлены». - Что они должны делать тогда?
Rotem
2
@scriptin Тогда, следуя этой логике, displayCompleteNameOnInvoiceможете просто вызвать исключение, если getCompleteNameвернетесь null, не так ли?
Rotem
2
@Rotem это может. Вопрос в том, должен ли он.
scriptin
22
По этому вопросу программисты Falsehoods считают, что имена очень хорошо читают, почему «имя» и «фамилия» являются очень узкими понятиями.
Ларс Виклунд
9
Если ваш объект может иметь недопустимое состояние, вы уже облажались.
user253751

Ответы:

151

Не позволяйте вашему классу быть построенным без назначения имени.

DeadMG
источник
9
Это интересное, но довольно радикальное решение. Много раз ваши классы должны быть более подвижными, что позволяет создавать состояния, которые становятся проблемой только в том случае, если и когда вызываются определенные методы, в противном случае вы получите множество маленьких классов, которые требуют слишком большого количества объяснений для использования.
AgostinoX
71
Это не комментарий. Если он применит совет в ответе, у него больше не будет описана проблема. Это ответ.
DeadMG
14
@AgostinoX: Вы должны делать свои классы текучими только в случае крайней необходимости. В противном случае они должны быть как можно более инвариантными.
DeadMG
25
@gnat: вы проделали большую работу, ставя под сомнение качество каждого вопроса и каждого ответа на PSE, но иногда вы ИМХО немного чуток.
Док Браун
9
Если они могут быть действительно необязательными, у него нет причин бросать в первую очередь.
DeadMG
75

Ответ, предоставленный DeadMG, в значительной степени пригвоздит его, но позвольте мне сформулировать его немного иначе: избегайте объектов с недопустимым состоянием. Объект должен быть «целым» в контексте задачи, которую он выполняет. Или не должно существовать.

Поэтому, если вы хотите плавности, используйте что-то вроде Pattern Builder . Или что-нибудь еще, что является отдельным воплощением объекта в конструкции, в отличие от «реальной вещи», где последний гарантированно имеет действительное состояние и является тем, который фактически выставляет операции, определенные в этом состоянии (например getCompleteName).

back2dos
источник
7
So if you want fluidity, use something like the builder pattern- текучесть или неизменность (если только это не то, что вы имеете в виду). Если кто-то хочет создать неизменяемые объекты, но ему нужно отложить установку некоторых значений, то шаблон, который следует выполнить, состоит в том, чтобы установить их один за другим на объекте-построителе и создать неизменяемый объект только после того, как мы собрали все значения, необходимые для правильного состояние ...
Конрад Моравский
1
@KonradMorawski: я в замешательстве. Вы разъясняете или противоречите моему утверждению? ;)
back2dos
3
Я пытался дополнить это ...
Конрад Моравский
40

Не иметь объект с недопустимым состоянием.

Цель конструктора или компоновщика состоит в том, чтобы установить состояние объекта в нечто согласованное и пригодное для использования. Без этой гарантии возникают проблемы с неправильно инициализированным состоянием объектов. Эта проблема усложняется, если вы имеете дело с параллелизмом, и что-то может получить доступ к объекту, прежде чем вы полностью закончите его настройку.

Итак, вопрос для этой части: «Почему вы допускаете, чтобы имя или фамилия были нулевыми?» Если это не то, что допустимо в классе для правильной работы, не позволяйте этому. Иметь конструктор или конструктор, который правильно проверяет создание объекта и, если это не так, поднимает проблему в этой точке. Вы также можете использовать одну из существующих @NotNullаннотаций , чтобы помочь понять, что «это не может быть нулем», и применять ее при кодировании и анализе.

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

Получатели, которые делают больше.

На эту тему есть немного. У вас есть Как много логики в Геттерз и что должно быть разрешено в добытчиками и сеттеров? отсюда и геттеры и сеттеры, выполняющие дополнительную логику над переполнением стека. Это проблема, которая возникает снова и снова в дизайне класса.

Суть этого исходит от:

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

И ты прав. Они не должны. Они должны выглядеть «глупыми» по отношению к остальному миру и делать то, что ожидается. Использование слишком большого количества логики приводит к проблемам, когда нарушается закон наименьшего удивления. И давайте посмотрим правде в глаза, вы действительно не хотите обернуть геттеры, try catchпотому что мы знаем, как мы любим делать это в целом.

Существуют также ситуации, когда вы должны использовать getFooметод, такой как инфраструктура JavaBeans, и когда у вас есть что-то из EL, вызывающего, ожидающего получить bean-компонент (так, чтобы <% bar.foo %>фактически вызывать getFoo()в классе - оставляя в стороне 'bean-компонент, выполняющий композицию или должен что оставить на усмотрение? », потому что можно легко придумать ситуации, когда один или другой может быть правильным ответом)

Поймите также, что для заданного получателя возможно быть указанным в интерфейсе или быть частью ранее открытого публичного API для класса, который подвергается рефакторингу (в предыдущей версии было только что возвращено «completeName», и рефакторинг прервался это в два поля).

В конце дня...

Делайте то, о чем легче рассуждать. Вы потратите больше времени на поддержку этого кода, чем на его разработку (хотя чем меньше времени вы тратите на разработку, тем больше времени вы тратите на его обслуживание). Идеальный выбор - спроектировать его таким образом, чтобы он не занимал столько времени на обслуживание, но не сидите и не думайте об этом в течение нескольких дней - если только это действительно не будет дизайнерским выбором, который будет иметь последствия спустя несколько дней. ,

Попытка придерживаться смысловой чистоты получающего существа private T foo; T getFoo() { return foo; }в какой-то момент вызовет у вас проблемы. Иногда код просто не вписывается в эту модель, и те сложности, через которые приходится пытаться сохранить его таким образом, просто не имеют смысла ... и в конечном итоге усложняют его проектирование.

Иногда принимайте, что идеалы дизайна не могут быть реализованы так, как вы хотите их в коде. Руководящие принципы - это руководящие принципы, а не кандалы.

Сообщество
источник
2
Очевидно, мой пример был просто предлогом для улучшения стиля кодирования, рассуждая об этом, а не о блокировке. Но я весьма озадачен тем значением, которое вы и другие, по-видимому, уделяете конструкторам. Теоретически они сдерживают свое обещание. На практике они становятся громоздкими, чтобы поддерживать их с наследованием, которые нельзя использовать, когда вы извлекаете интерфейс из класса, не принятый javabeans и другими средами, такими как spring, если я не ошибаюсь. Под эгидой «классовой» идеи живет много разных категорий объектов. У объекта-значения вполне может быть проверяющий конструктор, но это всего лишь один тип.
AgostinoX
2
Умение рассуждать о коде является ключом к способности писать код с использованием API или к тому, чтобы поддерживать код. Если у класса есть конструктор, который позволяет ему находиться в недопустимом состоянии, ему становится намного сложнее продумать «хорошо, что это делает?» потому что теперь вы должны учитывать больше ситуаций, чем разработчик рассматриваемого или намеченного класса. Эта дополнительная концептуальная нагрузка сопровождается большим количеством ошибок и более длительным временем написания кода. С другой стороны, если вы можете посмотреть на код и сказать, что «имя и фамилия никогда не будут нулевыми», с их помощью будет гораздо проще писать код.
2
Неполный не обязательно означает недействительным. Счет-фактура без квитанции может быть обработан, и открытый API, который он представляет, будет отражать, что это допустимое состояние. Если surnameили nameбыть нулевым является действительным состоянием для объекта, а затем обработать его соответствующим образом . Но если это недопустимое состояние, вызывающее методы в классе для генерирования исключений, следует предотвратить его нахождение в этом состоянии с момента инициализации. Вы можете обойти незавершенные объекты, но обойти недопустимые объекты проблематично. Если нулевая фамилия является исключительной, попытайтесь предотвратить ее раньше, чем позже.
2
Связанное с этим сообщение в блоге: Проектирование инициализации объекта и обратите внимание на четыре подхода для инициализации переменной экземпляра. Один из них - разрешить ему быть в недопустимом состоянии, хотя обратите внимание, что он вызывает исключение. Если вы пытаетесь избежать этого, этот подход недопустим, и вам нужно будет работать с другими подходами, которые всегда включают обеспечение действительного объекта. Из блога: «Объекты, которые могут иметь недопустимые состояния, сложнее использовать и понять сложнее, чем те, которые всегда действительны». и это ключ.
5
«Делай то, о чем легче рассуждать». Это
Бен
5

Я не Java-парень, но это похоже на оба ограничения, которые вы представили.

getCompleteNameне выдает исключение, если имена не инициализированы, и displayCompleteNameOnInvoiceделает.

public String getCompleteName()
{
    if (name == null || surname == null)
    {
        return null;
    }
    return name + " " + surname;
}

public void displayCompleteNameOnInvoice() 
{
    String completeName = getCompleteName();
    if (completeName == null)
    {
        //throw an exception.
    }
    //do something with it....
}
Rotem
источник
Чтобы понять, почему это правильное решение: вызывающему getCompleteName()не нужно заботиться о том, действительно ли свойство хранится или вычисляется на лету.
Барт ван Инген Шенау
18
Чтобы понять, почему это решение просто сумасшедшее, вам нужно проверять на ничтожность при каждом отдельном обращении getCompleteName, что отвратительно.
DeadMG
Нет, @DeadMG, вы должны проверять его только в тех случаях, когда должно быть сгенерировано исключение, если getCompleteName возвращает ноль. Как это и должно быть. Это решение правильное.
Дауд говорит восстановить Монику
1
@DeadMG Да, но мы не знаем, что ДРУГОЕ использует там getCompleteName()в остальной части класса, или даже в других частях программы. В некоторых случаях возвращение nullможет быть именно тем, что требуется. Но в то время как есть ОДИН метод, спецификация которого «бросить исключение в этом случае», это именно то, что мы должны кодировать.
Дауд говорит восстановить Монику
4
Могут быть ситуации, когда это лучшее решение, но я не могу себе этого представить. В большинстве случаев это кажется слишком сложным: один метод выбрасывает неполные данные, другой возвращает ноль. Я боюсь, что это может быть неправильно использовано звонящими.
слеске
4

Кажется, никто не отвечает на ваш вопрос.
В отличие от того, во что люди любят верить, «избегать недействительных объектов» не часто является практическим решением.

Ответ:

Да, это должно.

Простой пример: FileStream.Lengthв C # /. NET , который выдает, NotSupportedExceptionесли файл не доступен для поиска.

Mehrdad
источник
Комментарии не для расширенного обсуждения; этот разговор был перенесен в чат .
maple_shaft
1

У вас все проверяющее имя с ног на голову.

getCompleteName()не должен знать, какие функции будут его использовать. Таким образом, отсутствие nameзвонка displayCompleteNameOnInvoice()не является getCompleteName()ответственностью.

Но, nameэто явная предпосылка для displayCompleteNameOnInvoice(). Таким образом, он должен взять на себя ответственность за проверку состояния (или он должен делегировать ответственность за проверку другому методу, если вы предпочитаете).

sampathsris
источник