ガード句でRubyの防御力を上げる!gem Contract

  • 3
    いいね
  • 0
    コメント

今回紹介するのはRubyで型を縛る方法です!
(phperの方はごめんなさい!)

〜 読んでいただきたい人 〜

  • ジェンガコードに怯えている方
  • 人とコミュニケーションが取りづらい方
  • 人の入れ替わりが激しいプロジェクトにいる方
  • 適当なコメントやコメントが無い事に悩まされる方
  • エンジニアの皆さん

型を縛る??とは

今回はRubyのgem Contractsを使用します。
このgemは「メソッドの引数と戻り値が、ある条件を満たしているかをチェック」できるものです。

使用方法

インストール

まずはgemfileに記述します。

Gemfile
gem 'contracts'

ファイルへの記述

Contractsをclassやmoduleにincludeします。

sample.rb
class Sample
  include Contracts

これで使用できる準備ができました!

簡単な使用例

さっそく例に移ります!
まずは成功例からです。

class MyCalculate
  include Contracts

  Contract Num, Num => Num
  def self.add(a, b)
    a + b
  end
end

puts MyCalculate.add(1, 1)

解説です。
MyCalculateClassのaddメソッドの引数として2つの引数Numeric型を指定して出力しています。

Contractsの設定は
1.MyCalculateClassのaddメソッドの引数が 2つともNumeric型 であること
2.MyCalculateClassのaddメソッドの戻り値が Numeric型 であること

を確認しています。
ここでは問題なく成功したのでなにもおこりません。

こちらは失敗例です。

class MyCalculate
  include Contracts

  Contract Num, Num => Num
  def self.add(a, b)
    a + b
  end
end

puts MyCalculate.add(1, "good!!")

解説です。
MyCalculateClassのaddメソッドの引数として Numeric型・String型 を指定して出力しています。

先ほどと同様でContractの設定は
1.MyCalculateClassのaddメソッドの引数が 2つともNumeric型 であること
2.MyCalculateClassのaddメソッドの戻り値が Numeric型 であること

を確認しています。

しかしNumeric型を期待していたのにString型が指定されました。
そのため、これを実行すると以下のように例外が発生します。

./contracts.rb:4:in `failure_callback': Contract violation: (RuntimeError)
    Expected: Num,
    Actual: "good!!"
    Value guarded in: Object::add
    With Contract: Num, Num    

解説です。
1.Expected: Numで期待(予想)している引数はNumeric型であることがわかります。
2.Actual: "good!!"で実際に引数で指定した値を出力しています。

Numeric型が来るはずなのに値がNumeric型じゃ無いですよ!と出力されています。

値の型チェック

例1)

Contract String => nil
def greet(name)
  puts "hello, #{name}!"
end

解説です。

Contractsの設定は
1.greetメソッドの引数がString型であること
2.greetメソッドの戻り値がnilであること

を確認しています。

先ほどと違うところはメソッドの戻り値が何もない場合
nilを指定できるところです。

例2)

Contract ArrayOf[Num] => Num
def count_total_numbers(nums)
  nums.inject(:+)
end

解説です。
Contractsの設定は
1.count_total_numbersメソッドの引数が Array型 で配列の中は Numeric型 はあること
2.count_total_numbersメソッドの戻り値が Numeric型 であること

を確認しています。

この場合、配列の中まで確認することができます。

成功 : count_total_numbers([1, 2, 3, 4]
失敗 : count_total_numbers([1, 2, 3, "foo"])

そのほか、Contractsに標準で組み込まれている様々な型チェックが使用できます。

実際に業務でContractsを使用した際に、頻出した型チェック(のリスト)です。

型     確認できる型
Bool 真偽値(true or false)をチェックできます。
Or 型の条件が2つ存在する場合にどちらか一方の型をチェックできます。
HashOf Hash型の時に使用します。例)HashOf[Symbol,String] Hash値の中の型まで見ることができます。
Maybe 型にnilが来る場合に使用します。 Or[String,nil]と同じ意味です。
Any 型を見ずに何かしらの値が来ていることをチェックできます。
例えばHashが入れ子になりすぎている状態だと長すぎて邪魔なためAnyで省略したり
rubyでは最後に評価した値が戻り値になるので
引数や戻り値を見る必要がない場合などに使います。

もっと詳しい情報はこちらのリファレンスページに記載されています。

自作のContracts

型自体が標準で組み込まれていない、開発者側が定義した型が存在する場合
自分で型チェックを作ります!σ゚ロ゚)σ

Contractの標準で組み込まれているメソッドはこのようになっています。

class Num
  def self.valid? val
    val.is_a? Numeric
  end
end

解説です。

classメソッドにvalid?をつけることによってバリデーションを使用します。
引数で渡ってきた値をis_a?メソッドで型の判定をしています。

標準メソッドを応用して新たにチェックするメソッドを作成します。
railsのlibディレクトリの中にCustomContractsディレクトリを作成し
CustomContracts配下にUserファイルを作成しました。

module CustomContracts
  class User
    def self.valid? val
      val.is_a?(::User)
    end
  end
end  

呼び出し

Contracts CustomContracts::User => String

解説です。
基本的に標準メソッドと同じですがここでチェックしている型は
User(UserClass)型です。
呼び出し側はクラスメソッドになっているのでCustomContracts::User
で呼び出せます。

is_a?(::User)is_a?(User)にしてしまうとCustomContractsのUserクラス
を見てしまうのでこのようにしています。

最後に

最後までご覧いただきありがとうございます!

実際にContractsをプロジェクトに導入すると
メソッドの引数に制限が加えられたので、想定していない型で引数が
セットされることがなくなりました。
つまり、型の不一致によるバグを事前に防ぐことが出来ます!

皆さんも是非使ってみてはいかがでしょうか?

ネオキャリアのエンジニアブログでも同じ記事を投稿しています。
こちらのブログも随時更新していきますので宜しくお願いします。