Штерн В. - Основы C++. Методы программной инженерии - 2003
.pdf320 |
|
|
|
Часть II • Объектно-ориентированное програ1^1Уирование но C-^-f |
|||||||||
Листинг 8.7. |
Пример использования функций доступа для изолирования клиента |
||||||||||||
|
|
|
|
|
от имен полей данных |
|
|
|
|||||
#inclucle |
<iostream> |
|
|
|
|
|
/ / |
инкапсуляция в серверных функциях |
|||||
using |
namespace std; |
|
|
|
|
|
|
||||||
struct |
Cylinder |
{ |
|
|
|
|
|
/ / |
структура данных для доступа |
||||
|
double |
radius, |
height; |
} |
; |
|
|
|
|||||
void enterData(Cylinder &c, |
char number[]) |
|
|
|
|||||||||
{ |
cout |
« |
"Введите радиус и высоту "; |
|
|
|
|||||||
|
cout |
« |
number « |
" |
цилиндра: "; |
|
// инициализация цилиндра |
||||||
|
cin |
» |
с. radius » |
с.height; |
} |
|
|||||||
void validateCylinder(Cylinder |
c) |
|
// значения поумолчанию для данных |
||||||||||
{ |
i f |
(c.radius |
< 0) |
с |
radius = 10; |
|
|||||||
|
i f |
(c. height |
< 0) |
с height = 20; } |
|
|
|
||||||
double getVolume(const |
Cylinder &с) |
|
// вычислить объемы |
||||||||||
{ |
return |
с.height * |
с. radius |
* |
с. radius 3.141593; |
|
|
|
|||||
void scaleCylinder(Cylinder &c, double factor) |
|
// масштабирование размеров |
|||||||||||
{ с radius *=factor; c.height |
*=factor; } |
|
|||||||||||
void printCylinder(const Cylinder &c) |
|
// печать состояния объекта |
|||||||||||
{ cout « |
"радиус: |
« |
с. radius « " высота: " « |
с. height « endl;} |
|||||||||
int mainO |
|
|
|
|
|
|
|
|
|
||||
|
Cylinder с1, |
с2; |
|
|
|
|
|
// данные программы |
|||||
|
enterData(c1, |
"первого"); |
|
|
|
// инициализация первого цилиндра |
|||||||
|
validateCylinder(c1); |
|
|
|
// по умолчанию на случай порчи данных |
||||||||
|
enterData(c2, |
"второго"); |
|
|
|
// инициализация второго цилиндра |
|||||||
|
validateCylinder(c2); |
|
|
|
// по умолчанию на случай порчи данных |
||||||||
|
if (getVolume(cl) < getVolume(c2)) |
|
// сравнить объемы |
||||||||||
|
{ scaleCylinder(c1,1.2); |
|
|
// масштабировать |
|||||||||
|
|
cout « "\пИзмененный размер первого цилиндра\п"; |
// вывод нового размера |
||||||||||
|
printCylinder(cl); } |
|
|
|
|
|
|||||||
|
else |
|
|
"\пРа'змер первого цилиндра не изменен" |
« |
endl; |
|
||||||
|
cout « |
|
|||||||||||
|
return 0; |
|
|
|
|
|
|
|
|
|
|||
Как можно видеть, данный метод программирования действительно дает более понятный исходный код. В то же время в системах реального времени дополни тельные вызовы функций могут повлиять на производительность. Применение встраиваемых функций устранит данную проблему.
Преимущество такого подхода в том, что образуются две разные области: одна относится к проектированию определяемого программистом типа Cylinder и его доступа к функциям, а другая — к клиентскому коду, который использует объекты Cylinder и вызывает функции доступа Cylinder. При традиционном программиро вании (как в листинге 8.6) таких разделенных областей нет. Если имена полей определенной программистом структуры типа Cylinder изменяются, то придется проверять весь код, т. к. эти имена могут использоваться в любом месте програм мы. В новом варианте (как в листинге 8.7) изменение *в именах полей Cylinder повлияет только на функции доступа — хорошо определенный набор функций.
.Остальная часть программы (а она может быть очень большой) не затрагивается.
Глава 8 • Программирование с использованием функций |
с321 |
|||
|
|
main() |
|
|
enterDataO |
vaidateCylinderO |
firstlsSmaller() |
scaleCylinderO |
printCylinder() |
Рис. 8.7. Структурная диаграмма для программы из лист^инга 8.7
Рис. 8.7 иллюстрирует эту взаимосвязь клиентской и серверной части в виде струк турной диаграммы. Клиент main() вызывает серверные функции, обращающиеся к полям объектов Cylinder. Эти серверные функции инкапсулируют функциюклиент от деталей структуры Cylinder.
Инкапсуляция данных — относительно новая концепция, и ее не всегда хорошо понимают. Многие программисты считают, что инкапсуляция данных относится к защите функций от ошибочных и несанкционированных изменений. Без такой инкапсуляции клиентская программа, обращаясь к полям прямо по имени, может произвольно и незаметно изменять данные. При инкапсуляции данных клиент вызывает функции доступа, например scaleCylinerO, и эти функции изменяют данные.
Такой вопрос защиты данных аналогичен проблеме использования глобальных переменных. Если глобальные имена доступны во всей программе, то кто-то может некорректно присвоить им значения, что повлияет на другие части программы. Если имена полей данных доступны во всей программе, то может произойти нечто похожее. Передача параметров защищает глобальные переменные, инкапсуляция защищает поля данных.
Эти идеи насчет защиты данных передаются среди программистов из поколе ния в поколение. Звучит просто и разумно. Легче принять их, чем идти против общего мнения. Здесь нужно возразить. Хотя защита данных действительно играет здесь некую роль, но весьма небольшую. Инкапсуляция данных — прежде всего удобство чтения исходного кода и независимость компонентов программы. Что
исоставляет основную тему главы.
Вдействительности передача параметров не защищает переменные. Если кто-то ошибочно думает, что переменной нужно присвоить новое значение, это можно сделать с помощью прямого присваивания (если переменная глобальная) или присваивания значения параметру (если она передается как ссылка или параметруказатель). Аналогично, если кто-то ошибочно полагает, что полю с1. radius сле дует присвоить новое значение, то это также можно сделать с помощью прямого присваивания (если не используется инкапсуляция) или вызвать функцию доступа, например, setCylinder(), когда инкапсуляция применяется. Разницы нет.
Объяснение этого состоит в принципе разделения обязанностей, сформулиро ванном в начале главы. В процессе сопровождения это разделение работ между клиентом и функциями доступа имеет важное значение как для глобальных пере менных, так и для полей данных. Если нужно изменить имя глобальной перемен ной, придется искать ее во всех программных файлах, где она может встречаться, ведь любой файл может обратиться к ней или изменить ее. Четко очерченной сферы полномочий здесь нет — внимание программиста рассеивается по всей программе. Это требует больших трудозатрат и способствует ошибкам.
Аналогично изменение имени или типа поля данных в программе, где данные не инкапсулированы, вынуждает искать все файлы программы, где может встре чаться такое поле, поскольку в каждом файле поле может использоваться или модифицироваться. В подобной ситуации также нет ограниченной, небольшой сферы полномочий и нужно заниматься всей программой.
322 I Чость I! # Объектно-ориентировонное г1рогра1^1ллирование на С^--^
Обратите внимание, о чрезмерном объеме работы при внесении изменений в исходный код речи не идет. В конце концов, сколько времени уходит на его напи сание и изменение? В проекте разработки это самая простая и короткая часть. Дело в том, что должны быть четко помеченные части программы, где можно проверить изменения при модификации конструкции цилиндра или любых других переменах в структуре данных. Ведь нужно найти все подлежащие изменению места и убедиться, что не внесено никаких побочных эффектов. Вот почему сопро вождение программы столь подвержено ошибкам и обходится столь дорого.
В случае инкапсуляции при изменении имени или типа поля данных изменять приходится лишь набор функций доступа. На другие части программы это не влия ет. Перекомпилировать также потребуется только те части программы, которые обраш^аются к данным функциям, но их исходный код при этом не изменяется. Следовательно, сопровождаюш,ий приложение программист должен сосредоточить свое внимание на относительно узкой области, ограниченной кодом, имеюидим дело с именами полей данных. Вот в чем истинное преимущество инкапсуляции. Если имена полей данных не используются непосредственно, то в клиенте можно обойти зависимость обработки от архитектуры данных.
Очень важно научиться продумывать архитектуру программы с точки зрения инкапсуляции данных. В этом случае создается две разные сферы полномочий: сегменты кода, использующие имена полей данных и не использующие их.
Само по себе применение функций доступа не всегда улучшает читабельность программы и независимость ее фрагментов. Вот почему нужно учитывать еще один критерий, позволяющий судить о качестве программного кода: сокрытие ин формации.
Сокрытие информации
Принцип сокрытия информации также касается разделения полномочий. Обыч но, если намеренное сокрытие информации (о деталях реализации) от пользователя не применяется, программисту, разрабатывающему программу (или сопровожда ющему ее), приходится помнить одновременно о двух разных областях: архитектуре данных (например, типе Cylinder) и операциях с данными на уровне приложения (присваивании значений полям, сравнении объемов, масштабировании размеров и т.д.).
При сокрытии информации обязанности разделяются. Программист, который пишет (или сопровождает) клиентский код, занимается только операциями данных на уровне приложения, а не на уровне архитектуры данных. Программист, отвеча ющий за функции доступа к данным (или сопровождающий их), занимается лишь архитектурой данных, а не операциями с ними на уровне приложения.
Да, все это звучит похоже на принцип инкапсуляции данных. Нужно признать, что большинство определений сокрытия информации отличается туманностью и неопределенностью. Они не поясняют, как отличить сокрытие информации от инкапсуляции, как распознать недостаточное сокрытие информации и как реали зовать такое сокрытие.
Принцип инкапсуляции более узок: он предполагает инкапсуляцию имен и ти пов полей данных от клиентского кода, чтобы клиент явно не упоминал имен полей данных. В нашем примере будем предполагать, что клиент не должен упоминать с1. radius, с1. height и т. д. так явно, как в приведенном выше фрагменте. Инкап суляция через применение функций доступа улучшает качество программного кода, его читабельность и независимость компонентов программы.
Чем сокрытие информации отличается от инкапсуляции? Перед ответом на данный вопрос давайте рассмотрим не очень эффективный пример инкапсуляции. Попробуем реализовать инкапсуляцию, введя функции-серверы, выполняющие операции с объектом Cylinder, например возвращающие значения полей Cylinder
Глава 8 • Программирование с использованием функций |
323 |
или вычисляющие объем Cylinder. Эти функции-серверы также называются функ циями доступа, так как они обращаются к данным цилиндра от имени клиента. Под "обращением" здесь понимаются вовсе не разные типы доступа, они не разли чаются. Просто эти функции могут либо считывать поля данных, либо модифици ровать их.
void setRadius(Cylinder |
&с, |
double |
г) |
/ / |
функция-модификатор |
||
{ с.radius = г; |
} |
|
|
|
|
|
|
void setHeight(Cylinder |
&с, |
double |
h) |
/ / |
функция-модификатор |
||
{ с.height = h; |
} |
|
|
|
|
|
|
double getRadius(const |
Cylinder& c) |
/ / |
функция-селектор |
||||
{ return c.radius; |
} |
|
|
|
|
|
|
double getHeight |
(const |
Cylinder& |
c) |
/ / |
функция-селектор |
||
{ return с height; |
} |
|
|
|
|
|
|
Функция mainO не обязана использовать имена компонентов цилиндра. Если имена изменяются, то изменять придется функции setRadiusO, setHeightO, getRadiusO и getHeightO, а не main() или других клиентов Cylinder. Пример использования этих функций доступа показан в листинге 8.8. Результат данной программы будет тем же, что и у программы из листинга 8.6. Функциональность ее осталась той же.
Листинг 8.8. Пример неэффективной инкапсуляции
«include |
<iostream> |
|
|
|
|
// неуклюжая инкапсуляция |
|||||
using |
namespace std; |
|
|
|
|
|
|
||||
struct |
Cylinder |
{ |
|
|
|
|
|
// структура данных для доступа |
|||
|
double |
radius, |
height; } |
; |
|
|
|
|
|||
void setRadius(Cylinder |
&c, |
double |
r) |
// модификатор функции |
|||||||
{ |
c.radius = r; |
} |
|
|
|
|
|
|
|
||
void setHeight(Cylinder |
&c, |
double |
h) |
// модификатор функции |
|||||||
{ |
c.height = h; |
} |
|
|
|
|
|
|
|
||
double getRadius(const |
Cylinders |
c) |
|
// селектор |
функции |
||||||
{ |
return |
c.radius; |
} |
|
|
|
|
|
|
||
double getHeight |
(const |
Cylinders |
c) |
// селектор |
функции |
||||||
{ |
return |
c.height; |
} |
|
|
|
|
|
|
||
int mainO |
|
|
|
|
|
|
|
|
|||
{ |
Cylinder c1, c2; double radius, height; |
// данные программы |
|||||||||
|
|||||||||||
|
cout « |
"Введите радиус и высоту первого цилиндра: |
|
||||||||
|
cin » |
radius » |
height; |
|
|
|
// инициализация данных |
||||
|
setRadius(c1,radius); setHeight(c1,height); |
|
|||||||||
|
if (getRadius(c1)<0) setRadius(c1,10); |
// проверка данных |
|||||||||
|
if (getHeight(c1)<0) setHeight(c1,20); |
цилиндра: |
|
||||||||
|
cout « |
"Введите радиус и высоту второго |
|
||||||||
|
cin » |
radius » |
height; |
|
|
|
// инициализация данных |
||||
|
setRadius(c2,radius); setHeight(c2,heihgt); |
|
|||||||||
|
if (getRadius(c2)<0) setRadius(c2,10); |
// проверка данных |
|||||||||
|
if (getHeight |
(c2)<0) setHeight(c2,20); |
|
|
|||||||
Глава 8 « Программирование с использованием функций |
325 |
это не повлияет. Если к Cylinder добавляются поля данных, на клиенте такое изменение не отразится. (На самом деле, это не совсем так, поскольку операции ввода также нужно инкапсулировать.)
Информация, передаваемая от разработчика клиентской части, ограничивается именами и интерфейсами серверных функций. Зоны ответственности разработчи ков клиентской и серверной части разделены: одна охватывает связанные с прило жением операции высокого уровня, а другая ограничивается именами полей данных и вычислениями нижнего уровня.
Даже в этом небольшом примере видны преимущества применения функций доступа. Код клиента выражается в терминах осмысленных операций уровня при ложения. Что означает с1. heihgt*c1. raclius*c1. raclius*3.141593 в листинге 8.6? Программисту, сопровождающему приложение, придется это выяснить. То же са мое относится к операторам cl/radius* = 1.2; и с1. height* = 1.2;. Изменяются ли все размеры цилиндра? Применяется ли ко всем размерам один и тот же коэф фициент? Отображают ли операторы вывода все размеры цилиндра или только некоторые? Когда доступ к данным комбинируется с операциями приложения, смысл обработки уяснить труднее.
Применение функций доступа упрощает проверку вводимых пользователем данных — функция main() не перегружается деталями таких операций. Если из меняется представление данных (конструкция цилиндра или просто имена полей), то придется изменять серверные функции. Как уже упоминалось выше, это проб лема не только трудозатрат. Вопрос в том, насколько большой области придется уделять внимание. Без функции доступа потенциальной областью изменения явля ется вся программа. (Цилиндры могут использоваться в ней где угодно.) При нали чии функций доступа потенциальная область изменений будет четко определена. Она включает в себя функции, обращающиеся к представлению данных цилиндра.
Такой подход упрощает повторное использование программного кода. Без функ ций доступа любые алгоритмы, работающие с объектами-цилиндрами, придется писать и проверять сначала. Если такие функции имеются, в новых алгоритмах можно вызывать их. Проверять каждую подобную функцию потребуется только один раз.
Недостаток данного подхода состоит в том, что понадобится писать и тестиро вать больший объем исходного кода, однако можно возразить, что на самом деле это дает дополнительные преимущества. Учитывая общий баланс времени, собст венно набор текста (исходного кода) программы составляет лишь малую часть. Все другие шаги разработки — отладка, тестирование, интеграция и сопровож дение — требуют чтения исходного кода. Применение при написании клиентской части вызовов функций доступа (уже подготовленных и протестированных) упро щает эти шаги, способствует сокращению числа ошибок и обходится дешевле.
Что же добавляет сокрытие информации к инкапсуляции? Давайте снова рас смотрим серверные функции validateCylinderO и getVolume(). Первая функция инкапсулирует операции проверки данных, значения по умолчанию и т. д. Это хорошо, поскольку клиентскому коду не нужно знать всех деталей проверки до пустимости данных. Достаточно, что она выполняется. Вторая функция инкапсу лирует геометрические вычисления. Это тоже хорошо, потому что в клиентской программе можно не беспокоиться о правилах геометрии. Достаточно знать, что вычисляется объем цилиндра.
Однако обе эти функции не-хороши с точки зрения сокрытия информации. Они расширяют знания разработчика об архитектуре серверной части, увеличивают ту область, которой должен уделять внимание разработчик клиента, и переносят информацию для операций в клиентский код вместо того, чтобы работать с нею на уровне сервера.
Функция validateCylinderO требует проверки данных, в то время как она не должна быть в области внимания разработчика клиента и сопровождающего приложение программиста. Данный недостаток можно устранить с помощью
