Говорим о новом

Design by contract

Обзор гема contracts.ruby

Автор: Давыденков Михаил

Основная идея, история и требования к языку

  • Контракты предусматривают взаимные обязательства и преимущества (отношения клиент - поставщик).
  • Из языка Eiffel (Бертран Мейер). На уровне языка поддерживается в Clojure, Eiffell и других менее известных языках. В большинстве языков поддержка с помощью сторонних библиотек.
  • ЯП должен поддерживать наследование, динамическое связывание, обработку исключений и автоматическое документирования кода.

Что входит в контракт метода или функции

  • Обязательства (предусловия)
  • Свойства (постусловия).
  • Инварианты (валидации). State объекта, который должен оставаться консистентным до и после вызова метода/функции.

Контракты в Ruby


            gem install contracts # inspired by contracts.coffee
            require 'contracts' # в application.rb
            include Contracts

            Contract Num => Num
            def double(x)
              x * 2
            end
            puts double("oops")
          

Пример нарушения контракта


          ./contracts.rb:34:in 'failure_callback': Contract violation: (RuntimeError)
              Expected: Contracts::Num,
              Actual: "oops"
              Value guarded in: Object::double
              With Contract: Contracts::Num, Contracts::Num
              At: main.rb:6
              ...stack trace...
          

Структура failure_message


          {
            arg: the argument to the method,
            contract: the contract that got violated,
            class: the method's class,
            method: the method,
            contracts: the contract object
          }
          

Встроенные контракты

contracts.ruby предусматривает большое количество встроенных контрактов:


          Num, Pos, Neg, Nat, Bool, Any, None, 
          Or, Xor, Not, 
          ArrayOf, HashOf, Maybe, 
          RespondTo[:password, :credit_card], Send[:valid?], Exactly[Numeric]
          

Создание собственных контрактов

Контракты очень просто создать. Контрактом может быть:

  • Название класса (String или Fixnum)
  • Константа (nil или 1)
  • Proc, принимающий значение и возвращающий true/false
  • Класс, имеющий класс метод valid?
  • Объект, имеющий метод valid?

Кастомизации

Переписывание failure_callback


          # initializer
          Contract.override_failure_callback do |data|
            Rails.logger.error format(data)
            Airbrake.notify_or_ignore(error_from_data(data))
          end
          

Переписывание сообщений об ошибках


            def Num.to_s
              "a number please"
            end
          

Pattern matching

Wihtout


          Contract Num => Num
          def fact x
            if x == 1
              x
            else
              x * fact(x - 1)
            end
          end
          

With


          Contract 1 => 1
          def fact x
            x
          end

          Contract Num => Num
          def fact x
            x * fact(x - 1)
          end
          

Pattern matching 2


            Contract lambda{|n| n < 12 } => Ticket
            def get_ticket(age)
              ChildTicket.new(age: age)
            end

            Contract lambda{|n| n >= 12 } => Ticket
            def get_ticket(age)
              AdultTicket.new(age: age)
            end
          

Контракты в модулях


            module M
              include Contracts
              include Contracts::Modules

              Contract String => String
              def self.parse
                # do some hard parsing
              end
            end
          

Инварианты


            include Contracts::Invariants

            Invariant(:day) { 1 <= day && day <= 31 }
            Invariant(:month) { 1 <= month && month <= 12 }

            Contract None => Fixnum
            def silly_next_day!
              self.day += 1
            end
          

Производительность

Заявлено, что контракты имееют минимальный slowdown по производительности. Бенчмарки метода, возвращающего сумму двух чисел (1_000_000 раз):


                                  total        real
          testing read            2.530000 (  2.521314)
          testing contracts read  2.900000 (  2.903721)
          

По-моему, этот гем - хорошое начинание и может быть применён по ситуации (в маленьких хоум проджектах). Правда придётся чаще пользоваться бенчмарками, т.к. 100% доверия гему нет. Библиотеки с ним особо не попишешь, т.к. есть несколько пока нерешённых issues, связанные с динамическим созданием кода

Пример реализации контрактов в rails приложении