Лекции по программированию. Основные понятия классификация программного обеспечения
Скачать 1.57 Mb.
|
var MyObject: TMyClass; begin MyObject.IntField := 0; {Ошибка! Объект не создан конструктором} MyObject := TMyClass.Create; //Нужно создать объект MyObject.IntField := 0; //и обратиться к его полю MyObject.Free; //Уничтожаем ненужный объект end; Особенность вызова конструктора заключается в том, что он вызывается с помощью ссылки на класс, а не на объект. В этом заложен глубокий смысл, так как экземпляр класса в момент вызова конструктора еще не создан. Однако код конструктора класса TMyClassнаходится в памяти и поэтому такой вызов вполне корректен. При создании объекта с помощью конструктора компилятор гарантирует, что все поля объекта будут инициализированы. Все числовые поля будут обнулены, указатели примут значение Nill, а строки будут пусты. С помощью метода Free() освобождается выделенная для объекта память после его использования. Этот метод проверяет, не равен ли объект значению Nill, и затем вызывает деструктор объекта - метод Destroy(), который освобождает всю выделенную память и выполняет другие действия по освобождению захваченных конструктором объекта ресурсов. В отличие от вызова конструктора, вызов метода Free() выполняется с помощью ссылки на экземпляр, а не на класс. Большинство конструкторов реализует некоторые действия, необходимые для правильной работы объекта. Поэтому в конструкторе класса потомка следует сначала вызвать конструктор своего родителя, а затем уже осуществлять дополнительные действия. Вызов любого метода родительского класса достигается с помощью ключевого слова Inherited (унаследованный). Например: Constructor TmyClass.Create(Value: Integer); //Возможная реализация конструктора begin Inherited Create; {Вызываем унаследованный конструктор} IntField := Value; {Реализуем дополнительные действия} end; В большинстве примеров, поставляемых вместе с Delphi, нет вызовов конструкторов и деструкторов. Это объясняется тем, что любой компонент, попавший при визуальном проектировании в приложение из палитры компонентов, включается в определенную иерархию, которая замыкается на форме TForm, а для всех ее составных частей конструкторы и деструкторы вызываются автоматически. Создает и уничтожает формы глобальный объект с именем Application. Для объектов, создаваемых динамически (во время выполнения приложения), обязательно нужен вызов конструктора. 4.6. СВОЙСТВА Свойства объекта представляют собой специальный механизм классов, регулирующий доступ к полям объекта. По отношению к компонентам свойства являются теми элементами, сведения о которых отображаются в окне ObjectInspector. Свойства объявляются с помощью зарезервированных слов property, read и write. Слова read и write считаются зарезервированными только в контексте объявления свойства. Обычно свойство связано с некоторым полем и указывает те методы класса, которые должны использоваться при записи в это поле или при чтении из него. Например: type TaClass = class IntField: Integer; function GetField: Integer; procedure SetField(Value: Integer); property IntegerValue: Integer read GetField write SetField; end; В контексте программы свойство ведет себя как обычное поле. Поэтому возможен следующий оператор присваивания: aClass.IntField := NewValue; Разница между этим оператором и оператором aCIass.IntegerValue := NewValue; заключается в том, что при обращении к свойству автоматически подключается метод SetField, в котором могут реализовываться специфические действия. Когда нет необходимости в специальных действиях при чтении или записи свойства, вместо имени соответствующего метода можно указывать имя поля. Если нужно, чтобы поле было доступно только для чтения или только для записи, следует опустить соответственно часть write или read. Вся эта технология имеет два основных преимущества. Во-первых, она позволяет представить конечному пользователю некий интерфейс, полностью скрывающий реализацию объекта и обеспечивающий контроль за доступом к объекту. Во-вторых, она дает программисту возможность замещать методы в классах-потомках с целью обеспечения полиморфного поведения объектов. 4.7. ОПРЕДЕЛЕНИЕ ОБЛАСТИ ВИДИМОСТИ КЛАССА ObjectPascalпредоставляет дополнительный контроль над доступностью членов классов (полей и методов) с помощью директив private, protected, public, published. Синтаксис использования этих директив следующий: type TMyClass = class private AprivateVariable: Integer; AnotherPrivateVariable: Boolean; protected procedure AProtectedProcedure; function ProtectMe: Byte; public constructor APublicContructor; destructor APublicKiller; published property AProperty read AprivateVariable write APrivateVariable; end; За каждой из директив может следовать любое необходимое количество объявлений полей или методов. Эти директивы имеют следующий смысл. private - эта часть объекта доступна только для кода, находящегося в одном модуле с другим кодом данного объекта. Директива private скрывает особенности реализации объекта от пользователей и защищает члены этого объекта от непосредственного доступа и изменения извне. protected - члены объекта, описанные в этой секции, доступны для производных объектов, что позволяет скрыть внутреннее устройство объекта от пользователя и в то же время обеспечить необходимую гибкость и эффективность доступа к полям и методам объекта для его потомков. public - описанные подобным образом члены объекта доступны в любом месте данной программы. В этой секции всегда описываются конструкторы и деструкторы. published - для этой части объекта при компиляции будет сгенерирована информация о типе времени исполнения. Это даст возможность другим частям приложения получать информацию о части объекта, описанной в этой секции. В частности, подобная информация используется утилитой ObjectInspectorпри построении списков свойств объектов. В ObjectPascalразрешается многократно объявлять любую секцию, причем порядок следования секций не имеет значения. Любая секция может быть пустой. 4.8. ПРИНЦИПЫ РАБОТЫ ОБЪЕКТНО-ОРИЕНТИРОВАННЫХ ПРОГРАММ Технология объектно-ориентированного программирования предполагает, что любая процедура или функция в программе представляет собой метод объекта некоторого класса, причем класс должен формироваться в программе, как только возникает необходимость описания новых объектов программирования. Каждый новый шаг в разработке алгоритма также должен представлять собой разработку нового класса на основе уже существующих классов. Таким образом, формируется иерархия классов и, в конце концов, вся программа будет представлять собой объект некоторого класса с единственным методом run(выполнить). В технологии объектно-ориентированного программирования схема взаимодействия методов и данных принципиально иная, чем при технологии структурного программирования: метод, вызываемый для одного объекта, как правило, не вызывает другой метод непосредственно. Для начала он должен иметь доступ к другому объекту (создать, получить указатель, использовать внутренний объект в текущем объекте и т. д.), после чего он уже может вызвать для него один из известных методов. Таким образом, структура программы определяется взаимодействием объектов различных классов между собой. Взаимосвязь между объектами осуществляется посредством сообщений. Большинство объектно-ориентированных систем организованы так, что их объекты состоят из видимых и приватных частей. Это делается с целью того, чтобы не создавать лишних копий не меняющихся частей объектов, приводящее к разбрасыванию ресурсов памяти. Общие для всех объектов одного типа части кода запоминаются на уровне класса. Чаще всего сюда попадают коды методов и переменные класса, составляющие разделяемую часть объектов. Класс содержит информацию о том, какие переменные создавать, но запоминаются они самим экземпляром. К достоинствам объектно-ориентированного программирования следует отнести: исключение избыточного кода; возможность защиты объектов от кода других частей программы; поддержка повторного использования отдельных составляющих программ; создание более открытых систем; экономия времени за счет построения программы из готовых, отлаженных частей; К недостаткам объектно-ориентированного программирования относят: ухудшение быстродействия системы, которое обусловлено посылкой сообщений от одного объекта к другому. Обращение к методу может занимать в 2-2,5 раза больше времени, чем к обычной подпрограмме; необходимость создания методов для доступа к запрещенным переменным объекта, а многочисленность методов приводит к излишнему количеству вызовов. Тем не менее, достоинства объектно-ориентированных систем, как правило, перевешивают перечисленные недостатки. Опыт также показывает, что размер исполнимых модулей таких систем обычно меньше. ТЕСТИРОВАНИЕ, ОТЛАДКА И ОПТИМИЗАЦИЯ ПРОГРАММ 5.1. ПРОГРАММНЫЕ ОШИБКИ Программ без ошибок не существует. Практика пока-зывдет, что виновниками ошибок в программах чаще всего бывают сами программисты. Один из общих законов практического программирования состоит в том, что ни одна программа не дает желаемых результатов при первой попытке трансляции и выполнения. Некоторое представление о действительных причинах появления ошибок в работе программы дает следующее процентное соотношение источников сбоев: Входные данные 1% Ошибки пользователя 5% Аппаратура 1% Системное программное обеспечение 3% Разработка системы 15% Программирование 75% Программист должен не только писать эффективные программы, но и находить в них всевозможные ошибки. Современная практика обучения программированию ориентирована, в основном, только на выполнение программистом первой половины своей работы. Это все равно, как если обучать летчика только взлету, предполагая, что с посадкой машины он как-нибудь разберется сам, если будет выполнять все операции взлета в обратном порядке. Существуют два типа программных ошибок: синтаксические ошибки — возникают из-за нарушения правил языка программирования. Такие ошибки обычно выявляются во время компиляции. Могут быть исключены сравнительно легко. Даже если не просматривать текст программы можно быть уверенным, что компилятор на стадии трансляции найдет ошибки и выдаст соответствующие предупреждения. Фактически поиск ошибок осуществляет компилятор, а их исправление — программист; семантические (логические) ошибки - те, что приводят к некорректным вычислениям или ошибкам во время выполнения (run-timeerror). Семантические, ошибки устраняют обычно посредством выполнения программы с тщательно подобранными проверочными данными, для которых известен правильный ответ. 5.2. ТЕСТИРОВАНИЕ Тестирование (testing) - любой вид деятельности, в рамках которого путем реального выполнения каких-либо задач проверяется соответствующая работа либо системы в целом, либо составной ее части. Тестирование программы (programtesting) - проверка, которая проводится в ходе прогона программы с целью убедиться, работает ли она так, как требуется. Это осуществляется при выполнении одного или нескольких тестовых прогонов, при которых в программную систему подаются входные (тестовые данные), а реакция системы фиксируется для последующего анализа. Может осуществляться как с ЭВМ, так и без ЭВМ. Один из главных законов тестирования гласит: «Тестирование программы или ее отдельных модулей не должен осуществлять программист (группа программистов), создавший эту программу или модуль». Для обеспечения достаточной степени надежности тестирования должен быть специальный отдел в фирме или привлечены программисты из сторонних организаций. Первоначально разработчик сам устраняет мелкие ошибки. Когда компиляция модуля завершается, и компилятор выдает соответствующее сообщение «Compilesuccessful", программист обычно запускает откомпилированный фрагмент и производит несколько тестов. После такого поверхностного тестирования разработчиком законченный модуль должен быть протестирован другим программистом. Дело в том, что разработчик изначально настраивается на какой-то один вид возможной ошибки и, не обнаружив ее при предварительном тестировании, не склонен проверять другие участки модуля. Сторонний программист рассматривает модуль, подключенный к программе как своего рода «черный ящик» и пытается запускать его на предельных нагрузках. Статистика свидетельствует, что стоимость тестирования составляет не менее 50% всей стоимости начальной разработки. Сколько бы сил, времени и денег не было потрачено на тестирование, один из главных законов программирования действует с неотвратимостью рока: «Тесты могут доказать наличие ошибок в программе, но они не могут доказать их отсутствия» (Э. Дейкстра «Заметки по структурному программированию»). При тестировании могут возникать следующие вопросы:
Ответы на них во многом зависят от того, что считать качественной программой? Качественная программа - это программа, выполняющая заранее объявленные действия известным способом и не выполняющая никаких необъявленных действий. Под объявленными действиями понимаются, в том числе, и алгоритмы обработки данных. Таким образом, тестирование должно выполняться до тех пор, пока программа не избавится от всех необъявленных действий (частным случаем которых является неверная обработка, то есть, в сущности, просто порча данных). 5.3. ХОД ТЕСТИРОВАНИЯ В процессе тестирования программного обеспечения осуществляются следующие виды деятельности: Ручной прогон. На этом шаге программист с помощью карандаша и листа бумаги моделирует прохождение данных через программу. При изучении текста программы от начала до конца, трудно проверить всевозможные комбинации данных. Самое большее, что можно сделать на практике, - проверить всевозможные типы или наиболее вероятные наборы комбинаций данных. Если они дают правильные результаты, предполагается, что непроверенные комбинации также дадут правильные результаты. Проектирование тестов. Является наиболее ответственным процессом. Очень часто тест создается вручную. Иногда применяют генераторы тестовых данных - специальные программы, формирующие данные в соответствии со спецификациями, задаваемыми программистом. Тестовые данные могут систематически или случайно выбираться из другого заданного набора данных для уменьшения их общего количества. Выполнение тестов. На этом этапе осуществляется проверка всех возможных алгоритмов специально подготовленными тестами, а также выявляется, насколько интерфейс программы выдержит реальную нагрузку. Проблема заключается в том, что тестирование происходит на очень ограниченных объемах данных. Когда база данных будет насчитывать десятки, а то и сотни тысяч записей, скорость выполнения запросов пользователей может стать неприемлемой. Изучение результатов тестирования. Выявление и устранение ошибок часто имеет циклический характер. Устранение одной ошибки может порождать другие ошибки. Особенно это касается работы с глобальными переменными, которые коварны тем, что нельзя сказать с полной уверенностью, что где-то на нижнем уровне подпрограмм изменение состояния переменной не приведет к новой ошибке. 5.4. АВТОНОМНОЕ ТЕСТИРОВАНИЕ МОДУЛЕЙ ПРОГРАММЫ Программа состоит из модулей, что сильно облегчает тестирование, так как каждый модуль может быть отдельно проверен для всех возможных комбинаций, поскольку их число меньше. Если программа хорошо структурирована, необходимо проверять только интерфейс между проверенными ранее модулями. Даже если проверяются все варианты интерфейса, это является посильной ношей как для программиста, так и для вычислительной системы. К сожалению, объектно-ориентированное программирование ускоряет создание сложных программ, но зато увеличивает число ошибок, которые к тому же очень тяжело искать. Автономное тестирование модуля должно как минимум обеспечивать прохождение всех путей вычислений. Проектная процедура тестирования модуля состоит из следующих действий:
5.5. МЕТОДЫ ТЕСТИРОВАНИЯ Существует два крайних подхода к проектированию тестов. Сторонники первого подхода создают тесты, исследуя внешние спецификации сопряжения программы или модуля, который необходимо протестировать. В этом случае программа является как бы «черным ящиком». Необходимо проверить все возможные комбинации и значения на входе. Обычно их слишком много даже для простейших алгоритмов. Для программы расчета среднего арифметического четырех чисел надо готовить 107 тестовых данных. Сторонники второго подхода связывают тесты только с логикой программы. При этом стремятся, чтобы каждая команда была бы выполнена хотя бы один раз. Цикл должен выполняться один раз, ни разу, максимальное число раз. Такое тестирование всех путей извне также недостижимо. В программе из двух последовательных циклов, внутри каждого из которых включено ветвление на десять путей имеется 1018 путей расчета. Чтобы построить разумную стратегию тестирования надо разумно сочетать оба этих подхода и пользоваться математическими доказательствами. Восходящее тестирование. Сначала автономно тестируются модули нижних уровней, которые не вызывают других модулей. При этом достигается такая же их высокая надежность, как и у встроенных в компилятор функций. Затем тестируются модули более высоких уровней вместе с уже проверенными модулями и так далее по схеме иерархии. Нисходящее тестирование. При этом подходе изолированно тестируется головной модуль или группа модулей головного ядра. Программа собирается и тестируется сверху вниз. Недостающие модули заменяются заглушками. Достоинством этого подхода является то, что тестирование модуля совмещается с тестированием сопряжений. Модифицированный нисходящий метод. Согласно этому методу каждый модуль автономно тестируется перед включением в программу, собираемую сверху вниз. Метод большого скачка. Каждый модуль тестируется автономно. По окончании автономного тестирования все модули интегрируются в готовую программную систему. Этот метод применяется, если программа мала и хорошо спроектирована по сопряжениям. Метод сэндвича. Представляет собой компромисс между нисходящим и восходящим подходами. По этому методу реализация и тестирование ведется одновременно сверху и снизу, и два этих процесса встречаются в заранее намеченной точке. Модифицированный метод сэндвича. Нижние модули тестируются снизу вверх, а модули верхних модулей сначала тестируются автономно, а затем собираются нисходящим методом. |