Pull to refresh

Проектирование по контракту

Reading time5 min
Views23K
Два года назад мне посчастливилось побывать на лекции замечательного человека, одного из разработчиков языка Eiffel, Бертрана Мейера. Он читал в нашем университете (СПб ГУ ИТМО) лекцию о довольно интересной концепции проектирования ПО. Называется она «проектирование по контракту». Суть этой концепции я попытаюсь описать ниже.

Вот, например, когда вы с клиентом договариваетесь о совместной работе, то вы заключаете контракт. Т.е. вы описываете обязанности обоих сторон и возможные последствия в случае неожиданных ситуаций. Данный подход можно применить и к разработке ПО, где в качестве сторон выступают программные модули.
Проектирование по контракту является довольно простой, но, в то же время, мощной методикой, основанной на документировании прав и обязанностей программных модулей для обеспечения корректности программы. Я считаю, что корректная программа – это программа, которая выполняет не больше и не меньше того, на что она претендует.
Центральными фигурами этой концепции являются утверждения (assertions) – это булевы выражения, описывающие состояние программы. Можно выделить три основных типа утверждений: предусловия, постусловия и инварианты класса.

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

Постусловия
Постусловия выражают состояния «окружающего мира» на момент выполнения подпрограммы. Т.е. это условия, которые гарантируются самой подпрограммой. Кроме того, наличие постусловия в подпрограмме гарантирует ее завершение (т.е. не будет бесконечного цикла, например).

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

Попробую сформулировать основную идею, которой пользуюсь я:
«Если клиент, вызывающий подпрограмму, выполняет все предусловия, то вызываемая подпрограмма обязуется, что после ее выполнения все постусловия и инварианты будут истинными».

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

Чтобы не быть голословным приведу пример (здесь и далее весь код написан на C#).
Давайте рассмотрим такую ситуацию: пользователь вводит код зап. части по каталогу и хочет получить информацию об этой детали. Известно, что код состоит из 9 символов. Вот классический пример реализации данной функции:

private ComponentProvider componentProvider;

public ComponentInfo GetComponentInfo(string id)
{
   if( String.IsNullOrEmpty(id) || id.Length != 9)
   {
      throw new Exception(“Wrong id”);
   }
   return componentProvider.GetComponent(id);
}


* This source code was highlighted with Source Code Highlighter.


Во многих классических учебниках по созданию «качественного ПО» этот пример назвали бы отличным. Но вот с точки зрения принципа проектирования по контракту этот пример является ошибочным.
Начнем с того, что проверку на валидность значения атрибута id должна осуществлять вызывающая программа. Ведь именно она (вызывающая программа) может воспользоваться несколькими вариантами: завершить работу, выдать предупреждение и начать считывать новое число. А может существует возможность вводить только последние 4 цифры, а первые пять программа сформирует исходя из VIN-номера автомобиля. В любом случае, какой бы вариант не использовался, он ни как не связан с функцией GetComponentInfo().
Тогда исходный пример перепишем в следующем виде

public ComponentInfo GetComponentInfo(string id)
{
   return componentProvider.GetComponent(id);
}


* This source code was highlighted with Source Code Highlighter.


А вот дальше начинается самое интересное :). Если уж мы заявили, что данная функция возвращает объект типа ComponentInfo, то мы должны обеспечить это. Ведь метод GetComponent объекта componentProvider может вернуть значение null. И тогда уже вызывающей программе придется делать проверку на null-значение, иначе можем «нарваться» на «object reference» исключение. Т.е. пример стоит переписать так:

public ComponentInfo GetComponentInfo(string id)
{
   ComponentInfo componentInfo = this.componentProvider.GetComponent(id);
   if(componentInfo == null)
   {
      throw new ContractException(“Can’t find component”);
   }
   return componentInfo;
}


* This source code was highlighted with Source Code Highlighter.


По крайней мере, так говорится во многих статьях и примерах. НО. Давайте рассуждать логически. Если уж мы используем принцип проектирования по контракту, то, опираясь на мое «золотое правило», мы можем быть уверены, что метод GetComponent() объекта componentProvider вернет нам истинное значение (т.к. его параметр по определению истинный). Поэтому, я не вижу смысла загромождать программу лишним кодом. Но с другой стороны, объект типа ComponentProvider может быть спроектирован сторонним разработчиком, который не придерживался принципа проектирования по контракту. Вот тут и встает дилемма. Вот мой совет для данной ситуации – если вы вызываете подпрограмму, которая была написана вами, то не пишите лишнего кода. Доверяйте себе. Но если вы вызываете подпрограмму, написанную сторонним разработчиком, и вы не уверены в ней, то произведите проверку. Самый наглядный пример – функция извлечения квадратного корня Math.Sqrt(). Понятно, что нельзя извлечь квадратный корень из отрицательного числа, но если в данную функцию передать отрицательное число, то никакого исключения сгенерировано не будет, а вернется значение типа NaN.

«Доверительный» вариант
public ComponentInfo GetComponentInfo(string id)
{
   try
   {
      return this.componentProvider.GetComponent(id);
   }
   catch(ContractException ex)
   {
     throw new ContractException(ex.ToString());
   }
}


* This source code was highlighted with Source Code Highlighter.


Все приведенные примеры основываются на некоторых ваших (команды разработчиков) соглашениях. Но существуют и специальные расширения для различных языков программирования. Например, препроцессор iContract для Java или расширение eXtensible C#.

Самое главное, что использование принципа проектирования по контракту поможет вам обеспечить автоматическое тестирование вашего кода.
Данную статью можно назвать введением в принцип проектирования по контракту. Если появится интерес со стороны пользователей, то я продолжу серию об этом принципе. Ведь все, что я описал – это лишь верхушка айсберга.
Tags:
Hubs:
+53
Comments56

Articles

Change theme settings