男性インフラエンジニアが育児休業取得までにやったこと

こんにちは。インフラグループの夏目です。

今回は技術的な話ではなく、ちょっとプライベートに込み入った話というかエンジニアのワークライフバランス的なお話です。

まったくの私事なんですがこのたび子供を授かることになりまして、ついては育児休業を取ってみようかな?ということで、育休取得までの流れと業務面でどういった対応をしていったのか、といったことを書いていきます。

育児休業を取ってみよう

f:id:enigmo7:20200616134043p:plain

今回育休の取得に至った経緯は色々とあるのですが、ひとつにワークライフバランスを見直す良いきっかけになるかな、という意図があります。

時短勤務やリモートワークではなく一旦完全に業務から離れてみて、日常生活における育児の占める割合はどの程度のものなのか(全部だよという意見はごもっともです)ということを理解しておくべきだろう、と。これを把握せずに、妻が産休・育休で子供の面倒を見るのに対して自分だけこれまでどおりの働き方をしていくのは、先々を考えると良いスタンスではないように思えたため、第一子のタイミングで育休を取ることにしました。

エニグモでは裁量労働のメンバーが多いこともあり、育児都合で勤務時間を調整する様子が日常的に見受けられます。このため、いざ自分に子供が生まれますよということになっても、育休を取得すること自体は自然と受け入れてもらうことができました。

受け入れるも何も労働者の権利の行使なので、本来拒否権はないはずでしょうという話なのですが、そうもいかない会社が多くあることもまた事実です。この点についてはエニグモの風土に助けられた部分が大きいなと感じています。

取得までの流れ

さて、では実際に育休を取りましょうとなってからどういった行動を取ったか、時系列に沿って大雑把ですが書いてみます。

2019年10月に妊娠確定となり、ここから家庭内協議に入りました。自分の稼働状況など鑑みて育休が果たして取れるのか、取れるのであればどれほどの期間で収入面で問題がないのか、などについて妻と喧々諤々やり合いました。

法制度の話をするのであれば1年以上取得することも可能ですが、現実問題として夫婦共に育休となった場合の収入への影響は無視することができず、第一子で不安を抱える妻に多少折れてもらいつつも2ヶ月取得してみましょう、ということになりました。

結論が出たのが11月末、ひとまずチームリーダーへ相談です。自分がアサインされているプロジェクトの今後の見通しや、その他担当タスクのチームメンバーへの委譲可否などを考慮のうえ、積極的に育休を取得してほしいとの言葉を頂きました。チーム内の調整は大丈夫(な状態にするしかない)となったので、後は部長や人事総務の方に掛け合うだけ、というところまで持っていきました。

年が明けて2020年の2月、自身のタスクを育休までにどう調整するかといった大まかな線表を作成して部長へ共有し、社内の具体的な育休取得手続きに入ります。

というタイミングで、全世界的に新型コロナウィルス感染症が広がり、エニグモでもリモートワークに突入し手続きはいったいどうすれば…?という状態になってしまいました。幸い事前手続きはほぼないことと、書類のためにわざわざ出社する必要なく郵送してねということで柔軟な対応をして頂けました。ありがたい限りです。

タスク調整どうしたの

タスク調整は実際問題どのようにしたかという点については特筆すべきことはなく、予定日までに大きなタスクを終わらせつつドキュメントを整理したり、自動化可能なタスクは自動化、間に合わない場合は手順書を作るといった形で粛々と対応を進めました。不穏当な表現ですが、トラック係数やバス係数といったものをあらためて意識することができました。

また、カオスエンジニアリングとまではいきませんが、どれだけ自分が準備をして引き継ぎをしたところで不測の事態は発生しうるので、そのあたりはチームメンバーになんとか拾い上げてもらうしかないかな、と自分勝手なことを考えています。

私生活で準備したこと

育休とはあまり関係なく、プライベートで準備したことについては基本的に世間の皆さんと同じです。強いて言えば、以下のようなエンジニアチックな手法で試行錯誤をしています。

  • 書籍や雑誌などから育児に関する必要なものリストをピックアップしてGoogle SpreadSheetへ書き出し、購入期限や数量、消耗品か否かといったメタ情報を付与して購入状況を管理
  • 公的な手続きなど、忘れがちなものはTrelloのカンバンを作って進捗管理

f:id:enigmo7:20200616134313p:plain
Trelloのボードイメージ

おそらくこういったことに対応するための専用アプリやサービスもあるかと思うのですが、それらの使い方を覚えている暇があったら使い慣れたツールを使ったほうが早いだろう、と横着をしている状況です。

今後の見通し

前述したように雪崩式にリモートワークとなり、当初想定していた育休へのロードマップとは少々異なる現状なのですが、反面リモートワークでも業務への支障はほぼゼロに近いということがわかりました。2ヶ月間の育休取得後に世間がどういった状態になっているのか(おそらくあまり変化はないでしょうが)推測できませんが、リモートワークへの敷居が下がったこともあるので、たとえば保育園に入れるまではセミリモートワークや半育休といった形態で、柔軟な働き方を選ぶこともできれば良いなと思っています。

このエントリーを書いてる今現在、出産まであと数日といった状況です。2ヶ月後にこのエントリーを読み返して「全然準備できてへんやんけ!なに余裕ぶっとるんじゃ!」となりそうな気配が漂っていますが、2ヶ月後に改めて育児休業の間に起きたことや感想についてのエントリー投稿を予定していますので、そちらをお楽しみに。

結びに

少々自己矛盾してしまうのですが、冷静に考えるとこのエントリーのタイトルは筋が悪いなという印象があります。こんなタイトルのエントリーが企業ブログのバリューとならず、掃いて捨てるようなエントリーとして扱われる時代にしていきたいものですね。


株式会社エニグモ 正社員の求人一覧 hrmos.co

GitLabのプロジェクトラベルとグループラベルを重複させてしまって困った話

こんにちは。インフラグループの夏目です。

エニグモではメインのGitサービスとしてGitLabを使ってソースコードを管理しています。 GitLabはGitHubと同様に、IssueやMR(PR)にラベルを付与して作業の優先度やステータスを表すことができるのですが、このラベルの運用でちょっと困ったことが発生して泥臭く解消するはめになったので、経緯と顛末含めてご紹介します。

プロジェクトラベルとグループラベル

GitLabのラベルは、プロジェクト(リポジトリ)とプロジェクトを束ねたグループとでそれぞれ個別に定義できます。 グループラベルとして定義したものは配下のプロジェクトでも使用できるため、基本的にはグループ側で汎用的なラベルを設定して、プロジェクト固有のメタ情報としてプロジェクト名の省略形(例:Enigmo Greatest ProjectEGP)や、リリースバージョン(例:v1.4.3)などをプロジェクトラベルで定義するというのがベターな運用方法だと思われます。

