Модульный подход

Вопросы

Модульный подход

Абстракция и сокрытие информации

Каждый модуль, из которого состоит решение задачи, начинается строками, в которых указано, для чего он предназначен, но не написано, как именно он работает. Ни в одном модуле не описано, как работает другой — в лучшем случае — для какой задачи предназначены другие.

Например, если в какой-то части программы данные должны упорядочиваться, в одном из модулей выполняется алгоритм сортировки.

В других модулях описано, что здесь выполняется сортировка данных, но не — как именно она осуществляется.

Таким образом, разные части решения изолируются друг от друга.

Абстракция (abstraction)

Данный термин определяет предназначение модуля от его реализации. Модульность и абстракция дополняют друг друга.

Модульный подход позволяет разделить решение задачи на блоки; абстракция определяет содержание модуля до его реализации на конкретном языке программирования.

В спецификациях не указывается, как именно реализован модуль.

Указывайте, что делает функция, но не описывайте, как она это делает

В ходе решения задачи содержание каждого модуля постепенно уточняется, воплощаясь в итоге в виде функций на языке — «C++»

Предназначение функции следует отделять от её реализации. Этот процесс называется функциональной (процедурной) абстракцией (functional (procedural) abstaction).

Готовую функцию можно применять, не вникая в детали реализации алгоритма, поскольку для использования достаточно знать её предназначение и описание аргументов.

Если функция сопроваждается соответствующей документацией, её можно использовать, зная лишь объявление и первичное описание, реализацию можно не изучать.

Указывайте, что именно вы хотите сделать с данными, но не описывайте, как это нужно сделать

Рассмотрим теперь совокупность данных и набор операций над ними.

В этом наборе могут быть операции добавления данных в совокупность, удаления их оттуда или операции поиска.

Абстракция данных (data abstraction) сосредотачивает внимание на предназначении операций, а не на деталях их выполнения.

Другие модули программы будут хранить информацию о том, что именно делает та или иная операция, но не о том, как при этом хранятся данные или как именно выполняется данная операция.

Пример

Независимо от реализации массива — «year» — число — 1492 — всегда можно записать в ячейку массива с номером — «index» — используя следующий оператор.

years[index] = 1492;

Позднее это значение можно вывести на экран, воспользовавшись следующим оператором.

cout << years[index] << enld;

Абстрактный тип данных, или АТД (abstract data type)

АТД — совокупность данных и множество операторов над ними. Операции АТД можно применять, если известны их спецификации, при этом не обязательно знать детали их реализации или способы хранения данных.

АТД не является синонимом структуры данных.

Для реализации АТД можно использовать структуру данных (data structure), представляющую собой конструкцию, определённую в языке программирования для хранения совокупности данных.

Например, данные можно хранить в массивах целых чисел, объектов или массивах массивов.

Разработка алгоритма и АТД должны быть связаны друг с другом

В процессе решения задачи абстрактные типы данных помогают реализовывать алгоритм, предназначенный для решения задач, предполагает выполнение последовательности операций над данными, что, в свою очередь, приводит к определению АТД и алгоритму выполняющих эти операции.

Процедуру решения задачи можно выполнять и в обратном порядке. Вид применяемого АТД может диктовать выбор строгого глобального алгоритма решения задачи. Таким образом, зная, что операции над данными выполняь легко, а какие трудно, можно существенно повысить эффективность решения задачи.

Сокрытие информации

Абстракция вынуждает создавать функциональные спефицикации для каждого модуля, делая его открытым (public) для внешнего мира.

Она позволяет идентифицировать детали, что должны быть скрыты от публичного обозрения, быть закрытыми (private).

Принцип сокрытия информации (Information hiding) гарантирует, что такие детали будут не только скрыты внутри модуля, но и ни один другой модуль не будет даже подозревать об их существовании.

Принцип сокрытия информации ограничивает способы работы с функциями и данными. Пользователь модуля не должен интересоваться деталями его реализации. Разработчик модуля не должен заботиться о способах его использования.

Объектно-ориентированное проектирование

Объекты инкапсулируют данные и операции.

Один из способов модульного решения задачи — идентификация объектов (objects), объединяющих в единое целое данные и операции над ними. В результате такого объектно-ориентированного подхода (object oriented approach) к модульному решению задачи возникает совокупность объектов, обладающих определённым поведением.

Инкапсуляция скрывает внутренние детали

Инкапсулировать — упаковывать (вкладывать). Таким образом, это способ сокрытия внутренних деталей.

Функции инкапсулируют действия, объекты инкапсулируют данные, вместе с действиями.

Когда вы хотите, чтобы будильник зазвенел, вы не знаете, как он это делает, увидите лишь результат этой операции.

Спецификации программы для ввода на экран циферблата электронных часов

