WebテストでActiveRecordを使いたい!
これは CAMPHOR- Advent Calendar 2020 (https://advent.camph.net/) の13日目の記事です
背景
インターンのWebテストを受けていると、たまに ActiveRecord
っぽいものが欲しいなーと思うことがあります。
「あなたは〇〇のシステムを作っています」系の問題で、実際の構成ならデータベースを使う部分です。
大体の場合は保存したいデータのクラスを定義して、配列や連想配列をデータベースの代わりに使うことになると思います。
しかし時間が限られている中で上記を実装していると
- 時間がかかる
- コードが読みづらくなる
- バグを生む
ということが発生しやすいです。
そこであらかじめ仕組みを作っておいて、一瞬で かつ きれいなコードで 上記を実現したいと思いました。
タイトルに ActiveRecord
とありますが、Ruby
を使う人に限らず有効ではないかと考えています。
私は普段 Ruby on Rails
を使用しているので、今回は ActiveRecord
と似た記述で簡潔にかけることを目指しました。
具体例
まず簡単な例を考えてみます。
たとえば会計システムでポイントカードを扱いたいとします。ポイントカードは
プロパティ | 意味 | 例 |
---|---|---|
id | ID | '000001' |
point | ポイント合計 | 100 |
grade | グレード(0~3) | 1 |
のような情報を持っているとします。
その場合まずは
class PointCard attr_accessor :point, :grade def initialize(id, point, grade) @id = id @point = point @grade = grade end end
のようなクラスを作るのではないでしょうか。
そして作成したインスタンスを保存するために
class PointCard @@db = {} attr_accessor :point, :grade def initialize(id, point, grade) @id = id @point = point @grade = grade @@db[id] = self # new でできた自分自身をクラス変数に保存 end def self.db # PointCard.db で @@db を取り出せるようにする @@db end end
のようにするかもしれません。
ここまで来ると
PointCard.new('000001', 0, 0) # インスタンス作成 PointCard.new('000002', 0, 0) # インスタンス作成 point_card = PointCard.db['000001'] # id が '000001' のものを取得 point_card.point += 100 # ポイント加算
のようなこともできますし、たとえばポイント付与率が grade * 0.01
だとすると
class PointCard ... def rate grade * 0.01 end end
のようなものを定義することで、1000円のものを買った際のポイント加算は
point_card.point += 1000 * point_card.rate
と書けるようになります。
ただし限られた時間の中で
「長文を読む」-> 「要件を正しく把握する」-> 「上記を実装」-> 「上記を利用して、処理全体を完成させる」
という流れを完璧にこなすのは大変で、クラス定義でミスをするとそのまま時間切れになることもあります。また「標準入力」からの値の受け渡しなどもでてきて時間を食うことがあります。
本編
そこで以下のようなコードを作成しました
class MemoRecord @@count = 0 @@db = [] def initialize(hash) hash.merge!({ :id => @@count += 1}) if hash[:id].nil? hash.each do |key, value| eval("@#{key} = value") eval("MemoRecord.attr_accessor :#{key}") end end def save @@db << self if MemoRecord.find(self.id).nil? true end def self.create(hash) (@@db << MemoRecord.new(hash))[-1] end def update(hash) hash.each { |key, value| eval("@#{key} = value")} self end def destroy @@db.delete_if { |obj| obj == self } end def self.find(id) @@db.find { |obj| obj.id == id } end def self.find_by(hash) hash.each do |key, value| return @@db.find { |obj| obj.send(key) == value } end end def self.where(hash) hash.each do |key, value| return @@db.find_all { |obj| obj.send(key) == value } end end def self.all @@db end end
ただしこれはメンテ用で、実際に使う時には
class MemoRecord;@@c=0;@@d=[] def initialize(h);{id:@@c+=1}.merge(h).each{|k,v| eval("@#{k}=v;MemoRecord.attr_accessor :#{k}")};end def save;@@d<<self if MemoRecord.find(self.id).nil?;true;end def update(h);h.each{|k,v| eval("@#{k}=v")};self;end def destroy;@@d.delete_if{|o| o==self};end;def self.find(i);@@d.find{|o| o.id==i};end def self.create(h);(@@d<<MemoRecord.new(h))[-1];end;def self.all;@@d;end def self.find_by(h);h.each{|k,v| return @@d.find{|o| o.send(k)==v}};end def self.where(h);h.each{|k,v| return @@d.find_all{|o| o.send(k)==v}};end end
くらいまで圧縮しておきます。
上記の9行をペタッと貼り付けます。そして
class PointCard < MemoRecord end
とすることで以下のようなことができるようになります。
作成
point_card = PointCard.new(id: '000001', point: 0, grade: 0) point_card.save
(idを指定しない場合は、作成した順番に自動的にidが付きます)
あるいは
point_card = PointCard.create(id: '000001', point: 0, grade: 0)
で作成できます
更新
point_card.update(point: 100)
point_card.update(point: 100, grade: 1)
削除
point_card.destroy
検索
idで検索
PointCard.find(id)
プロパティで検索(一致する最初の一件)
PointCard.find_by(grade: 3)
プロパティで検索(一致するすべてのデータ)
PointCard.where(grade: 3)
全件取得
PointCard.all
のようなことができます
先ほどのコードで「条件による絞り込み」や「プロパティの同時更新」などをするのは手間がかかります。
もちろんメソッドを探せば書けますが、「時間がかかる」ことや「コードがみづらい」ことを考えると
PointCard.where(...)
のようにさっと書けるほうが良さそうだと考えました。
※ 上記の実装は未完成で、まだまだ改善の余地があります。
またWebテスト上でエラー処理を書きたくないので、find(id)
で合致するid
がなくてもnil
を返したり、1度save
をしていると2回目以降はsave
しなくて良い(@@dbに参照渡しされているので。その代わりsaveせずに捨てるというのは初回saveまでしかできない。)など、独自のルールを作っています。
あと最大の問題が残っていまして、MemoRecord
を継承すると @@db
も渡ってしまうので、2つ以上クラスを使いたいときは MemoRecord
のコードをコピペして MemoRecord2
のようにする必要があるということです。
それでもコードがわかっていれば(ルールに従えば)簡潔に書けるので、カチッとハマる問題がでたら使えそうだと思っています。
まとめ
今回実装していて、以下のようなことを思いました。
- 競プロではライブラリを使うことが当たり前なのに、要件を満たすコードを書く系の問題ではそういう文化が無いと感じた
- 「なんちゃってライブラリを作る」「解答のコードのスペースを取らないように限界まで短く書く(コードゴルフ)」というのは勉強になるし、面白い
とりあえず「ActiveRecord」を知っていたら、完全に感覚で使えるところまで頑張りたいと思います。 役に立つか/立たないかは置いといて、作っていて面白かったので、みなさんもぜひ自分の好きな言語でやってみてください!それでは!