背景
データベースのテーブル設計において、様々なカラムに対して NULL
を許容する場合があります。
カラムが NULL
となった際に、どのような振る舞いをすべきか? というのはアプリケーションに依存しますが、得てして、プログラムコードを複雑にしがちです。
この記事では、 Ruby on Rails の ActiveRecord を題材として、複雑性を低減させる為の一つの手法を紹介します。
やり方
- 多様な値を取りうるカラムに直接アクセスすることをやめて、別のメソッドを通して間接的にアクセスする
- 新規定義するメソッドでは、
NULL
か否かに関わらず、同じような振る舞いをするようにする -
NULL
が、どのような振る舞いであるべきか?に基づいて、新しいメソッドを設計する
至極当たり前のことな気がしますが、ちょっとした工夫で複雑性が大きく低減されるので、実例を見ていくことにしましょう。
例
数値型の場合
例えば、魔法の箱が存在し、中に入れるものに対して、許容される最小重量と、最大重量がある場合を考えてみましょう。
NULL
の場合は、それぞれ、「最小重量なし」、「最大重量なし」という仕様にして、最小限の validation を入れます。
create_table :magical_boxes do |t|
t.float min_weight
t.float max_weight
end
class MagicalBox < ApplicationRecord
validates :min_weight, numericality: { greater_than_or_equal_to: 0 }, allow_nil: true
validates :max_weight, numericality: { greater_than_or_equal_to: 0 }, allow_nil: true
end
ここに対して、以下の2つを追加実装したいと思います。
- 最大重量(
max_weight
)は 最小重量(min_weight
) 以上であるという大小関係に関する validation の追加 - 重量 (
weight
) を引数として受け取り、それが許容可能かを返すMagicalBox#allow_weight?
の実装
before (素直に実装した場合)
class MagicalBox < ApplicationRecord
validates :min_weight, numericality: { greater_than_or_equal_to: 0 }, allow_nil: true
validates :max_weight, numericality: { greater_than_or_equal_to: 0 }, allow_nil: true
validate :validate_weight
def validate_weight
return if !min_weight || !max_weight # 何れかが定義されていれば大小関係は問題ない
if max_weight < min_weight
errors.add(:max_weight, 'must be greater than or equal to `min_weight`')
end
end
def allow_weight?(weight)
if min_weight && weight < min_weight # 定義されている場合だけ大小関係のチェックを入れる
return false
end
if max_weight && weight > max_weight # 定義されている場合だけ大小関係のチェックを入れる
return false
end
true
end
end
nil
のハンドリングが様々な場所で要求されるようになっています。
素直なコードなので分かりやすいというメリットはありつつ、 null
の場合に動くのか? というのを頭の中で考え続ける必要があります。
after (工夫をした場合)
やり方を実践していきます。
- 多様な値を取りうるカラムに直接アクセスすることをやめて、別のメソッドを通して間接的にアクセスする
-
nil
を返したり、Float
を返したりする、max_weight
,min_weight
へのアクセスをやめてみます
-
- 新規定義するメソッドでは、
NULL
か否かに関わらず、同じような振る舞いをするようにする-
NULL
以外の場合と同じように、Float
として振る舞ってもらえると嬉しい感があります
-
-
NULL
が、どのような振る舞いであるべきか?に基づいて、新しいメソッドを設計する- 「最小重量なし」、「最大重量なし」 という仕様は、 「最小重量が0である」「最大重量が無限大である」とみなせます。
これらを踏まえて、 numerical_min_weight
, numerical_max_weight
を定義してみます。
class MagicalBox < ApplicationRecord
validates :min_weight, numericality: { greater_than_or_equal_to: 0 }, allow_nil: true
validates :max_weight, numericality: { greater_than_or_equal_to: 0 }, allow_nil: true
validates :numerical_max_weight, numericality: { greater_than_or_equal_to: :numerical_min_weight }
def allow_weight?(weight)
numerical_min_weight <= weight && weight <= numerical_max_weight
end
def numerical_min_weight
min_weight || 0.0 # nilの場合は最小重量が0であるとみなせる
end
def numerical_max_weight
max_weight || Float::Infinity # nilの場合は最大重量が無限大であるとみなせる
end
end
このようにすると、 nil
の場合に、「どういう振る舞いをすべきか?」という仕様が、
numerical_min_weight
や numerical_max_weight
というメソッドに、仕様通りに隠蔽されます。
仕様に注目することで、これらを扱うメソッド群は、 NULL
という実装の詳細に依存するのではなく、
「数値で表した最小重量」「数値で表した最大重量」という、抽象的な仕様に依存することが可能になり、
nil
チェックを省略できるだけでなく、コード全体の見通しも良くなります。
ちょっとした工夫で、コードをシンプルにすることが可能な場合もあるので、
もしお困りの方がいらっしゃれば、是非お試しあれ。(もちろん、やりすぎは禁物です。)