Часто выполняют следующие операции:

Индикаторы часов и минут также являются объектами, причём они очень похожи. Каждый из них выполняет следующие операции:

Объект — это экземпляр класса

Фактически оба индикатора представляют собой один и тот же тип объекта.

Множество объектов, имеющих один и тот же тип, называется классом (class). Таким образом, нам нужно указывать не конкретный объект, а класс объектов: класс часов и класс индикаторов.

Отдельные элементы данных, определённых в классе, называются данными-членами (data members), полями данных (data fields) или атрибутами (attributes).

Операции, заданные в классе, называются методами (methods) или функциями-членами (member functions).

Объектно-ориентированное программирование (object-oriented programming), или ООП

Дополняет инкапсуляцию двумя новыми принципами.

Три принципа объектно-ориентированного программирования:

Наследование

Классы могут наследовать (inherit) свойства других классов.

Например, определив класс часов, мы можем разработать класс будильников, исследующий свойства часов, добавив новые операции, свойственные будильникам. Это можно сделать быстро, поскольку класс часов уже разработан.

Наследование (inheritance) позволяет повторно использовать классы, определённые ранее (возможно, для других, но похожих целей), выполняя соответствующие модификации.

Полиморфизм

Полиморфизм (polymorphism) — буквально означающий изменчивость форм — позволяет выбать нужную операцию уже на этапе выполнения программы.

Например, если в программе используется оператор — «+» — операндами которого являются числа, то выполняется сложение чисел, но, если к строкам применяется перегруженный (overloaded) оператор — «+» — выполняется их конкатенация. Хотя в данном случае компилятор может сам определить правильный смысл данного оператора, полиморфизм допускает ситуации, когда смысл операции уточняется лишь на этапе выполнения программы.

Проектирование — «сверхну вниз»

Обычно объектно-ориентированный подход приводит к модульному решению задач, основываясь лишь на анализе данных. При разработке алгоритма для конкретной функции или в ситуациях, когда на первое место выходит алгоритм, а не данные, с которыми он работает, модульное решение можно получить с помощью проектирования — «сверхну вниз» (top-down design).

В то время как с помощью объектно-ориентированного подхода можно идентифицировать данные, основываясь на именах существительныхЮ использованных в описании задачи, проектирование — «сверхну вниз» — основано на анализе глаголов.

Структурная схема (structure chart)

Стратегия проектирования — «сверхну вниз» — основана на последовательном понижении уровня детализации задачи. Рассмотрим простой пример: допустим, что нам нужно вычислить среднюю экзаменационную оценку. Создаём структурную схему (structure chart), иллюстрирующую иеархию модулей и взаимодействие между ними.

Во-первых, для каждого модуля указывается лишь описание его предназначения, лишённое каких-либо деталей. Каждый модуль разбивается на несколько более мелких модулей. В результате возникает иеархия модулей.

Каждый модуль уточняется его наследником, решающим более мелкую задачу и содержащим больше информации о способе решения задаче, чем его предшественник.

Задача разбивается на три независимые подзадачи

Независимые подзадачи:

Если три задачи решаются тремя разными модулями, вызывая их, можно найти среднее значение оценки независимо от способов их реализации.

Разработка каждого модуля начинается с разбиения его на подзадачи. Например, задачу считывания оценок можно уточнить с помощью двух модулей:

Общие принципы проектирования

Для получения модульного подхода одновременно используйте объектно-ориентированное проектирование и подход — «сверхну вниз». Таким образом, абстрактные типы данных и алгоритмы нужно разрабатывать параллельно.

Для решения задач обработки данных используйте объектно-ориентированное проектирование.

Для разработки алгоритмов используйте подход — «сверхну вниз».

Главным в решении задачи являются алгоритмы, а не данные — применяйте проектирование — «сверхну вниз».

При разработке абстрактных типов данных и алгоритмов акцентируйте внимание на вопросе — «что» — а не — «как».

Старайтесь применять готовые компоненты программного обеспечения.

Моделирование объектно-ориентированных проектов с помощью языка — «UML»

Универсальный язык моделирования — «UML» — (unified modeling language)

Данный язык содержит спецификации диаграмм и тестовых описаний.

Диаграммы особенно полезны для общего описание проектов включая спецификации классов и разных способов взаимодействия между ними.

Обычно программа состоит из многих классов — возможность описывать взаимодействия между ними представляет собой ценное свойство данного языка.

Диаграммы

В диаграмме класса указываются его имя, данные-члены и операций.

Рассмотрим диаграмму класса — «clocks».

Нужно обратить внимание, что диаграмма носит довольно общий характер: она не диктует выбор фактической реализации класса.

Это типичное представление концептуальной модели класса, не зависящее от выбора языка его реализации.