グループラベルを設定しよう

ところがこのグループラベル、エニグモではあまり認識されていなかったため

  1. 各プロジェクトで汎用的な名前のプロジェクトラベルを個別に設定
  2. プロジェクトラベルを使ってIssueやMRを管理
  3. プロジェクトごとに同じラベルを定義するのが手間だったので、プロジェクトラベルと同じ名前のグループラベルを設定

という流れで最近になってようやくグループラベルが設定されました。各プロジェクトは個別にラベルを定義する必要がなくなってめでたしめでたしかと思いきや、そうではありません。さきほどのフローに妙なところがありませんでしたか?

プロジェクトラベルと同じ名前のグループラベルを設定

ここです。同じ名前のラベルを設定しましたね。グループラベルとプロジェクトラベルは別のオブジェクトとして扱われるため、同じ名前のラベルが設定できます。設定できるのは問題ないのですが、いざ使ってみようとすると

f:id:enigmo7:20200522105830p:plain

こんな感じでまったく区別のつかないラベルがラベル付与の候補として表示されるようになってしまいました。どうしてくれるんだ。しかも厄介親切なことにこのラベル候補、グループラベルかプロジェクトラベルかという情報は表示されません。

重複したらどうする?

区別がつかなくても同じ意味合いだったら別にどっちでもいいじゃん、と思わないでもありませんが、ラベルを付与するたびに「なぜ2つあるのか?どちらを付与したらいいのか?表示のバグ?」とチラッと考える時間が無駄ですね。じゃあこの重複を解消しましょう、と迂闊に既存のプロジェクトラベルを削除してしまうと、これまで該当のラベルを付与していたIssueやMRからラベルが削除されてしまいます。

仮にreview requestedなどのアクションが必要なラベルの場合、削除によって対応が漏れてしまうおそれがあります。この事象に対して、GitLab.orgのIssueで対応方法が提案されていました。

  1. 重複しているプロジェクトラベルかグループラベルをリネームする
  2. プロジェクトラベルが付与されたIssueやMRに、グループラベルを追加する
  3. プロジェクトラベルを削除する

たとえばin reviewという名前のプロジェクトラベルとグループラベルが設定されている状態では、以下のような対応になります。

  1. プロジェクトラベル:in reviewin review(project)へリネーム
  2. in review(project)が付与されたIssueやMRに、グループラベル:in reviewを付与
  3. プロジェクトラベル:in review(project)を削除

リネームと削除はグループ設定画面やプロジェクト設定画面から容易に対応できますが、活発に開発が行われているプロジェクトにおいて、Issue一覧やMR一覧で重複しているラベルが付与されているものを探してIssueやMRの編集画面でラベルを設定する、という流れを1つ1つ対応するのはそれこそ時間の無駄です。

APIを使ったラベルの修正

幸い、GitLabではプロジェクトラベルやグループラベル、MRを操作するAPIが提供されています。

これらのAPIを利用して以下のようなスクリプトを作成しました。

#!/bin/bash
# require
# - httpie
# - jq
# - GitLab Administrator Role & User Token 

