Тверской государственный университет
Опубликован: 03.10.2011 | Доступ: свободный | Студентов: 3284 / 60 | Оценка: 4.33 / 3.83 | Длительность: 19:48:00
ISBN: 978-5-9963-0573-5
Лекция 5:

Интерфейс класса

< Лекция 4 || Лекция 5: 1234 || Лекция 6 >

4.5. Команды

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

Построение линии метро

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

Если вы думаете: "Это чепуха! Программа не может создавать станции метро, и линии метро уже существуют", – то вам, вероятно, полезно перечитать тот раздел, где объясняется, что наши объекты являются искусственными созданиями – артефактами – ПО, они не являются реальными вещами.

Давайте перестроим Линию 8. О классе TOURISM можно предположить следующее: предопределенные методы, такие как Station_Balard, Station_La_Motte и другие, доступны для каждой станции. Имя метода для станции "xxx" строится как Station_Xxx; Если имя содержит несколько слов, как для станции "La Motte", то слова разделяются подчеркиванием – Station_La_Motte.

Поскольку Line8 предопределена в классе TOURISM, первое, что нам предстоит, – удалить из нее станции, сделав ее пустой. В контрактном облике класса SIMPLE_LINE вы могли заметить присутствие подходящей для этих целей команды:

remove_all_segments
      -- Удалить все станции, за исключением южного конца.

Наша программа будет использовать этот метод в вызове:

Line8.remove_all_segments

Напомним, что по соглашению наши линии всегда имеют по крайней мере одну станцию. Когда она только одна, как в результате вызова remove_all_segments (который, как указано в заголовочном комментарии, оставляет южную станцию), она является результатом обоих запросов south_end и north_end.

Теперь мы готовы добавлять станции. Снова вы можете видеть релевантные команды в контрактном облике класса:

extend (s: STATION)
      -- Добавить s в конец этой линии.

Это значит, что если li означает линию, то в ее конец можно добавить станцию st, благодаря вызову

li.extend (st)

Теперь можно заполнять текст примера этой лекции:

class QUERIES_AND_COMMANDS inherit
  TOURISM
feature
    tryout
      -- Воссоздать частичную версию Линии 8.
      do
        Line8.remove_all_segments
              -- Нет необходимости в добавлении Station_Balard, так как
              -- удаляются все сегменты, оставляя южный конец.
        Line8.extend (Station_La_Motte)
        Line8.extend (Station_Concorde)
        Line8.extend (Station_Invalides)
              -- Мы прекратим добавлять станции, чтобы отобразить
              -- полученные результаты:
        Console.show (Line8.count)
        Console.show (Line8.north_end.name)
      end
end

Время теста

Имя последней станции

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

Чтобы убедиться в корректности ваших рассуждений, выполните этот пример.

4.6. Контракты

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

Предусловия

Интерфейс для запроса i_th в классе SIMPLE_LINE, как показано ранее не упоминает, что только некоторые значения i имеют смысл: значение должно быть между 1 и числом станций на линии, count. Если Линия 8 имеет 20 станций, то было бы ошибкой использовать запрос ,Line8.i_th (300), или Line8.i_th (0), или Line8.i_th (–1).

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

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

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

Предупреждение: нерекомендуемый стиль, смотри далее.

i_th (i: INTEGER): STATION
        --i-я станция на этой линии
        --(Предупреждение: используйте запрос только при i между 1 и count,
        -- включительно)

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

Интерфейс метода показывает контракт, используя ключевое слово require. Поэтому контрактный облик класса LINE фактически описывает запрос i_th следующим образом

i_th (i: INTEGER): STATION
          -- i-я станция на этой линии
      require
          not_too_small: i >= 1
          not_too_big: i <= count

Предложение предусловия состоит из двух раздельных элементов, называемых утверждениями (assertions). Каждое выражает свойство: i ≥ 1 в первом утверждении и i ≤ count – во втором. Заметьте, из-за ограничений клавиатуры мы не можем для неравенств использовать обозначения, принятые в математике, и используем для неравенств два символа. Имена утверждений not_too_small и not_too_big называются тегами утверждений (assertion tags). Они служат для прояснения цели утверждений, но фактический смысл (семантика) следует из выражений: i >= 1 и i <= count. Мы можем опускать теги утверждений и двоеточие, как в следующем фрагменте:

require
  i >= 1
  i <= count

Смысл предусловий не меняется, но теги придают ясность, так что их необходимо включать, следуя правилам хорошего стиля.

Выражения, подобные i >= 1 и i <= count, обозначают условия, которые в любой момент выполнения программы могут быть либо истинными, либо ложными. Предыдущие примеры включали эквивалентность l.south_end = l.i_th (1), заданную для линии l. Выражения, которые имеют истинностные значения, записываемые в Eiffel как True и False, называются булевскими (boolean):

Определение: булевское значение

Булевское значение может быть либо True, либо False.

