メドピア開発者ブログ

集合知により医療を再発明しようと邁進しているヘルステックリーディングカンパニーのエンジニアブログです。PHPからRubyへ絶賛移行中!継続的にアウトプットを出し続けられるようにみんなでがんばりまっす!

松本の地でSass/SCSSの邪悪なアンパサンドを撲滅するために立ち上がった

メドピアCTO室フロントエンドエンジニアの小宮山です、よろしくおねがいします。
趣味はボルダリングとヨガとピラティスです、よろしくおねがいします。

6月某日、長野県松本市の地にて開催されたメドピア開発合宿で取り組んだことについて紹介していきたいと思います。

アンパサンドへのウラミツラミ

タイトルにも挙げたとおり、今回立ち向かったのはSass/SCSSのアンパサンド(&)です。 メドピアのリポジトリはほぼSCSSで統一されているので、この記事ではSCSSの記法ベースでコード例を載せていきます。

アンパサンド記法は、BEMライクなセレクタを書くときによく利用されるのではと思います。
例えばこのようなものです。

.header {
  &__foo {
    color: green;
  }
  &--bar {
    color: blue;
  }
}

アンパサンドを使い、冗長な記述を限りなく減らしたスマートな書き方ですね。書いてるときはDRYなライブ感と共にスタイルを書き進めることができます。

しかしこの書き方には、セレクタの検索可能性が損なわれるという非常に大きな欠点があります。

検索妨害

上述のようなスタイル指定がされたコードに手を入れる場面を想定してみます。変更を加えようとしているファイルはhtmlとjs側です。

html

<div class="header__foo">
  foo
</div>

htmlにはheader__fooというclassを持ったタグがあり、jQueryを使ってセレクタからそのタグを取得して何かの操作をしているとします。

js

$('.header__foo').hide()

何の変哲もない古き良きjQueryコードです。

続いてこの処理をVueで置き換えてみます。細かいところは省略しますが、こんな感じになるのではないでしょうか。

<template>

<div class="header__foo" v-show="show">
  foo
</div>

<script>

Vue.extend({
  data() {
    return { show: false }
  }
})

さくっと置き換えることができ、JavaScript側はもうタグに付けておいたclassを必要としなくなりました。
不要なものは消せるときに消してしまうのが正義です。早速header__fooというclassをタグから消してすっきりさせてしまいましょう。

<div v-show="show">
  foo
</div>

と迂闊に消してしまう人はさすがにいないと信じたいです。JavaScriptから参照していなくても、CSSからclassを参照している可能性は大いに残っています。

このclassに対するCSSを書いた本人なら、このclassがまだ必要であることを覚えているかもしれません。しかし人の記憶は頼りになりませんし、このclassを消していいか迷っているのは何も知らない別人かもしれません。消したいclassがまだどこかで使われているかの判断に迷ったときは、grepするに限ります

どんな結末が待っているかはもう想像がついたかと思います。アンパサンドによる文字列結合で指定されているこのセレクタは、タグに付いているclass名でgrepしても拾われません。

.header {
  &__foo {
    color: green;
  }
}

そして使われていないことを確信し、コード改善と思ってclassを消した開発者には、スタイル崩れという悲劇が待ち受けています。

このようにアンパサンドによってセレクタが文字列結合されていると、タグのidやclassがどこで使われているのか見つける手段が大幅に制限されてしまいます。 地道に探せば見つけられるかもしれませんが、その労力と、不要なセレクタを減らす地道なコード改善が釣り合うのかは微妙なところです。
そして結局は触らぬ神に祟りなし、必要なのかも分からないidやclassは消えることなく未来に向かって増え続けていきます。

purgecss

検索妨害と仕組みは同じですが、アンパサンドによるセレクタ結合はpurgecssによる未使用スタイル削除の妨げとなる可能性があります。

purgecssは非常に強力なものの、セレクタを使っているかの判定は静的解析によって為されています。どこまで正確に判定しているのかまでは精査できていませんが、不要なリスクを回避する意味でも、アンパサンドやスクリプトを用いた文字列結合によるセレクタ生成は避けたほうが無難です。

合宿の地、松本にて

grepを阻害する邪悪なアンパサンドによるセレクタ文字列結合は、すでにメドピアのリポジトリ群に多く入り込んでいました。この現状に立ち向かうべく、アンパサンドを使わない素のセレクタ文字列に変換してしまおうと決意を固めます。

そしてメドピアでは年に数回、2泊3日かけて自由なテーマに取り組むことができる開発合宿を開催しています。 ボリューム的にちょうど良さそうだったこともあり、アンパサンド変換用のCLIツール開発をテーマに決めて取り組むことにしました。

採用言語

CLIツールの開発言語にはRustを選びました。フロントエンド関連なのだからJavaScriptやTypeScriptでいいじゃないかという葛藤はもちろんあったものの、舞台はせっかくの開発合宿です。今の最善でないとしても、興味のある技術にフォーカスしてこそ開発合宿です。普段とは違う言語に四苦八苦したくなるときだってあるんです。

成果物

リポジトリはこちら。 github.com

CLI

まずは当初目的通りCLIツールです。clapというCLIツール作成用便利パッケージを使うと引数解析やヘルプ表示などを簡単に作れます。

USAGE:
    sassruist [FLAGS] <path>

FLAGS:
    -f, --fix        fix original file(s)
    -h, --help       Prints help information
    -V, --version    Prints version information

ARGS:
    <path>    target file or directory path

ファイルを書き換えたりといったCLI特有の機能以外はWebAssembly版と同じ動作なので、デモなどはそちらに引き継ぎます。

WebAssembly

フロントエンド、Rustというキーワードが来たら当然続くのはWebAssemblyでしょう。 冒頭で長々と書いた当初の目的を果たすにはCLIツールさえあれば十分ですが、舞台は開発合宿です。隙あらば気になる技術に全力投球していきます。

成果物

netlifyにデモサイトを用意したのでお試しください。 determined-wescoff-282115.netlify.com

妥協点

成果物発表直後ですが早速妥協点紹介です。というのも正直なところ今回の成果物・・・作った本人から見ても実用レベルには至りませんでした、無念。
以下その理由です。

変換結果

「アンパサンドを置換するだけだし実装なんて簡単だろう」と気楽に作り始めてしまったのが運の尽き、Sassアンパサンド(というよりSassそのもの)は想像以上に手強い相手でした。

まずはこの変換例を御覧ください。

f:id:robokomy:20190716135423p:plain
変換例

みにくい!!!

そうなんです。アンパサンドを解決するにはセレクタのネスト構造を解決する必要があり、そのために記述が複雑になることを避けられませんでした。

ちなみに余談ですが、当初は勢いで走りすぎてこういう変換をしていました。間違い探しとしてお収めください。

f:id:robokomy:20190716135917p:plain
間違い探し

制約

100歩譲ってみにくさは我慢するとしても(diffが死ぬので譲りにくいですが)、仕様的に無視しにくい大きな制約もいくつか残ってしまいました。

特に厳しいのは複数行のセレクタに対応できていないことです。

f:id:robokomy:20190716140516p:plain
🤔🤔🤔

見るも無残な姿になってしまいました。「1行ごとに処理すればいいだろう」という謎の自信に満ちた実装方針により、複数行セレクタへの対応が必要と気付いたときには既に軌道修正が間に合いませんでした。

あと地味にSassを諦めてSCSS専用になっています。妥協点満載です。

CLIとWebAssembly両対応ビルド

