Лекция: Интерфейс и реализация
Мейер [19] и Снайдерс [20] высказали идею контрактного программирования:
большие задачи надо разделить на много маленьких и перепоручить их мелким субподрядчикам. Нигде эта идея не проявляет себя так ярко, как в проектировании классов.
По своей природе, класс — это генеральный контракт между абстракцией и всеми ее клиентами. Выразителем обязательств класса служит его интерфейс, причем в языках с сильной типизацией потенциальные нарушения контракта можно обнаружить уже на стадии компиляции.
Идея контрактного программирования приводит нас к разграничению внешнего облика, то есть интерфейса, и внутреннего устройства класса, реализации. Главное в интерфейсе — объявление операций, поддерживаемых экземплярами класса. К нему можно добавить объявления других классов, переменных, констант и исключительных ситуаций, уточняющих абстракцию, которую класс должен выражать. Напротив, реализация класса никому, кроме него самого, не интересна. По большей части реализация состоит в определении операций, объявленных в интерфейсе класса.
Мы можем разделить интерфейс класса на три части:
- открытую (public) — видимую всем клиентам;
- защищенную (protected) — видимую самому классу, его подклассам и друзьям (friends);
- закрытую (private) — видимую только самому классу и его друзьям.
Разные языки программирования предусматривают различные комбинации этих частей. Разработчик может задать права доступа к той или иной части класса, определив тем самым зону видимости клиента.
В частности, в C++ все три перечисленных уровня доступа определяются явно. В дополнение к этому есть еще и механизм друзей, с помощью которого посторонним классам можно предоставить привилегию видеть закрытую и защищенную области класса. Тем самым нарушается инкапсуляция, поэтому, как и в жизни, друзей надо выбирать осторожно. В Ada объявления могут быть сделаны закрытыми или открытыми. В Smalltalk все переменные — закрыты, а методы — открыты. В Object Pascal все поля и операции открыты, то есть никакой инкапсуляции нет [Речь идет о старых версиях Object Pascal — Прим. редактора-2]. В CLOS обобщенные функции открыты, а слоты могут быть закрытыми, хотя узнать их значения все равно можно.
Состояние объекта задается в его классе через определения констант или переменных, помещаемые в его защищенной или закрытой части. Тем самым они инкапсулированы, и их изменения не влияют на клиентов.
Внимательный читатель может спросить, почему же представление объекта определяется в интерфейсной части класса, а не в его реализации. Причины чисто практические: в противном случае понадобились бы объектно-ориентированные процессоры или очень хитроумные компиляторы. Когда компилятор обрабатывает объявление объекта, например, такое:
DisplayItem item1;
он должен знать, сколько отвести под него памяти. Если бы эта информация содержалась в реализации класса, нам пришлось бы написать ее полностью, прежде, чем мы смогли бы задействовать его клиентов. То есть, весь смысл отделения интерфейса от реализации был бы потерян.
Константы и переменные, составляющие представление класса, известны под разными именами. В Smalltalk их называют переменные экземпляра, в Object Pascal — поля, в C++ — члены класса, а в CLOS — слоты. Мы часто будем использовать эти термины как синонимы.