Последний раз обновлено 27.06.01
Перед тем как узнать что-то о, ООП и ответить на все интересующие вопросы, надо договорится о том, как и что называть. В объяснении ООП методов я буду часто опираться на базовые понятия структурного программирования, по крайней мере, я не знаю, как можно работать на С++ без этого. Это простые определения, и хотя хочется быстрее начать программировать, будет нехорошо, если под одинаковыми словами мы будем подразумевать разное. Многие из определений зададим не сразу, а постепенно, по мере надобности расширяя старое новыми условиями, иначе их будет сложно объяснить и привести примеры.
Будем считать, что мы хорошо знакомы с основными положениями структурной программы, но кратко вспомним некоторые:
switch
, while
, for
…
Существует достаточно много хорошей литературы по вопросам структурного программирования, это довольно просто и почти совпадает с логикой "бытового здравого смысла". Для написания любой программы знать это обязательно, здесь об этом подробно говориться не будет.
Поддержку нормальной работы, тестирование и модернизацию программы называют сопровождением. Знатоки и корифеи программирования утверждают, что программирование процесс итерационный ( повторяющийся ), т.е. сразу скажем, что при программировании приходится изменять часть отлаженных и работающих определений программы, т.е. сопровождение программы начинается вскоре после начала программирования. Это не послепродажное обслуживание программы, как думают некоторые. Понятно, что чем меньше сложность сопровождения, тем лучше.
Если блок использует какую то внешнюю относительно него переменную или блок, то он называется связаным или сцепленным с этой используемой програмной частью. Число связей блока можно посчитать. Связь показывает зависимость одного элемента от другого.
Только активный, т.е. вызывающий, блок образует ( и добавляет себе ) связь. Для вызываемого блока или для переменной связь не добавляется, т.е. связь считается один раз. Нет ли здесь противоречия? Глобальная переменная, которую используют все блоки, казалось бы, является самым напряженным местом программы по числу пересекающихся блоков, по нашему определению оказывается самой несвязаной. И это правда. Переменная, как место хранения, не виновата что определена в глобальном контексте. Как бы принципиально мы не меняли ее тип, если никто ее не использует, никакая другая часть программы не подвергнется изменению, а сама переменная никого не использует.
Следует ввести понятие степень или сила связи. Это относительное понятие, одна связь сравнивается с другой на больше или меньше. Связь тем сильнее, чем больше незначительные изменения в структуре или другие отклонения от оригинального состояния элемента, который используется блоком, эквивалентны для этого блока замене его новым элементом.
Количество и сила связей соответствуют сложности сопровождения: чем меньше связей, и их сила, тем меньше зависимость элементов, тем легче сопровождать программу, т.к. каждый независимый элемент можно модернизировать отдельно от другого независимого элемента. Но не только это определяет сложность сопровождения.
Конечно, хотелось бы сократить число связей до минимума. Но именно наличие связей и позволяет программе выполняться. Без связей программа не работает. Большая программа будет всегда означать большое количество связей. Видимо, не столько число связей, сколько их хаотичность и бессистемность затрудняет сопровождение и не дает возможности скрывать их или обрабатывать автоматически компилятором.
Связи могут быть удачно расположены и даже при большом их числе охватывать лишь несколько элементов, т.е. быть ограниченными. Могут быть и другие условия, при которых связь можно сильно ослабить и учитывать ее лишь в исключительных случаях или не учитывать вовсе и обрабатывать автоматически.
В противовес связи, можно, интереса ради, ввести какую нибудь обратную величину, например, используемость. Это ссылки на блоки, которые используют данную часть программы. Это позволит при модификации этого элемента быстрее локализовать просмотр и модификацию вызывающего кода. Так как вызывающий код сам может быть используемым, то изменения могут лавинно нарастать и в конце концов привести к полному просмотру и модернизации всех блоков, при этом информация о используемости на некотором шаге может стать недействительной.
Можно различать две группы используемости: ссылки действительные при модификации или действительные только при удалении этого используемого элемента. Блоки попавшие в первую группу должны проверяться и модифицироваться даже при незначительных изменениях элемента. Вторая группа блоков проверяется и модифицируется только если элемент считается удаленным.
Наличие типа у переменной, как у элемента сохраняющего свое состояние:
Отсутствие типа у переменной:
На первый взгляд, отсутствие типа не дает никаких преимуществ и недостатки без типа очевидны, но иногда используют не типизированные параметры функции, когда тип параметра задается во время выполнения или нестандартным образом. Примером этого служит семейство функций printf().
Тип переменной ослабляет связь, т.к. не требует знания предыдущего типа состояния и ошибочный доступ отслеживаются автоматически на этапе компиляции.
Имя или идентификатор – это понятное и простое кодовое обозначение сложного явления. Древние люди называли вещи так: "дом у реки шумящей водопадом где встает солнце освещающее мощнорукого и мудрого вождя", включая в имя все известные и важные для них свойства предметов. Если круг одинаковых предметов был мал, то говорили просто: "дом". В нашем случае:
Для компилятора имя в большинстве случаев и есть элемент. Совпадение имен разных элементов называют конфликтом имен. Такую программу нельзя откомпилировать, так как два разных элемента разных типов не будут отличаются для компилятора друг от друга.
Скрытие или отбрасывание ненужных подробностей ( например, подробностей для использующего функцию или переменную блока ) называется абстракцией, т.е. абстракция это независимость от чего то. Примером абстракции может служить число "3". Трудно сказать, что скрыто за этим числом, но в своем контексте чисел с ним можно полноценно работать: складывать, вычитать и т.д. Другими словами, абстракция всегда относительно чего-то.
Две абстракции, как относительные величины, можно сравнивать на больше или меньше или приписать им порядок. Одна абстракция может быть большего или меньшего порядка чем другая. Абстракция соответствует силе связи: менее сильная связь, означает меньшую зависимость, т.е. большую абстракцию и легкость сопровождения.
Теперь мы определили достаточно, чтобы переменную и функцию можно было бы рассмотреть в этих контекстах. Интерес для нас представляют элементы, которые наиболее распространены в программе. Именно с ними и приходится иметь дело при сопровождении. Не будем рассматривать ситуации с volatile
переменными и другим разделением ресурсов с несколькими процессами, т.к. это отдельная тема. Отбросим и возможные другие особые случаи, т.к. для исключений годится любой способ создания, одна - две функции не определят среднюю сложность сопровождения для достаточно большой программы.
Изменяемые параметры для переменной:
Изменение имени не окажет существенного влияния на использующий ее код, если сохранится уникальность в пределах области видимости, т.е. переменная останется доступна. Тогда проблемы решаются автоматической заменой имени в использующем переменную коде. Иначе будет либо конфликт имен, либо для вызывающего кода переменнная будет недоступна.
Изменение в структуре типа наверняка приведет к существенной модификации использующего кода. Это эквивалентно удалению старой переменной и созданию новой, просто похожей.
Изменяемые параметры для функции:
Изменение имени не окажет существенного влияния на использующий ее код, если сохранится уникальность, тогда проблемы решаются автоматической заменой имени в использующем функцию коде, иначе будет конфликт имен.
Изменение списка параметров, типа результата или логического назначения функции, приведет наверняка к существенной модификации использующего кода. Это эквивалентно удалению старой функции и созданию новой, просто похожей.
Изменение внутреннего устройства блока функции никак не скажется на вызывающем блоке, даже если вызываемая функция нарастит или уменьшит свои связи, т.е. использование других блоков и переменных.
Исходя из этого, что в большинстве случаев изменения в функциях сосредоточены только в блоке функции, можно сказать, что связь с функцией, оставаясь связью, все же менее сильная, чем с переменной. Переменная безответственно относится к блокам ее использующим, а функция задает определенные правила ( интерфейс ) между вызывающим блоком и реальным кодом функции ( реализацией ) с помощью установки начальных условий, вызова функции и получения результата, скрывая за ними подробности своего блока.
Менее сильная связь, меньшая зависимость, означает большую абстракцию. Для вызывающего кода, функция – элемент более абстрактный чем переменная. Если считать, что только изменение интерфейса функции или логического назначения производит новую функцию, то связь с функцией образует используемость второй группы, а переменная всегда образует используемость первой группы.
В силу своей природы, блок кода более независим при связи, чем переменная, т.к. при выполнении блока, вызывающий блок ничего не делает, а при «выполнении», доступе к переменной, вызывайщий блок обеспечивает доступ сам.
Блок абстракция более высокого порядка чем переменная еще и потому, что локальные переменные могут входить в состав блока, а не наоборот, т.е. блок объединяет в одно код и данные.
Имена в структурной программе образуют некий логический контекст. Для компилятора структурного языка нужна лишь их уникальность в определенных пределах. С другой стороны, они предназначены для лучшего восприятия человеком структурной программы и являются мостом между языком программирования и человеком. Функция более абстрактна чем переменная не из-за имени, которое скрывает подробности функции, а из-за своего интерфейса доступа и своей природы кода, которые и ослабляют связь.
Для функции, мы можем взять на себя обязательство, что часть имени функции всегда соответствует логическому назначению и не будет изменяться независимо от него, а часть имени будет служить для создания уникальности. Это все равно, что сократить список изменяемых параметров, объединив имя и логическое назначение. Это позволяет легко видеть, когда функция при модификации своего блока становится эквивалентна новой, т.е. такое обязательство также облегчает сопровождение.
Рассмотрим функции, блоки которых состоят не только из кода, а еще и имеют переменные. Представим, что в этом блоке только одна локальная переменная некоторого типа. Список параметров и результат этой функции будет служить интерфейсом доступа к этой переменной. Структура типа такой переменной как бы транслируется в этот интерфейс, а код блока выполняет эту трансляцию и будет служить реализацией интерфейса. Конечно он может выполнять и что-то еще, но главное, что функцию можно использовать как механизм, обеспечивающий доступ к переменной. При этом возможно изменение реальной структуры локальной переменной в широких пределах и это не затронет вызывающий функцию код. Такая переменная в оболочке функции имеет абстракцию такого же порядка, как и функция.
Объединение кода доступа к состоянию и самого этого состояния называют инкапсуляцией.
К функции, как к блоку кода и локальных данных, можно приписать раздельный интерфейс и реализацию и использовать ее как элемент ослабляющий связь при доступе. Но проблема в том, что:
Все это уменьшает возможность реального применения функции как элемента доступа.
Во многих случаях можно выделить ряд функций и переменных в группу ( но не в один именованый блок-функцию ) так, что большая часть связей этих выделенных элементов будут находится в пределах группы. Такая группа называется модулем - это данные и функции для работы с ними.
Модуль обявляет фиксированное количество функций и переменных, доступных из этого модуля внешним блокам. Те имена блоков и переменных модуля, используемость которых выходит за рамки модуля, и образуют этот интерфейс модуля. Сами блоки и переменные интерфейса плюс остальные имена, блоки и переменные модуля образуют реализацию. Эти элементы недоступны снаружи, и их изменение не затронет код снаружи, хотя они и могут использовать интерфейс других модулей, что по возможности минимизируют.
Функции модуля выполняются в контексте реализации, отличном от контекста вызова. Эти контексты фактически разные программы, которые пересекаются интерфейсом модуля.
Для любой сложной структуры данных можно назначить модуль, который предоставит интерфейс доступа к ней, скрыв детали реализации. Для модуля эта структура создается глобально, в единственном экземпляре.
Если экземпляров такой структуры данных несколько, то для конкретной переменной, при ее внешнем создании и явном ее указании при вызове функций модуля модуль тоже можно использовать, но не автоматически. Абстракция такого модуля уменьшается.
Для функций модуля такую переменную можно указать как параметр и описывать функции модуля с учетом этого. Пример (Пример 1) программы с таким модулем. В случае одной глобальной структуры пропадет (t_my_complex_structure *) параметр.
Пример 1
Реализация модуля с несколькими контекстами в структурной программе
|
Модуль является одним из древнейших методов борьбы с явлением излишней связанности. Код, который соблюдает формальные требования интерфейса, с обеих сторон этого интерфейса может сопровождаться независимо. Модуль - абстракция более высокого порядка, чем функция. Он создает значительно более гибкий интерфейс и по сравнению с аналогичным кодом не модулем, большинство связей его реализации ограничены и находятся в пределах модуля.
Можно проиллюстрировать различие порядка абстракций функции и модуля так:
Все, кто пытался учиться в школе, знают, что физика это наука о природе. Кто учился лучше, тот знает что задача физики построить из реального мира его математическую модель, которую потом может посчитать математик. На пути образования модели надо проявиить умение абстрагироваться от ненужных свойств и знать, какие "законы природы" может применить математик. Легко заметить, что есть четко выраженные этапы: природа - модель - рассчет. Во время образования модели приходится делать определенные итерации: пробная модель, рассчет, улучшенная модель … и т.д.
Работа программиста сродни работе физика, если считать поставленную задачу природой, а математиком язык программирования. Модель выполняет роль той самой электрической розетки, позволяя существовать раздельно реальной задаче и ее реализации.
Структурное программирование не заставляет человека обращать активное внимание на модель. Как пишет Страуструп "…небольшую программу можно заставить работать методом грубой силы…". Для задач сложнее, многие придерживаются при структурном программировании метода "сверху вниз".Я напомню кратко один из его вариантов
В качестве примера возьмем задачу отображения файла (Пример 2). Получается некоторый набор детализаций. Причем сначала 1 2 3, на следующем этапе 1.х 2.х 3.х ну и так далее. Для удобства сравнения с программой, эти детализации здесь приведены вложенно.
Пример 2
Структурная модель отображения файла
Модель | Реализация |
|
|
Левая половина таблицы представляет собой настоящую структурную модель. Ее вид для компилятора-"математика" в правой половине. Можно легко представить себе объединенный вариант из этих половинок, в котором строки из левой половины будут служить комментариями. Велик соблазн совместить и образование модели и ее программный вид. В принципе, структурный язык для этого и разрабатывался, и его универсальные операторы и использование комметариев могут обеспечить это совмещение.
Но для не структурной модели это может быть не так. Иногда пренебрегать моделью, все равно что рисовать электрическую схему и сразу ее паять, не понравилось – стер и перепаял, опять не понравилось, опять перепаял. Модель же может вобрать в себя значительную часть этих итераций.
Перед использованием такого метода мы можем предварительно разделить программу на модули. Метод разделения программы на модули мы рассмотрим в разделе "Объектная модель", когда ознакомимся с модулями, с их улучшенными вариантами, более подробно.
Чем же плоха привычная структурная методика? Рассмотрим простой пример: программа моделирования океана. Есть океан в котором живет и плавает некоторое количество рыбы и травы. Рыба может есть траву.
Структурный подход заставит создать структуры данных, которые опишут рыбу, океан и траву, затем создать функции для работы с ними. При этом во время работы почти наверняка придется вносить некоторые изменения в эти структуры.
Пусть мы захотели добавить в океан летучую рыбу, возможно, что уже работоспособные функции тоже окажутся затронуты этими изменениями, т.к. ожидают старую структуру данных. При структурном программировании использование данных "размазано" по всей программе, поэтому вся программа становится сцепленной этими данными.
В предыдущем абзаце есть слово возможно. Что это значит, нельзя сказать затронуто или нет? Да! Вот именно, нельзя. Может и не затронуто. Но чтобы убедится в этом надо ознакомиться с тем что делает функция, знания одного лишь имени и типа не достаточно, в общем случае это не получится разрешить даже комментариями, иначе в них придется повторить весь алгоритм. К тому же комментарии не позволят автоматически корректировать изменения.
Подобная глобальная, она действительно достаточно большая, модификация или просмотр кода даже при несущественных изменениях структур данных трудна для сопровождения. Модули, функции и типы ведут борьбу с излишней связанностью не достаточно хорошо. В таблице (Таблица 1) стравниваются уровни абстракции для элементов СП.
Модули, конечно, разделяют данные для локального использования, но их недостатки, чтобы не забегать вперед, я приведу позже, когда мы рассмотрим свойства модулей подробнее.
Структурная модель программы задает сруктуры данных и последовательность действий для работы с ними. Чтобы раскритиковать этот способ надо ознакомиться хотя бы с еще одним способом создания программы и сравнить их.
Таблица 1
Уровни абстракций элементов структурной модели
Наименьшая абстракция | |
Переменная без типа (в параметре функции) | Блок кода без имени |
Переменная с типом | |
Переменная внутри функции | Функция |
Модуль |