はじめに
こんにちは、ITANDI株式会社でITANDI BBの開発をしている渡邉(海)です。 新卒4年目で、今年の2月末までは、toC向けのお部屋探しサイトの OHEYAGOを担当していました。 今年の3月からITANDI BBでサーバサイドを中心に開発しています。 ITANDI BBとは、ITANDIが提供している不動産会社向けの業者間サイトのことです。 バックエンドはRails、フロントはReact、インフラはAWS、エラー監視としてSentryを使っています。 趣味はバスケで、月に数回は行っています!
社内利用のAPIの検索結果を、オブジェクト化した際の知見を共有できればと思います。
背景
バックエンドから、社内の物件データ基盤の物件の検索結果を呼び出す必要が有りました。社内の物件データ基盤は、REST APIで、レスポンスはJSON形式で実装されています。こちらのレスポンスをオブジェクト指向に準拠した形でITANDI BBで取り扱うクラスを実装したいです。比較的テスタブルで、拡張性高く読みやすい実装を目指した結果、ファーストクラスコレクションをベースにするとよさそうでした。
ファーストクラスコレクション(コレクションクラス)とは?
デザインパターンの一種で、配列などをラップしたクラスを指します。 配列を扱うコードが、プログラムのあちこちに散らばらないというメリットがあります。 今回の要件では、検索結果は物件の一覧を取り扱うので相性がよさそうです。
class RoomCollection def initialize(rooms) @rooms = rooms end end
ファーストクラスコレクションとの違い
ファーストクラスコレクションに完全に準拠するだけでは、「検索結果」をうまくオブジェクトとして扱いきれなかったので、以下のような変更を加えました。
pageを要素として持つ
検索結果には、検索に引っかかった物件自体以外にも、「ページネーションで何ページ目か?」なども含みます。コレクションクラスの要素としてPage
クラスを実装することで、rooms
とpage
を一括のものとして取り扱えてわかりやすそうです。
class RoomCollection def initialize(rooms, page) @rooms = rooms @page = page end end
APIの呼出しをクラス関数で持つ
今回の要件では、APIの呼出し結果をわざわざclass外から流し込んで上げる必要性がなさそうでした。また、「社内の物件データ基盤の物件の検索結果」を取り扱うクラスと捉えれば、APIの呼出し自体もクラス内にあって問題なさそうです。クラス関数の命名をわかりやすいものとして、APIの呼出し自体をクラス関数で実行するようにしました。
class RoomCollection class << self def search(params) # 社内の物件データ基盤の物件の検索結果を取得する処理 response_body = Product::Client.search_rooms(params) new( Room.build_array(response_body[:rooms]), Page.build(response_body[:page]) ) end end def initialize(rooms, page) @rooms = rooms @page = page end end
完全コンストラクタパターンを採用する
現段階の要件としては、インスタンスを変更できないようにしておいてよさそうです。極力処理をイミュータブルにしておきます。
class RoomCollection attr_reader :rooms attr_reader :page class << self def search(params) response_body = Product::Client.search_rooms(params) new( Room.build_array(response_body[:rooms]), Page.build(response_body[:page]) ) end end def initialize(rooms, page) @rooms = rooms @page = page end end
結局テストしやすくなったの?
.search
のテストとしては、以下の項目を書いてあげると十分かなと思います。
- 返り値は、
RoomCollection
のインスタンスか? - roomsは、APIのレスポンスと同数のinstanceを持つか?
- 内部でAPIの呼出しは1回だけ行われているか?
Page
やRoom
に関することは、それぞれのクラスでテストを書けばよさそうな実装にできたので、比較的テスタブルな実装にできていそうです。
拡張性高く読みやすい実装になっている?
.search
自体は比較的自由度の高い検索でしたが、1つの条件のみで検索する(Room
のids
のみで検索する)ケースがありました。Room
のids
で検索する場合は.fetch_by_ids
のようなmethodを足していけば容易に実装できそうで、読みやすさも担保されていそうです。また、ids
が空の場合は、API自体呼ばないということにも対応できそうです。
class RoomCollection attr_reader :rooms attr_reader :page class << self def search(params) response_body = Product::Client.search_rooms(params) new( Room.build_array(response_body[:rooms]), Page.build(response_body[:page]) ) end def fetch_by_ids(ids) return new([], Page.build_empty) if ids.empty? response_body = Product::Client.search_rooms({ ids: ids }) new( Room.build_array(response_body[:rooms]), Page.build(response_body[:page]) ) end end def initialize(rooms, page) @rooms = rooms @page = page end end
おわりに
現段階の僕の知識内においては、比較的わかりやすい実装にできてよかったなと思いました。しかし、時の流れとともにこの設計も見直すべき箇所が出てくることと思います。また、僕が知らないだけでもっと良い設計や取り入れたほうがよいrubyの構文もあることでしょう。いつまでもより良い実装を探求し、将来自分の書いたコードを見る人にも、何よりもユーザに価値のあるものを作れるように力をつけていかないとなと改めて思いました。
この記事がだれかの一助になれば幸いです!!