気を取り直して、今回一番苦戦した点の紹介です。実は実装を差し置いて、CLIとWebAssemblyの両方にRustコードをビルドすることにかなり苦戦しました。
RustやCargo.toml自体への理解度もあまり深くなく、試行錯誤の末になんとか形になった方法をここで紹介したいと思います。

シンプルなWebAssemblyビルド

RustをWebAssembly化し、ウェブページとして公開する1連の流れ自体はこちらの記事を大いに参考とさせていただきました。

webbibouroku.com

bindgen、wasm-pack、create-wasm-appなど、便利なツールの導くままにWebAssemblyなウェブページを作ることができてしまいます。

RustをWebAssemblyにビルドするだけなら解説通りにwasm-packを使うだけです。しかし今回はもう1つのターゲット(こちらが本命のはずなんですが)としてbinaryにもビルドする必要があります。

異なる依存パッケージ

CLIツールとしてはディレクトリ内のファイルを一気に処理させたかったので、dependenciesにwalkdirというファイル探索用パッケージを追加していました。
ウェブページて使うWebAssemblyにファイル操作は不要なのですが、Cargo.tomlのdependenciesに書かれたパッケージがWebAssemblyビルドのときにも認識され、wasm-pack buildコマンドを実行すると以下のようなエラーを吐かれてしまいます。

error[E0433]: failed to resolve: use of undeclared type or module `imp`
   --> /~~~/.cargo/registry/src/github.com-1ecc6299db9ec823/same-file-1.0.4/src/lib.rs:261:9
    |
261 |         imp::Handle::stdout().map(Handle)
    |         ^^^ use of undeclared type or module `imp`

原因は推測ですが、WebAssemblyにビルドできない何か(walkdirなのでおそらくファイル操作系)が混ざっていると判定されているのだと思います。
その証拠に、Cargo.tomlのdependenciesからwalkdirをコメントアウトすると無事にビルドが成功します。

つまり、WebAssemblyにビルドするときだけ手動でその行をコメントアウトすれば解決します!!!

許されません、これは許されません。ビルドコマンドをただ実行するだけなのに、こんなにも露骨な運用でカバープロセスを挟み込むなんて許されるわけがありません。README.mdにこんなビルド方法の解説を書くこともとてもできません。

環境ごとの依存パッケージ変更

本体のコーディング以上に血眼になって探し、ついにここで答えを見つけました。
修正後のCargo.tomlの書き方はこちらです。

gist.github.com

ポイントはclapwalkdirパッケージに付けたoptional = trueというオプションで、featuresとしてbinを指定したとき(cargo build --feautres bin)だけビルド時の依存関係に含めてくれるようになります。

f:id:robokomy:20190705185245p:plain

大まかな流れは上図のようになっています。WebAssemblyとしてビルドするときは不要なパッケージをwasm-pack buildから除外することに成功しました。そしてCLIとしてmain.rscargo buildするときは、--feautres binを引数に加えることで依存パッケージをしっかりと認識してくれます。
引数追加程度ならREADME.mdに書くこともきっと許されることでしょう。

本当はCLIとしてビルドするときにWebAssemblyでしか使わないパッケージも除外したかったのですが、こちらはwasm-packをさらに解析する必要がありそうで今回は泣く泣く断念しました。こちらの問題にも対応した完全版両対応ビルド設定の模索は今後の課題です。

f:id:robokomy:20190705185931p:plain

成果物やデモを公開するには、やはりウェブブラウザで見れるというお手軽さは魅力的です。本命のCLIツールを作り込みつつ、さくっと触れるデモをウェブで公開したいという贅沢な欲求をWebAssemblyは見事に叶えてくれました。

まとめ

Sass/SCSSの邪悪なアンパサンドへのウラミツラミからスタートし、それっぽく動くけど妥協点満載な解決ツールを作り、WebAssemblyの知見を紹介して締めるという全くまとまりのない記事となってしまいましたが、メドピア開発合宿3日間が見事に集約されています。自分で言うのもあれですが見事に集約されています。

実は人生初の開発合宿というイベントでした。好きなテーマに取り組んでいいという我らがCTO福村の言葉をそのまま受け取り、このように本当に好きなことに取り組むことができた濃密な3日間となりました。これで当初目的だったアンパサンド撲滅も達成できていたら最高だったのですが、残念ながら力及ばず。

しかしなんと、同じく合宿に参加していたフロントエンドエンジニア村上(@pipopotamasu)がstylelint-scssのruleでセレクタのアンパサンド結合を禁止するというスマートすぎるPRをさくっと作って本家のルールに加えてくれました。

github.com

邪悪なアンパサンド勢力の拡大はこうして食い止められたのです。松本の地にて立ち上がっていたのは村上です。僕は何もしていません。やはり持つべきものは仲間です。メドピアではフロントエンドエンジニアの仲間を絶賛募集中です。


(☝︎ ՞ਊ ՞)☝︎是非読者になってください


メドピアでは一緒に働く仲間を募集しています。 ご応募をお待ちしております!

■募集ポジションはこちら

https://medpeer.co.jp/recruit/entry/

■開発環境はこちら

https://medpeer.co.jp/recruit/workplace/development.html

開発合宿に行ってきました!@松本

メドピアのサーバーサイドサウナーの川井田です。

メドピアでは、年2~3回のペースで日常業務から離れて、業務改善や、技術研鑽のための開発合宿を開催しており、恒例となっています。

前回の様子はこちら。 tech.medpeer.co.jp

6月26日から28日まで3日間、エンジニア13名で行ってきました!

初めて参加してきたのでレポートしたいと思います。

今回の合宿地は2020年rubykaigiの開催地松本です!

f:id:degwinthegreat:20190703203832j:plain

※我々は遊びに来たのではなく、業務改善や技術研鑽のために合宿に来ております。


開発の様子

ホテルの部屋で集まって開発したり、

f:id:degwinthegreat:20190702082223j:plainf:id:degwinthegreat:20190702082242j:plain

会議室を借りてモブプロしたり、

f:id:degwinthegreat:20190702084618j:plainf:id:degwinthegreat:20190702085548j:plain

早いWi-Fiを求めてカフェに移動したり、

f:id:degwinthegreat:20190702084812j:plain

業務より疲れるという声が聞こえてくるくらいモクモク作業していました。

f:id:degwinthegreat:20190703235502j:plain

※我々は遊びに来たのではなく、業務改善や技術研鑽のために合宿に来ております。


グルメメモリー

ずっとモクモクしていたら心が邪悪になります。

しっかり食事を取ることでピュアな心を取り戻します。

f:id:degwinthegreat:20190702072734j:plainf:id:degwinthegreat:20190703235822j:plainf:id:degwinthegreat:20190704000115j:plain
f:id:degwinthegreat:20190703204341j:plainf:id:degwinthegreat:20190703204944j:plainf:id:degwinthegreat:20190703204351j:plain
f:id:degwinthegreat:20190702072423j:plainf:id:degwinthegreat:20190704000238j:plainf:id:degwinthegreat:20190702072450j:plain

f:id:degwinthegreat:20190704150654j:plain

※我々は遊びに来たのではなく、業務改善や技術研鑽のために合宿に来ております。


20人は収容出来る超巨大ストーブサウナのある銭湯に一人で行くもの f:id:degwinthegreat:20190702082802p:plain

【公式】湯の華銭湯 瑞祥松本|長野県松本インターに近い日帰り温泉

ホテルの宿泊者限定の送迎付き温泉旅館に行くもの

f:id:degwinthegreat:20190704001249j:plainf:id:degwinthegreat:20190704001327j:plain