Текстовые описания для представление данных-членов и операций, выполняемых в классе

Эта записи можно включать в диаграммы классов, однако это усложняет диаграммы, снижая степень их общности.

В данном разделе мы будем использовать именно текстовые опиасния классов, поскольку они позволяют создавать более полные спецификации, чем диаграммы.

Синтаксис описания данных-членых на языке — «UML»

Модификатор_доступа имя: значение_по_умолчанию.

Здесь использованы следующие обозначения:

Модификатор доступа принимает значение — «+ (public)» или — «- (public)». Третье возможное значение — символ — «#» (protected).

Элемент — «имя» — означает имя атрибута.

Элемент — «тип» — означает тип атрибута.

Элемент — «значение_по_умолчанию» — задаёт начальное значение атрибута.

Как показывает диаграмма класса, нужно задать хотя бы имя класса

Элемент — «значение_по_умолчанию» — используется лишь в тех ситуациях, когда значение атрибута задаётся по умолчанию. В некоторых случаях нужно избегать явного указания типа атрибута, отложив рещение этого вопроса до этапа реализации.

В дальнейшем используем названия распространённых типов аргументов — «integer» (для целочисленных значений), «float» (для значений с плавающей точкой), «boolean» (для булевых значений), «string» (для строковых значений).

Следует обратить внимание, что эти имена не совпадают ссоответствующими названиями типов данных в языке — «C++» — поскольку текстовое описание класса не должно зависеть от языка его реализации.

Текстовое описание атрибутов класса — «clocks»

-hour: integer

-minute: integer

-second: integer

Следуя принципу сокрытия информации, данные-члены — «hours» — у — «minute» — и «second» объявлены закрытыми.

Синтаксические конструкции языка — «UML» — предназначенные для описания операций, выглядят немного сложнее.

модификатор_доступа имя(список_параметров):

тип_возвращаемого_значения(строка_свойств)

Здесь использованы обозначения

Модификатор доступа принимает те же значения, что и в предыдущем случае.

Элемент — «имя» — означает имя операции.

Элемент — «список_параметров» — содержит параметры, разделённые запятой. Синтаксическая конструкция для описания параметров выглядит следующим образом.

направления имя = значение_по_умолчанию.

Здесь элемент — «направление» — используется для индикации ввода (in), выводв (out) или ввода-вывода (input) параметра.

Элемент — «name» — является именем параметра.

Элемент — «type» — задаёт тип параметра.

Элемент — «значение_по_умолчанию» — задаёт значение, что следует присвоить параметру, если соответствующий аргумент пропущен.

Элемент — «тип возвращаемого значения» — задаёт тип значения, возвращаемого операцией. Если эта операция не возвращает никакого значения, место этого элемента остаётся пустым.

Элемент — «строка_свойств» — перечисляет свойства операции.

Как и для атрибутов, в диаграммах классов нужно указывать хотя бы имя операции.

Иногда в диаграмму включается элемент — «список_параметров» — если это позволяет дучще понять функциональные возможности класса.

Элемент — «строка_свойств» — может содержать множество разнообразных значений, однако нас будет интересовать лишь свойство — «query».

Это свойство позволяет идентифицировать операции, что не имеют права модифицировать данные, содержащиеся в классе.

Текстовое описание операций, предусмотренных в классе — «clocks» — имеет вид

+setTime(in hr: integer, in min: integer, in sec: integer)

-advanceTime()

+-displayTime() (query)

Здесь операции — «setTime» — и — «displayTime» — определены открытыми, а операция — «advanceTime» — закрытой.

Функция — «displayTime» — имеет свойство — «query» — означающее, что она не изменяет никаких данных. Эта функция лишь выводит данные на экран.

Преимущества объектно-ориентированного подхода

При использовании объектно-ориентированного подхода (ООП) время, затрачиваемое на проектирование программы, увеличивается.

Кроме того решение, к которому приводит этот подход, обычно носит более общий характер, чем это необходимо.

Дополнительные усилия, потраченные на ООП, обычно компенсируются.

Используя объектно-ориентированное проектирование при решении задач, необходимо идентифицировать возникающие классы.

При этом указывается предназначение каждого класса и способ его взаимодействия с другими классами.

Таким образом, возникает спецификация каждого класса, в которой указываются его данные и операции. Затем центр внимания перемещается на детали реализации каждого класса, используя подход — «сверхну вниз» — для разработки операций.

Классы легче реализовывать по отдельности. Реализовав класс, необходимо провести его двойное тестирование.

Во-первых, нужно проверить операции класса. Для этого обычно создают небольшие программы, вызывающие разные операции и проверяющие результаты в соответствии с их спецификациями. Проверив каждый класс, нужно провечти тестирование взаимодействий между классами, возникающих при решении задачи.