Соответствующий тип называется BOOLEAN. Это один из типов, имеющихся в нашем распоряжении наряду с INTEGER, STRING и именами определенных вами классов. Большинство типов, в отличие от булевского типа, имеют много значений, например, представление целого типа в компьютере поддерживает миллиарды возможных значений, но у типа BOOLEAN значений только два. Этот тип используется для представления условий, подобно тому, как это делается в обычном, не программистском мире ("достаточно ли снега" – условие, позволяющее решить, стоит ли кататься на лыжах). Наши булевские выражения в мире ПО должны быть определены совершенно точно, как и в мире математики: выражение i >= 1 недвусмысленно имеет значение true или false, если известно значение i, в то время как условие "достаточно ли снега" субъективно и зависит от решения субъекта, собирающегося кататься на лыжах.

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

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

require
  not_too_small: i >= 1
  not_too_big: i <= count

Клиент, не удовлетворяющий свойству, например, вызывающее создает неисправное ПО, пораженное "багом" ("жучком"), где баг в компьютерной терминологии – аналог ошибки.

Line8.i_th (1000)

Мы можем выразить это наблюдение как некоторый общий принцип.

Почувствуй методологию:

Принцип Предусловия

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

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

Некоторые методы всегда применимы; они не имеют предложения require. По соглашению это эквивалентно присутствию предусловия в форме: которое говорит, что предусловие всегда выполняется.

require
  always_OK: True

Контракты для отладки

Один из способов, благодаря которому предусловия и другие контракты помогают в разработке ПО, состоит в существовании инструментария, способного проверить контракты во время выполнения программы. Так что, если один из контрактов не работает, что свидетельствует о наличии жучка, то вы получите ясное сообщение, уведомляющее о случившемся. Сообщение укажет тег (такой как not_too_small) с нарушенным утверждением, и вы будете знать, что пошло не так, как требуется.

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

Раздел приложения EiffelStudio скажет вам, как настраивать параметры, определяющие, какие из контрактов будут проверяться во время выполнения.

Контракты для документирования интерфейса

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

Этот стиль, иллюстрируемый интерфейсом запроса i_th, станет стандартной формой описания интерфейса в оставшейся части этой книги. Это также тот вид представления интерфейса класса, который вы получаете, работая в EiffelStudio, – он, соответственно, назван контрактным обликом класса.

Постусловия

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

В отличие от предусловий, не всегда удается выражать постусловия полностью в виде релевантных свойств, но зачастую можно сказать много полезного. Вот пример интерфейса для метода remove_all_segments в классе LINE:

remove_all_segments
      -- Удалить все станции, кроме южного конца.
  ensure
      only_one_left: count = 1
      both_ends_same: south_end = north_end

Здесь нет предусловия, поскольку метод remove_all_segments всегда применим к маршруту. Ключевое слово ensure вводит постусловие. В нем метод гарантирует по окончании своей работы своим клиентам две вещи:

  • число станций, count, будет равно 1;
  • две конечные станции, south_end и north_end, теперь будут одной и той же станцией. Напомним, что это соответствует принятому соглашению: пустая линия не имеет сегментов; у нее только одна станция, которая является и южной, и северной.

Аналогичная ситуация (предусловие опущено) и для интерфейса команды extend, добавляющей станцию в конец линии:

extend (s: STATION)
      -- Добавить s в конец линии.
    ensure
      new_station_added: i_th (count) = s
      added_at_north: north_end = s
      one_more: count = old count + 1

Первое предложение постусловия использует запрос i_th. В нем утверждается, что если мы спросим после вызова команды extend, какая станция в позиции count (это эквивалентно вопросу, какая станция является последней), то ответ будет – s, станция, только что добавленная на линию. Так мы точно выражаем наше намерение относительно цели команды extend. Если текст команды написан без ошибок, то это свойство всегда будет существовать после окончания выполнения метода

Второе предложение говорит, что после выполнения команды конечной станцией north_end также будет s. Из инварианта, с которым мы познакомимся в следующем разделе, следует, что north_end должно быть эквивалентно i_th (count), так что наше предложение фактически избыточно, но это не должно нас тревожить.

Третье предложение говорит нам, что метод увеличивает на единицу число станций на линии. Оно использует ключевое слово old, с которым мы еще не встречались. Постусловие устанавливает свойство, существующее при завершении вызова метода. Часто необходимо связать значение выражения, полученного в результате выполнения, со значением на входе в процедуру. Отсюда следует полезность "Old" (старого выражения) в форме:

old some_expression

Это означает: "Значение some_expression, вычисленное в момент начала выполнения программы". Этим и определяется смысл предложения постусловия Old-выражения и ключевое слово old могут появляться только в постусловиях.

count = old count + 1

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

Почувствуй методологию:

Принцип Постусловия

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

Инварианты класса

Предусловия и постусловия являются логическими свойствами вашего ПО, каждое из них связано с определенным методом, скажем, i_th, remove_all_segments и extend, появлявшимся в вышеприведенных примерах.

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

Мы уже встречались с такими свойствами в примерах с линиями метро:

  • соглашение о том, что линия метро содержит по меньшей мере одну станцию;
  • наблюдение о свойствах линии l:

    l.south_end = l.i_th (1) и

    l.north_end = l.i_th (l.count).