f:id:degwinthegreat:20190704001337j:plain

www.hotel-shoho.jp

我々...


成果発表

時間も忘れてモクモク作業してきた成果を社内で発表します。 ここに全てをかけています! 外部に公開できそうな成果をまとめます。


開発環境全部構築するまで帰れま10

CTO福村自らみんなの開発環境をカイゼンしていきたい! との思いで取り組んだそうです。

10個のプロジェクトの開発環境を構築し、

立ち上げたコンテナの数はちょうど100!

hostsファイルには48個の*.testが追加されたそうです。

カイゼンPRお待ちしてます。

社員紹介アプリ

急激に社員が増え、顔と名前が一致しないのをカイゼンするために、3人チームで社員紹介アプリを開発していました。

コードは外部に公開されているので、ぜひPRを送ってみて下さい!

github.com

Firebaseを活用した猫監視システム

Firebaseへの好奇心の裏に隠れる愛猫を思う気持ちがとても印象的でした。

reireias-slides.firebaseapp.com

qiita.com

SASS/SCSSのclass名を結合する'&'を撲滅したい

後日詳しいブログを公開予定ですが、SASS/SCSSの'&'を撲滅するためにフロントエンドエンジニア2名が それぞれ違うアプローチで取り組んでいました。

一方はRustでフォーマッターを作ってWebSocketを使ったサーバーレスなリアルタイム変換アプリを公開し、

github.com https://determined-wescoff-282115.netlify.com/

一方は、stylelint-scssにルール追加のPRを作っていました!

※こちらのPRはマージされ、先日リリースされました! https://github.com/kristerkari/stylelint-scss/pull/338

|Added: selector-no-union-class-name rule. github.com


まとめ

私は、普段触らない技術に挑戦し続け、脳内メモリを加速度的に使い切り、 不完全燃焼に終わってしまい、3日間で成果を上げるためには、事前準備が大切であることを学びました。

我々メドピアでは、合宿を始め、輪読会、振り返り会などの、技術研鑽のための取り組みを 日々、行っています。

これらの取り組みを通して、さらに成長して、来年のRubykaigiでまたこの地松本に戻ってきたいと思います!

f:id:degwinthegreat:20190703200647j:plain f:id:degwinthegreat:20190701211105j:plain

(☝︎ ՞ਊ ՞)☝︎是非読者になってください


メドピアでは一緒に働く仲間を募集しています。 ご応募をお待ちしております!

■募集ポジションはこちら

https://medpeer.co.jp/recruit/entry/

■開発環境はこちら

https://medpeer.co.jp/recruit/workplace/development.html

Rails × ECS 運用してみたわかった起動タイプ EC2, Fargate の使い所

メドピアマッスル部上腕二頭筋担当、CTO室 kenzo0107 です。
今回はメドピアの直近のプロジェクトで採用している Rails × ECS Fargate についてです。

直近プロジェクト

直近プロジェクトでは AWS ECS を採用しています。

2018年10月にリリースした スギサポ deli は、メドピアで Fargate 初採用となったプロジェクトです。

スギサポ deli とは?

sugisapo.ws

病気で食事制限が必要な方やシニアの方々、より健康な食生活を目指す方など、誰もが美味しく召し上がれるお食事をお届けするサービスです。

「食事制限」 と聞くと、簡素な食事をイメージされる方もいらっしゃると思いますが
一度見て頂くとお分かりの通り、かなりバラエティに富んだ内容となっており、目にも美味しい品々が並んでおります。

是非一度お試しいただければ幸いです♪

今回お話ししたいこと

以前、 Rails x ECS でオートスケーリング&検証環境の自動構築を執筆しました。

tech.medpeer.co.jp

今回は ECS を運用してきてわかった起動タイプ EC2, Fargate の使い所、また、運用時に有用だったことについて話したいと思います。

ECS についておさらい

まずは ECS の起動タイプ Fargate と EC2 について、軽くおさらいです。

起動タイプ Fargate

インフラレイヤーが抽象化されており、EC2 の管理が不要です。

以下の運用コストがなくなることが何よりも有難いです。

  • EC2 インスタンスの定期メンテ
  • ECS エージェントのバージョン管理
  • EC2 オートスケーリング管理
起動タイプ EC2

EC2 を起動し、その上でコンテナを起動しています。

これらの特性により起動タイプ Fargate, EC2 の使い所を検討しました。

検討事項

  • コンテナへのアクセスはどうやってするの?
  • AutoScaling 速いのどっち?
  • お値段はどう?

コンテナへのアクセスはどうやってするの?

f:id:kenzo0107:20190210235843p:plain

起動タイプ EC2

EC2 へ ssh さえできれば、docker ps でコンテナの起動状態を確認したり、 docker exec でコンテナにアクセスし、 rails console を実行することも可能です。

起動タイプ Fargate

インフラレイヤーが抽象化されている為、サーバへ ssh ログインできません。*1

docker exec でコンテナにアクセスする様なことはできません。

AutoScaling 速いのどっち?

起動タイプ EC2

ECS のバックエンドとして AutoScaling Group で EC2 を起動させ、ECS に紐づけた ALB に EC2 を追加する運用をしています。

その為、 EC2 をスケールアウトさせた後に、タスクをスケールアウトする様にしないと、タスクに偏りが生じる等、正しくタスク配置されない時がありました。

起動タイプ Fargate

タスクのみ考慮すればよいです。

f:id:kenzo0107:20190621221227p:plain

特に、EC2 のスケール分を考慮する必要がないとしても、Fargate のスケールアウトが安定的で速いです。

お値段はどう?

f:id:kenzo0107:20190211002727p:plain
Price

Fargate の価格はタスク数, CPU, Memory に正比例します。Fargate pricing

2019年1月に Fargate の価格が下がったとは言え、タスク数が増えることを考えると、まだ Fargate の方が割高?と思います。

本番・ステージング環境での Fargate, EC2 の使い分け

これまでの起動タイプ Fargate, EC2 の性質を加味して、プロジェクトの性質にも依りますが、以下の様な構成を採用しているケースが多いです。

f:id:kenzo0107:20190619234356p:plain

※ 以下の前提です。

  • 一般ユーザがアクセスする方を App、弊社からのみアクセスする管理画面を Admin
  • App, Admin 共に同じ Rails プロジェクトがデプロイされている

