Модульный подход
Каждый модуль, из которого состоит решение задачи, начинается строками, в которых указано, для чего он предназначен, но не написано, как именно он работает. Ни в одном модуле не описано, как работает другой — в лучшем случае — для какой задачи предназначены другие.
Например, если в какой-то части программы данные должны упорядочиваться, в одном из модулей выполняется алгоритм сортировки.
В других модулях описано, что здесь выполняется сортировка данных, но не — как именно она осуществляется.
Таким образом, разные части решения изолируются друг от друга.
Данный термин определяет предназначение модуля от его реализации. Модульность и абстракция дополняют друг друга.
Модульный подход позволяет разделить решение задачи на блоки; абстракция определяет содержание модуля до его реализации на конкретном языке программирования.
В спецификациях не указывается, как именно реализован модуль.
В ходе решения задачи содержание каждого модуля постепенно уточняется, воплощаясь в итоге в виде функций на языке — «C++»
Предназначение функции следует отделять от её реализации. Этот процесс называется функциональной (процедурной) абстракцией (functional (procedural) abstaction).
Готовую функцию можно применять, не вникая в детали реализации алгоритма, поскольку для использования достаточно знать её предназначение и описание аргументов.
Если функция сопроваждается соответствующей документацией, её можно использовать, зная лишь объявление и первичное описание, реализацию можно не изучать.
Рассмотрим теперь совокупность данных и набор операций над ними.
В этом наборе могут быть операции добавления данных в совокупность, удаления их оттуда или операции поиска.
Абстракция данных (data abstraction) сосредотачивает внимание на предназначении операций, а не на деталях их выполнения.
Другие модули программы будут хранить информацию о том, что именно делает та или иная операция, но не о том, как при этом хранятся данные или как именно выполняется данная операция.
Независимо от реализации массива — «year» — число — 1492 — всегда можно записать в ячейку массива с номером — «index» — используя следующий оператор.
years[index] = 1492;
Позднее это значение можно вывести на экран, воспользовавшись следующим оператором.
cout << years[index] << enld;
АТД — совокупность данных и множество операторов над ними. Операции АТД можно применять, если известны их спецификации, при этом не обязательно знать детали их реализации или способы хранения данных.
АТД не является синонимом структуры данных.
Для реализации АТД можно использовать структуру данных (data structure), представляющую собой конструкцию, определённую в языке программирования для хранения совокупности данных.
Например, данные можно хранить в массивах целых чисел, объектов или массивах массивов.
В процессе решения задачи абстрактные типы данных помогают реализовывать алгоритм, предназначенный для решения задач, предполагает выполнение последовательности операций над данными, что, в свою очередь, приводит к определению АТД и алгоритму выполняющих эти операции.
Процедуру решения задачи можно выполнять и в обратном порядке. Вид применяемого АТД может диктовать выбор строгого глобального алгоритма решения задачи. Таким образом, зная, что операции над данными выполняь легко, а какие трудно, можно существенно повысить эффективность решения задачи.
Абстракция вынуждает создавать функциональные спефицикации для каждого модуля, делая его открытым (public) для внешнего мира.
Она позволяет идентифицировать детали, что должны быть скрыты от публичного обозрения, быть закрытыми (private).
Принцип сокрытия информации (Information hiding) гарантирует, что такие детали будут не только скрыты внутри модуля, но и ни один другой модуль не будет даже подозревать об их существовании.
Принцип сокрытия информации ограничивает способы работы с функциями и данными. Пользователь модуля не должен интересоваться деталями его реализации. Разработчик модуля не должен заботиться о способах его использования.
Объекты инкапсулируют данные и операции.
Один из способов модульного решения задачи — идентификация объектов (objects), объединяющих в единое целое данные и операции над ними. В результате такого объектно-ориентированного подхода (object oriented approach) к модульному решению задачи возникает совокупность объектов, обладающих определённым поведением.
Инкапсулировать — упаковывать (вкладывать). Таким образом, это способ сокрытия внутренних деталей.
Функции инкапсулируют действия, объекты инкапсулируют данные, вместе с действиями.
Когда вы хотите, чтобы будильник зазвенел, вы не знаете, как он это делает, увидите лишь результат этой операции.
Часто выполняют следующие операции:
Индикаторы часов и минут также являются объектами, причём они очень похожи. Каждый из них выполняет следующие операции:
Фактически оба индикатора представляют собой один и тот же тип объекта.
Множество объектов, имеющих один и тот же тип, называется классом (class). Таким образом, нам нужно указывать не конкретный объект, а класс объектов: класс часов и класс индикаторов.
Отдельные элементы данных, определённых в классе, называются данными-членами (data members), полями данных (data fields) или атрибутами (attributes).
Операции, заданные в классе, называются методами (methods) или функциями-членами (member functions).
Дополняет инкапсуляцию двумя новыми принципами.
Три принципа объектно-ориентированного программирования:
Классы могут наследовать (inherit) свойства других классов.
Например, определив класс часов, мы можем разработать класс будильников, исследующий свойства часов, добавив новые операции, свойственные будильникам. Это можно сделать быстро, поскольку класс часов уже разработан.
Наследование (inheritance) позволяет повторно использовать классы, определённые ранее (возможно, для других, но похожих целей), выполняя соответствующие модификации.
Полиморфизм (polymorphism) — буквально означающий изменчивость форм — позволяет выбать нужную операцию уже на этапе выполнения программы.
Например, если в программе используется оператор — «+» — операндами которого являются числа, то выполняется сложение чисел, но, если к строкам применяется перегруженный (overloaded) оператор — «+» — выполняется их конкатенация. Хотя в данном случае компилятор может сам определить правильный смысл данного оператора, полиморфизм допускает ситуации, когда смысл операции уточняется лишь на этапе выполнения программы.
Обычно объектно-ориентированный подход приводит к модульному решению задач, основываясь лишь на анализе данных. При разработке алгоритма для конкретной функции или в ситуациях, когда на первое место выходит алгоритм, а не данные, с которыми он работает, модульное решение можно получить с помощью проектирования — «сверхну вниз» (top-down design).
В то время как с помощью объектно-ориентированного подхода можно идентифицировать данные, основываясь на именах существительныхЮ использованных в описании задачи, проектирование — «сверхну вниз» — основано на анализе глаголов.
Стратегия проектирования — «сверхну вниз» — основана на последовательном понижении уровня детализации задачи. Рассмотрим простой пример: допустим, что нам нужно вычислить среднюю экзаменационную оценку. Создаём структурную схему (structure chart), иллюстрирующую иеархию модулей и взаимодействие между ними.
Во-первых, для каждого модуля указывается лишь описание его предназначения, лишённое каких-либо деталей. Каждый модуль разбивается на несколько более мелких модулей. В результате возникает иеархия модулей.
Каждый модуль уточняется его наследником, решающим более мелкую задачу и содержащим больше информации о способе решения задаче, чем его предшественник.
Независимые подзадачи:
Если три задачи решаются тремя разными модулями, вызывая их, можно найти среднее значение оценки независимо от способов их реализации.
Разработка каждого модуля начинается с разбиения его на подзадачи. Например, задачу считывания оценок можно уточнить с помощью двух модулей:
Для получения модульного подхода одновременно используйте объектно-ориентированное проектирование и подход — «сверхну вниз». Таким образом, абстрактные типы данных и алгоритмы нужно разрабатывать параллельно.
Для решения задач обработки данных используйте объектно-ориентированное проектирование.
Для разработки алгоритмов используйте подход — «сверхну вниз».
Главным в решении задачи являются алгоритмы, а не данные — применяйте проектирование — «сверхну вниз».
При разработке абстрактных типов данных и алгоритмов акцентируйте внимание на вопросе — «что» — а не — «как».
Старайтесь применять готовые компоненты программного обеспечения.
Универсальный язык моделирования — «UML» — (unified modeling language)
Данный язык содержит спецификации диаграмм и тестовых описаний.
Диаграммы особенно полезны для общего описание проектов включая спецификации классов и разных способов взаимодействия между ними.
Обычно программа состоит из многих классов — возможность описывать взаимодействия между ними представляет собой ценное свойство данного языка.
В диаграмме класса указываются его имя, данные-члены и операций.
Рассмотрим диаграмму класса — «clocks».
Нужно обратить внимание, что диаграмма носит довольно общий характер: она не диктует выбор фактической реализации класса.
Это типичное представление концептуальной модели класса, не зависящее от выбора языка его реализации.
Эта записи можно включать в диаграммы классов, однако это усложняет диаграммы, снижая степень их общности.
В данном разделе мы будем использовать именно текстовые опиасния классов, поскольку они позволяют создавать более полные спецификации, чем диаграммы.
Модификатор_доступа имя: значение_по_умолчанию.
Здесь использованы следующие обозначения:
Модификатор доступа принимает значение — «+ (public)» или — «- (public)». Третье возможное значение — символ — «#» (protected).
Элемент — «имя» — означает имя атрибута.
Элемент — «тип» — означает тип атрибута.
Элемент — «значение_по_умолчанию» — задаёт начальное значение атрибута.
Элемент — «значение_по_умолчанию» — используется лишь в тех ситуациях, когда значение атрибута задаётся по умолчанию. В некоторых случаях нужно избегать явного указания типа атрибута, отложив рещение этого вопроса до этапа реализации.
В дальнейшем используем названия распространённых типов аргументов — «integer» (для целочисленных значений), «float» (для значений с плавающей точкой), «boolean» (для булевых значений), «string» (для строковых значений).
Следует обратить внимание, что эти имена не совпадают ссоответствующими названиями типов данных в языке — «C++» — поскольку текстовое описание класса не должно зависеть от языка его реализации.
-hour: integer
-minute: integer
-second: integer
Следуя принципу сокрытия информации, данные-члены — «hours» — у — «minute» — и «second» объявлены закрытыми.
Синтаксические конструкции языка — «UML» — предназначенные для описания операций, выглядят немного сложнее.
модификатор_доступа имя(список_параметров):
тип_возвращаемого_значения(строка_свойств)
Модификатор доступа принимает те же значения, что и в предыдущем случае.
Элемент — «имя» — означает имя операции.
Элемент — «список_параметров» — содержит параметры, разделённые запятой. Синтаксическая конструкция для описания параметров выглядит следующим образом.
направления имя = значение_по_умолчанию.
Здесь элемент — «направление» — используется для индикации ввода (in), выводв (out) или ввода-вывода (input) параметра.
Элемент — «name» — является именем параметра.
Элемент — «type» — задаёт тип параметра.
Элемент — «значение_по_умолчанию» — задаёт значение, что следует присвоить параметру, если соответствующий аргумент пропущен.
Элемент — «тип возвращаемого значения» — задаёт тип значения, возвращаемого операцией. Если эта операция не возвращает никакого значения, место этого элемента остаётся пустым.
Элемент — «строка_свойств» — перечисляет свойства операции.
Как и для атрибутов, в диаграммах классов нужно указывать хотя бы имя операции.
Иногда в диаграмму включается элемент — «список_параметров» — если это позволяет дучще понять функциональные возможности класса.
Элемент — «строка_свойств» — может содержать множество разнообразных значений, однако нас будет интересовать лишь свойство — «query».
Это свойство позволяет идентифицировать операции, что не имеют права модифицировать данные, содержащиеся в классе.
+setTime(in hr: integer, in min: integer, in sec: integer)
-advanceTime()
+-displayTime() (query)
Здесь операции — «setTime» — и — «displayTime» — определены открытыми, а операция — «advanceTime» — закрытой.
Функция — «displayTime» — имеет свойство — «query» — означающее, что она не изменяет никаких данных. Эта функция лишь выводит данные на экран.
При использовании объектно-ориентированного подхода (ООП) время, затрачиваемое на проектирование программы, увеличивается.
Кроме того решение, к которому приводит этот подход, обычно носит более общий характер, чем это необходимо.
Дополнительные усилия, потраченные на ООП, обычно компенсируются.
Используя объектно-ориентированное проектирование при решении задач, необходимо идентифицировать возникающие классы.
При этом указывается предназначение каждого класса и способ его взаимодействия с другими классами.
Таким образом, возникает спецификация каждого класса, в которой указываются его данные и операции. Затем центр внимания перемещается на детали реализации каждого класса, используя подход — «сверхну вниз» — для разработки операций.
Классы легче реализовывать по отдельности. Реализовав класс, необходимо провести его двойное тестирование.
Во-первых, нужно проверить операции класса. Для этого обычно создают небольшие программы, вызывающие разные операции и проверяющие результаты в соответствии с их спецификациями. Проверив каждый класс, нужно провечти тестирование взаимодействий между классами, возникающих при решении задачи.