Эти свойства верны для всех линий, а не только для конкретной линии l. Другими словами, они характеризуют класс в целом. Поэтому такие свойства являются инвариантами класса. Они появляются в конце текста класса в виде специальных предложений:

invariant
   at_least_one_station: count >= 1
   south_is_first: south_end = i_th (1)
   north_is_last: north_end = i_th (count)
   identical_ends_if_empty: (count = 1) implies (south_end = north_end)

Последнее выражение использует изучаемую в следующей лекции логическую операцию implies (следование, импликация), из истинности импликации a implies b следует, что b имеет значение True всегда, когда a имеет значение True.

Этот пример типичен для роли инвариантов класса: выражает согласованные требования к запросам класса. Здесь эти требования отражают некоторую избыточность, существующую между запросами south_end и north_end класса LINE, которые поставляют информацию, которая также доступна через запрос i_th, применимый с аргументами 1 и count.

Другим примером мог бы служить класс CAR_TRIP, обеспечивающий запросы: initial_odometer_reading, trip_time, average_speed и final_odometer_reading. Их имена позволяют судить об их роли – " odometer reading " – это общее число пройденных километров во время путешествия. И снова здесь присутствует избыточность, отражаемая в инвариантах класса:

invariant
  consistent: final_odometer_reading = initial_odometer_reading +
               trip_time * average_speed

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

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

Почувствуй методологию: Принцип Инварианта класса

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

Контракты: определение

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

Определение: Контракт

Контракт – это спецификация свойств программного элемента, предназначенная для потенциальных клиентов.

Мы будем использовать контракты повсюду в ПО, с целью сделать ясным каждый его элемент – класс или метод. Как отмечалось, они служат для документирования ПО, особенно библиотек компонентов, предназначенных (подобно Traffic) для повторного использования многими приложениями. Контракты помогают в отладке, помогают избежать появления ошибок и, прежде всего, создавать корректное ПО.

4.7. Ключевые концепции, изученные в этой лекции

  • Программный элемент представляет один или несколько интерфейсов остальному миру.
  • Классы существуют только в программных текстах. Объекты существуют только во время выполнения ПО.
  • Класс описывает категорию возможных объектов.
  • Каждый запрос возвращает результат, тип которого указан в объявлении запроса
  • Мы можем специфицировать интерфейс класса через "контрактный облик", который перечисляет все компоненты класса – команды и запросы – и для каждого из них свойства, важные для клиентов (других классов, использующих класс поставщика).
  • Метод может иметь предусловия, которые специфицируют начальные свойства, позволяющие "законно" вызывать метод, и постусловия, которые задают финальные свойства, гарантируемые при завершении метода.
  • Класс может иметь инварианты, которые специфицируют согласованные условия, связывающие значения различных запросов класса.
  • Предусловия, постусловия и инварианты являются примерами контрактов.
  • Наряду с другими применениями контракты помогают при проектировании ПО, его документировании и отладке.

Новый словарь

API Абстрактный программный интерфейс Assertion Утверждение Assertion tag Тег утверждения
Boolean Булевский Bug Баг (жучок, ошибка) Class invariant Инвариант класса
Client Клиент Client Programmer Клиент-программист Contract Контракт
Generating class Генерирующий класс GUI Графический интерфейс пользователя Implementation Реализация
Instance Экземпляр Interface Интерфейс Library Библиотека
Postcondition Постусловие Precondition Предусловие Program interface Программный интерфейс
Software design Проектирование ПО Supplier Поставщик Type Тип
User interface Интерфейс пользователя

4-У. Упражнения

4-У.1. Словарь

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

4-У.2. Карта концепций

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

4-У.3. Нарушение контрактов

  1. Напишите простую программу (начав с примера этой лекции), в которой использовался бы запрос i_th класса LINE. Выполните его, используя известный LINE-объект, например Line8.
  2. Измените аргумент, передаваемый запросу i_th, так, чтобы аргумент выходил за предписанные границы (меньше 1 или больше числа станций на линии). Выполните программу снова. Что произошло? Объясните сообщение, которое вы получите.

4-У.4. Нарушение инварианта

Инвариант должен выполняться при создании объекта, затем перед и после выполнения методов, вызванных клиентом. Однако не требуется выполнение инварианта во время выполнения метода. Можете ли вы придумать пример метода, при выполнении которого было бы разумно нарушить инвариант, а затем в конце восстановить его корректность?

4-У.5. Постусловие против инварианта

Возможно, вы не уверены, стоит ли включать некоторое условие в постусловие метода или включить его в инвариант класса. Какие критерии могут помочь в решении этой дилеммы?

4-У.6. Когда писать контракты

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

< Лекция 4 || Лекция 5: 1234 || Лекция 6 >
Кирилл Юлаев
Кирилл Юлаев
Федор Антонов
Федор Антонов

Здравствуйте!

Записался на ваш курс, но не понимаю как произвести оплату.

Надо ли писать заявление и, если да, то куда отправлять?

как я получу диплом о профессиональной переподготовке?