ГЛАВА 6 Классы
129
쐽
Вариант 3: использовать неявные экземпляры (с большой осторожностью). Вы должны создать новый сервис — скажем,
SetCurrentFont ( fontId ), — при вызо- ве которого заданный экземпляр шрифта делается текущим. После этого все остальные сервисы используют текущий шрифт, благодаря чему в них не нуж- но передавать параметр
fontId. При разработке простых приложений такой под- ход может облегчить использование нескольких экземпляров данных. В слож- ных приложениях подобная зависимость от состояния в масштабе всей сис- темы подразумевает, что вы должны следить за текущим экземпляром шрифта во всем коде, вызывающем методы
Font; разумеется, сложность программы при этом повышается. Каким бы ни был размер приложения, всегда можно найти более удачные альтернативы данному подходу.
Внутри АТД вы можете реализовать работу с несколькими экземплярами данных как угодно, но вне его при использовании языка, не являющегося объектно-ори- ентированным, возможны только три указанных варианта.
АТД и классы
Абстрактные типы данных лежат в основе концепции классов. В языках, поддержи- вающих классы, каждый АТД можно реализовать как отдельный класс. Однако обыч- но с классами связывают еще две концепции: наследование и полиморфизм. Може- те рассматривать класс как АТД, поддерживающий наследование и полиморфизм.
6.2. Качественные интерфейсы классов
Первый и, наверное, самый важный этап разработки высококачественного клас- са — создание адекватного интерфейса. Это подразумевает, что интерфейс дол- жен представлять хорошую абстракцию, скрывающую детали реализации класса.
Хорошая абстракция
Как я говорил в подразделе «Определите согласованные абстракции» раздела 5.3,
под абстракцией понимается представление сложной операции в упрощенной форме. Интерфейс класса — это абстракция реализации класса, скрытой за ин- терфейсом. Интерфейс класса должен предоставлять группу методов, четко согла- сующихся друг с другом.
Рассмотрим для примера класс «сотрудник». Он может содержать такие данные,
как фамилия сотрудника, адрес, номер телефона и т. д., и предлагать методы ини- циализации и использования этих данных. Вот как мог бы выглядеть такой класс:
Пример интерфейса, формирующего хорошую абстракцию (C++)
class Employee {
public:
// открытые конструкторы и деструкторы
Employee();
Employee(
FullName name,
String address,
String workPhone,
Перекрестная ссылка Примеры кода в этой книге отформати- рованы с использованием кон- венции, поддерживающей сход- ство стилей между нескольки- ми языками. Об этой конвенции
(и разных стилях кодирования)
см. подраздел «Программирова- ние с использованием несколь- ких языков» раздела 11.4.
130
ЧАСТЬ II Высококачественный код
String homePhone,
TaxId taxIdNumber,
JobClassification jobClass
);
virtual Employee();
// открытые методы
FullName GetName() const;
String GetAddress() const;
String GetWorkPhone() const;
String GetHomePhone() const;
TaxId GetTaxIdNumber() const;
JobClassification GetJobClassification() const;
private:
};
Внутри этот класс может иметь дополнительные методы и данные, поддержива-
ющие работу этих сервисов, но пользователям класса знать о них не нужно. Пред- ставляемая интерфейсом этого класса абстракция великолепна, потому что все методы интерфейса служат единой согласованной цели.
Интерфейс, представляющий плохую абстракцию, содержал бы набор разнород- ных методов, например:
Пример интерфейса, формирующегоплохую абстракцию (C++)class Program {
public:
// открытые методы void InitializeCommandStack();
void PushCommand( Command command );
Command PopCommand();
void ShutdownCommandStack();
void InitializeReportFormatting();
void FormatReport( Report report );
void PrintReport( Report report );
void InitializeGlobalData();
void ShutdownGlobalData();
private:
};
Похоже, этот класс содержит методы работы со стеком команд, форматирования отчетов, печати отчетов и инициализации глобальных данных. Трудно увидеть связь между стеком команд, обработкой отчетов и глобальными данными. Интерфейс такого класса не формирует согласованную абстракцию, и класс обладает плохой
ГЛАВА 6 Классы
131
связностью. В данном случае методы следует реорганизовать в более четкие классы,
интерфейсы которых будут представлять более удачные абстракции.
Если бы эти методы были частью класса
Program, для формирования согласован- ной абстракции их можно было бы изменить так:
Пример интерфейса, формирующего более удачную абстракцию (C++)class Program {
public:
// открытые методы void InitializeUserInterface();
void ShutDownUserInterface();
void InitializeReports();
void ShutDownReports();
private:
};
В ходе очистки интерфейса одни его методы были перемещены в более подходя- щие классы, а другие были преобразованы в закрытые методы, используемые методом
InitializeUserInterface() и другими методами.
Данный способ оценки абстракции класса основан на изучении открытых методов класса, т. е. его интерфейса. Однако из того, что класс в целом формирует хорошую абстракцию, вовсе не следует, что его отдельные методы также представляют удач- ные абстракции. Рекомендации по проектированию методов см. в разделе 7.2.
Чтобы ваши классы имели высококачественные абстрактные интерфейсы, соблю- дайте при их проектировании следующие принципы.
Выражайте в интерфейсе класса согласованный уровень абстракцииКлассы полезно рассматривать как механизмы реализации абстрактных типов дан- ных, описанных в разделе 6.1. В идеале каждый класс должен быть реализацией только одного АТД. Если класс реализует более
одного АТД или если вам не уда- ется определить, реализацией какого АТД класс является, самое время реоргани- зовать класс в один или несколько хорошо определенных АТД.
Так, следующий класс имеет несогласованный интерфейс, потому что формируе- мый им уровень абстракции непостоянен:
Пример интерфейса, включающего разныеуровни абстракции (C++)class EmployeeCensus: public ListContainer {
public:
// открытые методы
132
ЧАСТЬ II Высококачественный код
Абстракция, формируемая этими методами, относится к уровню «employee» (сотрудник).
void AddEmployee( Employee employee );
void RemoveEmployee( Employee employee );
Абстракция, формируемая этими методами, относится к уровню «list» (список).
Employee NextItemInList();
Employee FirstItem();
Employee LastItem();
private:
};
Этот класс представляет два АТД:
Employee и ListContainer (список-контейнер).
Подобные смешанные абстракции часто возникают, когда программист реализу- ет класс при помощи класса-контейнера или других библиотечных классов и не скрывает этот факт. Спросите себя, должна ли информация об использовании класса-контейнера быть частью абстракции. Обычно это является деталью реали- зации, которую следует скрыть от остальных частей программы, например так:
Пример интерфейса, формирующего согласованную абстракцию (C++)
class EmployeeCensus {
public:
// открытые методы
Абстракция, формируемая всеми этими методами, теперь относится к уровню «employee».
void AddEmployee( Employee employee );
void RemoveEmployee( Employee employee );
Employee NextEmployee();
Employee FirstEmployee();
Employee LastEmployee();
private:
Тот факт, что класс использует библиотеку ListContainer, теперь скрыт.
ListContainer m_EmployeeList;
};
Программисты могут утверждать, что наследование от
ListContainer удобно, потому что оно поддерживает полиморфизм, позволяя создать внешний метод поиска или сортировки, принимающий объект
ListContainer. Но этот аргумент не проходит главный тест на уместность наследования: «Используется ли наследование толь- ко для моделирования отношения „является“?» Наследование класса
EmployeeCensus
(каталог личных дел сотрудников) от класса
ListContainer означало бы, что Employee-
Census «является» ListContainer, что, очевидно, неверно. Если абстракция объекта
EmployeeCensus заключается в том, что он поддерживает поиск или сортировку,
>
>
>
>
ГЛАВА 6 Классы
133
эти возможности должны быть явными согласованными частями интерфейса класса.
Если представить открытые методы класса как люк, предотвращающий попадание воды в подводную лодку, несогласованные открытые методы — это щели. Вода не будет протекать через них так быстро, как
через открытый люк, но позже лодка все же потонет. На практике при смешении уровней абстракции именно это и проис- ходит. По мере изменений программы смешанные уровни абстракции делают ее все менее и менее понятной, пока в итоге код не станет совсем загадочным.
Убедитесь, что вы понимаете, реализацией какой абстракцииявляется класс Некоторые классы очень похожи, поэтому при разра- ботке класса нужно понимать, какую абстракцию должен представлять его интерфейс. Однажды я работал над программой, которая должна была под- держивать редактирование информации в табличном формате. Сначала мы хо- тели использовать простой элемент управления «grid» (сетка), но доступные эле- менты управления этого типа не позволяли закрашивать ячейки ввода данных в другой цвет, поэтому мы выбрали элемент управления «spreadsheet» (электронная таблица), который такую возможность поддерживал.
Элемент управления «электронная таблица» был гораздо сложнее «сетки» и пре- доставлял около 150 методов в сравнении с 15 методами «сетки». Так как наша цель заключалась в использовании «сетки», а не «электронной таблицы», мы поручили одному программисту написать класс-оболочку, который скрывал бы тот факт, что мы подменили один элемент управления другим. Он поворчал по поводу ненуж- ных затрат и бюрократии, ушел и вернулся через пару дней с классом-оболочкой,
который честно предоставлял все 150 методов «электронной таблицы».
Но нам было нужно не это — нам требовался интерфейс «сетки», инкапсулирую- щий тот факт, что за кулисами мы использовали гораздо более сложную «элект- ронную таблицу». Программисту следовало предоставить доступ только к 15 ме- тодам «сетки» и еще одному, шестнадцатому методу, поддерживающему закраши- вание ячеек. Открыв доступ ко всем 150 методам, программист подверг нас риску того, что после нескольких изменений реализации класса нам в итоге придется поддерживать все 150 открытых методов. Он не смог обеспечить нужную нам инкапсуляцию и проделал гораздо больше работы, чем стоило.
В зависимости от конкретных обстоятельств оптимальной абстракцией может оказаться как «сетка», так и «электронная таблица». Если приходится выбирать между двумя похожими абстракциями, убедитесь, что выбор правилен.
Предоставляйте методы вместе с противоположными им методамиБольшинство операций имеет соответствующие противоположные операции. Если одна из операций включает свет, вам, вероятно, понадобится и операция, его вы- ключающая. Если одна операция добавляет элемент в список, элементы скорее всего нужно будет и удалять. Если
одна операция активизирует элемент меню, вторая,
наверное, должна будет его деактивизировать. При проектировании класса про- верьте каждый открытый метод на предмет того, требуется ли вам его противо- положность. Создавать противоположные методы, не имея на то причин, не сле- дует, но проверить их целесообразность нужно.
134
ЧАСТЬ II Высококачественный код
Убирайте постороннюю информацию в другие классы Иногда вы будете обнаруживать, что одни методы класса работают с одной половиной данных, а другие — с другой. Это значит, что вы имеете дело с двумя классами, скрывающи- мися под маской одного. Разделите их!
По мере возможности делайте интерфейсы программными, а не семан-тическими Каждый интерфейс состоит из программной и семантической ча- стей. Первая включает типы данных и другие атрибуты интерфейса, которые могут быть проверены компилятором. Вторая складывается из предположений об ис- пользовании интерфейса, которые компилятор проверить не может. Семантический интерфейс может включать такие соображения, как «Метод А должен быть выз- ван перед Методом B» или «Метод А вызовет ошибку, если переданный в него Эле- мент Данных 1 не будет перед этим инициализирован». Семантический интерфейс следует документировать в комментариях, но вообще интерфейсы должны как можно меньше зависеть от документации. Любой аспект интерфейса, который не может быть проверен компилятором, является потенциальным источником оши- бок. Старайтесь преобразовывать семантические элементы интерфейса в программ- ные, используя утверждения (assertions) или иными способами.
Опасайтесь нарушения целостности интерфейса приизменении класса При модификации и расширении клас- са часто обнаруживается дополнительная нужная функци- ональность, которая не совсем хорошо
соответствует интер- фейсу первоначального класса, но плохо поддается реализации иным образом. Так,
класс
Employee может превратиться во что-нибудь вроде:
Пример интерфейса, изуродованного при сопровождениипрограммы (C++)class Employee {
public:
// открытые методы
FullName GetName() const;
Address GetAddress() const;
PhoneNumber GetWorkPhone() const;
bool IsJobClassificationValid( JobClassification jobClass );
bool IsZipCodeValid( Address address );
bool IsPhoneNumberValid( PhoneNumber phoneNumber );
SqlQuery GetQueryToCreateNewEmployee() const;
SqlQuery GetQueryToModifyEmployee() const;
SqlQuery GetQueryToRetrieveEmployee() const;
private:
};
То, что начиналось как ясная абстракция, превратилось в смесь почти несогласо- ванных методов. Между сотрудниками и методами, проверяющими корректность
Перекрестная ссылка О поддер- жании качества кода при его изменении см. главу 24.
ГЛАВА 6 Классы
135
почтового индекса, номера телефона или ставки зарплаты (job classification), нет логической связи. Методы, предоставляющие доступ к деталям SQL-запросов, от- носятся к гораздо более низкому уровню абстракции, чем класс
Employee, нару- шая общую абстракцию класса.
Не включайте в класс открытые члены, плохо согласующиеся с абстрак-цией интерфейса Добавляя новый метод в интерфейс класса, всегда спраши- вайте себя: «Согласуется ли этот метод с абстракцией, формируемой существую- щим интерфейсом?» Если нет, найдите другой способ внесения изменения, позво- ляющий сохранить согласованность абстракции.
Рассматривайте абстракцию и связность вместе Понятия абстракции и связности (cohesion) тесно связаны: интерфейс класса, представляющий хорошую абстракцию, обычно отличается высокой связностью. И наоборот: классы, имею- щие высокую связность, обычно представляют хорошие абстракции, хотя эта связь выражена слабее.
Я обнаружил, что при повышенном внимании к абстракции, формируемой ин- терфейсом класса, проект класса получается более удачным, чем при концентра- ции на связности класса. Если вы видите, что
класс имеет низкую связность и не знаете, как это исправить, спросите себя, представляет ли он согласованную аб- стракцию.
Хорошая инкапсуляцияКак я уже говорил в разделе 5.3, инкапсуляция является бо- лее строгой концепцией, чем абстракция. Абстракция по- могает управлять сложностью, предоставляя модели, позво- ляющие игнорировать детали реализации. Инкапсуляция не позволяет узнать детали реализации, даже если вы этого захотите.
Две этих концепции связаны: без инкапсуляции абстракция обычно разрушается.
По своему опыту могу сказать, что вы или имеете и абстракцию, и инкапсуляцию,
или не имеете ни того, ни другого. Промежуточных вариантов нет.
Минимизируйте доступность классов и их членов Ми- нимизация доступности — одно из нескольких правил, под- держивающих инкапсуляцию. Если вы не можете понять,
каким делать конкретный метод: открытым, закрытым или защищенным, — некоторые авторы советуют выбирать са- мый строгий уровень защиты, который работает (Meyers,
1998; Bloch, 2001). По-моему, это прекрасное правило, но мне кажется, что еще важнее спросить себя: «Какой вари- ант лучше всего сохраняет целостность абстракции интер- фейса?» Если предоставление доступа к методу согласуется с абстракцией, сделайте его открытым. Если вы не уверены, скрыть больше обычно предпочтительнее, чем скрыть меньше.
Не делайте данные-члены открытыми Предоставление доступа к данным- членам нарушает инкапсуляцию и ограничивает контроль над абстракцией. Как
Перекрестная ссылка Об инкап- суляции см. подраздел «Инкап- сулируйте детали реализации»
раздела 5.3.
Самым важным отличием хоро- шо спроектированного модуля от плохо спроектированного яв- ляется степень, в которой мо- дуль скрывает свои внутренние данные и другие детали реали- зации от других модулей.
Джошуа Блох (
Joshua Bloch)
136
ЧАСТЬ II Высококачественный код указывает Артур Риэль, класс
Point (точка), который предоставляет доступ к дан- ным:
float x;
float y;
float z;
нарушает инкапсуляцию, потому что клиентский код может свободно делать с данными
Point что угодно, при этом сам класс может даже не узнать об их изме- нении (Riel, 1996). В то же время класс
Point, включающий члены:
float GetX();
float GetY();
float GetZ();
void SetX( float x );
void SetY( float y );
void SetZ( float z );
поддерживает прекрасную инкапсуляцию. Вы не
имеете понятия о том, реализо- ваны ли данные как
float x,
y и
z, хранит ли класс
Point эти элементы как
double,
преобразуя их в
float, или же он хранит их на Луне и получает через спутник.
Не включайте в интерфейс класса закрытые детали реализации Истинная инкапсуляция не позволяла бы узнать детали реализации вообще. Они были бы скрыты и в прямом, и в переносном смыслах. Однако популярные языки — в том числе C++ — требуют, чтобы программисты раскрывали детали реализации в интерфейсе класса, например:
Пример обнародования деталей реализации класса (C++)class Employee {
public:
Employee(
FullName name,
String address,
String workPhone,
String homePhone,
TaxId taxIdNumber,
JobClassification jobClass
);
FullName GetName() const;
String GetAddress() const;
private:
Обнародованные детали реализации.
String m_Name;
String m_Address;
int m_jobClass;
};
>