Point

  1. 本番環境 App のみ ECS 起動タイプ Fargate

    • ややコストは上がるものの、スケーラビリティに柔軟性がある
  2. その他は ECS 起動タイプ EC2

    • docker exec でコンテナに入りデバッグ可能にする
    • 前回の記事 の様に qa/* ブランチ毎のタスクが複数起動している為、タスク数が増えてもコストに影響しない
    • 本番環境 Admin は、高トラフィックとなる様なことは弊社ではない為、スケーリングを考慮する必要性がない。
    • 本番でも docker exec しコンテナに入りデバッグしたいという要望があり、Admin を 起動タイプ EC2 にすることで担保

開発時に有用だったこと

デプロイ

以下ブランチにマージすることで自動的に試験→デプロイする様にしています。

  • master
  • develop
  • qa/*

f:id:kenzo0107:20190619232701p:plain

CircleCI で試験をパスすると、 aws codepipeline start-pipeline-execution を実行し 指定の CodePipeline を開始する様にしています。

CodePipeline

こちらが実質 ECS へのデプロイをしている箇所です。

f:id:kenzo0107:20190528171331p:plain

デプロイ関連の処理は Capistrano でラップしています。

検証環境自動構築

f:id:kenzo0107:20190619235429p:plain

前回記事 検証環境の自動構築 をご参照ください。

ブランチ qa/* push により以下処理が実行され、検証環境が構築されます。

Rails master.key は?

AWS パラメータストアに登録しており、Rails イメージビルド時に aws ssm get-parameters で取得しています。

その他、イメージタグ付け( :tag_image )、ECR へ登録処理( :push_image_to_ecr )も併記しておきます。

namespace :rails do
  task :build_image do
    run_locally do
      within fetch(:deploy_work_path) do
        execute 'aws', 'ssm', '--profile', fetch(:profile).to_s,
                'get-parameters',
                '--with-decryption',
                '--region', 'ap-northeast-1',
                '--name', "/#{fetch(:application)}/rails/master_key",
                '--query', '"Parameters[0].Value"',
                '--output', 'text', '>', 'config/master.key'
        execute 'docker', 'build', '--no-cache=true',
                '-t', "#{fetch(:ecr_host)}/#{fetch(:env)}-#{fetch(:application)}-rails:#{fetch(:rails_tag)}",
                '--build-arg', "RAILS_ENV=#{fetch(:rails_env)}",
                '-f', 'docker/deploy/rails/Dockerfile', '.'
      end
    end
  end

  task :tag_image do
    run_locally do
      within fetch(:deploy_work_path) do
        execute 'docker', 'tag',
                "#{fetch(:ecr_host)}/#{fetch(:env)}-#{fetch(:application)}-rails:#{fetch(:rails_tag)}",
                "#{fetch(:ecr_host)}/#{fetch(:env)}-#{fetch(:application)}-rails:latest"
      end
    end
  end

  task :push_image_to_ecr do
    run_locally do
      within fetch(:deploy_work_path) do
        push_image_to_ecr("#{fetch(:ecr_host)}/#{fetch(:env)}-#{fetch(:application)}-rails:#{fetch(:rails_tag)}")
        push_image_to_ecr("#{fetch(:ecr_host)}/#{fetch(:env)}-#{fetch(:application)}-rails:latest")
      end
    end
  end
end

def push_image_to_ecr(image)
  execute 'ecs-cli', 'push', "#{image}",
          '--aws-profile', fetch(:profile).to_s,
          '--region', fetch(:region).to_s
end
イメージビルド処理短縮

以前は Rails イメージビルド時に asset_sync を利用し、 assets を S3 に同期していましたが、この同期処理に非常に時間が掛かっていました。

ですが、弊社フロントエンドエンジニア 村上 ( @pipopotamasu )medpacker により、 Sprockets によるアセットのビルド処理をしないようにした為、デプロイ時間が大幅に短縮されました。*2

是非以下ご一読ください。 tech.medpeer.co.jp

Rails メトリクスを Datadog へ送信

デプロイ後に Rails の以下メトリクスを Datadog に送信する様にしました。*3

  • Rails Load Time
  • Rails CodeStats
  • Gem Dependency Count

post rails metrics to datadog · GitHub

f:id:kenzo0107:20190625144500p:plain

こちらは以前 2018年9月12日 に開催された 『MedBeer -Rails開発での技術的負債との付き合い方-』にて、クックパッド社の 小室 直さん (@hogelog) の発表を参考にさせていただきました。

ありがとうございます!

tech.medpeer.co.jp

技術的負債となる指標をプロジェクト初期から意識することで、返済への意識も育まれると思います。(願い)

ロギング

f:id:kenzo0107:20190619235524p:plain

Lambda で LogGroup を S3 に日時バックアップする処理はこちらの Serverless Framework プロジェクトで構築しています。

github.com

ログ閲覧

CloudWatch Logs Insight で非常にログの閲覧がスムーズになりました。

以下の様なクエリで、logStream を rails をプリフィックスとしフィルターをかけると、Rails コンテナのログを抽出できます。

fields @timestamp, @message
| sort @timestamp desc
| limit 20
| filter @logStream like /^rails/

f:id:kenzo0107:20190530222814p:plain

時系列で複数コンテナログを閲覧したい場合は、以下の様にすれば簡単に取得できます。

fields @timestamp, @message, @logStream
| sort @timestamp desc
| limit 20
| filter @logStream like /^rails|^nginx/

以上からインサイトで検索しやすい様、ECS の Service 単位でコンテナのロググループは統一しています。

まとめ

  • 負荷の多い箇所は Fargate がオススメ
    • その他は EC2 がコスト的に良い
  • デプロイ自動化
    • テストは CircleCI、デプロイは CodePipeline と役割分け大事
  • Rails config/master.key は AWS パラメータストアで管理
  • medpacker で脱 webpacker & デプロイ時間短縮
  • Rails メトリクスを Datadog に Post で定点観測
  • ログは CloudWatch Logs に一時保存
    • 日次で S3 保存し長期保存
    • CloudWatch Logs Insight でログ閲覧がスムーズ
    • ECS Service 毎にロググループを統一しとくとコンテナ毎の時系列ログが確認しやすい

上記に加えて、開発時に最も有用な、ECS を利用した検証環境自動構築については、またの執筆の機会にと思います。

以上、参考になれば幸いです。


(☝︎ ՞ਊ ՞)☝︎是非読者になってください


メドピアでは一緒に働く仲間を募集しています。 ご応募をお待ちしております!

■募集ポジションはこちら

https://medpeer.co.jp/recruit/entry/

■開発環境はこちら

https://medpeer.co.jp/recruit/workplace/development.html

*1:ssh コンテナを起動させ、アクセスさせることは可能です。

*2:大凡15→8分程度に短縮

*3:Gem の最新度については、現在、定期的な bundle update 当番により解消している為、送信していないです。

メドピアエンジニアに聞いてみたアンケート

こんにちは。 メドピアでエンジニアのゆるふわマネジメントをしている@arihhです。

事業拡大とともにエンジニアの数が増えている弊社ですが、 今回はそんな弊社エンジニアにどんな人がいるのか社内外向けに知ってもらうべく、 エンジニアのみなさんにアンケートをとってみました。

Q1.エディタは何を使っていますか?

みんな大好きエディタ論争です。 f:id:arihh:20190516141551p:plain

社内のemacs・vim戦争の現状をお見せしようと思ってのアンケートだったのですが、 VSCodeが2/3を占めるという一強状態の結果となりました。

Q2.shellは何を使っていますか?

エディタは何を使っているのかのアンケートは見たことがあるのですが、 開発のお供のshellはあまりみたことがないので聞いてみました。

f:id:arihh:20190516152555p:plain

こちらはfish・zsh・bashの3すくみとなりました。

Q3.はじめてHello Worldした言語は?

普段業務ではRubyを書いているエンジニアが多い弊社ですが、初めてのプログラミングを学んだときの言語を聞いてみました。

f:id:arihh:20190516184926p:plain

PHP・Rubyという人は0でした!!

初めての言語はRuby以外の言語の人が多く、またBASICからプログラムをやっている人も多いようです。 その他ですが、VB、MATLAB、Delphiなどの声がありました。

Q4.マウスやキーボードは何を使っていますか?

メドピアでは入社したエンジニアにMacBookPro(JIS/US選択可)を支給していますが、 それ以外にもキーボードを持参してきている方も多いので聞いてみました。

こちらはキーボードは標準のままが半数、HHKBが2割、Mac純正品が1割強といった結果でした。 また、キーボード・マウスの組み合わせでは以下のような回答がありました。

  • Majestouch MINILA / Logicool MX ERGO
  • ARCHISS ProgresTouch TKL US配列(Cherry MX 静音赤軸) / Kensington ExpertMouse ワイヤレストラックボール K72359JP
  • HHKB Pro type-S / SlimBlade Trackball
  • MiSTEL BAROCCO MD600(分離キーボード) / Apple Magic Trackpad 2
  • 素手

Q5.メドピアのいいところは?

ちょっと弊社のアピール的な内容になりますが、メドピアのいいところを聞いてみました。 挙がった声をまとめるとこんな感じです。

  • 働き方
    • 裁量労働制のため勤務時間が自由
    • 出社時間が自由
  • 制度や環境
    • テックサポート制度
    • 各種補助制度
    • エンジニアの成長補助が手厚く、Rubyコミュニティへの貢献もしている。社内勉強会も活発。
    • オフィスが静かで良い、集中出来る。色々な場所で仕事もOK
  • 風土・文化
    • 個々に裁量があり、決定する際にも議論がある所
    • 手を挙げれば挑戦させてくれるところ
    • 自分から手を伸ばせばなんでもできる、やらせてもらえるところ
    • コードをリファクタリングしていく文化
    • エンジニア文化が根付いている!医療に関わる事業なので社会貢献に直結している!
    • スキルアップ・新技術選定を柔軟に検討できるところ!
  • メンバー
    • 自分にできないことをできる人が近くにいて、安心して挑戦できるところ
    • 落ち着いたエンジニアが多いと感じるところ
  • CTO
    • CTOの雰囲気
    • なんか文句あってもCTOにいえば解決してくれる
    • CTOにメンションすると本を買ってもらえるところ

裁量労働制が本当に裁量労働制として勤務時間に裁量をもって業務ができたり、 テックサポート制度やRubyKaigiの参加などを会社が支援してくれたりするのはいいと思います! また、リファクタリングをしていく文化やCTOをイジる文化があるのもいいところかなと思います。

Q6.影響を受けた本は?

こちらの結果は思った以上にバラバラになってしまったので、並べて公開します (Amazonさんへのリンクはないです💦)

  • 達人プログラマー
  • エクストリームプログラミング
  • SCRUM BOOT CAMP THE BOOK
  • カイゼン・ジャーニー
  • ティール組織
  • [24時間365日]サーバ/インフラを支える技術 ……スケーラビリティ,ハイパフォーマンス,省力運用
  • カッコウはコンピュータに卵を産む
  • 実践ドメイン駆動設計
  • エンタープライズアプリケーションアーキテクチャパターン
  • オブジェクト指向のこころ
  • イノベーションのジレンマ
  • プロを目指す人のためのRuby入門
  • メタプログラミングRuby
  • 達人に学ぶ SQL徹底指南書
  • UNIXという考え方―その設計思想と哲学
  • 初めてのPerl
  • リーダブルコード
  • シリコンバレー流世界最先端の働き方
  • Exceptional C++
  • ブッダの教え一日一話
  • 自分の小さな「箱」から脱出する方法
  • すべてがFになる
  • ぼくらの七日間戦争

Q7.好きなコマンド

最後はネタ項目です!

peco
fzf
git checkout branchname origin/branchname
# shutdown -r now
git diff --cached
tail -f
vmstat(だが、medpeerのサーバに入ってない)
tap
hub
grep
z - https://github.com/rupa/z
whois
yes "sl" | sh
history | grep "hoge"
man
grap..less..?
open

最後に

アンケートに協力していただいたエンジニアの皆さんに、この場を借りて協力ありがとうございました。

今回のアンケートで想像以上に個性豊かなメンバーが集まっていることが再確認できました。 今回の記事で雰囲気など掴んでいただければうれしいです。


(☝︎ ՞ਊ ՞)☝︎是非読者になってください


メドピアでは一緒に働く仲間を募集しています。 ご応募をお待ちしております!

■募集ポジションはこちら

https://medpeer.co.jp/recruit/entry/

■開発環境はこちら

https://medpeer.co.jp/recruit/workplace/development.html

Ruby2.7の(実験的)新機能「パターンマッチ」で遊ぶ

はじめまして、メドピアのサーバサイドエンジニアの草分です。

RubyKaigi2019 1日目のセッションにてRubyのexperimental(実験的)な新機能「パターンマッチ」(Pattern Matching)が発表されましたね。

speakerdeck.com

この記事では発表で紹介されたパターンマッチの各種機能を確認し、遊んでみます。

概要

Rubyのパターンマッチとは?

Rubyist向けの説明として以下のような説明がなされていました。

  • データ構造の評価によるcase/when条件分岐の提供
  • 変数への多重代入

実際に例文を見てみましょう。

例文

case [0, [1, 2, 3]]
in [a, [b, *c]]
  p a #=> 0
  p b #=> 1
  p c #=> [2, 3]
end

case/whenではなくcase/inが追加され、caseで指定したオブジェクトとinに記載したデータ構造(パターン)を比較し、マッチした場合処理が行われます。

また、マッチした場合はパターンに記載した各変数に値がバインドされ、内部の処理で利用することが可能となります。


通常の多重代入とは異なり、オブジェクトのデータ構造とパターンが一致しない場合は処理されません。

case [0, [1, 2, 3]]
in [a]
  :unreachable # 配列構造が異なるため処理されない
in [0, [a, 2, b]]
  p a #=> 1
  p b #=> 3
end

その他、Hashもサポートされています。

case {a: 0, b: 1}
in {a: 0, x: 1}
  :unreachable # Hashの構造が異なるため処理されない
in {a: 0, b: var}
  p var #=> 1
end

それではこの機能が実装されているRubyを導入し、実際に試してみましょう。

準備

最新のRubyを導入する

  • まずは最新のRubyのソースコードをcloneし、trunkをチェックアウトします。
$ git clone https://github.com/ruby/ruby.git
$ cd ruby
$ git checkout origin/trunk
$ autoconf
$ ./configure
$ make
$ make install
  • しばらく待てば最新Rubyの完成です。

  • rbenvをお使いの場合、2019年5月時点では2.7.0-devバージョンを用いるとパターンマッチが利用可能なrubyをインストールすることができます。
$ rbenv install 2.7.0-dev

解説

基本文法/仕様

試す前に、パターンマッチの基本文法を確認してみましょう。

case expr
in pattern [if|unless condition]
 ...
in pattern [if|unless condition]
 ...
else
 ...
end

case 句に記載したものを検査対象として、以下のように動作します。

  • 記載したパターンを上から順番に評価し、最初にマッチしたものが処理される。
  • 1つもマッチしない場合はelse句の内容が処理される。
  • 1つもマッチせず、else句も存在しなかった場合は NoMatchingPatternError例外が発生する。

また、if/unlessによるguardも可能となっています。

case [1, 1]
in [a, b] unless a == b
  :unreachable # ここは処理されない
end

パターンマッチの基本文法は以上ですが、実行するとwarning: Pattern matching is experimental, and the behavior may change in future versions of Ruby!という警告が表示されます。
警告の通り、パターンマッチは実験的な機能であり将来振る舞いが変更される可能性があります。ご注意ください。

次は、現時点で利用可能なパターンの記法を1つずつ確認してみましょう。

Value pattern

case 0
in 0
in -1..1
in Integer
end
  • ===で比較して一致した対象にマッチします。
  • そのため、上記3パターンはいずれもマッチさせることができます。
    • ※処理が実行されるのは最初にマッチしたパターンのみです。

Variable pattern

case 0
in a
  puts a #=> 0
end
  • 任意の値にマッチし、マッチした値はローカル変数としてバインドされます。
case [0, 1]
in [_, _]
  :reachable
end
  • また、変数として利用しない場合は_を使うことができます。

Alternative pattern

case 0
in 0 | 1 | 2
  :reachable
end
  • | を用いることで複数のパターンにマッチさせることができます。

As pattern

case 0
in Integer => a
  a #=> 0
end
  • =>を用いることでマッチした値を任意の変数にバインドすることができます。
  • rescueの使い方と似た記法ですね。
    • rescue StandardError => e

Array pattern

ここから少し複雑になってきます。

Array patternでは以下のいずれかの記法がパターンとして利用できます。

pat: Constant(pat, ..., *var, pat, ...)
   | Constant[pat, ..., *var, pat, ...]
   | [pat, ..., *var, pat, ...] # BasicObject(...)のシンタックスシュガー

そして以下の条件を満たすとマッチします。

  • Constant(何らかのclassを指定) === 検査対象objectがtrueを返す
  • 検査対象objectの#deconstruct メソッドが配列を返す
  • そこで返した配列と指定したパターンがマッチする

この#deconstructメソッドの使い方が鍵になります。
まずはArrayを対象としたArray patternのパターンマッチから見ていきましょう。

class Array
  def deconstruct
    self
  end
end

case [0, 1, 2] # ↓4種のいずれの記法でもマッチ可能
in Array(0, *a, 2)
in Object[0, *a, 2]
in [0, *a, 2] 
in 0, *a, 2   # []は省略可能
end

次はStructを対象としたArray patternのパターンマッチを見てみましょう。

class Struct
  alias deconstruct to_a
end

Color = Struct.new(:r, :g, :b)
color = Color[255, 0, 0]

case color
in [0, 0, 0]
  puts "Black"
in [255, 0, 0]
  puts "Red"
in [r, g ,b]
  puts "#{r}, #{g}, #{b}"
end

このように、独自に#deconstructを定義することで任意のobjectに対してArray patternによるパターンマッチを行うことができるようになります。
これがRubyのパターンマッチの大きな特徴の1つとなっています。

Hash pattern

Hash patternでは以下のいずれかの記法がパターンとして利用できます。

pat: Constant(id: pat, id:, ..., **var)
   | Constant[id: pat, id:, ..., **var]
   | {id:, id: pat, **var} # BasicObject(...)のシンタックスシュガー

そして以下の条件を満たすとマッチします。

  • Constant(何らかのclassを指定) === 検査対象objectがtrueを返す
  • 検査対象objectの#deconstruct_keys メソッドがHashを返す
  • そこで返したHashと指定したパターンがマッチする

前述のArray patternと似た形式で、Hashのようにkey名を指定してパターンマッチを行うことができます。

class Hash
  def deconstruct_keys(keys)
    self
  end
end

case {a: 0, b: 1} # ↓4種のいずれの記法でもマッチ可能
in Hash(a: a, b: 1)
in Object[a: a]
in {a: a}
in {a: a, **rest}
  p rest #=> {b: 1}
end

こちらも独自に#deconstruct_keysを定義することで任意のobjectに対してパターンマッチを行うことができるようになるため、Rubyのパターンマッチの大きな特徴の1つとなっています。

また、#deconstruct_keysの引数keysには処理の効率化のためのヒントとして、パターンの処理に必要なkey名の配列が渡されます。 その配列に含まれないkeyについては返却せず無視してよく、処理コストの削減を行うことができます。

遊ぶ

クイックソートしてみる

「クイックソートはパターンマッチを用いると書きやすい」という情報を社内で耳にしたため試してみます。

処理対象は数値の配列であることを前提として、ソート処理を書いてみました。

def qsort(ary) 
  case ary
  in [piv, *xs] # 要素が2個以上
    lt, rt = xs.partition{|x| x < piv}
    qsort(lt) + [piv] + qsort(rt)
  else # 要素が0個または1個
    ary
  end
end

ピボットの位置は先頭で決め打ちですが、かなりシンプルに書けますね!

逆にパターンマッチを使用せずに書くとすればこんな感じでしょうか。

def qsort2(ary)
  return ary if ary.length <= 1
  piv = ary[0]

  lt, rt = ary[1..].partition{|x| x < piv}
  qsort2(lt) + [piv] + qsort2(rt)
end

比べてみると、配列長の確認や配列の分割が冗長に見えてきますね。 オブジェクト構造による条件分岐と変数へのバインドが同時に行えるパターンマッチは、かなり強力な文法なのではないでしょうか。

独自拡張クラスを作ってみる

Hash patternをHash以外で使ってみたかったので、Timeクラスを独自拡張し、令和表記で年号を返すメソッドを作ってみました。
As patternと組み合わせて利用しています。

class Time
  def deconstruct_keys(keys)
    { year: year, month: month, day: day }
  end

  def to_reiwa
    case self
    in year: 2019, month: (5..)
      "令和元年"
    in year: (2020..) => year
      "令和#{year - 2018}"
    else
      "not令和"
    end
  end
end

実行してみます。ちゃんと動きましたね。

p Time.local(2019, 5, 1).to_reiwa  # => 令和元年
p Time.local(2019, 8, 1).to_reiwa  # => 令和元年
p Time.local(2020, 1, 1).to_reiwa  # => 令和2年
p Time.local(2019, 4, 30).to_reiwa # => not令和

引数の型指定をしてみる

当日の発表では「引数に対してパターンマッチを実行可能とするのは技術的には可能だが、型の指定に使われることが想定されるので実装しなかった。」という内容の発言がありました。

ということで言いつけを破ってTryしてみましょう。

def arg(num, str)
  case [num, str]
  in [Integer, String]
    :ok
  end
end

このメソッドを呼び出してみると…

p arg(3, "3")     # => :ok
p arg("2", 2)     # => NoMatchingPatternError
p arg(:a, "a")    # => NoMatchingPatternError
p arg(1, :a.to_s) # => :ok

引数の型を間違えると例外をraiseするメソッドになりました! 全くRubyらしくありませんね。

まとめ

RubyKaigi2019で紹介されたパターンマッチの機能を一通り触ってみました。まだ実験的な機能であるとのことで仕様変更の可能性はありますが、正式リリースが楽しみな機能です。

この他にもRuby3で実装が予定されている静的解析や、逆に搭載が見送られた仕様など、将来のRubyに関する具体的な情報が次々発表されたRubyKaigi2019でした。今後のRubyの動向から目が離せませんね!


(☝︎ ՞ਊ ՞)☝︎是非読者になってください


メドピアでは一緒に働く仲間を募集しています。 ご応募をお待ちしております!

■募集ポジションはこちら

https://medpeer.co.jp/recruit/entry/

■開発環境はこちら

https://medpeer.co.jp/recruit/workplace/development.html

Railsで処理を別クラスに切り出す方法について

こんにちは。メドピアのRuby(Rails)化をお手伝いしている@willnetです。最近はエンジニアが増えた影響か、Railsの質問に答えていることが多いです。

以前、Railsの太ったモデルをダイエットさせる方法についてというタイトルでPOROを使っていこうという話を書きました。その際にコード例などもなるべく多く載せるようにしたのですが、このエントリだけを読んだ状態では、いざ「POROを使ってみよう!」としたときにまだ悩む余地がありそうです。

POROはその名の通り普通のRubyオブジェクトなので、いろんな書き方ができてしまいます。それなりに経験がある人でないと、どのように書いたらいいんだろう…と悩んで時間を使ってしまいそうですね。さらに、複数人で開発しているチームだと書き方のバラツキも気になるところです。きっと、POROを書くときのお作法が決まっている方が開発しやすいはず。

そこで、お作法を決める手助けをするために例を出してみます。

コード例

slackのようなサービスを作っていると想像してみてください。Messageモデルをsaveしたときに、それがhereメンションのときはチャンネル内のアクティブなメンバーのみ、channelメンションのときはチャンネル内のすべてのメンバーに対してMentionを作るという処理をMessageモデルに定義しています。

class Message < ApplicationRecord
  has_many :mentions
  belongs_to :creator, class_name: 'User'
  belongs_to :channel

  after_create :create_here_mention, if: :here?
  after_create :create_channel_mention, if: :channel?

  def here?; end # 省略
  def channel?; end #省略

  def create_here_mention
    members = channel.members.active - [creator]
    create_mentions(members)
  end

  def create_channel_mention
    members = channel.members - [creator]
    create_mentions(members)
  end

  private

  def create_mentions(members)
    members.each do |member|
      mentions.create!(to: member, chennel: channel)
    end
  end
end

そもそもコールバック使うのどうなの?など議論の余地があるコードですが、そこまで考え出すとこのエントリで取り上げる範囲が広がりすぎてしまうためそのあたりは無視してください*1。このコードからcreate_here_mentioncreate_channel_mentioncreate_mentionsを別クラスに切り出してみるとします。さてどう切り出すのが良いでしょうか。

切り出し方にいろいろな選択肢が存在します。

  • クラスやメソッドの名前はどのような観点で決めると良いでしょうか?
  • メソッドはクラスメソッドにすべきでしょうか。インスタンスメソッドにしたほうが良いでしょうか?
  • クラスは一つでいいでしょうか?切り出すメソッドごとにクラスを作ったほうがよいでしょうか?

これらの点について、僕は自分なりの意見を持っています。それが正しいかはさておき、他の人がどういう観点で判断をしているのかを知ることで、みなさんがPOROを書くときに迷うことが減るのではないかと思います。

POROに切り出した後のコード

説明の前に、切り出した後のコードを載せます。このコードを参考にしつつ、どういう観点で切り出しているのか書いていきます。

class Message < ApplicationRecord
  has_many :mentions
  belongs_to :creator, class_name: 'User'
  belongs_to :channel

  after_create :create_here_mention, if: :here?
  after_create :create_channel_mention, if: :channel?

  def here?; end # 省略
  def channel?; end #省略

  def create_here_mention
    HereMentionCreator.call(message: self)
  end

  def create_channel_mention
    ChannelMentionCreator.call(message: self)
  end
end
class HereMentionCreator
  delegate :channel, :creator, to: :message

  def self.call(message:)
    new(message: message).call
  end

  def initialize(message:)
    @message = message
  end

  def call
    members.each do |member|
      message.mentions.create!(to: member, chennel: channel)
    end
  end

  private

  attr_reader :message

  def members
    @members ||= channel.members.active - [creator]
  end
end
class ChannelMentionCreator
  delegate :channel, :creator, to: :message

  def self.call(message:)
    new(message: message).call
  end

  def initialize(message:)
    @message = message
  end

  def call
    members.each do |member|
      message.mentions.create!(to: member, chennel: channel)
    end
  end

  private

  attr_reader :message

  def members
    @members ||= channel.members - [creator]
  end
end

メソッド名は統一する

POROに切り出したとき、publicなインターフェースはcallもしくはnew(つまりinitialize)で統一するようにしています。基本的にはcallで、インスタンス化したオブジェクトを返すだけでよいときのみnewという使い分けをしています。

まずメソッド名を考え、それから属するクラスを決めるものだ、という言説があるのは知っていて(要出典)以前はそのように実装していました。しかしHereMentionCreatorのようなクラス名をつけることで、callメソッドがhereメンションを作るのだな、と十分推測可能です。またメソッド名が統一されていると「このクラスのメソッド名ってなんだっけ?」とならずに便利なので最近は統一するようにしています。

処理の実態はインスタンスメソッドに書く

今回やろうとしていることは、「hereメンションを作る」と「channelメンションを作る」という手続きを切り出すことです。なので次のようにクラスメソッドで実装する人も時々見かけます。

class HereMentionCreator
  def self.call(message:)  
    channel = message.channel
    members = channel.members.active - [message.creator]
    
    members.each do |member|
      message.mentions.create!(to: member, chennel: channel)
    end
  end
end

サンプルコードが簡単なので、なんだかこれでも問題なさそうに見えますね。しかし手続きがもっと多くなるとどうでしょうか。

チャンネルのミュートの概念を追加し、さらにプッシュ通知もするように機能追加したコードを書いてみます。

class HereMentionCreator
  PUSH_NOTIFICATION_LIMIT = 100

  def self.call(message:)
    channel = message.channel
    members = channel.members.includes(:mute_channels).active - [message.creator]

    members.each do |member|
      message.mentions.create!(to: member, chennel: channel, mute: member.mute?(channel))
    end

    not_mute_members = members.reject { |member| member.mute?(channel) }
    not_mute_members.map(&:id).each_slice(PUSH_NOTIFICATION_LIMIT).with_index do |ids, index|
      PushNotificationWorker.perform_in(index.minutes, message.id, uids)
    end
  end
end

これでも読める人は問題なく読めると思いますが、さっきよりも概要を掴みづらくなったのは間違いないはず。

インスタンスメソッドで実装すると次のように書くことができます。

class HereMentionCreator
  PUSH_NOTIFICATION_LIMIT = 100

  delegate :channel, :creator, to: :message

  def self.call(message:)
    new(message: message).call
  end

  def initialize(message:)
    @message = message
  end

  def call
    create_notifications
    create_push_notifications
  end

  private

  attr_reader :message

  def create_notifications
    members.each do |member|
      message.mentions.create!(to: member, chennel: channel, mute: member.mute?(channel))
    end
  end

  def create_push_notifications
    not_mute_members.map(&:id).each_slice(PUSH_NOTIFICATION_LIMIT).with_index do |ids, index|
      PushNotificationWorker.perform_in(index.minutes, message.id, uids)
    end
  end

  def members
    @members ||= channel.members.includes(:mute_channels).active - [message.creator]
  end

  def not_mute_members
    @not_mute_members ||= members.reject { |member| member.mute?(channel) }
  end
end

callメソッドがcreate_notificationsメソッドとcreate_push_notificationsメソッドを呼ぶだけになり、処理の概要がつかみやすくなりました。また、membersnot_mute_membersもローカル変数からインスタンスメソッドに切り出されたことで、それぞれのメソッドの行数が減り、処理の内容を把握しやすくなっています。

このように、メソッド分割することで抽象化がしやすくなるのがインスタンスメソッドを利用する主な理由です。

こう書くとクラスメソッドでもメソッド分割できるのでは?という意見がでてきそうですが、クラスメソッドで同様のことをやろうとするとクラスインスタンス変数を更新するコードになり、結果としてスレッドセーフではないコードになってしまいます。

一つのクラスには一つの公開インターフェース

今回はHereMentionCreatorとChannelMentionCreatorのように2つのクラスに切り出しましたが、次のように単一のクラスにhereメンションをするメソッドとchannelメンションをするメソッドを定義する人もいるのではないでしょうか。

class MentionCreator
  delegate :channel, :creator, to: :message

  def self.here(message:)
    new(message: message, type: :here).call
  end

  def self.channel(message:)
    new(message: message, type: :channel).call
  end

  def initialize(message:, type:)
    @message = message
    @type = type
  end

  def call
    members.each do |member|
      message.mentions.create!(to: member, chennel: channel)
    end
  end

  private

  attr_reader :message, :type

  def members
    @members ||= if type == :here
      channel.members.active - [creator]
    else
      channel.members - [creator]
    end
  end
end

一見これでも問題なさそうに見えます。しかし仕様が変更されるについてメンテナンスが難しくなってきます。例えば新しくeveryoneメンションもMentionCreatorで扱うようにするとどうなるでしょうか。everyoneメンションは基本的にchannelメンションと同じですが、すべての人が参加しているチャンネル(generalチャンネル)以外ではメンションとして扱われないという仕様です。

素直にMentionCreatorクラスを拡張してみます。

class MentionCreator
  delegate :channel, :creator, to: :message

  def self.here(message:)
    new(message: message, type: :here).call
  end

  def self.channel(message:)
    new(message: message, type: :channel).call
  end

  # 追加
  def self.everyone(message:)
    new(message: message, type: :everyone).call
  end

  def initialize(message:, type:)
    @message = message
    @type = type
  end

  def call
    return if type == :everyone && !channel.general? # 追加

    members.each do |member|
      message.mentions.create!(to: member, chennel: channel)
    end
  end

  private

  attr_reader :message, :type

  def members
    @members ||= if type == :here
      channel.members.active - [creator]
    else
      channel.members - [creator]
    end
  end
end

結果として、callメソッドに分岐が一つ増えることになりました。このように分岐が増えていくと、1つのユースケース(例えばhereメンションのとき)だけについて考えたい状況でも別のユースケース(channelやeveryoneメンションのとき)のコードについて理解しなければいけなくなり、そのぶん可読性が落ちます。また、コードを修正したときに想定していない箇所でバグを仕込んでしまう、というケースも次第に増えていくことでしょう。

最初の例のように1つの処理ごとにクラスを作り、できるかぎり分岐を避けることでメンテナンスしやすくなります。

まとめ

僕がPOROを書くときの書き方について、それぞれ根拠を添えて説明しました。他にも良いやり方はあると思うので「俺はもっと良い書きかたを採用している!」という人がいたらどのように書いているのか教えていただけると嬉しいです(\( ⁰⊖⁰)/)


(☝︎ ՞ਊ ՞)☝︎是非読者になってください


メドピアでは一緒に働く仲間を募集しています。 ご応募をお待ちしております!

■募集ポジションはこちら

https://medpeer.co.jp/recruit/entry/

■開発環境はこちら

https://medpeer.co.jp/recruit/workplace/development.html

*1:コールバックについてはまた別のエントリでとりあげるかもしれません

RubyKaigi2019にプラチナスポンサーとして参加しました!👉🍻

f:id:chmv:20190424190137j:plain

サーバーサイドエンジニアのhirapi(@chmv71)です。
普段は、スギ薬局さんと共同で提供している、食事制限が必要な方に向けた栄養管理食宅配サービス「スギサポdeli」の開発を担当しています🍱
4/18〜4/20の3日間、福岡は博多で開催されたRubyKaigi2019に参加してきましたので、今回はそのレポートをお送りします🎶

アルコールパッチテストスポンサー🍻

もといプラチナスポンサー💎ということで、3階メインホール前にブースを展開しておりました。

f:id:chmv:20190424214318p:plain
こんなブースでした!

メドピアグループが医療業界やセルフケア領域でサービスを提供していることにちなんで、お越しくださった方々にアルコールパッチテスト*1を試していただきました。

健康管理はまず自分の体質を知るところから。推測するな、計測せよ👀

f:id:chmv:20190424191625p:plain
赤くなるのは弱い体質です

特殊なジェルのついたシールを貼ったまま20分待つと皮膚の色が変わります。
写真のように赤くなると耐性低いので注意、変わらない方は平気な気持ちでつい飲み過ぎてしまうので注意です。

こちらが予想外の大好評を頂き、ご用意していた400名分がなんと完売!🎊
Twitterでも話題にしていだいて嬉しい限りです。
(投稿してくださった方から抽選で20名様にTシャツをプレゼントいたします。お楽しみに😉)
これを機に医療の世界と人々の健康を支援するメドピアという会社の存在を知っていただけたらいいなと思います。
パッチテストをお試しくださった皆様、ご自分の体質に合わせたペースでお酒を楽しんでください!🍻

他にも、我が社の誇るかわいいくまのお医者さん「メドベアちゃん」のチロルチョコをお配りしていました。
いくつかバージョンがある中で、一番人気はしれっと官房長官ごっこをしているこちらでした😍

f:id:chmv:20190424183130j:plain
初春の令月にして 気淑く風和ぎ……

私含めスポンサーとして初めてブースに立つエンジニアメンバーも多くおりましたが、足を運んでくださった皆様とお話しできてとても刺激になりました。
Rubyコミュニティの一員として、Rubyでものづくりをするエンジニアとして、これからも皆様と関わっていけたらなと思います。
お越しくださった皆様、改めてありがとうございました!✨

会議 in RubyKaigi

タイムテーブルを改めて見てみると、3日間で計66ものセッションが行われていたことがわかります。
貴重な知見を惜しげもなく発表してくださった登壇者の皆様、トラブルひとつ無く運営してくださったスタッフの皆様、本当にありがとうございました。おつかれさまでした。

個人的に印象的なのは、やはり最終日の開発者会議です。バージョンアップの度にわくわくさせてくれるRubyの新仕様はこのような議論を経て作られているんだ、と胸が熱くなりました。
大盛り上がりだった👉絵文字演算子👉*2も、壇上・会場・タイムラインがエンジニアらしい遊び心で一体になっていてとても素敵でした。Ruby 3.0の目玉になりそうですね!(笑)

メドピアチームとして

さて、前回の記事にもあった通り、今回メドピアからは16名のエンジニアがRubyKaigiに参加しました。
サービス全体の開発を引っ張るテックリードもいれば、つい最近Rubyを書き始めたジュニア層もいます。興味のある分野も様々です🌈
せっかくこの多様なメンバーで参加したので、今回のRubyKaigiで得た学びを皆で深めつつ、チームの力にしていきたいと考えています。

もちろん、その学びの過程や成果は当ブログで広く発信していく所存です💪
メドピアチームとしてRubyコミュニティ・Rubyist仲間の皆様に貢献できるよう準備を進めておりますので、お楽しみに!

また、Rubyコミュニティをさらに盛り上げていくため、イベントもどんどん開催していく予定です🍺
Rubyistの皆様におかれましてはRubyKaigi2020 in 松本に向けてアップを始めているところかと思いますが、こちらも併せてチェックしていただけると嬉しいです!

5/23(木)イベントやります!

そして早速、5月に新規Railsプロジェクト開発のノウハウについてのイベントを開催します💡 詳細はこちら💨
弊社事例を存分にご紹介いたしますので、東京近郊にお住まいの方ぜひお越しください! 軽食をご用意してお待ちしております😋

---

最後に、参加エンジニア全員で撮った1枚を。また皆様にお会いできることを一同楽しみにしております!

f:id:chmv:20190424181111j:plain
また来年、松本でお会いしましょうー!


(☝︎ ՞ਊ ՞)☝︎是非読者になってください


メドピアでは一緒に働く仲間を募集しています。 ご応募をお待ちしております!

■募集ポジションはこちら

https://medpeer.co.jp/recruit/entry/

■開発環境はこちら

https://medpeer.co.jp/recruit/workplace/development.html

*1:※ 体質的なアルコール耐性を判定する簡易検査キットのことです。

*2:弊社エンジニア・ぱんのTwitterより↓↓