TOKEN=************
total_pages=$(http --ignore-stdin 'https://gitlab.example.com/api/v4/groups/1/merge_requests?labels='"review requested(project)"'&per_page=100' Private-Token:$TOKEN --verbose | grep X-Total-Pages | awk {'print $2'} | sed -e 's/
//g')
for page in $(seq 1 ${total_pages})
do
   http --body --ignore-stdin \
   'https://gitlab.example.com/api/v4/groups/1/merge_requests?labels='"review requested(project)"'&per_page=100&page='${page}'' \
   Private-Token:$TOKEN | jq '.[]|{ id: .id, iid: .iid, project_id: .project_id, labels: .labels}'
done  > request.json

jq -c '.' request.json | while read mr
do
  id=$(echo ${mr} | jq '.id')
  iid=$(echo ${mr} | jq '.iid')
  pid=$(echo ${mr} | jq '.project_id')
  labels=$(echo ${mr} | jq '.labels|join(",")')
  replaced_labels=$(echo ${labels} | jq -r 'sub("review requested\\(project\\)";"review requested")')
  http --body --ignore-stdin \
      PUT 'https://gitlab.example.com/api/v4/projects/'${pid}'/merge_requests/'${iid}'?labels='"${replaced_labels}"'' \
      Private-Token:$TOKEN
  sleep 5
done

今回は重複しているラベルが少なかったため、ラベル名は引数ではなく直接定義してしまっています。大雑把には以下のようなフローです。

  1. 対象グループ内でリネーム後のプロジェクトラベルが付与されたMRの一覧(ラベル定義の変更に必要なメタ情報を含むJSON)を作成
  2. ラベル定義を修正したJSONを使って各MRの情報を更新
    • スクリプト内ではラベルの置換をしていますが、前述の対応方法に記載されているようにプロジェクトラベルを削除することで更新されるため、置換ではなく単純な追加でもOKです

各プロジェクトのラベル一覧を確認して重複しているラベルがあったらスクリプトを適宜修正して実行、という対応で重複を解消しました。

すべてのグループやプロジェクトに対してラベル重複のチェックをして解消したい場合は、以下のような各言語向けのモジュールを使ったスクリプトを作成するのも良いかもしれません。

結論

  • 汎用的なラベルはグループラベルに定義すること
  • 既存のラベルと同名のラベルを適当に作らない
  • 実際にラベルを使う場面をちゃんと考えましょう

株式会社エニグモ 正社員の求人一覧 hrmos.co

CA × atmaCupに参加しました

はじめに

こんにちは、初投稿になります。
今年の1月からエニグモでデータサイエンティストをしている堀部と申します。

1/25(土)に行われたCA × atmaCup というオフラインのデータコンペティションのイベントに参加しました。

【東京・大阪同時開催】CA × atmaCup - connpass

85チーム中、Public 12th/Private 5th*1とKaggle Expertとしてはまさかの最上位という結果でした。

コンペ前、コンペ中、コンペ後に分けて振り返ります。

※本記事ではコンペのデータやタスクについては情報公開が難しいため触れません。

コンペ前

コード整理

以前のコンペや業務で使っていたコードをなるべくクラス・メソッド化してgitにまとめて整理しました。綺麗なコードとは言えないですがまとめておいたおかげで、新しい特徴量の作成やタスクに特化したデータの前処理に時間を割くことができました。主に汎用的な特徴量生成の処理やGBDTモデルの実験をやりやすくする下記のようなコードをまとめていました。

  • EDA

  • 特徴量生成

    • 1種類しかないカラムを削除
    • 集約特徴量の生成(mean, median, max, min, std)
    • label encording
    • count encording
    • target encording
  • 学習(モデル)

    • ベース:lightgbm 5-fold
    • オプション:random seed averageやlightgbm tunerをオンオフできるように追加

コンペ中

素敵な環境でした

サイバーエージェントさんのできたてほやほやのオフィスのスクランブル交差点の見える席でもくもくと作業しました。頭を使いすぎて最後2時間くらいは少し頭がぼーっとしてました。 f:id:enigmo7:20200203165320j:plain

コンペティションサイトの出来が素晴らしかったです。○aggleと違ってすいすいsubmitができストレスなく作業に集中することができました。

コンペ中の作業方針

  1. 必要最低限の特徴量でベースライン作成してsubmit
  2. EDAやfeature importanceを見て特徴量追加
  3. CVもpublic LBも伸びたら採用
  4. ひたすら、2,3を繰り返す

モデルはずっとlightgbmシングルでアンサンブルはしませんでした。lightgbmのハイパーパラメータはcat_smoothのみ手動チューニングしました。
そして、特徴量のアイディアが一旦つきたところで lightgbm tunerを使って一度パラメータ探索をした結果を最後まで使いました。

コンペ後

表彰式、懇親会

コンペが終わってすぐ結果発表&表彰式がありました。上位3名の方の解法を聞くことができました。 その特徴量効きましたよねと共感したり、そのアイディアは思いつかなかったなとメモしたりすぐに情報共有できるオフラインコンペならではのよさを実感しました。

その後の懇親会でも当日のコンペだけでなく別のコンペについての取り組みについても話を聞くことができ、普段からコンペに参加しておくメリットも感じました。

次の日

次の日は日曜日だったので表彰式や懇親会での話を受けて思いついたアイディアを実装して追試をしていました。少し考えればこの特徴量は要らないかもと思ったものを削除しただけで、スコアが伸びたりとなぜコンペ中に気がつかなかったのかと後悔もありました。

また、上位の方が公開してくれたコードやgitを見て自分のコードの汚さに愕然としました。
普段から綺麗なコードを書く、他の人のコードを見て学ぶということを日頃から意識しようと胸に刻み込みました。

最後に

(真剣に取り組んだ)コンペの数だけ強くなる、強い人に会いに行くという大切さ、オフラインコンペならではの大変さ、楽しさを味わうことができたとても充実した一日でした。次回があればまたぜひ参加したいです。

また、エニグモ に入社して1ヶ月が経ちましたが楽しい日々を過ごしています。ABテストの効果検証×傾向スコア、商品のクラスタリング、回帰予測モデリングなど、自分で思いついてビジネスに貢献できそうなものであれば自由に取り組んでよいボトムアップの環境です。

個人的には、

  • ファッションEC × CtoCというドメインならでは多様性のあるデータ(テーブル、テキスト、画像)が扱えること
  • 社内の半数以上の方がSQLをかけデータをビジネスに活用する文化が定着していること
  • BigQuery, AWS, Lookerなど、ツールへの投資が当たり前のように行われていること

によさを感じています。

コンペ以上に業務も楽しく取り組める強いチームをつくれたらと思っております。興味のある方はぜひ気軽にご応募ください。選考とは別にカジュアル面談も受け付けております。データサイエンティスト、データアナリスト、データエンジニア、検索エンジニアなど各種募集中です。

データアナリスト hrmos.co

検索エンジニア hrmos.co

株式会社エニグモ 正社員の求人一覧 hrmos.co

*1:ランキングシステムがPublicとPrivate で分かれていて、コンペティションが終了するまではテストデータの一部だけを使ってスコアを算出されたPublicのみのランキングが公開されます

BUYMAの検索システムを刷新したお話

こんにちは。主にBUYMAの検索周りを担当しているエンジニアの伊藤です。

BUYMAではSolrを利用した検索システムがいくつかあります。
BUYMAの検索というと検索ボリュームが一番大きな商品検索を想像されると思いますが、
今回はデータボリュームが一番大きい検索システムをターゲットとして、インフラ周りを含め全面的にシステムの刷新を行いました。

ここでは、

  • 既存の検索システムがどういったものだったのか
  • なぜシステム更改が必要だったのか(どういう課題があったのか)
  • 更改後の検索システムはどういったものか
  • 今後の課題について

等々についてご紹介したいと思います。

既存の検索システムについて

既存の検索システムは下記の通り、シンプルという点ではとても素晴らしいものでした。

f:id:pma1013:20200203151456p:plain

ただし下記のような問題を抱えている状況でした。

  • スケールアウトしない構成である
  • スケールアップの限界
  • Solrのバージョンが古い
  • 本番環境のデータを利用した検証が難しい
  • 開発担当者にもSolrの知識が必要とされる
  • 問題に気付けない

以降でその内容について補足していきます。

スケールアウトしない構成である

  • 検索システムはデータ増とアクセス増の2軸でスケールアウト構成を取る必要があります。
  • アクセス増にはSolrサーバを追加することで対応できるようにはなっていましたが、該当の検索システムはアクセス数というよりデータが多いことが特徴でこのデータ増に追従出来なくなっていました。

スケールアップの限界

  • 該当の検索システムは元々オンプレ環境で運用していましたが、ヒープサイズ不足やFull GCが頻発するようになっていたため、暫定対処としてAWS側に移行し、GCチューニングを行いました。
  • ただしこの対応も一時的なもので、データ数自体は日々増加している状況の中で使用しているGCアルゴリズム的に、メモリを増やしてどうこうできる状況ではなくなっていました。
    • データ増に伴いOOM Killer、ヒープサイズ不足が発生 → ヒープサイズを拡張 → STWのインパクトが大きくなるという負のループに陥る

Solrのバージョンが古い

  • 既存の検索システムで利用しているSolrのバージョンは3.2.0でした
  • バージョンが古くても通常時の運用にはそれほど支障はありませんが、何か問題があった場合の調査が大変でした(ドキュメントが見つからない)
  • またSolrのバージョンupに伴いindexing/検索性能はupしていますので、性能面でのデメリットであったり
  • 以降のバージョンで提供された様々な機能が利用できない等のデメリットがありました

本番環境のデータを利用した検証が難しい

  • 既存システムで本番環境相当のデータで検証する際にはindexing処理だけで数日かかるという状況で、環境構築自体がハードルとなっていました。

開発担当者にもSolrの知識が必要とされる

  • Solrへの検索時はphprubyといったクライアント側でSolrのクエリを組み立てて実行していました。
  • クライアント側を担当する開発担当者はその時々で変わり、ある程度はSolrを知っている人もいれば、全く知らない人まで様々です。
  • 一方Solrのクエリは書き方によってはキャッシュを使えていなかったり、または逆にキャッシュを無駄に汚してしまったりと、同じ検索結果を得るにしてもある程度Solrの知識を必要とします。

問題に気付けない

  • 必要なデータやログが適切に可視化され監視されていないため、サービス影響、ユーザ問い合わせがあって初めて問題に気付くケースがありました

更改後の検索システム

上記で挙げた課題感を解決するために設計したアーキテクチャが下記になります。

f:id:pma1013:20200203172549p:plain

  • 変化に強いシステムへ

    • SolrCloud構成に変更しつつ、データ増にはデータsharding、アクセス増にはデータreplicationでスケールアウト可能な構成に変更
  • 環境再現性

    • 全てコンテナベースのアプリケーションとなっているため、本番と同じマシンスペック、構成を簡単に構築可能
    • 本番データを利用した検証も短時間で可能となった
    • indexing処理についてもBUYMAで利用しているメインのDBにアクセスする必要がなく、中間DBを利用することで数時間でfull-import可能
  • 耐障害性の向上

    • プラットフォームにKubernetesを採用し、想定外のアプリケーション停止、ノードdown時にもセルフヒーリングで自動復旧可能
  • マイクロサービス化(隠蔽化)

    • アプリケーション〜Solr間に検索APIを新たに用意。これによりアプリケーション側はSolrの独自クエリを意識することなく、検索APIを介したシンプルなI/Fをベースとした開発が可能に。
    • Solrの独自クエリなど、専門的な知識を開発者が持つ必要がなくなり、学習コストが抑えられることで開発効率が上がる
    • 不適切なクエリにより、Solr側のキャッシュをうまく使えないなどの問題回避
  • 障害検知

    • 監視、ログ運用基盤としてDatadogを利用し、インフラ/ミドル/アプリケーションのメトリクス情報やログを統合的に管理可能とした
  • 属人性の排除

    • 検索システム間でバラバラだったスキーマ定義を汎用的なものにし、複数の検索システムをシームレスに対応可能に
    • dynamicフィールドをベースとしているため、スキーマ定義自体も運用上ほぼ変更する必要がない
  • GitOps対応

    • 全てのシステム設定をコードとしてGit上で管理
    • 運用上変更頻度が高いものはコマンド実行不要でCI/CDで可能

苦労した点

今回せっかくシステム更改するならCloudNativeなシステムにしたいという思いがありこのような構成にしました。
SolrのようなstatefulなアプリケーションをコンテナとしてKubernetes上で扱うのも初めてでしたし、 監視、ログ運用周りも今回の対応と合わせて刷新したことで対応範囲が広くなり苦労した点も多くありました。
ここではいくつかピックアップして紹介したいと思います。

  • Solr/ZookeeperをKubernetes上に構築、運用した経験がない

    • Solrに関してはあまり構築事例もなく問題に遭遇した際の解析に難航
    • リソース設計、ログ周りなど、コンテナ/Kubernetesだからこそプラスαで考慮しなければ行けない点が多々あった
  • 監視、ログ運用基盤の検討

    • コンテナ/Kubernetesベースのシステムをどう監視すべきか?
    • ログはどう扱うべきか?
  • 検索APIの設計、開発

    • I/F仕様どうするか?
    • 開発しながらSolr/Zookeeper周りの設定や問題対応をするのが辛い
  • Solrバージョンアップによる未知の問題対応

    • indexing処理周りで問題多発
    • SolrCloudかつDIHで並列indexing処理を行うこと自体が事例がなく、色々とはまる

今後に向けて

システム更改により当初挙げていた課題感というのは概ね解決することが出来ました。
ただし今回のようなシステムを構築、運用したことでてきた新たな課題や、まだまだ改善が必要だなと思えるところもあります。

  • indexing処理などはクライアント側の処理と密になっている箇所がまだまだあり、今後もブラッシュアップが必要
  • Solrのような複雑かつstatefulなアプリケーションは運用がやはり大変。Kubernetes operatorを利用するなどして運用をもっと楽にしつつ、属人性もなくしていきたい。

さいごに

今回は検索のインフラ面を中心としたお話を紹介させていただきましたが、 弊社では検索のサービス向上(ex パーソナライズサーチなど)や自然言語処理機械学習を活用したサービス導入なども積極的に行っています。
今回技術的なところにはほぼ触れませんでしたが、ここどうなってるの?など少しでも興味、関心を持たれたら是非お気軽にお話だけでも聞きにいただければと思います。

検索エンジニア hrmos.co

株式会社エニグモ 正社員の求人一覧 hrmos.co

データサイエンティストとしてアウトプットを改善するために必要な4つの力

はじめに

エニグモでデータサイエンティストを名乗っている庄子です。こちらは Enigmo Advent Calendar 2019 の25日目の記事です。 今年の振り返りも兼ねてのポエムとなります。

さて、データサイエンティストが活躍するためのスキル要件として、いくらでも切りようがあると思いますが、特に自分自身に感じている課題について、4つの力という観点で書きたいと思います。

その1 提案力

  • PoCとして小規模のデモを行う
    • そのデータサイエンスのアウトプットが使えそうか、事業に詳しい人に想像してもらう

実際にデータサイエンスを使って問題解決できそうな場合も、実際にやってみないと分からないですし、得られたアウトプットが事業に有効かどうかを、事業に詳しい人に意見をいただいた方が良いでしょう。筋が悪そうな分析は早めに判断してもらうためにも、なるべく小規模でPoCを行います。 本年度の振り返りとしては、このPoCの手数がもっとあった方が良いと感じました。

また、データサイエンティストが使う手法でできることは、データサイエンティストがもっとも良く分かるはずです。もちろん、使われる手法に関して詳しい人がデータサイエンティスト以外のポジションを担っているケースもあると思いますが、そのケースはここでは考えないことにします。

なお、課題設定が済んでおり、入出力が決まっているテーブルデータに対しては、AutoMLを十分ですし、AutoMLに変えられるようなタスクをこなすだけであれば、データサイエンティストは価値を発揮できません。 課題設定と分析を繰り返し、仮説検証を繰り返しながら課題設定の質を上げるスキルこそがデータサイエンティストの価値を差別化すると思います。

  • 提案を誤解なく伝えるために、共通言語を関係者と揃える
    • 使う手法や方法論を伝える概念を伝える
    • 伝えたことをドキュメントとして残し、説明の場にいなくてもそのドキュメントを読めば共通言語が理解できるようにする
    • 関係者が運用している具体的なデータを使って示す

共通言語を揃える場合は、最初に自分が受ける説明として、こんな説明ならすっと理解できそうというのを思い出しながら、説明するということは普段気をつけているつもりでしたが、後から考えると考慮が不十分だと思うことは多々ありました。特に、背景や経緯を共有できている前提から資料を作ってしまう場合です。 分析レポートの報告の際に、なるべく普段気をつけていたはずだと思いますが、ドキュメントの作成や整備も、振り返るともっとやりようがあると思います。 質問された点について補足を付け加えていくなどして、社内のナレッジの質を高め、リテラシーを共有できるのかと思います。

その2 ヒアリング力

  • 定期的に情報交換の場を現場のメンバーと設ける

こちらの取り組みを参考にし、ざっくばらんにデータ活用で解決してほしい課題について話すというランチミーティングを設定しました。

データ民主化を加速させる「分析ワクワクタイム」 - pixiv inside

このランチミーティングをきっかけにとあるPoCに取り組みましたが、中途半端なところで頓挫してしまっている状況です。 さらに、定期的にということも実現できていません。やったら効果的かもしれないと思うことをすぐに実現できず、せっかくヒアリングしたのに形にできないということに負い目を感じてしまった部分もあります。

いずれにせよ、定期的にコミュニケーションを行うことで、風通しをよくする、心理的安全性を担保することが重要だったりするので、またどこかの機会で復活させていきたいと思います。

  • 現場での運用について体験させてもらう

今年は取り組めなかったですが、ビジネス理解をもっとも得られる方法が実際に体験することにあると思います。データサイエンティストが共通言語の理解を得にいくということで矢印の向きは反対になりますが、先に挙げた共通言語を揃えるということにもつながると思います。

その3 タスク依頼力

  • 得意な領域に専念し、タスクを切り出してより得意な人に任せることを考える
    • 実験的なコードを書くことに場合に専念する
    • 仕様が固まる、あるいはプロダクトに関わる部分はエンジニアに任せる

プロダクトに乗るべきコードの要件はここで詳しく触れませんが、手元で自分が扱うコードが読みやすく、条件や機能の拡張がしやすいものだったら良いなと何度も思いました。そのようなコーディングを行うためには言語やフレームワークのベストプラクティスを踏まえている、専門家に任せた方がよいです。

データサイエンスに関するプロジェクトを実行するにあたり、必要なスキルで全てに秀でるのは多くの人にとって現実的ではないでしょう。依頼をする内容を、その領域を詳しい人へ誤解なく橋渡しができるくらいの知識があるという前提は置きますが、なるべく、得意な領域に専念した方が、組織としてのアウトプット力は上げられるはずです。

個人の領域として広くやれるのが弊社の良いところでもあるのですが、実験のためのコードを書く時間の割合を多くすることが、現在ネックになっていると思いました。

  • タスクを分解する
    • 分解した単位で、インプットとアウトプットを明確にする

こうやって書くと、基本的な仕事の進め方の話になりような気がします。。達成基準が明確な、細切れなタスクに分解できるということは、仕事の依頼もしやすくなり、今後育成という観点でも必要になってくる力です。

その4 採用力

  • 来て欲しい人に、その会社に来る魅力をきっちり伝える
    • 具体的なタスクや、データサイエンティストの要件を上手く伝える

人的リソースを増やすために、データサイエンティスト自体の人員を増やすということも当然選択肢に入ると思います。今年度は、データサイエンティスト、および、データアナリストの採用にも、面接などを通し関わらせていただきました。

もちろん活躍できそうな人を採用することが目的になりますが、活躍を期待できる人材ほど、他社からも内定が出ることは容易に想像できます。給与といった待遇面以外で考えるとすると、仕事内容やその会社で働くことの魅力を伝えるかどうかにかかっていると思います。

そこで、ミスマッチをなくすということに務めてきました。課題と思っているとこと、なるべく包み隠さず伝えたということはできていたと思います。実際に業務を行うイメージを固めるために、面接官以外のメンバーとの面談の場を設けることも実現することができました。

  • 応募者の良いところを引き出して聞き出す

会社のフェーズを考えると、データサイエンスに関する施策をドライブしてくれる人が活躍できそうという思いがあったため、提案力に優れている人を通す傾向が強かったですが、応募要件を上げてしまう大きな要因となっていたような気がします。

今後は、始めは提案が少々不得意でも、技術力を活かして活躍できる人も採用したいという想いがあります。課題はたくさんある中で、その人の経験と相性が良さそうなタスクの質問をして、考えを掘り下げる、という質問の組み立てについても、改善していきたいと思います。

一方、できていた部分としては、データサイエンティストの業務経験が想定より不足していても、社内のメンバーとコミュニーケーションが安心して任せられ、学習やそれまでの業務への取り組みを伺い、間違いなく必要なスキルはキャッチアップできると判断できる方は面接を通していました。このケースでは応募者の良いところを上手く引き出せていたのではないかと思います。

さいごに

その1,2がコミュケーションに関する話題、その3,4が人的リソースに関する話題になります。大きな枠組の話題としては、如何にスキルや知識をアップデートするか、という日々のインプット・アウトプットも挙げられますが、また機会があれば書くかも、、しれません。

なお、今回できていないことを中心に書いてしまったせいか、ちょっと胃もたれしそうな気分になりました。ただし、やりたいのにやれてないことが多いということは、伸び代がすごい!とも言えるのかと思います。一つ一つやるべきことに取り組んでいくしかしないですね! こんな私でも暖かく受け入れてくださり、(きっと)裁量を最大限に与えてくれる弊社なので、共に切磋琢磨してEnigmoをグロースさせたい仲間を絶賛募集中です。

株式会社エニグモ 正社員の求人一覧

hrmos.co

Rubyでのデザインパターンの使用例を説明する!!

こんにちは、サーバーサイドエンジニアの平井です。

こちらは、Enigmo Advent Calendar 2019 、24日目の記事です。

昨年の1月にエニグモインターンとして入社してから一年が経とうとしています。早いもので、新卒の肩書きもそろそろ無くなってしまいますね。

今回は、Rubyによるデザインパターンを読んで、デザインパターンを勉強したので、そのアウトプットをさせていただきます。 タイトルの通り、デザインパターンについて実際の使用例を探してみました。そのパターンと使用例は以下になります。

まずは、Strategyパターンから説明します。

Strategyパターン

Strategyパターンとは

メソッドの中に溶け込んでいるアルゴリズムを別クラスとして分けて、切り替えができるようにするパターンです。 サンプルコードを利用し、悪い例を修正していく形で、さらに、Strategyパターンについて説明していきます。

適用前
class Report
  def initialize
    @title = '月次報告'
    @text = [ '順調', '最高の調子' ]
  end

  def output_report(format)
    if format == :plan
      puts("#{title}")
    elsif format == :html
      puts("<title>#{@title}</title>")
    else
      raise "Unknown format: #{format}"
    end

    @text.each do |line|
      if format == :plan
        puts(line)
      else
        puts(" <p>#{line}</p>")
      end
    end
  end
end

問題点としては以下が挙げられます。

  • output_reportメソッドについて、formatの種類が増えるにつれて、メソッドが長くなる。

Strategyパターンを適用すると、以下のようになります。

class HTMLFormatter
  def output_report(title, text)
    puts("<title>#{@title}</title>")
    text.each do |line|
      puts(" <p>#{line}</p>")
    end
  end
end

class PlainTextFormatter
  def output_report(title, text)
    puts("#{title}")
    text.each do |line|
      puts(line)
    end
  end
end

class Report
  def initialize(formatter)
    @title = '月次報告'
    @text = [ '順調', '最高の調子' ]
    @formatter = formatter
  end

  def output_report
    @formatter.outpout_report(@title, @text)
  end
end

report = Report.new(HTMLFormatter.new)
report.ouput_report

このように、同じ目的(レポートをフォーマットする)を持ったオブジェクトを、ストラテジとして定義し、そのストラテジをごっそり交換できるようにするのがStrategyパターンです。ストラテジは同じインターフェースを持っているので、ストラテジの利用者(コンテキストと呼ぶそうです)は、どのストラテジを利用するかを知らずにすみます。適用前は、ReportクラスはHTMLとPlainText、それぞれの出力方式を知っていました。ただ、Strategyパターンを適用した後は、出力方式に関する知識はReportクラスからストラテジオブジェクトに移りました。

実例

Strategyパターンは、Wardenという認証フレームワークで使われています。

github.com

Warden::Strategies::Baseのサブクラスに認証の方法を実装することで、その認証方法を切り替えることができます。 そのWardenを使った、Deviseというgem(ログイン方法を簡単に実装できる)のソースコードを見ると、実際にどのようにStrategyパターンが使われているのかを確認できます。 下のソースコードを見るとわかるように認証するオブジェクトはwardenですが、認証方法に関する知識はDatabaseAuthenticatableクラスRememberableクラスが持っています。

module Devise
  module Strategies
    class DatabaseAuthenticatable < Authenticatable
      def authenticate!
        # データベースにあるデータで認証を行う処理
      end
    end
  end
end
module Devise
  module Strategies
    class Rememberable < Authenticatable
      def authenticate!
        # クッキーを使って認証を行う処理
      end

DatabaseAuthenticatableは、データベースにあるメールアドレスとパスワードで認証します。Rememberableは、クッキーからデータベースにあるレコードを探してきて認証します。

Observerパターン

Observerパターンとは

あるオブジェクトの状態の変化を、そのオブジェクト自身がその変化を知りたいオブジェクトに対して知らせる仕組みがObserverパターンです。 システムの各部分が、あるオブジェクトの状態を知りたいとき、例えば、誰かの給料が変わった時に、その変更を経理部門が知る必要がある時を想定します。 悪いパターンのサンプルコードを見ていきます。

class Payroll
  def update(changed_employee)
    puts("#{changed_employee.name}の給料が#{changed_employee.salary}に変わりました")
  end
end

class Employee
  attr_reader :name, :employee

  def initialize(name, title, salary, payroll)
    @name = name
    @title = title
    @salary = salary
    @payroll = payroll
  end

  def salary=(new_salary)
    @salary = new_salary
    @payroll.update(self)
  end
end

payroll = Payroll.new
taro = Employee.new('Taro', 'President', 200, payroll)
taro.salary = 300

このソースコードの悪い点は以下になります。

  • 他のオブジェクトが、財務情報を知る必要が出た時に、実際に変化したのは、Employeeではなく他のオブジェクトなのにも関わらず、Employeeクラスを修正する必要がある。

Rubyによるデザインパターンに書かれていた設計原則として、変わるものを変わらなものから分離すると述べられています。この原則を当てはめて、変わるもの(誰がtaroの財務状況を知る必要があるか)を変わらないもの(Employeeクラス)から分離させます。 Observerパターンを当てはめると以下のようになります。

class Payroll
  def update(changed_employee)
    puts("#{changed_employee.name}の給料が#{changed_employee.salary}に変わりました")
  end
end

class TaxMan
  def update(changed_employee)
    puts("#{changed_employee.name}に新しい請求書を送ります")
  end
end

class Employee
  attr_reader :name, :employee
  include Subject

  def initialize(name, title, salary)
    super()
    @name = name
    @title = title
    @salary = salary
  end

  def salary=(new_salary)
    @salary = new_salary
    notify_observers
  end
end

module Subject
  def initialize
    @observers = []
  end

  def add_observer(observer)
    @observers << observer
  end

  def delete_observer(observer)
    @observers.delete(observer)
  end

  def notify_observers
    @observers.each do |observer|
      observer.update(self)
    end
  end
end

payroll = Payroll.new
taxman = Taxman.new

taro = Employee.new('Taro', 'President', 200)

taro.add_observers(payroll)
taro.add_observers(taxman)

fred.salary = 300

このように、ニュースを持っているオブジェクトとそれを受け取る側にきれいなインターフェースを作るアイデアがObserverパターンです。ニュースを持っているオブジェクトはSubjectと呼ばれ、それに関心のあるオブジェクトはオブザーバーです。今の状態だと、Employeeはどれだけのオブザーバーが自分の給料の変更に関心があるかを知らなくて済みます。なので、新しくtaroの給料の変更を知る必要があるオブジェクトが出てきた場合は、そのオブジェクトがオブザーバーとしての共通のインターフェースであるupdateメソッドを実装して、add_observersするだけで、Employee自身は何も変化しません。

実例

こちらは実際に使われている例ではなく、rails-observersを使うと簡単にActiveRecord用のobserverクラスを作ることができるという説明をします。 githubのREADMEのサンプルコードですが、

class CommentObserver < ActiveRecord::Observer
  def after_save(comment)
    Notifications.comment("admin@do.com",
                          "New comment was posted",
                           comment).deliver
  end
end

この場合、Comment#saveが終了した際に、after_save内の処理を実行します。 サンプルコードでは、Subjectのセッターに、オブジェクトが変更した際に実行するメソッドを追加する必要がありましたが、rails-observersを使えば必要ありません。

Iteratorパターン

Iteratorパターンとは

Iteratorパターンとは集約オブジェクトが子オブジェクトのコレクションにアクセスする方法を外部に提供するテクニックです。 外部イテレーターと内部イテレータがあります。 外部イテレーターについて、サンプルコードを使って説明します。

class ArrayIteratoor
  def initialize(array)
    @array = array
    @index = 0
  end

  def has_next?
    @index < @array.length
  end

  def item
    @array[@index]
  end

  def next_item
    value = @array[@index]
    @index += 1
    value
  end
end

array = ['red', 'green', 'blue']
i = ArrayIterator.new(array)

while i.has_next?
  puts("item: #{i.next_item}")
end
=>item: red
item: green
item: blue

外部イテレーターの場合は上のように、子オブジェクトに対して処理内容を伝えます。 一方、内部イテレーターとは、eachのように、イテレーター自身がブロックを受け取って、 その処理内容を子オブジェクトに伝えます。

実例

内部イテレーターが使われている箇所を、railsソースコード内のactionpack/lib/action_dispatch/http/mime_type.rbから簡単に探すことができました。

module Mime
  class Mimes
    include Enumerable

    def initialize
      @mimes = []
      @symbols = nil
    end

    def each
      @mimes.each { |x| yield x }
    end

    def <<(type)
      @mimes << type
      @symbols = nil
    end

    def delete_if
      @mimes.delete_if { |x| yield x }.tap { @symbols = nil }
    end

    def symbols
      @symbols ||= map(&:to_sym)
    end
  end

  SET = Mimes.new

  class Type
  
  def register
  # 他の処理
  
  SET << new_mime
  # 他の処理
end

上の例の場合、Mimesクラスの子オブジェクトに対して処理内容を伝える場合は、コードブロックを利用します。 また、Enumberableモジュールをincludeしてeachメソッドを定義することで、include?, map, selectなどの配列を走査する際に便利なメソッドを使うことができます。 actionpack/lib/action_dispatch/http/mime_types.rbに下のような処理がありました。

Mime::Type.register "text/html", :html, %w( application/xhtml+xml ), %w( xhtml )
Mime::Type.register "text/plain", :text, [], %w(txt)
Mime::Type.register "text/javascript", :js, %w( application/javascript application/x-javascript )
Mime::Type.register "text/css", :css
Mime::Type.register "text/calendar", :ics
Mime::Type.register "text/csv", :csv
Mime::Type.register "text/vcard", :vcf
Mime::Type.register "text/vtt", :vtt, %w(vtt)

# 他のMimeタイプをMimeクラスに追加する処理が続く

Typeクラスのregisterメソッド内でMimesクラスに追加をする処理があり、actionpack/lib/action_dispatch/http/mime_types.rbでMimeタイプを追加していました。

Builderパターン

Builderパターンとは

オブジェクトの構築のロジックに対して責任をもつBuilderを作り、そのBuilderを使ってオブジェクトを作成するパターンです。 例としては、以下になります。

class Computer
  attr_reader :drives

 def initialize(drives=[])
    @drives = drives
  end
end

class Drive
  attr_reader :type
  attr_reader :size
  attr_reader :writable

  def initialize(type, size, writable)
    @type = type
    @size = size
    @writable = writable
  end
end

class ComputerBuilder
  attr_reader :computer
  
  def initialize
    @computer = Computer.new
  end

  def turbo(has_turbo_cpu = true)
    @couputer.motherboard_cpu = TurboCpu.new
  end

  def add_cd(writer = false)
    @computer.drives << Drive.new(:cd, 760, writer)
  end

  def add_dvd(writer = false)
    @computer.drives << Drive.new(:dvd, 4000, writer)
  end

  def add_hard_disk(size_in_mb)
    @computer.drives << Drive.new(:hard_disk, :size_in_bm, true)
  end
end

builder = ComputerBuilder.new
builder.add_cd(true)
builder.add_dvd
builder.add_hard_disk(1000000)

computer = builder.computer

上の場合は、Computerを作るために必要なcdのsizeが760であるというような、Computerクラスのインスタンスを作成するためのロジックが、ComputerBuilderのクラスに集まっています。Computerを作るための実装の詳細をBuilderクラスが隠蔽しています。

実例

BuilderパターンはBuilderでグレップすれば良いので、簡単に見つけることができました。 以下のソースコードは、mastodonという短文投稿型のSNSサイトのソースです。

github.com

# app/lib/rss_builder.rb

class RSSBuilder

  def initialize
    @document = Ox::Document.new(version: '1.0')
    @channel  = Ox::Element.new('channel')

    @document << (rss << @channel)
  end

  def title(str)
    @channel << (Ox::Element.new('title') << str)

    self
  end

  def link(str)
    @channel << (Ox::Element.new('link') << str)

    self
  end

 ## 他のメソッドが続く

RSSとは、Webサイトのニュースやブログなどの、更新情報の日付やタイトル、その内容の要約などを配信するため技術で、XML形式で記述されます。 このRSSBuilderは、titleがchannelの子要素であるなどのXMLの構成の詳細を隠蔽しています。 このRSSapp/serializers/rss/account_serializer.rb上でRSSBuilderクラスを使って作成されています。

class RSS::AccountSerializer

  def render(account, statuses, tag)
    builder.title("#タイトルの名前")
            .link("#タグのurl")
#他のRSSを作成する処理が続く

    builder.to_xml
  end

このAccountSerializerクラスはRSSxmlの構成の詳細を知りません。タイトルやlinkの情報を与えるだけでRSSを完成させることができます。

まとめ

Rubyによるデザインパターンを読んでみて、実際の使用例を探してみようと意気込んでみたものの中々見つけることができませんでした。 普段から、ソースコードを読む際にデザインパターンを意識して読んでみるのもアリなのかなと思いました。

また、実際に使用例を見つけるためのソースコードリーディングを通して、デザインパターンに対する理解を深めることができただけでなく、雑多に新たな知識を得ることができました。 そして、モヤモヤしていたことをはっきり理解できた時の快感がソースコードリーディングの楽しみだなと改めて感じたので、積極的に読んでいこうと思いました。

最後まで読んで頂き誠にありがとうございました。

参考

株式会社エニグモ 正社員の求人一覧

hrmos.co

STI、Polymorphic関連を実際に使用した話

こんにちは!サーバーサイドエンジニアの@hokita222です!
有酸素運動は脳を活性化させると聞いて、最近は朝会社に出社せずにランニングしております!

それはさておき、これは Enigmo Advent Calendar 2019 23日目の記事です!

今回は弊社が運営するサイトのBUYMA (Ruby on Rails)に追加した機能で、STIポリモーフィック関連を使ってみたので、どういう設計にしたかを書いていこうと思います。
※使ってみたって話で、それぞれどういう特徴なのかなどの詳しい説明はしておりません。

どんな機能作ったの?

「〇〇キャンペーン」などの施策で、その日あった取引の中で特定の条件(商品ID、カテゴリーID、何円以上など)のものを絞り込み、その対象の取引に対して特定のアクションをさせます。

今回はこの機能の「特定の条件で絞る」の設計を説明していきたいと思います。

設計するなかで実現したかったこと

「特定の条件で絞る」機能を作るなかで重要視していたのは、「条件が増える可能性が高い」ことです。今は商品ID、カテゴリーIDで絞れるけど、将来的にはブランドID、購入数などで絞れるようにするかもしれません。なので条件が容易に追加できることを意識しました。

テーブル設計

テーブル名 説明
promotions 施策テーブル(施策名、施策期間など)
rules 施策の条件テーブル(どの取引を対象にするかの条件)
actions 施策のアクションテーブル(対象の取引に対しての行うアクション)※今回こちらは割愛
rule_targets 施策の条件テーブルとターゲット(itemsやcategoriesなど)との中間テーブル
items 商品テーブル(元々あるテーブル)
categories カテゴリーテーブル(元々あるテーブル)

※promotions, rules, actionsの形はsolidusを参考にさせてもらいました。

STI

f:id:hokita:20191216185531p:plain:w400

rulesテーブル(ruleモデル)に対してitem, category, price_gteというサブタイプクラスを作成しました。

なぜSTI

  • STIだと条件が増えたときにクラスの追加のみで済む。
    • 具象テーブル継承、クラステーブル継承だとテーブルまで作らないといけないので面倒くさい。
  • 将来的に追加されるカラムが少ない。
    • あるサブタイプクラスしか使わないカラムが増えていくと、テーブルにカラムが増えすぎるってこともあると思いますが、今回は問題ないと判断。
  • サブタイプクラスで共通のメソッドでも、それぞれ処理が異なる。
    • 処理が同じなら親クラスで共通のメソッド生やせばすむし、結果enumを使って異なる処理だけを例外的に書けばいいよねってなりますが、今回はそれぞれ異なります。

RailsSTIの機能は使わなかった

Rails標準のSTIの機能は使いませんでした。理由としては「継承」を使いたくないから。今回は委譲で対応しております。(RailsSTIを期待されてた方すみません。)
※と言ってみたは良いものの、Railsから外れるツラミも結構大きいです。今回の機能では問題ないのですが、あまりおすすめはしないです。

ソースコード

# 施策(調整予約)ルールテーブル
#
# カラム
#  - type: どの条件か(どの条件のクラスを使用するか。例: item, category, price_gte)
#  - value: 条件に必要な任意の値(〇〇円以上とか。商品IDとかはここには入らない。)
# 
class Rule < ActiveRecord::Base
  self.inheritance_column = nil

  belongs_to :promotion
  has_many :rule_targets

  delegate :build_relation to: :sub_rule

  private

  # サブタイプクラス
  def sub_rule
    "Rules::#{type.classify}"
      .constantize
      .new(self)
  end
end

継承を使用しないので、サブタイプクラスを呼ぶメソッドを作ってます。

module Subtypeable
  extend ActiveSupport::Concern

  attr_accessor :rule

  delegate :value, :rule_targets, to: :@rule

  def initialize(rule)
    @rule = rule
  end

  # 特定のrelationをactiverecordのメソッドを使用して絞り込む
  def build_relation(relation)
    relation
  end
end

rubyではダックタイピングできるので基本インターフェースは不要ですが、他のエンジニアにもこのメソッド使ってくれという願いを込めてmodule作りました。(ちょっとインターフェース以上のこと書いているのはご愛嬌)

# 商品ルール
class Syohin
  include Subtypeable

  # override
  def build_relation(relation)
    targetable_ids = rule_targets.pluck(:targetable_id)
    relation.where('trades.item_id in (?)', targetable_ids)
  end
end

引数のrelationに取引のRelationを渡すことによって中間テーブルにある対象のIDたちで絞ることができます。(正確にはRelationに条件を付加します。)

# 価格(以上)ルール
class PriceGte
  include Subtypeable

  # override
  def build_relation(relation)
    relation.where('trades.price >= ?', value)
  end
end

こちらは商品ルールとは異なり、rulesテーブルのvalueカラムを使用します。

Polymorphic関連

f:id:hokita:20191216185605p:plain:w400

なぜPolymorphic関連か

  • rule_targetsモデルに対して、複数のモデルを紐付けたかったため。
  • rule_targetsからはitemscategoriestargetsという同類の関係
  • rulestargetsは多対多なので中間テーブルrule_targetsとのPolymorphic関連

ソースコード

# 中間テーブル
class RuleTarget < ActiveRecord::Base
  belongs_to :rule
  belongs_to :targetable, polymorphic: true
end
class Item < ActiveRecord::Base
  has_many :rule_targets, as: :targetable
end
class Cateogry < ActiveRecord::Base
  has_many :rule_targets, as: :targetable
end

※本当はSTIと同じくインターフェース用のmodule作ったほうがいいのですが、共通のメソッドがないので作ってません。

ちなみに

promotionモデルはこうなっております。

# 施策テーブル
class Promotion < ActiveRecord::Base
  has_many :rules
  has_many :actions

  # いろんな条件をまとめたrelationを受け取ることができる。
  def detect(relation)
    rules.inject(relation) do |rel, rule|
      rule.build_relation(rel)
    end
  end
end

Promotionモデルが窓口となっており、コントローラーなどの呼び出し元はdetectメソッドを呼ぶだけで条件での絞り込みが可能になります。

各クラスがそれぞれの仕事を担ってくれているので結構シンプルなのではないでしょうか。またSTI、Polymorphic関連で実装したおかげでif文, case文が全くありません。

さいごに

現状新しい施策条件が増えた場合でもクラス一つ作るだけで解決するので、小一時間あれば条件の追加が可能となりました。

明日は弊社新卒の平井くんです!どんな記事書くんでしょうね。楽しみ!わくわく


弊社では一緒にレガシーコード脱却を目指すエンジニア大募集中です! hrmos.co