Как писать корректные классы
По материалам “Object Oriented Software Construction”, Bertrand Meyer.
Как писать корректные классы
У нас почти всегда есть какие-то ожидания от класса, как у автора класса, так и у клиента. В частности, это касается того, как должны вести себя методы класса и какого состояния мы можем ожидать от его объектов. Это можно назвать некой “моделью” или спецификацией класса. Корректный класс — это тот, который ведёт себя в соответствии с этой спецификацией, которая должна быть зафиксирована автором класса.
Проблема в том что если спецификация не зафиксирована где-либо, то у каждого кто взаимодействует с классом она своя. Мы можем решить эту проблему с помощью контрактного программирования. Тогда мы, как авторы, фиксируем спецификацию нашего класса используя три новых понятия: предусловия, постусловия, инвариант класса.
Предусловия относятся к методам класса. У каждого метода свои предусловия. Предусловия это то, что должен гарантировать клиент класса, перед вызовом того или иного метода, иначе поведение метода можно считать неопределенным (обычно это выбрасывание исключения). Постусловия — это то, что класс гарантирует клиенту после завершения метода при условии, что предусловия были соблюдены.
Инвариант касается всего класса в целом. Он описывает состояние объекта, которое должно быть верным как до, так и после вызова любого публичного метода класса. При этом инвариант не обязан сохраняться внутри самого метода, но его восстановление обязательно между вызовами методов.
Снабдив каждый класс инвариантом, а все методы пред и пост условиями (и, конечно, гарантировав их выполнение) мы можем сказать что у нас корректные классы. В С# мы можем в некой степени реализовать контракты класса с помощью ассертов, хоть это и не является полноценной реализацией контрактов
Следует также обсудить обработку исключений. Исключения сигнализируют о том, что программа находится в особом состоянии, с которым она не знает как справится. Если метод сталкивается с исключением (чаще всего из-за нарушения контракта), он должен восстановить инвариант. Если метод не может гарантировать выполнение постусловий (а в случае исключения это почти всегда так), он должен пробросить исключение дальше по цепочке вызовов, чтобы клиент был уведомлён о проблеме и не ожидал нормального завершения метода.