Quantcast
Browsing Latest Articles All 25 Live
Mark channel Not-Safe-For-Work? (0 votes)
Are you the publisher? or about this channel.
No ratings yet.
Articles:

ぴよぴよSRE

この記事は クラウドワークス Advent Calendar 2018 の1日目の記事です。

はじめに

クラウドワークスでSREとして働いている @kangaechu です。クラウドワークスには2018年9月に入社しました。
この3ヶ月にSREとして働いた中でこういう技術的な知識が必要だった、ここら辺をもっと勉強しておけばよかったということをまとめてみました。前職との間で感じたギャップも合わせて書いています。
ちなみに前職はエンプラ系SEで、職歴の前半はインフラエンジニアとして働いていました。メインフレームでJCL書いてたりExcelで設定一覧の表を作って手でサーバに反映してたりしてましたが何か?

SREとして働くのに必要だった技術的な知識

TerraformによるAWSのInfrastructure as Code

クラウドワークスではサーバのほとんどがAWS上にあり、構成はTerraformというツールにより管理されています。TerraformはAWSなどの構成をコードで管理することができます。たとえばEC2のサーバを1台立ち上げるのであれば、

ec2.tf
provider "aws" {
  access_key = "ACCESS_KEY_HERE"
  secret_key = "SECRET_KEY_HERE"
  region     = "us-east-1"
}

resource "aws_instance" "example" {
  ami           = "ami-2757f631"
  instance_type = "t2.micro"
}

のようなファイルを準備し、terraform apply コマンドを実行することで作成することができます。

前職ではオンプレのサーバを使うことが多かったのですが、AWSなどを使う場合はAWSのWebのコンソールからぽちぽちすることで作っていました。

インフラをコード化することにより、GitHubのPull Requestを使ったレビューが可能となります。
これにより、Excelのパラメーターシートを作ってレビューしてもらい、パラメーターシートから再度設定を作るような不毛な作業が不要となります。
また、変更の経緯や修正の履歴が残ることによりなぜインフラの構成を変更したのかを後で追うことができるようになります。

最初はどんなリソースを記述すればいいのかわからず悩むことが多かったですが、最近は割とサクサクとかけるようになり楽しいです。既存のリソースをTerraform化する作業をしているのですが、AWS自体のリソースについても学べたりと両側からキャッチアップできている感があります。

Terraformを学ぶためには Getting Started を事前にやっていましたが、結局は業務で使っているTerraformを触るのが一番勉強になった気がする。
クラウドワークスのTerraform職人である @minamijoyo のまとめが参考になります。

Docker

クラウドワークスサービスのいくつかはDocker化されており、残りのサービスも順次Docker化を進めています。
Dockerはアプリとその実行環境をコンテナという形でパッケージングし、実行・配布などを容易にすることができます。

前職で構築したシステムはサーバ本体にJavaなどの実行環境を直接インストールしていたので、バージョンアップ時の手順は課題でした。オンプレのサーバだったので戻し作業も簡単ではなく、システム切り替えまで先延ばしすることも多くありました。

Docker化により、どのコンピュータでも同じ環境を再現することができます。最近はAWS Elastic Container Service(ECS)やAWS Kubernetes Service(EKS)のようなサービスを使うことにより、クラウド上に作成したコンテナを簡単にデプロイ・実行することができるようになりました。

Docker化を進めるにあたり、Dockerがない時期に作られたサービスがほとんどのため、サービス内に状態を持つものは順次状態を持たないように修正する手間はありますが、動的なスケールアップ/ダウンやカナリアリリースなども今まで以上に簡単にできるようになるのでぜひ進めていきたいところです。

Dockerまわりはちょっと勉強していたことが役にたっているかなと思います。
Dockerはそんなに難しくないので、チュートリアルを終わらせて、身の回りのOSSを勝手にDocker化してプルリクを投げることで武者修行的に強くなれると思います。

さまざまなWebサービスの利用

クラウドワークスではサービスの開発・運用に様々なWebサービスを使用しています。主なところだけでも監視系だとDatadogRollbarNew Relic。GitサービスはGitHub。CI/CDにCircleCI。コミュニケーションにQiita:TeamSlack。などなど。入社時にもらうWebサービスのアカウントの多さにびっくりしました(ログインはGoogle認証なので楽なのですが)。

前職ではお客様のシステム構築時にはオンプレの監視ツール(Tivoliなんちゃらとか)を設定していましたが、運用チームは別チームのためどう使われているのかとかはよくわかりませんでした。また、監視系のサービスは落ちるとまずいので、構成が複雑になりがちです。

既存のWebサービスを有効活用することにより、本来の目的ではない監視作業をそれらに任せることができ、サービスの開発や改善に注力できるのはいいことだと思います。
また、監視系のサービスは自分たちが状態を監視する人でもあるため、監視項目やグラフを簡単に修正できるところもポイント高いです。
問題があったときもSlackを見れば今どうなっているかわかるというのもシンプルでいいですね。

全部のサービスについて詳しくなっているわけではないですが、使いながら学んでいるところです。最近好きなサービスはDatadogで、家でも使っています。

Webサービスを作る言語への理解

クラウドワークスのメインのサービスはRuby on Railsで作られています。コードはGitHubにあり、エンジニアであればアクセス可能です。

クラウドワークスに入って驚いたのはSREのメンバーもコードを修正してプルリクを出してもいいということです。前職であれば、「このコードのこの部分は〇〇さんの担当だから修正を依頼して」という感じでした。また、インフラのチームはソースにアクセスすることすらできないのが当たり前でした。

運用などで見つけた不具合も特定のチームに依存せず修正をしあうことができ、改善のしやすさや開発の効率化に一役買っているのではと思います。

私は7年くらい前にRails Tutorialを一度やったことがあるくらいで、まだまだコードをうまく読めないので、少しずつ勉強していきたいです。

まとめ

今回は自分がSREとして働き始める中で必要となった技術について紹介しました。

上記で挙げた項目はWeb系のSREであれば割とどの会社でも必要となるスキルセットではないかと思います。これからSREになろうという人はここらから取り組んでみてはいかがでしょうか。
エンプラ系のSIerからの転職時には見知らぬスキルが多くハードルが高く感じますが、自分もこれを書きながら「スキル足りないのによくSREになれたなー」と思うので飛び込んでみれば意外に大丈夫なんだと思います。

6万行の大規模リファクタリングを完遂する上でPOとしてやってよかった5つのこと

この記事はCrowdWorks Advent Calendar 2018 の2日目の記事です。

はじめに

こんにちは。
クラウドワークスでプロダクトオーナーをしている @shiba_319 です。

私の担当するWEB開発チームでは、今年6月から10月の約5ヶ月間、クラウドワークスのコア機能の一つである「仕事依頼画面」の大規模なリファクタリングプロジェクトを行なっていました。

私は、普段コードを触るのはちょっとしたスタイル修正や簡単なコード修正程度の非エンジニアPOなのですが、
今日はそんな自分が 「約6万行の大規模リファクタリングを完遂するうえでPOとしてやってよかったこと」を書きたいと思います。

4名の開発チームで大規模リファクタリングをすることになった経緯

私たちの開発チームは、当時4名チームで、クラウドワークス発注者向けのUI/UX改善を担当していました。
(構成はエンジニア2名(!)、デザイナー1名、PO1名。中盤からエンジニア1名増え、計5名に)

リファクタリング対象になった「仕事依頼画面」は、クラウドワークスの発注者が仕事を発注する際に、仕事内容や予算・納期などの仕事情報を入力する画面です。

< クラウドワークスの仕事依頼画面 >

この画面はクラウドワークスの仕事が生まれる源泉であり、重要なコア機能なのですが、一方で社内のエンジニアから長年「闇」と呼ばれており

  • クラウドワークス内で最も複雑なスパゲティーコード
  • JavaScriptによって200以上ある仕事カテゴリ数ぶんのUI描画パターンがある

等々の問題を抱えたまま約2年間ほぼ手をつけることができていませんでした。
結果、私たちのチームがUI/UX改善を行おうとした際に

  • 通常の数倍の開発工数に膨れることによるROI悪化
  • 何か機能実装しようとすると闇の中にさらに闇を作ってしまうのでエンジニアが頭を抱える

といった状況となり、
「ビジネス的にやりたいことがあっても技術的負債に行く手を阻まれる」という大きな問題を抱えていました。

そのような状況下で、開発チームのエンジニアからPOへアラートが上がり、チームとしてリファクタリングに腰を据えて取り組むことにしました。

--
※エンジニア目線で何がどうヤバかったのか、興味のある方は、我が開発チームのエンジニアの記事をご覧ください。
混沌を極める jQuery のコードをいかにして Vue.js に頼らずに整理したか - Qiita

--

POとしてやってよかった5つのこと

まず結論としては、このリファクタリングは完遂することができ、
・6万行のコード変更
・200通り以上のUIパターン→6通りに凝縮
・疎結合でシンプルな内部構造
・コード簡素化による結果的なパフォーマンス改善
という結果をおさめることができました。

リファクタするぞ!とチームの意思が固まってからこれをやり切るまでに、
POとしてやってよかったことを記述していきたいと思います。

1.ステークホルダーにリファクタリングの価値を伝え、理解を得ること

これがリファクタリングプロジェクトのPOとして一番最初にやったことで、一番重要だと思っています。

6万行のリファクタリングともなると大変な規模で(特に当初はチームのエンジニアは、シニアエンジニア1名とジュニアエンジニア1名のたった2名)、ざっと半年間の開発工数を確保する必要がありました。

ステークホルダーには、

「現在どういう状況で、何がまずいのか」
「ビジネス的にどういう価値があるのか」

という点について説明し、具体的には

  • 仕事依頼画面はプロダクトのコア機能の一つであり、今後ビジネス的に必ず改修したくなる時がくること
  • いざビジネス的に改修する時がきた際、今の内部構造ではやりたいことができないこと
  • 短期的に見ればユーザーに価値を与える仕事には見えないが、長期的な視点で見ると、ビジネス価値はとても大きなものになること

ここではざっくりですが、上記のようなことを伝えました。

リファクタリングのような内向きの仕事が生み出す利得は暗黙知的です。
マネージャーや周りのプロダクトオーナーにそれをやる意義を伝えることで、リファクタリングの仕事にしっかりと価値を持たせるのがPOの仕事といえるでしょう。

2. 短期的なチーム施策を全部止め、全リソースをリファクタに投入したこと

当時チームとしてあるKPIを持っていたので、すでに2〜3ヶ月分の施策プロダクトバックログを用意してありました。

が、リファクタリングに全てを注ぎ込むために、KPIはもう追わないと決め、短期施策は全て中止する判断をしました。
そして「リファクタをやりきることだけ」をチームの共通目標にすることにしました。

結果的に、開発メンバーのスイッチングコストを減り、一つのことに集中できる環境を作ることができたので、この意思決定は正解だったなと思っています。

3.リファクタリング範囲内にある、価値の低い機能を消す意思決定をすること

リファクタリングの実装フェーズに入ると、基本的にはエンジニアメインの作業のため非エンジニアPOとしては直接貢献できないのですが、

  • ユーザーに使われていない機能
  • メンテし続ける価値がないと判断した機能

を見つけ、「思い切って消す判断をする」ということを行いました。

全体作業量を減らすという意味で、POとしてもリファクタリング作業に貢献できたのではないかと思っています。

4. 短いスパンで途中で何度も立ち止まり、軌道修正すること

スパゲッティコードなので、とにかく内部にどんな強敵が潜んでいるかわかりません。
こういった不確実性が高い状況においては、最初のプランニングで全てを正確に見積もるのは不可能です。

ですので、計画は変わるものと捉え、1週間ごとのスプリントプランニングやリファクタの区切りのタイミング(約2〜3週間ごと)で何度も再計画していました。

(プロジェクト当初はこのことをPOとして理解しておらず、計画に固執してしまっていましたが、徐々に意味がないと気づきました)

5.リファクタリングの成果を、部署ないし全社に発信すること

最後に、無事リファクタリングをやり遂げたら、周囲に発信することです。

リファクタリングは内向きな仕事ですし、画面の見た目に変化があまりないので、何をやったのか伝わりづらいんですよね。

私たちの場合は、ちょうどタイミング良く全社キックオフのアワードがあったので、そこに自薦し、全社の前でプレゼンテーションをしました。
(ちなみに、全社MVPをいただきましたw)

 

以上が、POとしてやってよかったこと5つになります。

(オマケ) POとしてやるべきではなかったこと

最後に、リファクタリングプロジェクトにおいて、逆に「これはやっちゃいけなかったなー」という懺悔を連ねておきます。

POが実装計画の指揮をとること

自分は非エンジニアPOですし、リファクタリングはエンジニアが主体なので、はじめから実装計画はエンジニアに指揮をとってもらうべきでした。

開発チームがつくった実装計画に干渉すること

プロジェクト初期では、どうしても気になって口を出してしまっていましたが、エンジニア自身が最も状況を理解しているので、よくわかっていないPOが「ここもっと早くできないんですか?」などと干渉するのはやめましょう。

計画に固執すること

「最初の計画では〜」という会話を何度かしてしまいましたが、不確実性の高いプロジェクトにおいて、最初に立てた計画は途中でほぼ役に立たなくなることを学びました。
計画は細かいスパンで見直し、変更を歓迎しましょう。


どれも、スクラムのアンチパターンそのものですねw

おわりに

以上、「大規模リファクタリングを完遂するうえでPOとしてやってよかった5つのこと(と、やるべきではなかったこと)」でした。

プロジェクト中にいろいろな失敗を経験しながら、自分たちなりの良いやり方を模索できたのは良かったのかなと思っています。
少しでもプロジェクトを推進する方の参考になれば幸いです。

デザイナーだけで作りはじめたデザインシステムにまつわる失敗 2018

CrowdWorks Advent Calendar 2018 の3日目の記事です。

はじめに

こんにちは。
UXデザイン部でデザイナーをしている @kanako16 です。

クラウドワークスでは2018年1月から、チーフデザイナーの上田と新人デザイナーの私を中心に、 クラウドワークスの新しいデザインシステムを作りはじめました。

デザインシステムはまだまだ完成していませんが、「今年の懺悔は今年のうちに!」ということで、私がしてしまった「デザインシステムにまつわる失敗 2018」を懺悔していきます。

デザインシステムってなに?

デザインシステムとは、UIコンポーネント、スタイルガイド、情報アーキテクチャ、フロントエンドフレームワーク、さらにはデザイン原則やブランド、アクセシビリティなどから構成されています。

デザインシステムやデザインガイドラインなどいくつかの似ている概念があり、その線引きは曖昧なため、自社サービスやプラットフォームの状況に合わせて、デザインシステムを構成する要素を決めている企業も少なくありません。

デザインシステムの目的は、一貫したデザインを行うための指針・デザインを提供することプロダクト・組織全体における共通言語になり、チームのコミュニケーションの効率化をすること だと考えています。

有名なデザインシステムには、Googleの「Google Material Design」やAppleの「Human Interface Guidelines」があります。

デザイナーだけで作りはじめたデザインシステムにまつわる失敗

デザイナーだけでは実装面に配慮したコンポーネント設計やデザインが難しかった

2018年1月。私たちはデザイナー2名でデザインシステムを作りはじめ、下記のような流れでデザインしていきました。

① 各社が公表するデザインシステムやスタイルガイドの事例研究
② Atomic Design などコンポーネント指向をインプット
③ プロダクトを構成するコンポーネントを収集・分類し、俯瞰的に観察する(Interface Inventory)
④ Interface Inventory で分類を元に、コンポーネントをリデザインする
⑤ スタイルガイドの実装(ここはコーダーさんに依頼)

デザインが終わり、「いざ実装するぞ」という段階で、デザインに対してたくさんのフォードバックがあり、実装が足踏みしてしまう場面がありました。もちろんデザインについてフィードバックがあることは想定していたことですが、デザインと実装のフェーズを切り離して進めてしまったことで、エンジニアとデザイナーが密にコミュニケーションをとることが難しくなってしまったように感じています。

さらに、数ヶ月のブランクを経て、デザインシステムの構築を本格始動しようとしたとき、また課題が見つかりました。それは、エンジニアがデザインシステムを使ってフロントエンド実装するときに、デザイナーが定義したスタイルガイドの設計では、実装が行いづらい可能性がある ということです。

私たちがデザインをする時、色々なデザインガイドラインやAtomic Designを参考にしながら、定義すべきコンポーネントを設計しました。しかし、デザイナーはクラウドワークスのフロントエンドの設計やエンジニアが実際にどのような手順で実装を進めていくかを知りません。スタイルガイドを作っていたけれど、それがどのようにエンジニアに活用されていくか具体的な方法がイメージできていなかったことが、コンポーネントの設計について課題が出てきてしまった理由なのではないかと思います。

デザインルールの可視化が曖昧で、エンジニアとデザイナーで共通認識を持つのが難しくなってしまった

デザインシステムの大きな役割の一つに「共通言語である」というものがあります。しかし、私たちは共通言語を作る過程で、すでに共通言語を作れていませんでした。

では、私たちがどのようにデザインを共有いき、なぜ失敗したのか見ていきましょう。

どのようにデザインのルールを共有していたか。

私たちは、「zeplin」というデザイン共有ツールを用いて、デザインのルールを共有していました。

デザイナーがsketchでデザインしたデザインデータをzeplinにアップロードすると、他のデザイナーやエンジニアは、zeplin上でスタイルを確認したい部分を選択することで、選択部分のスタイルを確認することができます。

そのため、デザインルールの共有も、コンポーネントごとに細かく明文化せず、基本的にはzeplinに任せて共有をしていました。

コンポーネントごとの組み合わせやページの中で使われるときのルールを明文化しなかったこと

私たちは、デザインシステムを考えていくとき、コンポーネントのデザインやルール決めを中心に行いました。しかし、実務の中でデザインデータが使われるときには、それぞれのコンポーネントは、ページの中の他の要素との関係でレイアウトされていきます。

コンポーネントのデザインの検証段階で、モックアップなどコンポーネントを使ってページのデザインを行いましたが、その際に「どのようなコンポーネントの組み合わせで、どのような余白を持たせるべきか」など、ページ内でレイアウトする場合のデザインルールを明確に決めていませんでした。

そのため、コンポーネントが使用されるときのデザインルールは、各デザイナーの心の中に仕舞われてしまい、実装段階で、エンジニアはどのように実装すべきかわからないという状況になってしまいました。

デザインデータの作り方がデザイナーごとに異なっていたこと

次に、下の画像のボタンのコンポーネントを見てください。AとBのボタンは、同じボタンに見えますよね?
image.png

それでは、このボタン実装しようとzeplinでスタイルを確認してみると....

image.png
image.png

上の画像のように、ボタンのwidthやhight、borderなど、細かい部分の設定が異なっています。

今までデザインデータの作り方を決めてこなかった私たちは、デザイナー同士の細かなデザインデータの作り方が異なっていました。そのため、デザイナー同士でも、デザイナーとエンジニアの間でも、デザインルールの理解が異なってしまい、最終的なコミュニケーションコストが大きくなってしまいました。

みんなが話しやすくなるための共有言語を作る過程が、もはや違う国から来た人たちが通訳なしで話していて意思疎通できていない、そんな状況にみんながモヤモヤとしてしまいました。

救世主現る!デザインシステムを作るフロントエンド専属チーム爆誕

2018年10月に、クラウドワークス初のフロントエンド専属チーム「TRF(Team Rebuild Frontend)」が誕生しました。フロントエンドチームでは、さまざまなフロントエンド領域の課題やテーマについて活動していきますが、まずはじめに「すばやくデザインを反映させる仕組みの構築」を目指して、デザインシステム作りを進めています。

ここで紹介したいくつか失敗は、このチームが発足したころに、実際にプロダクトのフロントエンド実装を担うエンジニアとデザイナーが一緒にデザインシステムを考え始めたことで見えてきました。

デザインシステムの目的は、一貫性のあるデザインを維持することだけでなく、デザインシステムを見れば、誰でもどのようにデザインすべきかわかること です。そのため、デザイナーだけでなく、エンジニア、さらにフロントエンドに関わる全ての人にとっても使いやすいものでなければいけないと思います。

エンジニアにとっても使いやすいデザインシステムを実現するためには、エンジニアの開発プロセスやプロダクトのコード、設計などの理解も欠かせませんが、デザイナーがそれらを十分に理解した上で、デザインシステムの設計やコンポーネントのデザインを行うことはとてもハードルが高いです......。

新デザインシステムを使って、エンジニアもデザイナーも、スイスイーっとプロダクトのデザインができるようになり、ピクセル単位のデザインの相談よりもユーザーに向き合う時間や本質的な議論ができる時間を増やしていきたいと思っています。

より実務の中で有益に使えるデザインシステムを作るために、エンジニアとデザイナーが一緒にデザインシステムについて考え、最高のデザインシステムを作っていきます!

謝辞

最後になりますが、このような課題を抱えたデザインシステムのプロジェクトを、途中参加でありながらガツガツと一緒に進めてくれているエンジニアに心より感謝いたします。そして、新デザインシステムが完成した暁には、エンジニアとデザイナーが一緒にデザインシステムを作った軌跡や苦悩を、どこかで公開したいと思っています。

それでは、引き続き Crowdworks Advent Calendar 2018 をよろしくお願いします!

サーバーサイド経験者に贈るフロントエンド技術の学び方

この記事は クラウドワークス Advent Calendar 2018 の4日目の記事です。

はじめに

クラウドワークスのフロントエンドチームでエンジニアをしています @eighty8 です。
2018年9月に入社しました。
長らくソシャゲのサーバーサイドエンジニアをやってまして、本格的にフロントエンド開発に携わるのはクラウドワークスに来てからになります。

フロントエンドエンジニア1年生になってみて最初に感じたことは、
「周辺技術が多過ぎてどっから手をつけていいのかわからんちん。。。」
です。

Frontend Roadmap というのを見みてもわかる通り、フロントエンドもこのボリューム ∑(゚ω゚ノ)ノ
今まで断片的に情報は掻い摘んできてたんですが、それじゃダメだ・・・と痛感しました。
まあ、これらすべてに精通している必要はないと思いますが、HTML書けます、CSS書けます、ついでにJavaScript(古来の)も!
だけではもう通用しなくなってきてる辛さ。。。

そこで、取っ掛かりが見つけられない初学者の人に、これからどのようにフロントエンドの技術を学んでいけばいいのか、ひとつ道筋を示してみたいと思います。

なお、この記事では、フロントエンドの中でもJavaScriptの話題を中心に書いていきます。

オススメ5選

まずは、細かいことは抜きにして、最低限以下の技術を学んでみることをオススメします。

  1. Node.js
  2. npm
    1. yarn
  3. ES6(ECMAScript2015)
  4. Module
  5. Webpack

なぜこのチョイスか?

  • Node.js/npm はフロントエンドの根幹にある技術で、これなしに今のフロントエンド開発は成り立たない
  • ES6は 新しいJavaScriptの中核となる仕様で、すぐに廃れることはない
  • Moduleは JavaScriptの保守性、再利用性を最大限に高められる機能
  • Webpackは 万能なかつハイブリッドなモジュールバンドラーで、フロントエンド開発の規模が大きくなるほど威力を発揮する

といった理由があるからです。

ここに挙げた技術をひとまず習得できれば、今のフロントエンド開発にビビらず立ち向かうことができると思います。
また、次にくる新しい波に乗っていくことができるはず!

ちなみに、オススメから外したもの

  • CSSプリプロセッサ, CSSポストプロセッサー, CSSフレームワーク
    • SassやLess, PostCSS
    • BootstrapやBulma

正直、フロントエンド開発では重要な立ち位置の技術ではありますが、ひとまずJavaScriptでまとめたっかたので外しました。

  • タスクランナー
    • GulpやRollup, npm-script

Web環境の構築を自動化してくれる便利ツールではあるものの、これがなければ環境構築ができないわけではないので外しました。

  • テストツール
    • JestやMocha, Jasmin

タスクランナー同様、これがなければフロントエンド開発ができないわけではないので外しました。

  • JavaScriptフレームワーク
    • ReactやVue, Angular

フレームワークを使う上でも5選に挙げた技術はベースになっているので、それを習得してからでも遅くはないので外しました。

1. Node.js

Node.jsはサーバーサイドJavaScriptの実行環境として登場しましたが、今はフロントエンド開発の最も根幹にある技術でもあります。
また、フロントエンドで使われる各種ツールのほとんどはNode.jsで書かれており、それらを駆使して開発を行っていく上では、避けて通れない技術になります。

ところで
「Node.jsって環境なの? それとも言語なの?」
という疑問にぶち当たったことありませんか?(私はぶち当たりました)

これはある意味どちらも正解です。

Node.js自体はJavaScript開発で使うツールをまとめたもの(JavaでいうところのJDK)で、その中には、インタープリターや後述するnpm(ライブラリ群を管理するツール)が含まれています。

Node.js配下を展開

$ tree /usr/local/Cellar/node/10.10.0 -L 3
/usr/local/Cellar/node/10.10.0
├── AUTHORS
├── CHANGELOG.md
├── INSTALL_RECEIPT.json
├── LICENSE
├── README.md
├── bin
│   └── node
├── etc
│   └── bash_completion.d
│       └── npm
├── include
│   └── node
│       ├── common.gypi
│       ├── config.gypi
|   〜 省略 〜
│       ├── v8-util.h
│       ├── v8-value-serializer-version.h
│       ├── v8-version-string.h
│       ├── v8-version.h
│       ├── v8.h
│       ├── v8config.h
│       ├── zconf.h
│       └── zlib.h
├── lib
│   └── dtrace
│       └── node.d
├── libexec
│   ├── bin
│   │   ├── npm -> ../lib/node_modules/npm/bin/npm-cli.js
│   │   └── npx -> ../lib/node_modules/npm/bin/npx-cli.js
│   └── lib
│       └── node_modules
└── share
    ├── doc
    │   └── node
    ├── man
    │   └── man1
    └── systemtap
        └── tapset

そして、Node.jsはスクリプト言語であって、インタープリターで実行できます。

Node.jsの構文

hello_world.js
const http = require('http');

const hostname = '127.0.0.1';
const port = 3000;

const server = http.createServer((req, res) => {
  res.statusCode = 200;
  res.setHeader('Content-Type', 'text/plain');
  res.end('Hello World\n');
});

server.listen(port, hostname, () => {
  console.log(`Server running at http://${hostname}:${port}/`);
});

インタープリターで実行

$ node hello_world.js
Server running at http://127.0.0.1:3000/

よって広義な意味では環境であり、狭義の意味では言語です。

なぜ混乱するか。
それは、実行環境の名前に拡張子を付けたから?

2. npm

npmは、Node.jsで書かれた開発ツール(パッケージ)を管理するためのツールで、Node.jsをインストールすると一緒にくっついてきます。
npmコマンドを介して、パッケージをインストールしたり、アップデートしたり、自作のパッケージをレジストリに公開したりできます。
Rubyの経験者であれば、npmはBundler、パッケージはGem、package.jsonはGemfile、という感じで置き換えてもらえるとわかりやすいかも。

npmコマンドでできること

$ npm -h

Usage: npm <command>

where <command> is one of:
    access, adduser, bin, bugs, c, cache, completion, config,
    ddp, dedupe, deprecate, dist-tag, docs, doctor, edit,
    explore, get, help, help-search, i, init, install,
    install-test, it, link, list, ln, login, logout, ls,
    outdated, owner, pack, ping, prefix, profile, prune,
    publish, rb, rebuild, repo, restart, root, run, run-script,
    s, se, search, set, shrinkwrap, star, stars, start, stop, t,
    team, test, token, tst, un, uninstall, unpublish, unstar,
    up, update, v, version, view, whoami

けっこう多いですが、使うものは意外と限られいてる。

開発でよく使うであろうコマンド

・パッケージのインストール(グローバルに)

$ npm install -g [パッケージ名]

・パッケージのインストール(ローカルに)

$ npm install [パッケージ名]

・パッケージのインストール(公開用オプション付き)

$ npm install --save [パッケージ名]

※ 後述するpackage.jsonのdependenciesに依存関係が追記される

・パッケージのインストール(開発用オプション付き)

$ npm install --save-dev [パッケージ名]

※ 後述するpackage.jsonのdevDependenciesに依存関係が追記される

・パッケージのインストール(特定のバージョン)

$ npm install [オプション] [パッケージ名@バージョン]

・インストール済みのパッケージを表示(依存パッケージは非表示)

$ npm ls --depth=0 | grep 絞り込みたいパッケージ名

・インストール済みのパッケージを表示(依存パッケージも表示)

$ npm ls | grep 絞り込みたいパッケージ名

・インストール済みのパッケージが最新かどうかチェック

$ npm outdated

・タスクの実行

$ npm run [パッケージ名]

2-1. yarn

パッケージマネージャーには、 npm の他に yarn というツールがあります。
これは、Facebook、Google、Exponent、Tildeによって開発された、新しいJavaScriptパッケージマネージャーのことで、npm と互換性があり、npm の以下の問題解決を目的としています。

  • インストールが遅い
  • インストール結果に一貫性がない
  • セキュリティ上の懸念がある

yarn の特徴

1. インストールが早い

yarnは一度ダウンロードしたパッケージをグローバルにキャッシュとして保持し、次回以降そこからコピーするため高速にインストールできます。 またダウンロード自体も並列で行なうため、初回ダウンロードも npm に比べて速い。

2. インストール結果に一貫性がある

yarnでインストールを行なうと、 yarn.lock というファイルが生成されます。
これは依存パッケージのバージョン情報を示しているファイルで、これを参照することで正確にパッケージを管理することができます。
(npmでもVer5からは package-lock.json が採用されました)

3. よりセキュアである

Yarnはインストール前にチェックサムで整合性を確認するため、より安全であるといえます。
なによりも、FacebookやGoogleが開発に関わっていることの信頼感や、インストール時の出力がクリーンであることなどなど、多くのメリットを享受できます。

package.json

npmでインストールしたパッケージは依存関係を含め、package.json という、ファイルで管理します。
また、インストールされたパッケージは node_modules というディレクトリに配置されます。

package.json
{
  "private": "true",
  "name": "myapp",
  "version": "1.0.0",
  "description": "",
  "main": "./lib/main.js",
  "scripts": {
    "server": "webpack-dev-server",
    "build_artifacts": "RELATIVE_PATH=true webpack",
    "build_lib": "webpack --config ./webpack.config.release.js",
    "test": "NODE_ENV=test npm run jest",
    "lint": "npm run eslint src/*"
  },
  "devDependencies": {
    "@types/jest": "^23.1.4",
    "axios": "^0.18.0",
    "babel-core": "^6.26.3",
    "babel-eslint": "8",
    "babel-jest": "^23.0.1",

     省略 

    "ts-loader": "^4.4.0",
    "typescript": "^2.9.1",
    "typescript-eslint-parser": "^16.0.0",
    "webpack": "^4.1.1",
    "webpack-cli": "^2.0.12",
    "webpack-dev-server": "^3.1.1",
    "webpack-glob-entry": "^2.1.1",
    "webpack-spritesmith": "^0.4.1"
  },
  "dependencies": {
    "popper.js": "^1.14.4"
  },
  "author": "",
  "license": "ISC"

パラメーターの説明

項目 意味 必須
name パッケージ名
version パッケージのバージョン
description パッケージの説明
main パッケージの中で最初に呼ばれるスクリプトファイル
scripts npmコマンドから実行できるタスク
dependencies 公開時に必要なパッケージの情報
devDependencies 開発時のみ必要なパッケージの情報
author パッケージ作者の情報
license パッケージのライセンス情報

npmやpackage.jsonを深掘りしたい場合は、
https://docs.npmjs.com/Packages and modulesCLI documentation がとても参考になります。

3. ES6(ECMAScript2015)

ES6(ECMAScript2015) は次世代JavaScriptの標準規格です。
旧来のJavaScriptに新しい機能や文法が追加され、さらに既存機能もアップデートされています。

安全、便利、効率的にプログラムを書くことができるようになっているので、ES6のシンタックスを使って開発するのが今のスタンダードです。(おそらく)
また、モダンなブラウザでは、ES6を旧来のJavaScript(ES5)に変換(トランスパイル)せずとも、そのままのシンタックスで実行できるので、今後ますます主流になっていくはずです。
compatibility table

Node.jsもES6に対応済みで、のちに登場するWebpackも、公式のチュートリアルはES6で書かれているので、早めに慣れておくといいかも。

追加された機能

  • let・constキーワードによる変数宣言
  • classキーワードによるクラス宣言
  • テンプレート文字列(Template strings)
  • 配列ヘルパー(foreach、map、filter、reduce、every、some、reduce、for..of)
  • アロー関数(Arrow functions)
  • オブジェクトリテラルとデフォルト関数の引数(Object literals, Default function arguments)
  • 関数の可変長引数(Rest and Spread operator)
  • 分割代入(Destructuring assignment)
  • promissとfetch
  • モジュール(Modules)
  • ジェネレータ関数(Generators)
  • mapとset

チュートリアルで参考にしたサイト
https://learn.co/lessons/introduction-to-es6
https://codeburst.io/es6-tutorial-for-beginners-5f3c4e7960be

4. Module

モジュールとは、JavaScriptをある役割や機能ごとに分割したそれぞれのファイル単位のことです。

特徴

  • ひとつのJavaScriptモジュールは、ひとつのJavaScriptファイルに対応する
  • モジュールは変数や関数などを外部に提供(export)できる
  • 別のモジュールで宣言された変数や関数などを取り込める(import)

利点

  • 保守性
    • 依存性を増やさずにコードベースを拡張していくことができる
    • モジュールを書き換えるとき、依存性が少ないほど書き換えは容易になります
  • 名前空間
    • モジュールごとに分かれたスコープがあるので、命名の競合が起きない
    • たとえば、module1の関数 x( ) はmodule2の関数 x( ) と衝突することはない
  • 再利用性
    • 同じモジュールを他のアプリケーションで共有することができる
    • 便利な変数や関数を複数の場所にコピペせずとも、モジュールとして再利用できる

また、モジュールにはいくつかの形式が存在しますが、今後は ES Module だけ押さえておけばいいかと。

モジュール形式

  • CommonJS
    • サーバーサイドをはじめ、Javascriptでアプリ開発をするための標準APIを目指したプロジェクト
    • Node.jsで採用されて認知度が上がった
  • AMD(Asynchronous Module Definition)
    • モジュールとその依存関係を定義し、必要に応じて非同期にロードするためのAPIを定義
    • RequireJSで実装された
  • ES Modules
    • ECMAScript2015で標準化されたモジュール仕様で、ブラウザが直接JavaScriptモジュールの仕組みを解釈できるようになった

モジュールを理解することで、後述するモジュールバンドラーの理解も早まります。

JavaScript モジュールの全体像を知るのに参考になるサイト
https://postd.cc/the-state-of-javascript-modules/

CommonJSとES Modulesの比較を知るのに参考になるサイト
https://qiita.com/rooooomania/items/4c999d93ae745e9d8657

ES Modulesの構文を知るのに参考になるサイト
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/export
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import
https://sbfl.net/blog/2017/07/26/es-modules-basics/

5. Webpack

Webpack は、ある役割ごと(クラスや関数単位)に分割されたJavaScriptファイル(モジュール)を、依存関係(読み込み順)を解決しつつ、1つのファイルにまとめる(バンドルする)機能を提供するツールで、モジュールバンドラーと呼ばれています。
loader(ローダー)を使うことで、CSSや画像ファイルといった、JavaScript以外のリソースもバンドルしてくれます。

ちなみに、モジュールバンドラーはWebpackの他にも、BrowserifyやRequireJSといったものがありますが、今は圧倒的にWebpackの時代です!

モジュールバンドラーが登場する前の課題

複数のJavascriptを組み合わせて一つの機能を実現したい場合、
従来の方法だと、HTMLに実行順を考慮した <script> タグを列挙しなければいけませんでした。

たとえば、

  • main.js(本体)
  • lib_parent.js(親ライブラリ)
  • lib_child.js(子ライブラリ)

の3つのファイルがあり、本体から親ライブラリと子ライブラリの機能を使いたいとします。(子ライブラリは親ライブラリに依存している前提)

このときHTML側に

<script src="./main.js"></script>
<script src="./lib_parent.js"></script>
<script src="./lib_child.js"></script>

と書くと、main.jsは使いたいlib_parent.jsを参照できずエラーになります。

正しくは

<script src="./lib_child.js"></script>
<script src="./lib_parent.js"></script>
<script src="./main.js"></script>

ファイル数が少なければ問題ないですが、これが5個6個..10個と増えていくと、依存関係も比例して増えていくので途端に管理が難しくなります。
また、変数名や関数名のコンフリクトが起きる可能性も高くなるので、命名には気を使うことになります。
そもそもHTML側で依存関係を意識しないといけない状態がよろしくない!

モジュールバンドラーが登場したことで得られるメリット

1. 自動的に依存性を解決する

複数のJavaScriptコードの依存関係を自動で解析し、最終的に1つのファイルとしてバンドルしてくれます。
よって、HTML側で依存関係をほとんど気にすることなく、バンドルされたファイルを読み込みさえすれば問題なく機能します。

反面、1つのファイルにバンドルしてしまうと、ファイルサイズが肥大化し、読み込みに時間が掛かってしまうといったデメリットもありますが、Webpackにはコード分割を行って、複数のバンドルファイルを生成するような機能も持っています。

2. サーバーへのリクエスト回数が減らせる

リクエスト回数が減れば、転送効率がアップします。

3. 大規模な開発がやりやすくなる

コードをクラス単位、関数単位で分割できるようになったため、コードの見通しが良くなり分業が楽になります。
また、モジュール化できることによって再利用性や保守性が高まり開発効率が上がります。

Webpackの優位点

1. 複数のモジュール形式に対応している

BrowserifyやRequireJSといった他のモジュールバンドラーは特定のモジュール形式しかサポートされていません。
その点、Webpackは、Moduleのところでも紹介した、すべてのモジュール形式を解釈できるので、過去のモジュール資産が活用できます。

2. ローダーやプラグインが豊富で拡張性が高い

Webpack自体は、あくまでJavascriptのモジュールをバンドルするだけのシンプルなツールですが、
ローダーを追加することで、altJSやaltCSS、画像ファイルといったものをコンパイルした上でバンドルすることも可能になります。
またプラグインを利用すれば、コードの圧縮やバンドルしたファイルを実行するためのページ生成なんかも行ってくれたりします。

3. フレームワークで採用されはじめた

Vue, Angular, Reactなどに内部的に利用されている他、Ruby on Rails でもVer5.1から標準採用されています。

以上の点から、最近のJavaScriptによるフロントエンド開発では、ほぼ欠かせないツールになっています。

おわりに

ここに挙げた技術は、あくまで開発のベースになりうるものであって、その他にも、オススメ5選から外した技術やら概念やら、とにかく覚えることは山ほどあります。
また、エンジニアとしてのスキル以外でも、UI/UX、ビジュアルデザインといった、デザイナーよりの知識が必要とされる場合もあるでしょう。

これらの状況が、辛いと思うか楽しいと思うかはあなた次第・・・

Rails5に更新するためMassAssignmentを撲滅しているお話

みなさんこんばんは。クラウドワークス Advent Calendar 2018 5日目担当のじゅんてつです。

Rails5、使ってますか?クラウドワークスではまだ使えていない現状がありまして、絶賛Railsの更新中でございます。この記事は、Rails5に更新するためMassAssignmentを撲滅することになったお話をします。

68747470733a2f2f73332d61702d6e6f727468656173742d312e616d617a6f6e6177732e636f6d2f63726f7764776f726b732d6861636b6d642f75706c6f6164732f75706c6f61645f30653939336432373934643931353731353138333638656163663337383631612e706e67.png

どうやってRails5更新しようとしてるのか?

Rails5に更新をするためには、何はともあれ変更点を把握しないと始まらないので変更点を洗い、次にRails4の状態で更新できるものは最新にし、最後にRails5にあげて動くように整えてあげます。これだけです。シンプルでわかりやすいですね。


  • 変更点の洗い出し
  • Gemの削除と更新
  • MassAssingmentの撲滅 (←いまこの辺)
  • Gemの追加、Rails5.x分
  • 後方互換が廃止されたコードの置き換え
  • 非推奨となったコードの置き換え
  • Rails5サーバの本番稼働インフラの整備
  • 受け入れテスト

いまやってることは、MassAssingmentで実装されてる箇所の撲滅です。コントローラーをStrongParameterで置き換えて、モデルの attr_accessibleattr_protected 外して回ることをしています。簡単そうでしょ?それがそうでもなかったんです。Rails4以前で開発されたプロダクトを触ってる方は、同じような経験をする方もいるのではと思っています。そんなとき、この記事がなんらかの助力になればと思っています。

なぜMassAssignmentを撲滅するのか?

Rails4で非推奨だった attr_accsessible attr_protected が、Rails5ではついに使えなくなります。昔からMassAssignmentの脆弱性が指摘されていたからしかたないですね。パラメータでのレコード更新は、モデル層でセーフティを掛けていましたが、今後はコントローラ層でセーフティを掛けるような仕組みにしなければなりません。

対応規模はどれくらいか?

弊社はまだStrongParameterへの移行が終わっていないコントローラーが残っています。Rails4にするときの対応で、 params.permit! を全コントローラに書いて回っていて、「日頃の業務で少しずつ気づいたら直していってね」ってやってたところほとんど直されていませんでした\(^o^)/ 対象を洗い出してみたら、なんと200本弱のコントローラーで対応が必要なことがわかりました。これを最初に知ったとき、心理的に結構辛かったです。

どう撲滅するのか?

辛いのはわかった。でも千里の道も一歩からなので、地道に頑張ることにしました。調べて結果、どうやら以下の対応をすることで撲滅することができそうです。

  1. コントローラーをStrongParameterで実装する
    • コントローラーでレコード更新している params[:hoge] を StrongParameter に置き換える
  2. モデルから attr_accessible attr_protected を消して回る
  3. Gemfileから gem protected_attributes を消す
  4. CI通しつつ念の為動作確認する

最終的には、勇気があれば何でも出来ます。

コントローラーをStrongParameterで実装する

どうやらコントローラーで受け取る params をStrongParameterにしてあげれば良いことが分かりました。params.require(:hoge).permit(:fuga) に置き換えて終わりでしょ?そんなの楽勝じゃん?余裕余裕!とはじめのうちは思っていました。

置き換えを進めていくうちに、パラメータの構造によって permit の仕方がいくつかあることがわかりました。また、業務ロジックを考慮しつつ、あとからStrongParameterの実装しようとするとそこそこ大変なことにも気づきました。正直なめてました、スミマセン。

StrongParameterの実装パターン集

反省を踏まえ、実装パターンを共有します。だいたいはこれでカバーしきれるので、今後対応する方の参考になればと思います。いま見えている範囲では、これらのパターンと勇気だけで撲滅できそうな感じがしています。

指定したカラムだけ取得(基本形)
params.require(:article).permit(:title, :content)
ネストしたパラメータを取得
params.require(:article).permit(:title, :content, comments: [:user_id, :content])
キーがない場合にエラーを出さないようにする
params.fetch(:article, {}).permit(:title, :content)

対象のキーそのものが飛んでこない場合はfetchを使います。知らずに require で実装しちゃうとエラー吐くので気をつけてください。

配列が入ってくるパラメータを受け取りたい

例えばこんな形の params があるとして

{
  user: {
    occupation_ids: [1, 2],
    job_category_ids:[1, 2]
  }
}

occupation_idsjob_category_ids を受け取りたいという場合。

以下の方法だと上手くいかない。

# 上手くいかないパターン; {} が返ってくる
params.require(:user).permit(:occupation_ids, :job_category_ids)

それぞれ、ネストした値として [] を指定しておけば期待通りに動く。
以下のようにするのが正解。

params.require(:user).permit(occupation_ids: [], job_category_ids: [])

モデルから attr_accessible attr_protected を消して回る

勇気を持って attr_accessible attr_protected を消してまわります。基本的にはコントローラー側がStrongParameterに置き換わったら消せるようになっているので、CI通ることを確認しつつ、念のため動作確認します。あとは祈ってリリースするだけです。信仰心が試されます。

attr_* を消して回っていたら気付いたこと

さて、いくつかのモデル側の対応を進めていたら、以下のような対応が必要だと気が付きました。

  • モデルのカラムでは無い項目が attr_accessible に含まれていたら
    • その項目のバリデーションが有る場合、attr_accessible から削除しても大丈夫
    • その項目のバリデーションが無い場合、エラーを吐くのでバリデーションを実装

こちらもサンプルとしてコードを晒しておきます。

モデルのカラムでは無い項目が attr_accessible に含まれていたら

例えばですが、コードはこんな感じ。

class HogeHogeApplication < ActiveRecord::Base
  attr_accessible :course, :agreement

  validates :agreement, acceptance: true
  ...
end

これをこうするとちゃんと動きます。

class HogeHogeApplication < ActiveRecord::Base
  # attr_accessible を全削除

  validates :agreement, acceptance: true
  ...
end

バリデーションを消すと

class WelfareIijApplication < ActiveRecord::Base
  # attr_accessible を全削除
  # validates :agreement, acceptance: true
  ...
end

「agreement とか知らんし」と怒られる(´・ω・`)

# rails consolet
> HogeHogeApplication.new(course: "a", agreement: "1")

# => ActiveRecord::UnknownAttributeError: unknown attribute 'agreement' for HogeHogeApplication.

まとめ

  • コントローラー側の対応はStrongParameter
    • いろんな permit の仕方があるがサンプル集のどれかのパターンになる
    • 業務ロジック知らないと後付が難しいのでちまちま直しておいたほうがベター
  • モデル側の対応は attr_accessible attr_protected を削除
    • 基本的に消すだけ
    • モデルのカラムに存在しない項目が含まれている場合、バリデーション掛けると動く
    • CI回しつつ念の為動作確認。

最後に

Rails5更新を進めていますが、まだ序盤のため本記事ではミクロな話題をピックしました。今後様々な課題に直面するはずです。どの工程が一番つかったかとか、思ったほどじゃなかったなーとか、こんな伏兵がいた!等などをプロジェクトが終わったらアウトプットしたいと思っています。

次回は、wonda-tea-coffeeが等間隔に並ぶ素数を見つけるそうです。引き続き クラウドワークス Advent Calendar 2018をよろしくおねがいします。

等間隔に並ぶ素数を探そう

等間隔に並ぶ素数を探そう

どうもこんにちは。2018年10月入社の@wonda-tea-coffeeです。
本当はReactNativeで何か作る予定だったのですが、時間が足りず数学ネタに走ってしまいました。

というわけで(?)等間隔に並ぶ素数のお話を始めます。

等間隔に並ぶ素数とは?

素数とは、1と自分自身でしか割り切れない正の整数です。
また、等間隔に並ぶ、というのは等差数列を意味しています。
等差数列についても復習しましょう。例えば、

$$7, 13, 19$$

は初項が7、項差が6の素数のみから成る等差数列です。

今回のモチベーション

可能な限り長い等間隔に並ぶ素数を見つけたい。

探してみた

実装方針

  • 事前にエラトステネスの篩で指定範囲内の素数を洗い出す
  • 初項をずらしつつ、項差を30ずつ増やしていく
     探索イメージ)
     7は素数!
      項差30: 7, 37, 67, 97, 127, 157, 187(←素数じゃないので探索終わり!)
      項差60: 7, 67, 127, 187, 247(←素数じゃないので探索終わり!)
     ...
     9は素数じゃない!
     11は素数
      項差30: 11, 41, 71, 101, 131, 161(←素数じゃないので探索終わり!)
      項差60: 11, 71, 131, 191, 221(←素数じゃないので探索終わり!)
  • 目的の長さの数列が見つかったらDone!

コード@Java

import java.util.*;

public class Solve {

  public static void main(String[] args) {
    long s = System.currentTimeMillis();

    final int LIM = 100000000;
    boolean[] isPrime = sieve(LIM);

    int target_length = 15;

    // 探索開始位置のループ
    main: for (int p = 7; p < LIM; p += 2) {
      // 項差のループ
      final int tl_minus_1 = target_length - 1;
      for (int d = 30; p + d * tl_minus_1 < LIM; d += 30) {
        int length = 0;
        for (int m = p; m < LIM && isPrime[m]; m += d)
          length++;

        if (length >= target_length) {
          System.out.print("長さ" + length + ": ");
          disp_sequence(p, d, length);
          break main;
        }
      }
    }

    long e = System.currentTimeMillis();
    System.out.println("time: " + (e - s) + "ms");
  }

  // 初項、項差、長さを元に数列を表示
  static void disp_sequence(int a, int d, int length) {
    System.out.print(a);
    for (int i = 1; i < length; i++)
      System.out.print(", " + (a + d * i));
    System.out.println();
  }

  // エラトステネスの篩を用いて与えられた数未満の素数を求める
  static boolean[] sieve(int n) {
    boolean[] isPrime = new boolean[n];
    Arrays.fill(isPrime, true);

    // 0と1は素数でないため除外
    isPrime[0] = isPrime[1] = false;

    // 2より大きな偶数は素数でないため除外
    for (int i = 4; i < n; i += 2)
      isPrime[i] = false;

    // 1より大きな奇数同士の積で表せるものは素数でないため除外
    for (int i = 3; i * i <= n; i += 2) {
      for (int j = i; i * j < n; j += 2)
        isPrime[i * j] = false;
    }

    return isPrime;
  }

}

今回見つけた等間隔に並ぶ素数たち

長さ3
$$3, 5, 7$$

長さ4
$$7, 19, 31, 43$$

長さ5
$$5, 11, 17, 23, 29$$

長さ6
$$7, 37, 67, 97, 127, 157$$

長さ7
$$7, 157, 307, 457, 607, 757, 907$$

長さ8
$$11, 1210241, 2420471, 3630701, 4840931, 6051161, 7261391, 8471621$$

長さ9
$$17, 6947, 13877, 20807, 27737, 34667, 41597, 48527, 55457$$

長さ10
$$37, 2040607, 4081177, 6121747, 8162317, 10202887, 12243457, 14284027, 16324597, 18365167$$

楽しいですね。

考えたこと・試したこと

  • 項差は3の倍数でなければならない
     →項差が3の倍数でない場合、数列の長さは2より長くなり得ない。
      ただし、数列に3を含まないものとする(これは読者への練習問題とする)。
  • 項差は10の倍数でなければならない
     →項差が10の倍数でない場合、数列の長さは5より長くなり得ない(こちらも読者への練習問題とする)
  • 求めた素数をHashSetに格納すればmainのループ先頭で素数判定をしなくて良い!と思いきやかえって遅くなった
     →containsが響いたかな・・・?

残る課題

・項差を30の倍数にしたとはいえ何の捻りもない全探索なのでLIMを増やすと極端に遅くなる
・ メモリ食い過ぎ

もっと長く

現在人類が見つけている等間隔に並ぶ素数の長さは26です。

では長さ27, 28, 29, ...の等間隔に並ぶ素数は存在するのでしょうか。
2004年、その問いにBen GreenとTerence Taoは解答を出しました。

彼らによれば、
素数の列は任意の長さの等差数列を含んでいる
そうなのです。

長さ100でも、1000でも、10000でも、存在すると言っているのです!
コンピュータの力を持ってしてもたったの長さ26の等間隔に並ぶ素数しか見つけられないにも関わらず!

数学ってすごいですよね・・・

本当に?

こちらのブログでGreen-Taoの定理の証明の日本語での(!)解説が見られます。

http://integers.hatenablog.com/archive/category/%E7%AD%89%E9%96%93%E9%9A%94%E3%81%AB%E4%B8%A6%E3%81%B6%E7%B4%A0%E6%95%B0%E3%82%92%E8%BF%BD%E3%81%84%E6%B1%82%E3%82%81%E3%81%A6

大学2年次程度の数学の知識があれば十分に理解できるそうです。

終わりに

いかがでしたでしょうか。
これを機に数学に少しでも興味を持っていただければ幸いです。

また、業務中にこんな趣味全開の記事が書けてとても満足しています。

明日は@yizknnさんで、
「RubyでRettyからランチ候補をオススメしてくれるSlackBotを作りました」
どうぞお楽しみに〜

RubyでRettyからランチ候補をオススメしてくれるSlackのBotを作りました

はじめに

クラウドワークス1年生の@yizknnです。2018年8月にエンジニアとして入社しました。
外部向けに投稿するのは初めてなので部屋の隅で震えながら書いています。

前置き

ある時、先輩がこう仰りました。
「ランチは毎日違うお店に行きたい。エンジニアたる者、未知への探究心を忘れたくない」
そんな訳で、修行も兼ねて社内版OSS Gateの時間を利用して、ランチ候補を調べてくれるbotを作成してみました。

クラウドワークスのOSSへの取り組みは、@minamijoyoさんの記事で詳しく紹介していますので宜しければ併せてご覧ください。
OSS開発に参加してみたい人の背中を押して回る - クラウドワークス エンジニアブログ

どんなBot?

:octocat: GitHub: https://github.com/yizknn/slack-lunch-bot

@bot lunch

Botユーザーにメンションを付けて「lunch」と発言すると、恵比寿のランキングからランダムに3件のお店を提案してくれます。
slack_lunch_bot_sample.png

3点仕様

コードを読みはすれど碌に書いた事が無く、記事のネタにするには時間も少なかったので仕様は小さくしました。

  • Slackでお手軽に使える
    • 弊社はSlackBotが多いので、同じような使用感だと良い
  • まとめサイトからデータを取得する
    • 未知のお店を知りたいので、Rettyからスクレイピングする
    • サイトに負荷を掛けたくないので、データをDBに保存するようにアップデート予定
  • サーバーにお金を掛けない
    • GASは権限の問題で断念
    • 勉強目的でもあるので、自分でHerokuに立てる

動作環境

  • MacBook Pro (Mojave 10.14.1)
  • Ruby 2.5.1
  • Heroku
  • Slack

各コードの働き

とても簡単なコードですが、処理の流れを追って説明します。
slack-ruby-botmechanizeをミニマムに利用しているので、行数は抑えられているような気がします。

ページ情報の取得

mechanizegetメソッドで指定ページの情報を丸ごと取得します。
Rettyは検索フォームから絞り込むと、動的に表示している為かデータが取得できません。
「恵比寿の美味しいランチ20選」のように、データセットが固定されているページを指定します。

lunchbot.rb
query = 'https://retty.me/area/PRE13/PUR1/city/131131500013/'

agent = Mechanize.new
page = agent.get(query)

データの選別

続けてmechanize.searchメソッドでページから指定したクラスの要素だけ抜き出します。
店名とリンクが欲しかったので<a>タグにしています。
Rettyは店舗情報なども取れてしまうので、改行で分けるという荒技で対応しました。

lunchbot.rb
restaurants = {}
elements = page.search('a.restaurant__block-link')

elements.each do |element|
  info = element.inner_text.split("\n")
  restaurants.merge!(info[1].strip => element[:href])
end

restaurants = restaurants.to_a.sample(3)

Slackへの送信

あとは適当に発言内容を整形して、slack-ruby-botsayメソッドでSlackに送ります。

client.say(text: recommend_comment, channel: data.channel)

お使いになるには

STEP 1: SlackAPIを作る

  1. 公式サイトの「Start Building」ボタンをクリックしてAPIを作成します
    usage_lunch_bot_step1-1.png

  2. [Bot User]ページでBotを作成します
    usage_lunch_bot_step1-2.png

  3. [OAuth & Permissions]ページの「Scopes」でBotがメッセージを送信できる権限を許可します
    usage_lunch_bot_step1-3.png

  4. 同じページの「Install App to Workspace」ボタンをクリックしてBotの利用を許可します
    usage_lunch_bot_step1-4.png

  5. アプリの認証に必要な「Bot User OAth Access Token」を控えておきましょう
    usage_lunch_bot_step1-5.png

STEP 2: Herokuで起動する

  1. Herokuで新しくAppを作成します
  2. Herokuにアプリをアップロードしてデプロイします
    • CLIでもGitHub連携でもお好みの方法でどうぞ
    • GitHubのアカウントがあれば、Forkして連携すると楽だと思います
  3. SlackのTokenを環境変数に保存します
    • 変数名は「SLACK_API_TOKEN」としてください usage_lunch_bot_step2-3.png

参考リンク

おわりに

今回は力量を超えた初挑戦を盛り込み過ぎて、これだけ小さくしても50時間くらい掛かりました。
改善の余地しかないので、これからも更新し続けていきたいと思います。
ゆくゆくは早くも先輩方から「機械学習でベストなランチを提案すると良いよね」「AWSも使えると良いね」と下知された機能まで搭載できると良いなと夢見ています。

iOSアプリでクリーンアーキテクチャをやったときの回顧録

この記事は クラウドワークス Advent Calendar 2018 の8日目の記事です。

はじめに

こんにちはクラウドワークスでエンジニアをしている @tkoshida です。

つい最近、新規のiOSアプリを開発するプロジェクトが始まったのですが、そのプロジェクトのiOSアプリ開発でクリーンアーキテクチャを採用しようということになりました。

しかし開発メンバーの一人から「クリーンアーキテクチャは嫌だ」と言う声があがりまして、ここでは何でそんなことになったんだっけ、というのを回想してみたいと思います。

TL;DR

  • クリーンアーキテクチャやってみたけどメンバーから実装が面倒でやりたくない的なことを言われた。
  • 別にクリーンアーキテクチャが悪いわけではない。
  • ただ確かに実装効率も検討してアーキテクチャを考えたほうが良いかもと思った。

反発の起きたポイント

冒頭の開発メンバーの意見をよくよく聞いてみると、以前作ったクライアントアプリでのことを気にしており、そのときのアプリのアーキテクチャ構成だと

  • 一々プロトコルなどを用意したり登場するコンポーネントが多くて実装効率が悪い。
  • 機能を実装してるというよりクリーンアーキテクチャを実装しているという感じになってしまう。
  • 自分は素早く機能を実現したい。
  • Fat ViewControllerにこだわる人多いが、そもそも登場人物が多くなると全体的にFatになることを気にしないのがよくわからない。
  • クリーンアーキテクチャはそもそも実用的ではない「机上の空論」と思っている。

ということで、元も子もない発言もありましたが確かにそういう意見もあるだろうなと思う次第でした。

クライアントアプリというのは、クラウドワークスではメンバー(受注者)向けのアプリをメインに開発していますが、クライアント(発注者)向けのアプリも機能が少ないながら開発、リリースしています。
このクライアントアプリの開発にはクリーンアーキテクチャを採用していました。

メンバーアプリの振り返り

クライアントアプリについてふり返る前に、クライアントアプリに先立って開発したメンバーアプリの開発について振り返ってみます。

クラウドワークスはもともとメンバー向けのアプリを2015年の夏に最初のアプリとしてリリースしています。
これは私がクラウドワークスに2015年春にジョインしてサイト仕様も何もわからず、どういうアプリを作るからの要件定義から結合テスト、リリースするまで3ヶ月ちょっとの期間で開発したアプリでした。
アプリの実装は私が担当して、その他デザイナやサーバーサイドのエンジニア、アルバイトの方などと作っていきました。

その頃は、Swift 1.2で、ReactiveCocoa/MVVMが流行っていた時期(解釈)で、私もその流れにのりObjective-Cしかやったことがなかった中Swiftに取り組み、Rxも利用したことがなかったですが
ReactieveCocoaを使って楽しく開発していました。
アプリのアーキテクチャに関してはMVVM(これも実践で使うのは初めて)を採用し、「Fat ViewControllerにならないな、よしよし」と開発していたのを覚えています。

ただ、振り返って見ると、初めて採用する言語であったり、Rxのライブラリも初めてで、MVVMにも挑戦したのでコードの書き方には若干独自ルールのようなものが存在したり、コードの責務の分け方が若干揺れていたりして、後にiOSアプリエンジニアの方が新しく入ってきたときに一々説明しないダメな箇所が発生したりで若干の課題感を抱えていました。

クライアントアプリの振り返り

2016年のある日、新しいアプリ開発の話があがりました。
それまではメンバー向けアプリは存在していたものの、クライアント向けのアプリがなかったためそのアプリを開発することとなったのです。

当時は、新しいiOSアプリ開発エンジニアも入っていた関係で、メンバーアプリとは違いアプリ開発も複数人で行うこととなりました。
そこで、下記は当時の経緯をまとめたものとしてあったのですが、このような課題感もありクリーンアーキテクチャを採用する方向に動きました。

メンバーアプリについてはMVVMをMVCのFat ViewControllerを解消する、ということで導入しました。
がやはりMVVMのViewModelでも肥大化しやすい、という状況になってきました。
(これでもだいぶMVCと比べるとましにはなっていると思いますが。。)

それに加えて、一人で実装しているときは良かったのですが自分以外の担当者が実装するようになると、ViewController, ViewModel, Modelの役割分担やクラスの分け方など、もともと意図していたような構成を保ちつつエンハンスしていくのが難しいことがわかりました。
それはどのような基準でクラスや責務を分割していくというのが不明確であったところも大きいかと思っています。

そこで、今回一般的にある程度は知られているクリーンアーキテクチャを採用しました。
クリーンアーキテクチャによって役割分担の仕方がより明確になり、担当者によりぶれない作りになることが期待されます。

Clean Swift

Clean Swiftがクライアントアプリで採用したアーキテクチャです。
Clean Swiftは、Swiftでクリーンアーキテクチャを実装する具体的な方法についてまとめたものとなっており、コンポーネントとして

  • View
  • ViewController
  • Presenter
  • Interactor
  • Model
  • Router
  • Worker
  • Configurator

といったものが定義されています。
その具体的な仕様については、ここでは詳しく述べませんのでリンク先を参照ください。

クライアントアプリでの各種コンポーネントの全体像は以下のようになります。

f7546919-b19a-deea-163a-27ea4512c3cd.png

Clean SwiftではWorkerがビジネスロジックを扱うコンポーネントとして登場していたのですが丸っとした印象をうけたので、WorkerをクリーンアーキテクチャのUseCase以降で置き換えました。

そしてこのアーキテクチャで、とある一覧画面を実装したクラス構成は次のようになりました。(ViewやModelなども含めるとものすごい数になるので割愛)

ZLHDQnin4BtFhn3qx90UMYYKaCIXeU10STFsFDYpQn6jP4Qp6DFwlrUEd0YZ7QFRCc_UVBmTlTieo38E3kABzLja7CpAOOpcBzpZwgNdCFhrFBZ0viFrzMVgEhXZ3hjJwEEdBvX2zu7Nn3csiFr67Xdty2ruoR4_6oi41u1S-td3XEk5ZGdsdUr7HWaCi4qTiIVpWocI5nGR_0Btn6aQwuMumnKLjlosj5k6CG7Vtz2gGyOd.png

各コンポーネントについて

Presentation Layer

ViewControllerとPresenter、Interactorについては以下のように一方向のつながりにするよう制約します。

69251b1b-2804-16af-74ae-7d8e4cdf5b60.png

  • ViewControllerは、Viewが検知したタップイベントなどをうけて、Interactorにそのイベントに対応するビジネスロジックを実行するよう依頼します。
  • InteractorはUseCaseを呼び出しビジネスロジックを実行してPresenterにその結果を通知します。
  • PresenterはInteractorからの結果通知をうけてViewControllerに表示内容を指示します。
  • ViewControllerはPresenterからの指示に従いViewの更新を行ったり、Routerを介して画面遷移を行ったりします。

以下のコンポーネント間はプロトコルでI/Fを定義しそれぞれI/Fに依存した実装をします。

  • ViewController -> Interactor
  • Interactor -> Presenter
  • Presenter -> ViewController

また、それぞれのコンポーネント間のメッセージは、以下のModel(struct)を定義してやりとりします。

Model 方向 説明
Request ViewController -> Interactor Viewのイベントを通知する
Response Interactor -> Presenter Interactorで処理した結果を通知する
ViewModel Presenter -> ViewController 表示する内容を指示する
struct ThreadList {
    struct Filter {
        struct Request {
            var star: Bool
        }
        struct Response {
            var star: Bool
        }
        struct ViewModel {
            var filterLabel: String
        }
    }
}

Domain Layer

UseCaseはInteractorから依頼されたビジネスロジックを実行します。
必要に応じてUseCaseからはRepositoryを介してDataStoreへアクセスしDBアクセスやAPI通信行います。
ここでもそれぞれプロトコルでI/Fを定義して、そのプロトコルに依存した実装にします。

また、UseCaseはDBアクセスなどの結果取得したEntityを、TranslatorにてPresentation Layerで利用できるModelに変換してから渡すようにします。

Data Layer

実際にDBにアクセスしたりAPI通信をしたりするDataStoreを置きます。
ここでもプロトコルでI/Fを定義して、そのプロトコルに依存した実装にします。
RepositoryからはそのI/FでDataStoreを呼びます。

実際に開発してみて

ポジティブな印象

上記に見てきたように一つの画面を実装するにも登場するコンポーネントが多いですが、確かにそれぞに意味はあって、FatなViewControllerが生まれにくいな、というイメージがあります。

ネガティヴな印象

一方、一つのviewアクションを追加するにしても ViewController->Interactor->UseCase->Repository->DataStore->Repository->UseCase->Interactor->Presenter とまわってやっと画面更新が行えることになります。しかもPresentation Layerでは各コンポーネント間はModelを定義してメッセージやりとりをするようにするのと、Domain Layerで扱うEntityについてはPresentation Layerに渡す前にTranslatorで変換してやったりで結構大変です。

これまでMVVMでやっていたように、ViewControllerでViewイベントを受け取ったらViewModelにイベント通知して(API通信などを行った上で)その結果をうけてViewControllerが画面を更新する流れに比べると、だいぶ細分化されている感があって手数が多く開発のオーバーヘッドがかかる印象です。

テンプレートも作ったよ

ちなみに上記の反省もあるので、開発時に一気に必要なファイルを自動生成するようにXcodeテンプレートを作成したりしてます。(Clean Swiftで配布されていたテンプレートをカスタマイズしたものです)

プロジェクトディレクトリ直下にあるXcodeTemplatesディレクトリ内でmakeするとインストールでき、そのあとはXcodeからNew File ...で作成したテンプレートを選択すると、関連したファイルを一気に作れます。

% cd XcodeTemplates/

% make install_templates 
mkdir -p ~/Library/Developer/Xcode/Templates/File\ Templates
rm -fR ~/Library/Developer/Xcode/Templates/File\ Templates/CW\ Client\ iOS
cp -R CW\ Client\ iOS ~/Library/Developer/Xcode/Templates/File\ Templates

6c9bad9a-978c-6091-ad14-fb1211341311.png

6a306893-4026-ddb1-62d5-9a91252d2b51.png

それぞれ自動生成されたファイルには、実装しておくべきプロトコルなどが事前に定義されているので、間違いなく実装していけるようになると思います。

例えば、ViewControlllerは以下のように生成されます。

AppFeedbackViewController.swift
//
//  AppFeedbackViewController.swift
//  cw-client-ios
//
//  Created by Takayoshi Koshida on 2017/06/04.
//  Copyright (c) 2017年 CrowdWorks Inc. All rights reserved.
//
//  This file was generated by the CW Client iOS Templates.
//

import UIKit

protocol AppFeedbackPresenterViewInterface: class {

}

class AppFeedbackViewController: UIViewController {

    var output: AppFeedbackViewInteractorInterface!
    var router: AppFeedbackViewRouterInterface!


    // MARK: - Private Methods

}


// MARK: - AppFeedbackPresenterViewInterface

extension AppFeedbackViewController: AppFeedbackPresenterViewInterface {

}

おわりに

今回のチームメンバーからの反発はClean Swiftやクリーンアーキテクチャによる問題ではなく、単に深く検証せずにClean Swiftをカスタマイズして拡張したためコンポーネントの数が増えてしまったためかと思います。が、ビジネスロジックが外部を知らないようにするには必要な手段だったようにも思うので難しいところです。

クリーンアーキテクチャを採用することは、コンポーネント間の責務もより明確になりコンポーネント間の依存も減らすことができ、Fat ViewContorollerのようなものが発生しなくなり、メンテナンス性の向上といった恩恵が得られると思いメリットは大きいと思います。
開発効率が落ちるから単純にMVVMに戻るなどではなく、開発効率も考慮しながらより現場の実情にあわせたアーキテクチャを検討していければと思いました。

検索機能を提供する前に確認しておきたいこと、みたいな感じの何か書く

External article

2018年マネジメント振り返り

External article

マーケターがSQLを使ってデータ分析ができるの3つの効能

クラウドワークスマーケティングチームの安藤です。
クラウドワークス Advent Calendar 2018の11日目として、「マーケターがSQLを使ってデータ分析ができることの3つの効能」をお届けします。

なぜこの話をすることにしたのか

今回、クラウドワークスアドベントカレンダー初参加のマーケティングチームですが、チームメンバー全員がSQLを使える、というのがちょっとした自慢ポイントです。
他社の方の話を聞くと全員がSQLを使える、というのは意外と珍しいようですので、今回はチームとしてSQLが使えるからこそのメリットをお伝えしようと思います。

SQLのいいところ

SQLを使えることのメリットの話をする前に、

  • そもそもSQLってなんですか?
  • SQLの何がいいの?Google Analyticsでよくないですか?

という点についてお話したいと思います。

SQLとは

Wikipediaによると、

SQL(エスキューエル[1]ˈɛs kjuː ˈɛl、シークェル[1]ˈsiːkwəl、シーケル[2])は、関係データベース管理システム (RDBMS) において、データの操作や定義を行うためのデータベース言語(問い合わせ言語)、ドメイン固有言語である。
引用:「SQL」『フリー百科事典 ウィキペディア日本語版』。2018年11月6日 (火) 12:12 UTC、URL: http://ja.wikipedia.org

とのことです。

つまり、データベースを扱うための言語なわけですが、多くのマーケターが扱う場合には、データベースから必要なデータを取り出し、分析するための言語、として利用される場合が多いかと思います。

Google Analyticsじゃだめなの?

Google Analytics(以下、GA)は便利ですよね。
手軽にアクセスができ、ユーザーのアクセス状況、経路などの分析が直観的に行え、弊社でももちろん利用しています。

ですが、GAにも限界はあると考えています。

GAでセッション以外の情報を取得するには、イベントトラッキング(やカスタムディメンション)を設定して計測を行っている必要があります。
裏を返すと、設定を行う以前の情報や、設定をしていない行動の計測はできないわけです。

また、セグメントなどである程度のカバーはできるものの、特定の行動をとったユーザーがその後どうしているのか?といった分析を詳細に行うのは難しい部分があります。

その点、SQLでは、蓄積されたデータベースの情報に直接アクセスできるため、例えば「会員登録からN日後のユーザーの継続ログイン状況」「仕事応募までに必要な仕事の閲覧数はどれくらいか」といったような、詳細な行動分析を行うことが(データさえとっていれば)比較的容易です。

ですので、弊社ではSEOで必要な分析や、大まかなアクセス状況の把握にはGAを利用しており、ユーザー行動の変化や、詳細な行動分析にはSQLを活用しています。

SQLを使ってデータ分析ができる効能

効能①:データ「分析」ドリブンになれる

データドリブンではまだ甘い。
これからはデータ分析ドリブンの時代です。(言いたかった)

データドリブンとは、
得られたデータをもとに施策決定や意思決定を行う
という、皆さまご存知のやつですね。

私が勝手に命名したデータ「分析」ドリブンとは、
得られたデータをもとに施策決定や意思決定を、その結果をどう分析し、次の意思決定に活かすか考えたうえで行う
というものです。

分析という行為は、「何を」知りたいのかを正確に定義することから始まります。
なので、データ「分析」ドリブンになるとは、

  • 施策が「何を」狙っているのかを正確に言語化し、
  • その目的をどのような指標で計測するかを決定し、
  • その指標を計測して、次のアクションにどう活かすのか

を決めて施策を動かすということを意味しています。

これを行うことで、直接的なメリットとして次のような事態がなくなります。

  • 施策をリリースしてから、「あの数字を見たかったけど、今からは見れないなぁ」となって正確な分析ができなくなる
  • リリースした施策が、後から考えると様々な数値に影響を与える内容になっており、結局何が起きたのかわからなくなる
  • 施策をどんどんリリースした結果、影響が混ざり合って結局何が起きたのかよくわからなくなる

これだけでも大きなメリットですが、副次的大きなメリットとして、

  • 施策の影響範囲を正確に考えてからリリースするので、事前の考慮漏れが減る
  • 施策が何を狙っているのかを言語化してからリリースするので、風呂敷の広げすぎが減り、施策を尖ったものにしやすくなる

という点があります。

例えば、最近マーケティングチームでは、利用者のプロフィール文章の入力例の改善をリリースしました。
小規模な改善ではありますが、この施策一つとっても、

  • 入力例の利用率はどの程度なのか?
  • 利用したユーザーのプロフィールは改善しているのか?
  • 逆に似たようなプロフィールばかりになる、などの事態は発生していないのか?

など、様々な影響が考えられます。

その中で、何が本質で計測すべきものなのか?
そして、その指標をどのように計測可能な状態にしてリリースするか、を考えることで、施策の狙う本質まで考える癖がつきます。

この学びは、何を分析するかを自分で決め、自分でデータを抽出するからこその学びだと思います。

効能②:アイデアが増える

異論はあるかもしれませんが、新しいアイデアとは既存の知識の組み合わせによって生まれると私は信じています。
要は、使える状態の情報をどれだけ保持しているか、がアイデアの幅を決めると思います。(それを引き出せるかは別の問題ですが)

image.png

さて、突然ですが皆さまは、自社のサービスに関わる「新しい切り口」のデータは1カ月にどれくらい見ているでしょうか?

1つでしょうか?
5つでしょうか?

私が、10月に新たに作成したクエリ(データ抽出用のSQL文)は18個でした。
他の月にも同様のペースで作成しており、平均すると月に20個のクエリを作成しています。

つまり、自分が作ったものだけでも新しい切り口でのデータを、月に20種類、 1年だと240種類、作成してインプットしているわけです。

もちろん、そのすべてを詳細に覚えているわけではありませんが、調べたという事実や印象的な傾向は覚えているものです。
常に新しい切り口でサービスを見て、そこから気付きを得られる、というのは自身でデータ抽出ができればこそのメリットだと思います。

効能③:なんか楽しい

単純に楽しいです。
普段やらないコードを書く、という作業も楽しいですし、自分が見たいデータが即座に見られる、という結果にもワクワクします。
一つのデータを見ると、
「他の角度から見るとどうなのだろう」
「どうしてここの数字だけ大きく動くなんてことが起きるのだろう」
と非常に好奇心が刺激されます。

唯一の難点としては、ついついいろいろなデータを見てしまい、今すぐ必要がないものまで調べてしまうことがある、という点でしょうか。

まとめ

いかがでしたでしょうか?
SQLはやってみると思った以上にカンタンにデータを扱えるようになります。

マーケターの役割は、広告やSEOなどの領域だけでなく、いかにデータを扱うか、という領域を中心に現在も、そして今後ますます多様性をもっていくことと思います。

データを扱うには、まずデータを知ることから。
ということで、社内の分析環境に触れられる方は、ぜひSQLでの分析を始めてみてください。

ReactNativeアプリに強制アップデート機能を導入する方法

この記事は クラウドワークス Advent Calendar 2018 の12日目の記事です。

こんにちは。エンジニアの kinakobo です。

最近アプリの開発をReactNativeで行ったのですが、強制的にアプリをアップデートする機能をどう実装するかちょっと悩みました。
私のようにアプリ開発はReactNativeが初めてという方は同じような悩みを持つかもしれないので、今回行った方法を紹介しようと思います。

強制アップデート機能とは?

端末にインストールされているアプリが要求バージョンを満たさないときにアラートなどでアップデートを促す機能です。

Image.png

「後で通知」ボタンがなかったり、いくつかパターンはあるかと思います。

この機能、アプリを使っているとたまに見かけると思うのですが、正式名称がわからず検索ワードに困りました。
「強制アップデート機能」でいくつか関連記事が見つかったので、この記事では「強制アップデート機能」と呼ぶことにします。

アプリの最新バージョンをどう取得するか

強制アップデート機能を実装するためには、アプリの最新バージョンを何らかの方法で取得する必要があります。

まずはReactNativeで定番の方法があるか調べてみましたが、残念ながら見つかりませんでした。
なのでiOS、Androidアプリでどのような方法を取っているかを調べてみました。

すると、iOSではiTunesSearchAPIというものが提供されており、これを使用して配信されているアプリの最新バージョンを取得できることがわかりました。
以下のURLを叩くとアプリの最新バージョンを含むレスポンスを取得できます。

https://itunes.apple.com/lookup?id=${appId}&country=JP // appIDはAppStoreのURLから確認できるアプリ固有の数値

ということは、Androidでも似たようなAPIが提供されているかもしれないと思いましたが、そのようなものは見つかりませんでした。
Playストアのページをスクレイピングして取得することもできますが、PlayストアのHTMLに変更があった場合に対応が必要になることを考えると面倒です。

ではAndroidアプリではどのような方法で最新バージョンを取得しているのか調べてみると、以下の方法が行われていました。

  • サーバーに最新バージョンを記述したjsonファイルなどを置く
  • 最新バージョンを返すAPIを作る

どちらもサーバーに仕組みを用意するという点では変わりません。
jsonを置く方が実装は簡単ではあります。
APIを作る方はバージョンをハードコードしてもいいですし、環境変数などで切り替えられるようにするとデプロイ不要でバージョンを切り替えられそうです。

この方法ならiOS、Android関係なく同じ仕組みで最新バージョンを取得することができるので良さそうです。

サーバーがない場合はLambdaCloudFunctionsなどでサーバーレスなAPIにしても良いと思います。
さらにお金をかけたくない場合はFirebaseRemoteConfigというサービスを利用する方法も良いかと思います。これについては後ほど説明します。

また、別の方法としてCodePushを導入していれば古いバージョン向けに強制アップデートのアラートを表示するJSコードを配信することもできますが、リリース作業が複雑になりそうであまり現実的ではないと思います。
そんなことをせずともCodePushで最新バージョンのコードを配布してしまえば良いかと一瞬思いましたが、CodePushではネイティブコード部分を更新できないので困るケースが出てきます。

アプリ側の実装

最新バージョン取得の方法がわかったので、次にアプリ側の実装を簡単に紹介します。
アプリ側で行うこととしては以下になります。

  • 最新のアプリと現在利用しているアプリのバージョンを取得して比較する
  • 更新を促すアラートを表示する
  • ストアのURLを開く

利用しているアプリのバージョンを取得する為にreact-native-device-infoというライブラリを使用しています。
使用したことはありませんが、より軽量なものだとreact-native-version-checkというライブラリがあります。

import { Alert, Linking, Platform } from "react-native";
import DeviceInfo from "react-native-device-info";

// アプリの最新バージョンを取得する実装
const latestAppVersion = XXXX
// 現在利用しているアプリのバージョンを取得する
const appVersion = DeviceInfo.getVersion();

// 新しいバージョンのアプリがストアに配布されている場合は更新を促す
if (appVersion < latestAppVersion) {
  showUpdateAlert();
}

// アラートの表示
function showUpdateAlert() {
  Alert.alert("更新情報", "新しいバージョンが利用可能です。最新版にアップデートしてご利用ください。", [
    { text: "後で通知", style: "cancel" },
    { text: "アップデート", onPress: () => openStoreUrl() }
  ]);
};

// ストアのURLを開く
function openStoreUrl() {
  // iOSとAndroidでストアのURLが違うので分岐する
  if (Platform.OS === "ios") {
    const appId = XXXXXX; // AppStoreのURLから確認できるアプリ固有の数値
    const itunesURLScheme = `itms-apps://itunes.apple.com/jp/app/id${appId}?mt=8`;
    const itunesURL = `https://itunes.apple.com/jp/app/id${appId}?mt=8`;

    Linking.canOpenURL(itunesURLScheme).then(supported => {
      // AppStoreアプリが開ける場合はAppStoreアプリで開く。開けない場合はブラウザで開く。
      if (supported) {
        Linking.openURL(itunesURLScheme);
      } else {
        Linking.openURL(itunesURL);
      }
    });
  } else {
    const appId = "com.example"; // PlayストアのURLから確認できるid=?の部分
    const playStoreURLScheme = `market://details?id=${appId}`;
    const playStoreURL = `https://play.google.com/store/apps/details?id=${appId}`;

    Linking.canOpenURL(playStoreURLScheme).then(supported => {
      // Playストアアプリが開ける場合はPlayストアアプリで開く。開けない場合はブラウザで開く。
      if (supported) {
        Linking.openURL(playStoreURLScheme);
      } else {
        Linking.openURL(playStoreURL);
      }
    });
  }
};

上記のコードをアプリ起動時に実行する場合はcomponentDidMount()などに書きます。
アプリがバックグラウンドからフォアグラウンドに復帰した際に実行したければ、AppStateを利用して書くと良さそうです。

RemoteConfigの導入方法

先ほど名前を出したRemoteConfigを使用する方法は一般的ではないと思うので、導入方法を簡単に紹介します。
RemoteConfigとは、アプリ向けに任意の値を返すAPIを提供できるサービスです。

必要なライブラリはreact-native-firebaseです。
このライブラリはReactNativeでFirebaseのサービスを使う際に利用することが多いので、FirebaseAnalyticsなどを利用している場合はすでに導入してあるかもしれません。

導入手順はライブラリの公式ドキュメントに沿って行えばOKです。
1. react-native-firebaseのセットアップ https://rnfirebase.io/docs/master/installation/initial-setup
2. RemoteConfigモジュールのセットアップ https://rnfirebase.io/docs/master/config/ios
3. アプリ側のコードサンプル https://rnfirebase.io/docs/master/config/example

あとはこのような管理画面で任意のキーと値を設定することで、アプリ側からこの値を取得することができます。

Remote_Config_–_Firebase_console.png

注意点として、RemoteConfigで取得した値はデフォルトでは12時間キャッシュされ、その間再取得されません。
fetch関数に引数を渡すことで最短0秒まで短くすることができますが、Firebaseの公式ドキュメントには

https://firebase.google.com/docs/remote-config/android?hl=ja#caching_and_throttling
60 分の期間内でアプリは最大 5 回フェッチできます。それを超えると SDK によってスロットル処理が開始され、FirebaseRemoteConfigFetchThrottledException が返されます。

と書いてあるので注意が必要です。

おわりに

もしかしたら今回紹介した以外にも良い方法があるかもしれません。
オススメの方法がありましたら、コメント等で教えていただけると嬉しいです。

OpenID Connectの始め方

この記事はCrowdWorks Advent Calendar 2018の13日目の記事です。

認証チームの@sawadashotaです。
認証チームでは、クラウドワークスの周辺サービスがクラウドワークスのリソースにアクセスするための認証基盤をOpenID Connectの規格で開発中です。

認証チームにジョインしてまず思ったことは、「OpenID Connectってなんぞ?何からキャッチアップすればいいんだ・・・?」ということです。
今日は認証について知識ゼロだった私がOpenID Connectをどうやってキャッチアップしてきたかを書きたいと思います。

公開鍵暗号・電子署名

「暗号」と言われると、「私、文系なので・・・」と逃げたくなりますが、一旦アルゴリズムは置いといて大丈夫です。

OpenID Connectでも公開鍵暗号や電子署名の技術が使われており、「なぜ公開鍵暗号・電子署名を使うのか」「なにが嬉しいのか」は知っておいたほうがいいです。

おすすめとしては結城 浩さんの暗号技術入門 第3版ですが、
本を買わないとキャッチアップできないのも微妙なので、いい感じに説明してくれているリンクも貼っておきます。

非エンジニアに捧げる公開鍵暗号方式 | DevelopersIO

読むだけだと、なかなか理解が深まらなかったので、GPGで遊んでみるといいかもしれません。
Keybaseterraformのaws_iam_access_keyあたりが実践としてとっつきやすかったです。

OAuth2.0

OpenID ConnectはOAuth2.0をベースに作られているので、まずはOAuth2.0を理解するところから始めましょう。

OAuth2.0はRFCが策定した、3rdパーティに限定的なリソースアクセスを可能にする認可フレームワークです。これにより、ユーザのID/Passwordが3rdパーティで入力されることなく、APIを提供することができます

いきなりRFC読み始めても意味がわからないので、わかりやすく説明してくれている記事から読み始めます。
OAuth 2.0 全フローの図解と動画 - Qiita

RFCのリンクも貼っておきますが、ここで時間をかけすぎるとモチベーションが保たないので、そこそこにして次に進むことをおすすめします。
(OpenID Connectのドキュメントを読み進めていると、きっとここに戻ってきます)

RFC 6749 - The OAuth 2.0 Authorization Framework
The OAuth 2.0 Authorization Framework(日本語訳)

PKCE

ネイティブアプリでは、認可コード横取り攻撃という攻撃方法があり、PKCE(Proof Key for Code Exchange)というプラスアルファでケアが必要です。

RFC 7636 - Proof Key for Code Exchange by OAuth Public Clients
PKCE: 認可コード横取り攻撃対策のために OAuth サーバーとクライアントが実装すべきこと - Qiita

この攻撃はCustom URL Schemeを使っている場合のみ成立するので、iOS9以上および、Android M以上で使えるUniversal LinksApp Linksを使えば対応する必要はなくなります。

JW*シリーズ

OpenID Connectでは、Tokenを渡すためにJWTを使ったり、Tokenの電子署名を複合するための暗号鍵を受け渡しするためにJWKが使われていたりします。
事前に理解する、というよりは、OpenID Connectを読み進めながら、調べるスタイルでもいいかもしれません。

JWS(JSONで電子署名を表現するための標準仕様)
RFC 7515 - JSON Web Signature (JWS)

JWE(JWSを暗号化するための標準仕様)
RFC 7516 - JSON Web Encryption (JWE)

JWK(JSONで暗号鍵を表現するための標準仕様)
RFC 7517 - JSON Web Key (JWK)
JSON Web Key (JWK)(日本語訳)

JWA(JSONを暗号化するためのアルゴリズムを定めた標準仕様。JWEで使う。)
RFC 7518 - JSON Web Algorithms (JWA)

JWT(JSONでTokenを表現するための標準仕様)
RFC 7519 - JSON Web Token (JWT)

OpenID Connect

ついに本題のOpenID Connectです。
OpenID ConnectはOAuth2.0に認証レイヤーを追加したものです。もっと言うと、OAuth2.0で標準化されていないID連携の部分を標準化したものです。
・・・と言われても理解できないので、まずは、以下の記事を3周くらいします

OpenID Connectユースケース、OAuth 2.0の違い・共通点まとめ - Build Insider
OpenID Connect 全フロー解説 - Qiita
一番分かりやすい OpenID Connect の説明 - Qiita
[前編] IDトークンが分かれば OpenID Connect が分かる - Qiita

なんとなくわかった気になるので、自分でシーケンス図とかを書きはじめてみます。
わからないことがあれば公式のドキュメントを参考にしましょう。

Final: OpenID Connect Core 1.0 incorporating errata set 1

私の場合はこんな感じで書いていました。

download.png

他社の事例を見てみる

実際に自社の都合に合わせてOpenID Connectを適応しようとすると、「あれ?これってどうすればいいんだ?」って思うことがあります。
そんなときは他社の事例を見てみると解決するかもしれません。

OpenID Connect 対応してるWebサービス/製品のログイン認証関連のドキュメントリンク集 - Qiita

日本語に起因する悩みはYahoo! ID連携が参考になります。

OpenID ConnectのOSSライブラリ実装を読んで見る

ドキュメントを読んでシーケンス図を書いてみたものの、「自分の理解って正しいんだっけ?」と不安になります。
公式認定のライブラリの実装を読んでみたり、実際に動かしながらログを眺めてみると、間違いに気づけるのでおすすめです。

Certified OpenID Connect Implementations – OpenID

私はory/hydraを読んでいますが、ライブラリ独自の概念が登場するので、OpenID Connectのドキュメントと行き来しながら、OpenID Connectの概念なのかを見分ける必要があります。

Next?

ここまで実践みると、なんとなくOpenID Connectの輪郭が見えてくるのではないでしょうか。
とはいえ、まだまだ読んでおきたい資料はたくさんあるので、ここまで実践したら次に読みたいものを雑にリストアップしておきます。

関連仕様を読み漁ってみる
OAuth & OpenID Connect 関連仕様まとめ - Qiita

各パラメータを追ってみる
Final: OpenID Connect Core 1.0 incorporating errata set 1

セキュリティーに関して考慮すべき事項を読み漁ってみる
RFC 6819 - OAuth 2.0 Threat Model and Security Considerations
OAuth 2.0 Threat Model and Security Considerations(日本語訳)

攻撃方法を読み漁ってみる
OAuth / Connect における CSRF Attack の新パターン - OAuth.jp
OAuth IdP Mix-Up Attack とは? - OAuth.jp
Implicit Flow では Covert Redirect で Token 漏れるね - OAuth.jp
HTTPS でも Full URL が漏れる?OAuth の code も漏れるんじゃね?? - OAuth.jp
OpenID Connect Security Considerations

OAuth2.0のImplcit Flowを認証に使っちゃいけない話
単なる OAuth 2.0 を認証に使うと、車が通れるほどのどでかいセキュリティー・ホールができる – @_Nat Zone

セキュリティインシデントの事例を読んで危機感を高める
記事一覧 - piyolog

おわりに

私の場合、ここまでキャッチアップするのに1ヶ月くらいかかりました。

幸い、一緒にキャッチアップしてくれる仲間がいたので、自分の理解をぶつけることで、話しながら自分の理解を定着させたり、早めに間違いに気づけたりできたかなと思っています。

読み物が多く、進んでる感が出にくいので、シーケンス図を書いてみたり、ライブラリを動かしてみたりと手を動かす作業をはさんでいくと、自分の理解が進んでいることを確認できるのでおすすめです。

混沌を極める jQuery のコードをいかにして Vue.js に頼らずに整理したか

師走ですね。年の瀬が近づいてくると、酔っ払った元社員に絡まれることが稀によくあります。

私は jQuery から Vue.js への置き換えで何をやらかしたのか - Qiita

可能ならいきなりフロントエンドのライブラリを導入するよりも jQuery のみで MVVM パターンへ移行したほうがよかったかなぁと今になると思います。 結局のところ、jQuery で苦しんでいたのは、複雑な「状態」が表示やイベントハンドル系のコードとごっちゃになっていたから です。

うん、分かる。当時、この取組みを「大変そうだなー」と思いながら横で眺めていました。
まさか、続きを自分でやることになるとは夢にも思っていませんでしたが(。◉ᆺ◉)

ごあいさつ

どうも、 @cesare と申します。
クラウドワークスでサービスの開発や運用を手がける傍ら、たまに機械学習とか VR とかに手を出して遊んでいます。

このエントリーはクラウドワークス Advent Calendar 2018 の 14 日目の記事です。
ちなみに昨日は @sawadashota さんが OpenID Connectの始め方 - Qiita というエントリーを書いていますので、良ければそちらも合わせてどうぞ。

今年は ex-crowdworks Advent Calendar 2018 という、クラウドワークスを退職した元社員の人たちが書いているアドベントカレンダーもありまして。冒頭で紹介した @ayasuda さんの記事もその一つでした。て言うかお前らどんだけクラウドワークスのこと好きやねん。

で、せっかく @ayasuda さんが前振りを務めてくれたので1、冒頭で紹介したエントリーで言及されている正にその、弊社サービスが抱える超複雑な入力フォームを持つ UI を、フレームワークなどの飛び道具を用いることなく再編した顛末をご紹介します。
とても長い話になりますので、お手元に :coffee::beer: などをご用意いただけると良いかなと。

なぜリファクタリングすることになったのか

問題の画面は、以下でも紹介するようにカオスを極めており、過去に @ayasuda さんも含めて数々の猛者が改善に挑戦するも悉く返り討ちにされ、ここ数年は禁忌の地として誰も近寄ろうとしない場所になっていました。

しかし、お客様に依頼を出していただいてクラウドワーカーさんが解決する場を提供しているサービスである以上、依頼を投稿するという機能はサービスの根幹に関わる箇所であり、依頼入力画面はサービスの心臓部と言える存在です。

依頼画面を改善してより良いユーザー体験を提供できるようになれば、サービス全体の活性化も期待できるでしょうし、より質の高い依頼が多く投稿されるような施策も考えられるようになるかもしれません。逆に、メンテナンスもできないまま放置することはすなわちサービスの衰退にも繋がりかねない危険を孕んでいると言えます。

よって、この画面のカオスに秩序を取り戻し、様々な施策を打てるように整理するのはいつか誰かがやらないといけないミッションでした。その「いつか」は今年の春に訪れました。そして「誰か」の方は、より良い依頼投稿のユーザー体験を提供することを目標に掲げた我々のチームに回ってきたという次第です。

そもそも依頼入力画面とは

クラウドワークスのアカウントをお持ちの方は
https://crowdworks.jp/job_offers/new
で実物を見ることができます。

入力項目は大きく分けて6つのステップに分かれています。例えばステップ1では案件が属するカテゴリーを選択する UI があり、ステップ2では依頼の形式 (コンペとかタスクとか、一般的なプロジェクトとか) を選択する UI があり、ステップ3では依頼のタイトルや詳細を入力する UI がまとめられており・・・(以下省略) といった塩梅です。

STEP1.png

画面を見られる方は、カテゴリーの選択肢から何か選択するとステップ2以降の項目が全て見えるようになりますので、試してみてください。

たとえば「システム開発」を選ぶとステップ3の入力項目は

  • 依頼タイトル
  • 依頼詳細
  • 求めるスキル
  • 添付ファイル
  • 特記事項

の5つが表示されます。

2.png

ここでカテゴリーの選択を、大カテゴリーを「デザイン」少カテゴリーを「ロゴ作成」に変えると、ステップ3の入力項目は

  • 依頼タイトル
  • 依頼詳細
  • ロゴ文字列
  • ロゴイメージ
  • 希望イメージ
  • 希望する色
  • 参考URL
  • 利用用途
  • 商標登録予定
  • 納品ファイル
  • 求めるスキル
  • 添付ファイル
  • 特記事項

と、変わります。

3.png

ロゴ作成に特化した項目が増えていることにお気づきかと思います。
このような、カテゴリー選択などに伴って出てきたり出てこなかったりする入力項目がこれ以外にもいろいろあります。ご興味のある方は、ステップ1のカテゴリー選択で「ランディング・記事作成」を選び、ステップ2の依頼形式選択で「タスク形式」を選んでみてください。興味深いものが出てくると思います。

実は、リファクタリング以前のかつての画面では、これらの「画面に出てくる可能性がある入力項目」のほとんど全てがサーバーサイドのレンダリング時に描画されていました。つまり、DOM 上は全ての入力要素が存在していて、それらの表示を on/off しているだけという造りになっていたわけです。

そして、見た目だけでも複雑なこの画面の裏側は、主に jQuery を活用したイベントハンドリングと DOM 操作によって支えられていました。コードの行数は、コア部分だけでも数千行2

・・・そろそろ嫌な予感がしてきましたね?w

改変前の残念だったところ

縦横無尽に張り巡らされたイベントハンドラー群

jQuery で作られたアプリケーションにありがちですが、基本的な骨格はイベントハンドラーで構成されています。あるボタンが押されたらこの処理が発動する、みたいなやつですね。敢えてコード例を出すまでもないかもしれませんが、こんな感じのやつです。

$("#submit-button").click(function(event) {
  // ボタンを押された時に発動させたい処理をここに書く
});

上に出したような「送信ボタンが押されたらこの処理を行う」ぐらいのシンプルなものが一つだけ存在しているのであれば問題にはならないのですが、数が増えると複雑度が増します。例を挙げてみると、

  • ラジオボタンの選択が変更されたら、画面に出ている入力項目のうち、いくつかを表示したり、あるいは非表示に変更したりする
  • ボタンが押されると、入力項目を一つ生成して画面に差し込む
  • ラジオボタンの選択が変更されたら、関連する項目のスタイルを変更してハイライトされるように変える (その一方で選択解除された方に関連していた項目のスタイルを通常に戻す)

などなど。単純に数が多くなるということもありますが、一つのイベントから複数の操作が発生するということも起きるようになります。

上の例だと、ラジオボタンの選択が変更されたら、

  • ある場所では枠が出たり消えたりして、
  • 別の場所ではスタイルが変更されてハイライト表示に変わったりする

というように2つの変化が起きるようになっていて、一箇所で起きたイベントをきっかけに様々な箇所に影響が起きるという状態になります。

イベントハンドラー連鎖

とは言え、イベントハンドラーの処理が画面の見た目を調整するぐらいにとどまっていれば、そこまで複雑なことにはならないでしょう。ラジオボタンの選択が変更されたらこことここに影響がある、ということを把握できていれば大丈夫。

しかし、イベントハンドラーの処理で別のイベントを発生させるということをやり始めると、複雑性が加速的に増大します。たとえばこんな感じ。

$("#radio-button").change(function(event) {
  if (ある条件) {
    $("#checkbox").click();
  }

  // それ以外の処理が続く
  // ...
});

$("#checkbox").click(function() {
  // 何かの処理
});

この例だと、

  • ラジオボタンの選択が変更されたときのイベントハンドラー
  • チェックボックスがクリックされたときのイベントハンドラー

の2つが存在していて、それぞれユーザーが操作を行ったときの処理を記述してあるわけですが、前者のラジオボタンのイベントハンドリング処理の中で後者のチェックボックスをクリックされた時と同じ処理が走るようにしたいという場合に、単にそのチェックボックスがクリックされたイベントを発生させてやることで済ませています。

一見して、このような構成は手軽に書けて便利そうに思えるのですが、その便利さの陰に複雑性を生み出しています。その複雑性とは、ラジオボタンの選択を変更すると、別の場所にあるチェックボックスがクリックされた時に起きるべき変化が発動するということで、一般化すると

  • ある場所で起きたイベントが別のイベントを発火させる

ということが起きています。これはすなわち、

  • ある場所で起きたイベントが別のイベントを発火させる
  • その別の箇所で発火したイベントが、さらに別の場所のイベントを起こす

というように数珠つなぎになっていく可能性があります。
さらに、あるイベントハンドラーの影響する先が複数ある可能性も考慮に入れると、

  • ある場所Aで起きたイベントが別の箇所BとCにそれぞれイベントを発火させる
  • その別の箇所Bで発火したイベントが、さらに別の場所D, Eでイベントを起こす
  • その別の箇所Cで発火したイベントが、さらに別の場所F, G, Hでイベントを起こす

のようにツリー状に際限なく広がっていく可能性を秘めています。
さながら「北京で蝶が羽ばたくとテキサスで竜巻が起こり、ペルー沿岸がエルニーニョに見舞われ、東京で桶屋が儲かる」ぐらいのカオスな状況が出現します。

依頼入力画面のカオスぶり

クラウドワークスの依頼入力画面が、まさにこのような状態に陥っていました。(以下、リファクタリング前の状況を説明しています)
たとえば画面を開いたばかりの初期状態では、まずカテゴリーを選択できるだけの表示になっていますが、

4.png

ここで大カテゴリー「デザイン」を選んでみます。
このイベントは、

  • 右側に小カテゴリー枠を表示し、
  • その中に表示されているカテゴリーの中からデフォルトのものを選んでクリックするアクションをトリガーする

ということを行っていました。大カテゴリーで「デザイン」を選んだ場合のデフォルトの小カテゴリーは「ロゴ作成」ということになっているので、対応する input:radio 要素をクリックするような状態だと理解していただければ。

次に、この小カテゴリーを選択してクリックするというイベントが、

  • ステップ2にある依頼形式のボタンのうち、選択可能なものを表示する
  • さらに、デフォルトの依頼形式のボタンをクリックするアクションをトリガーする

という動作を行います。
ここまでをまとめると、

  • 大カテゴリーから「デザイン」を選ぶ (これはユーザーの操作)
  • 小カテゴリー枠を出し、デフォルトのカテゴリーをクリックして選択 (JS による処理)
  • 小カテゴリーのクリックの副作用で依頼形式ボタンを表示し、(JS による処理)
  • デフォルトの依頼形式ボタンをクリックして選択 (JS による処理)

という形で、大カテゴリー選択→少カテゴリーが自動的に選択され→依頼形式が自動的に選択されるというようにアクションが繋がっていきます。

さらに、話はここで終わりません。
ここまでの動作でカテゴリーと依頼形式が確定できたわけですが、この2つが決まったことによって別のイベントが発動します。簡単に言うとステップ3以降の入力項目が全て表示されて入力可能状態になるわけですが、裏側ではさまざまな処理が動いていて

  • デフォルト状態で表示されていた、ステップ3以降を覆っていた半透明のレイヤーを取り除く
  • ステップ3〜6の枠内を表示状態に変える
  • ステップ3〜6の枠内に表示される入力項目のうち、いま選択されているカテゴリーと依頼形式のペアに必要なものだけを表示状態に変える

といった動きが起こります。

ひとまず初期状態から大カテゴリーを選択すると何が起きるのかを説明してみました。
ここからさらにカテゴリーや依頼形式を別のものに変更すると、またいろいろと処理が走ります。たとえば

  • 選択されたカテゴリーでは必要がない入力項目を画面から消す
  • 選択されたカテゴリーで必要になる入力項目のうち、まだ画面に出ていないものを表示する
  • いくつかの入力項目は、変更前に設置されていた場所から別の場所へ移動させる

などの処理があります。
表示の on/off ぐらいであればまだ良かったのですが、この「要素を別の場所へ移動させる」のが曲者でした。移動先の決め方が「特定の id が付いているノードの下を置き換え」ではなくて「特定のクラスが振られているノードの後ろへ追加」みたいになっていると大変です。その「特定のクラスが振られているノード」が何かの理由で別の場所へ移動していたら、移動対象のノードもそれに釣られてすぐ下の位置に出てくることになります。またノードを移動する作業の順序も大事になってきます。順番を間違えたら入力項目が意図しない場所に現れたりするなどの怪奇現象が起こるようになります。実際、「この項目はなぜこんな場所に表示されているのであろうか?」と首をひねるような現象が起きることが知られていました。

我々が直面したのは、このようなカオスを抱えた巨大なコードに、どうにかして秩序を取り戻さなければいけないという試練でした。

カオスに秩序をもたらす試み

作戦

まず、「一から作り直す」という選択肢はありませんでした。作り直すには現状の依頼入力画面の正しい状態・振る舞いを把握できていることが前提になりますが、残念ながらこの前提を満たしていませんでした。そして、まさにこの「誰も何が正解なのか分かっていない」ことこそが、この問題の解決を困難にしている最大の要因だったのです。

また、「フレームワークを導入する」という選択肢もありません。フレームワークを導入することはすなわち、既存コードはほぼ全て捨てて「一から作り直す」のと変わらないと判断したためです。

したがって、我々の取りうる作戦の選択肢はあまり多くなく、ひとことで言うと「リファクタリングを頑張る」しかありませんでした。

既存コードの整理

まず、我々は既存コードを整理するところから始める必要がありました。対象のコードは、コア部分のファイルだけでも数千行というボリュームで、長年の機能追加やリファクタリングによって様々な機能が思い思いの場所に書いてあるという、大変に見通しの悪いものでした。まず、このコード群を一つずつ読み解き、整理していきます。

整理の方針としては、依頼入力画面の各ステップごとにコードをまとめていくようにしました。たとえば「このコードはステップ1に関連するので上の方にまとめる」というように、既存コードを移動させて、同じステップに属するものが隣接するように編集していきます。この過程で、「このコードはいったい何をしているのか」が理解できるので、その理解をコメントとして追記していくことも同時に行います3
このようにして、カオスだったコード群の見通しが少し改善できました。もっとも、全てのコードがきれいにどこかのステップに分類できるとは限らず、全体を横断してさまざまな処理を行うようなものも存在するので、完全に整理できたというわけではないのですが。それでも、以前よりは見通しが良くなったことによって、次の段階へ進む準備はできました。

表示パターンの整理

コードの整理を行う傍らで、プロダクトオーナーやデザイナーも巻き込んで、画面の見た目レベルでの整理を行いました。依頼入力画面が見た目にも複雑になっている原因は、200以上も存在する仕事カテゴリーひとつひとつに対して項目の表示 on/off や表示位置などを細かく定義できる仕組みを構築していたことにありました。カテゴリーごとに細かくカスタマイズできるのは柔軟であるというメリットがある反面で、複雑性を生んでしまうというデメリットを抱えています。

チームで議論した結果、200以上あるカテゴリーを6つのグループに分類し、同じグループに属するカテゴリー同士は同じ見た目の依頼入力画面が表示されるということに決めました。具体的には、すべてのカテゴリーは

  • デザインの仕事を依頼する画面
  • デザインの中でもロゴ制作に特化した依頼をする画面
  • web デザインの仕事を依頼する画面
  • タスクの仕事を依頼する画面
  • ライティングの仕事を依頼する画面
  • デフォルトの画面

のどれかに分類されるイメージです。
かつ、それぞれの入力項目がどこに配置されるかを固定にしました。これによって、カテゴリー選択が変更されると入力項目がどこか別の場所へ移動されるということはなくなり、複雑さが減少するとともに、以前よりは画面の一貫性が出るようになりました。

ちなみに、同じチームに所属する @shiba_319 がプロダクトオーナーから見た回顧録 (?) を書いていますので、もし良ければそちらも合わせてどうぞ。
6万行の大規模リファクタリングを完遂する上でPOとしてやってよかった5つのこと - Qiita

画面の裏側の再設計

次の段階では、イベントハンドリングと DOM 操作を統制の取れた形に変えようということになりました。

この2つをカオスにしていた原因とは、

  • ある DOM を監視しているイベントハンドラーがどこから仕掛けられてるか分かったもんじゃない
  • ある DOM を操作しに来るコードがどこにいるか分かったもんじゃない

ということなのではないかとの仮説を持っていました。
以下、このイベントハンドラーを仕掛けるDOMを操作するの2つをまとめて、DOM に干渉すると表現することにします。

この仮説が正しければ、ある DOM へ干渉できるモノは一つしか存在しないという形に持ち込めれば統制が取れそうです。

ということで、全体の設計を以下のようにしようと決めました。

  • 画面中の限られた範囲の DOM 操作やイベントハンドリングに責務を持つクラスを設け、これを「ビュー (view)」と呼ぶことにする
  • あるビューは自分の管轄下の DOM に排他的に責務を持つ
    • 言い換えると、別のビューの管轄下には干渉しない
  • あるビューは子のビューを持つことができる
    • 子のビューも自らの責任範囲を持つが、その範囲は必ず親の責任範囲のサブセットになるようにする
    • 親は子に任せた責任範囲には干渉しない (= 子に任せる)

具体的には、次のようになります。

  • 画面全体を統括するビューが一つ存在する (以降、全体統括ビューと呼びます)
  • 全体統括ビューは、ステップ1〜6にそれぞれ対応する子ビューを持つ
  • 各ステップごとのビューは、自らの配下にある入力項目ごとの子ビューを持つ

あるビューが責任を持つ DOM の範囲は限定されているので、そのビューの子供たちの責任範囲は、自らの範囲から逸脱することはありませんし、責任範囲は排他的になるので、兄弟要素同士で範囲が重複することもありません。また、子ビューを持つということは、親は子に任せた範囲には干渉しないのが原則になります。

このようにして、全体統括ビューをルートにしてビューのツリー構造ができあがることがお分かりかと思います。

tree.jpg

これ以降、ビューのツリー構造はルートが一番上にあって、子要素が下方向に繋がる形を想定して説明します。つまりと言ったら親や先祖を指しています。なら子孫です。

コードに起こす

ビュークラス

まず、ビューを表現するためのクラスを作ります。
ちなみに、依頼入力画面の JS は CoffeeScript で書かれているので4、このエントリーの説明コードもそれに倣います。
コードにするとこんな感じ。

class FormView

FormView というクラスを作りました。これを、全てのビューの基本となる抽象クラスと位置づけます。

全体統括ビューも一つのビューなので、このクラスを継承して

class RootFormView extends FormView

と表現できますし、その全体統括ビュー直下にある、各ステップごとのビュー群はこういう感じになります。

class Step1View extends FormView
class Step2View extends FormView
# 以下、ステップ6まで同様に

これらの子要素となるビューはこんな感じ。

# タイトル入力の枠を管轄するビュー
class TitleInputView extends FormView

# 依頼本文の入力枠を管轄するビュー
class DescriptionInputView extends FormView

# これ以外の入力項目についても同様に

管轄する DOM の範囲を規定する

FormView クラスは、自身が責任を持つ DOM の範囲を知っている必要があります。これは、対象 DOM のトップレベルにある要素を把握していれば良さそう。ということで、責任範囲のトップレベル要素をコンストラクタで渡すようにします。

class FormView
  constructor: (@rootNode) ->

なぜ外から渡すようにしたかと言うと、この要素を決定するのは親であるということにしたいからです。親ビューが責任を持っている範囲の中から該当要素を確定し、その要素を使って子ビューを生成するという関係にします。

ちなみに、@rootNode が指すのは jQuery オブジェクトです。たとえば全体統括ビューだと

rootNode = $("#form-root")
new RootFormView(rootNode)

のような形で生成していると考えてもらえれば。

さらに、この @rootNode に基づいて、自らの責任範囲から任意の要素を探すメソッドを用意します。

class FormView
  findNode: (pattern) ->
    @rootNode.find(pattern)

たとえば、あるビューが配下に持っているラジオボタンにイベントハンドラーを仕掛ける場合はこんな感じに書けるようになります。

class CategorySelectionView extends FormView
  registerEventHandler: ->
    @findNode("input:radio").click (event) =>
      # 以下略

そして

  • FormView の実装は、配下の DOM 要素を探すときは必ずこのメソッドを介して行う
  • findNode を使う場合も parentsiblings など上や横方向には検索しない

ということをコーディング規約で縛ります。

規約なので破ることは簡単にできてしまうのですが、findNode() を使わずに検索しようとすると jQuery を使って

$("#some-other-node .foo")

みたいなコードが出てくることになるので目立ちますし、コードレビューで「これはなぜ findNode() を使っていないのですか?」という指摘がしやすくなります。

もっとも、どうしても管轄外の箇所に手を出さないといけないような例外的な事情というのは出てくるものですが、そういう場合でも、まず設計に問題がないかどうかを考えるということにしました。考えた上でやはりしょうがないね、という話になればコード中のコメントに事情を記しておきつつ容認するといったやり方にしました5
ここまでで、それぞれのビューが勝手気ままに任意の DOM を操作するという心配をしなくて済むようになりました。

子ビューとの関係

あるビューは子を持つことができます。もう少し正確には、

  • あるビューは別のビューを生成し、子として持っておくことができる

と言えます。
ということを以下のようなコードで実現しました。

class FormView
  constructor: (@rootNode) ->
    @subviews = {}
    @setupSubviews()

  registerSubview: (name, view) ->
    @subviews[name] = view

  setupSubviews: ->
    # 派生クラスでこの中身を実装する
    # 子となるビューを生成して @registerSubview() を呼び出すコードを並べる

派生クラスでは例えば

class Step3View extends FormView
  setupSubviews: ->
    # タイトル入力のビュー
    titleInputView = new TitleInputView(@findNode("#title-form"))
    @registerSubview("titleInputView", titleInputView)

    # 依頼詳細入力のビュー
    descriptionInputView = new DescriptionInputView(@findNode("#description-form"))
    @registerSubview("descriptionInputView", descriptionInputView)

    # 以下同様に

というような形になります。

こうして、親→子への関係が作られます。

親戚づきあいの作法

あるビューは、

  • 直接の子にのみ、介入することができる
  • 親が誰なのかは知らないし、介入もしない
  • 兄弟に誰がいるのかは知らないし、介入もしない
  • 直接の子が持つ子孫については知らないし、介入もしない

ここで言う知っているとは相手となるビューのインスタンスへの参照を持っていること、介入とは、相手となるビューのインスタンスに対してメソッドを呼び出して何らかの指示を出すことを指します6
見ての通り、何かのアクションを起こし得る相手は直接の子しかありません。
それ以外の存在は知らないことにします。

コード的には、

class Step3View extends FormView
  doSomeAction: ->
    @subviews.titleInputForm.doThis()

のような形で子が持つメソッドを呼び出すことによって指示を出したりすることができる形にします。

一方で、親や兄弟については @subviews のような参照はないので、指示を出したりすることはできません。

子ではないビューへ影響を及ぼす方法

ここまでの説明で「遠戚のビューに影響を及ぼすにはどうすれば良いんだ?」と疑問に思われた方もいるでしょう。

たとえば、ステップ1にあるカテゴリー選択の変更をトリガーにして、隣のステップ2の状態を変化させたい場合はどうすれば良いでしょう?

兄弟同士は存在すら知らない間柄だということにしたので、Step1View の実装で Step2View インスタンスの参照を取得してきてメソッドを呼び出すといったことはできません7
答えは「親に任せる」です。
ステップ1でカテゴリー選択の変更が発生したら、Step1View はその変更を親ビューに伝えます。変更を知った親は、ステップ2のビューである Step2View インスタンスを子として持っているため、指示を出すことができます。

しかし、先に説明した通り、ビューは親を知らないのでした。親ビューのインスタンスにカテゴリー選択の変更を伝えるメソッドを用意しておいて、それを呼び出すという手は使えません。

代わりに、オブザーバーを登録しておいて何かのイベントが起きた場合に通知を出せる仕組みを作りました。

class FormView
  constructor: (@rootNode) ->
    @observers = []

  registerObserver: (name, receiver, callbackFunction) ->
    @observers.push({name: name, receiver: receiver, callback: callbackFunction})

  notify: (name, notificationObject) ->
    for observer in @observers
      if observer.name == name
        observer.callback.call(observer.receiver, notificationObject)

2つのメソッドが出てきましたが、 registerObserver を呼び出すのは親です。たとえばこんな感じにします。

class RootView extends FormView
  registerChildren: ->
    step1view = new Step1View(@findNode("#step1-root"))
    @registerSubview("step1view", step1view)

    # ステップ1で categorySelected という名のイベントが起きたら
    # categorySelectionEventHandler() が呼ばれるように登録する
    step1view.registerObserver("categorySelected", this, @categorySelectionEventHandler)

  categorySelectionEventHandler: (notification) ->
    # ステップ1で起きたカテゴリー選択変更が通知されてきたら、
    # ステップ2にそれを伝える
    @subviews.step2view.handleCategoryChange(...)

もう一つのメソッド notify を使うのは自分です。

class Step1View extends FormView
  setupEventHandler: ->
    @findNode("input:radio").click (event) =>
      # 配下のラジオボタンがクリックされたら
      # 選択されたカテゴリーをオブザーバーに通知する
      selectedCategory = ...
      notification = {
        category: selectedCateogry
      }
      @notify("categorySelected", notification)

ここに挙げた例だと、

  • ステップ1でカテゴリー選択のラジオボタンがクリックされると、クリックの結果生じたカテゴリー選択変更の内容がオブザーバーに通知される
  • ステップ1のオブザーバーは親ビューであるので、したがって親にカテゴリー選択変更の詳細が伝わる
  • 親ビューは自らの子であるステップ2に対して、ステップ1で生じたカテゴリー選択変更を伝え、どう振る舞うかを一任する

という流れを作ることができ、めでたくステップ1で生じたイベントを起点にステップ2に影響を及ぼすことができました。

ちなみに、勘の良い方はすでにお気づきかもしれませんが、registerObserver() を呼び出せるのは親しかいません。なぜなら、自分を知っているのは親以外に存在しないからです。すなわち、オブザーバーとは親のことに他なりません。

ということを考えると、こんなややこしい仕組みを作らずに素直に親への参照を持つようにして、親の持つメソッドを直接呼び出すようにしても良かったのでは、という気もしないでもないのですがw

ともあれ、基本的にはこの

  • 親に通知を送る
  • 子に指示を出して任せる
  • 自分でどうにかする

の組み合わせだけで、任意の場所で起きたイベントを任意の別の場所に影響させることができるようになります。

管轄範囲の取扱い

ここまでの設計方針でビュー同士の繋がりは整理できました。
残るは自らの管轄する範囲の取扱い方を考えるだけです。やるべきことは

  • DOM イベントの監視 (= イベントハンドラーの設置)
  • DOM 操作

です。くどいようですが、管轄外の DOM には干渉しない原則なので、これらのコードは自らの管轄下のみを対象に振る舞いを定義していく形になります。

以前であれば画面内全域を対象に特定のクラスが振られているノードにまとめてイベントハンドラーを仕掛けるということが行われていましたが、今後はそういうやり方はダメということにしました。面倒でも自分の管轄下だけを対象にするようにします8。結果、複数箇所に同じような処理が記述される場合も出てきますが DRY 性よりも他所に干渉しない原則の方を優先します。
実際のリファクタリング作業は、すでにビューのクラスが存在して且つビューインスタンス同士の結びつきが定義されている状態になっていたので、既存コードに書かれていたイベントハンドラーや関数などを、対応するビュークラスのメソッドになるような形で移動してくるのが主な作業になりました。

若干面倒だったのは、既存の DOM イベントハンドラーが

$("#some-botton").click (event) ->
  # ボタンがクリックされたら、押されたそのボタンを disabled にする
  # 「押されたそのボタン」を this を使って特定しているところがポイント
  $(@).prop("disabled", true)

というように、関数に渡される this に頼っていることが多かったことですね。これをビュークラスに持ってくると、こういう書き方にしたいわけですが

class SomeButtonView extends FormView
  setupButtonClickedHandler: ->
    @findButtonElement().click (event) ->
      $(@).prop("disabled", true)  # これは意図通りに動くが・・・

      # この this は自オブジェクトを指しているのではないので、間違い
      @notify("some-botton-clicked", {...}) 

このように this が自オブジェクトの方を指していてほしいときに困ります。
CoffeeScript 的には上の例の @notify を参照できるようにするのは簡単で fat arrow を使う形に変えれば良いのですが、これだと

class SomeButtonView extends FormView
  setupButtonClickedHandler: ->
    @findButtonElement().click (event) =>  # ← fat arrow に変えた
      $(@).prop("disabled", true)  # 今度はこの $(@) が間違いになる

      # この this は自オブジェクトを指している
      @notify("some-botton-clicked", {...}) 

このように別の問題が起きてしまいます。要するに this の取り合いみたいな状況が発生しているわけです。

しょうがないので、次のように書き換えて解決しました。

class SomeButtonView extends FormView
  setupButtonClickedHandler: ->
    @findButtonElement().click (event) =>  # ← fat arrow を使う
      # this を使わず、引数に渡された event から該当ノードを特定する
      $(event.currentTarget).prop("disabled", true)

      # この this は自オブジェクトを指している
      @notify("some-botton-clicked", {...}) 

というような細かい調整が随所に発生しましたが、概ね既存コードの移動 (= すなわちコピペ) + 微調整で何とかなりました。

ビュークラスへ移動を終えて用済みになったオリジナルのコードは、めでたく古い方のファイルから削除できるようになります。このような作業を地道に進めていくにつれて、カオスだったオリジナルのファイルからは徐々にコードが減っていき、新しく作ったビュークラスを記述するファイルの方が育っていく流れになります。

複雑さの鍵を握っていたモノ

コードの整理を進めていく中で、過去のコードでカオスを引き起こしていたモノの正体が浮き彫りになってきました。まぁ「浮き彫りに」とか言いつつも、実際には予想通りのモノであったことが改めて明らかになったぐらいで、感想としてや「せやな」というところではありますが。

結局のところ、鍵を握っていたのはカテゴリーと依頼形式の状態と変更でした。この2つの情報の状態によって画面のあるべき形が決まり、状態遷移によって画面の様々なところが影響を受けて変化する。この状態と画面上の表現の結びつきが強すぎたり、自由気ままに振る舞いすぎていたせいでカオスが生じていたのでした。

実際、カテゴリー選択の状態とは、ラジオボタン要素のどれが選択状態になっているかで管理されているようなものでしたし、状態遷移とはすなわちラジオボタンの選択状態の変更を指すといった造りになっていました。

ということで、この2つの情報 (= カテゴリーと依頼形式の選択状態) を DOM から分離し、状態を表すものとして管理するようにします。こんな感じのオブジェクトを作りました。

// category がカテゴリーのID、jobType が依頼形式を表すイメージ
// この例だと、カテゴリーID が123 で依頼形式がコンペが選択されているという意味
{
  "category": 123,
  "jobType": "competition"
}

この情報の変化は画面各所に影響を及ぼすので、トップレベルにいる全体統括ビューが管理するのが適切でしょう。全体統括ビューは、子ビューからカテゴリーまたは依頼形式の選択を変更したいというリクエストが起きたという通知を待っています。通知が届いたら、果たしてその選択変更は可能かどうかを判断し、ok であれば、状態データの category を書き換え、直下にいるステップ1〜6のそれぞれの子ビューにその変更を伝え、画面の表示切り替えなどを任せます。

ここで「ステップ1〜6のそれぞれの子ビューにその変更を伝え」と書いたことにお気づきでしょうか? 変更を伝える相手には、起点となったイベントが発生した当のステップ1も含まれています。

カテゴリーの選択を行うための UI はステップ1に配置されていますが、ラジオボタンがクリックされても該当 input:radio 要素の選択状態は変化させません。クリックされた場合のイベントハンドラーの仕事は、カテゴリーの選択を変更したいというリクエストが起きたという通知を投げることだけです。投げられた通知は親である全体統括ビューに拾われます。そしてこの変更リクエストを受け入れて良いと判断されて状態が変化すると、今度は全体統括ビューからカテゴリーの選択が変更されたので、各自画面表示を調整せよという指示が降りてきます。このタイミングでやっと、ステップ1のカテゴリー選択 UI の input:radio 要素を checked にするなどの処理が走るようになります。

こういう回り道をして何が嬉しいかと言うと、選択の変更を許可しない場合とか、間に「本当にカテゴリー変えても良いですか?」とダイアログをはさみたい場合などの制御がやりやすくなることです9
もし、ラジオボタンのクリックが即その要素が checked になってしまう造りだと、変更を許可しない場合は元の状態に戻してやる必要が出てきます。ということは、変更前に checked だった要素はどれなのかを覚えておく必要があるでしょう。

あるいは、イベントハンドラー内で画面内の状況を確認しつつ、checked に変えてよいかどうかを判断するという手もあります。が、切り替え可否の判断は画面内各所の状態に左右されるという事情があるため、しがらみを持っている各要素に現状を問い合わせつつ複雑な条件分岐を構成するということになります10
どちらにしても複雑になるイメージしかないですね。これよりは、

  • イベントハンドラーはクリックされた事実を伝えるだけ
  • 状態遷移に責任を持つ親が判断を行い、画面全体に号令を出す

という造りの方が遥かにシンプルで分かりやすいと判断して、このような構成にしました。結果として、イベントハンドリング状態の管理, UIへの反映が分離され、コードの見通しが改善されました。

まとめ

以上のような方針に基づいてコードの整理を進めた結果、以前とは比較にならないぐらいに見通しが良くなりました。

ひと通り終わってから振り返ってみると、いちばん効いていたのはビューというクラスを作ったことであるように感じています。クラスが存在しているおかげで、そのビューに関連しているコードを記述する場所が自ずから決まってくるので、どこに書こうかと迷うことがありません。また、特定のビューに関連するコードを探すときも、書いてある場所が明確に決まっているおかげで、すぐに特定できます。

また、管轄外の DOM には干渉しない原則のおかげで、予期しない方角から DOM 操作が行われる心配がなくなりました。当初の問題であった縦横無尽に張り巡らされたイベントハンドラー群は概ね解消されましたし、イベントハンドラー連鎖も起こり得なくなりました。

反面で、遠隔地に影響を及ぼしたい場合は親を経由しないといけないなどの手間は発生していますし、イベントハンドラーを一つ一つ個別に設定していく必要があるので似たようなコードが複数箇所に現れて DRY じゃないといった課題はあります。実際、リファクタリング前と後とでは、後の方がコードの行数的には増えています。

しかし、それらのデメリットを上回って、コードの見通しが改善された恩恵が大きい。今なら、画面の表示が何かおかしい場合は、

  • 該当 view の処理が間違っている
  • 親から間違った指示が来た
  • 子から間違った通知が来た

のどれかだと予想がつきます。もはや、数千行のコードの海のどこかにおかしなことをしている奴がいるのではないかと探し回る必要はありません。

かつてのカオスだったコードは秩序を取り戻し、再びメンテナンスや機能拡張・再編が可能になりました。

感想

無事に最後までやり終えられて良かったですw
かなり大規模な改修になってしまったこともあってリリース作業がかなり緊張感ある感じになりましたが、幸いにして大きな問題は起こらず。あまりに平和で逆に拍子抜けしてしまったぐらいでした。

一方で、本来やりたかった依頼入力画面を改善してより良いユーザー体験を提供するという施策はまだ何も前進してなくてこれからなのですが、それでもやっと何かの施策を始めるための準備は整ったとは言えるので、時間をかけて整理した価値はあったかな、と思います。

@ayasuda さんへの返信

つまり、本来やるべきだったのは、「状態」「表示変更」「イベントハンドリング」のコードの分離 だったのでした。

私は jQuery から Vue.js への置き換えで何をやらかしたのか - Qiita

全部やったよ!


  1. というのは半分嘘で、最初からこの話を書く予定だったところに件のエントリーが投下されたので乗っかったというのが正しいです。 

  2. ちなみに、一部だけ Vue.js で置き換えられているというオマケが付いています。つまり jQuery と Vue.js が共存する状態。幸いにして、両者の境界線がはっきりしていたので、既存 Vue.js コードに振り回されることはありませんでした。 

  3. 逆に言うと、以前はそれすらもちゃんとできていなかった、ということでもあります 

  4. そこ、「まだ CoffeeScript なんて使ってるの?」とか言わない 

  5. 実際には、管轄外に手を出さないといけない事態はほとんど起こりませんでしたが 

  6. 先に出てきた干渉と紛らわしいですが、ここでは干渉とは DOM に対する監視や操作、介入とはビューインスタンス同士の関係を表すもの、という使い分けをしています 

  7. と言うか、そういうことができないように、このような設計にしたのでした 

  8. 具体例としては、テキストボックスに入力された文字列の長さをカウントして上限と比較しつつ、近くに「xxx/yyy文字」みたいな表示を出すやつが挙げられます。リファクタリング前は、特定クラスが振られているノードに一括で設定されていましたが、個別ビューごとに設定する形に改められました。 

  9. 選択されたカテゴリーによって入力項目が異なるため、変更先いかんによっては入力項目が消える場合があって、それはすなわち入力した内容が失われるということなので、そういうときは警告用のダイアログを出したりしています。もちろん、ダイアログを見たユーザーが拒否すれば、選択変更は行われなかったことにしたい。 

  10. 整理前のコードがまさにこんな状態でした 

エンジニアのためのデザインレビュー入門 | より良いプロダクトをつくる

はじめに

この記事は、『CrowdWorks Advent Calendar 2018』の15日目の記事です。

みなさんこんにちは!UIデザイナーの井上です。
今日は2018年7月にDXEL.1 エンジニアとデザイナーが「いい関係」を築くためにというイベントでLTをさせてもらった、エンジニアのためのデザインレビューの話をもう少し詳しく解説していこうかなと思います。

登壇資料

https://speakerdeck.com/noainoue/jin-ri-karahazimerudezainrebiyu

エンジニアさんもデザインレビューに参加しよう!

そもそもこの資料を作ったきっかけとして、知り合いのエンジニアさんに「デザインレビューってどうすればいいんや、デザインわからんちー」と言われたのがきっかけでした。

具体的には

- 何を指摘すればいいのかわからない
- どこまで言っていいものなのかわからない
- 自分はデザインのプロではないので任せたい
- ユーザーにとっていいデザインであれば構わない

と言った声を耳にしました。

これを聞いて私が思ったのが、UIデザイン=ビジュアルのデザインだと思ってる方が多いのかな?という点です。
本来UIデザインとはユーザーの使い勝手を設計するもの。
デザイナーとしてはこれを踏まえて、レビューしてほしいと思っています。

悪いデザインレビューの例

スクリーンショット 2018-12-15 9.30.09.png
前職で行われたレビュー会でのお話なんですが、

- なんかこれ、バランス悪くて気持ち悪くない?
- これじゃ綺麗なコードがかけないんだけど...

と言われたことがあります。
サービスデザインをする上で大事なのはユーザーになりきること、ユーザー目線になることです。ほしいのは作り手としての意見ではないんですね。

サービスを作る上でもちろんシステム的な話も重要なのですが、
まずはユーザーのことを一番に考えてほしいと思ってます。
その上で、システム要件と経営とのバランスを取り、松竹梅でデザイン案を提案したいです。

良いデザインレビューの例

(1) : iOS,Androidのガイドラインを軸とするレビュー

スクリーンショット 2018-12-15 9.39.07.png
そこで最も効果的なのが、

iOS=Human Interface Guidelines(※以降HIG)
Android=Material Design

を一つの基準、または共通言語として設ける。ということです。
普段レビュー会をしていて思うのですが、デザインのレビューって話の軸がぶれることが多いです。
それこそシステムの話になっちゃったり。

ですが、HIGとMaterial Designを基準として持っておくことで
サービスの使い勝手を考えるという軸がぶれることはありません。

あとはそもそもこの二つのガイドラインはユーザビリティを考え抜かれているものである、
というところが大きく作用します。
わざわざ1からユーザビリティを考える必要はないので、作業コストが小さく且つ「デザインがわからーん!」となっているエンジニアさんもデザインを理解しやすいですよね。

また、デザイナーは「このガイドラインを踏まえた上で、こういう意図で崩し〇〇を表現する為にこうデザインしてみました!」と明確な意図をより伝えやすく、また受け取り手もそれを理解しやすいです。

(2) : ユーザー(使い手)としてレビュー

スクリーンショット 2018-12-15 9.39.38.png

あとは言わずもがな、自分がサービスのターゲットユーザーとして
プロトタイプなどを用いてサービスに触れ、その上で使いにくかったところをデザイナーに伝えてあげることが重要となってきます。

サービスには必ずペルソナとターゲットユーザーが設定されていると思います。
そのターゲットユーザーの思考を反映しながらサービスを触ると、

- この機能欲しいな
- この機能いらんな
- ボタンちっさいなー

などのデザイン改善点を見つけることが可能です。

オススメのデザインレビューツール

ここまでレビューの仕方についてお話ししましたが、
クラウドワークスの一部のチームではSketchではなくFigmaというデザインツールを用いてデザインをしています。
このツール、簡単に言うと"Sketch+Zeplin+共同編集機能"といった感じです。

68747470733a2f2f71696974612d696d6167652d73746f72652e73332e616d617a6f6e6177732e636f6d2f302f3230303138302f65613139313332662d656631662d633131332d323665622d3837396463666633316330662e706e67.png

Figmaは、CSS / iOS / Androidのコード出力が可能です。
またプロトタイプツールとしても使うことができます。

スクリーンショット 2018-12-15 10.19.44.png
また、このような形で作ったデザインに直接コメントをすることが可能です。
↓誰でもコメント可能なデザインデータを作成しましたので、ぜひFigma使ってみてください↓
https://www.figma.com/file/OKj2heggy1kjHsp3Z6uNAXBE/Karamtaj-Giftshop

Figmaの詳細が気になる方はぜひ、こちらの記事を参考にしてみてください!
https://qiita.com/hikaru_tayama/items/49373412ec1a515ff05d

最後に

ここまで読んでいただきありがとうございました。
いかがだったでしょうか?

より良いプロダクトをつくるために、ユーザー視点になり
より良いプロダクトをつくるために、いいチームを作っていきたいと思っています。

エンジニア、デザイナーお互いがお互いを理解し、寄り添える存在であるといいな!
これからもクラウドワークスのデザイナーとして、良いサービス作りを追求していきます。

それでは、引き続き『CrowdWorks Advent Calendar 2018』をよろしくお願いします!

公開鍵と証明書による安全な通信とは?

この記事は、『CrowdWorks Advent Calendar 2018』の16日目の記事です。

12月に入社しましたSREチームの久村です。
認証周りの開発をしていくのに暗号周りの知識が浅いので、第一歩として通信の安全化にはどんな技術が使用されているのかをイラストを使って自分なりにまとめました。

そもそも安全な通信とは?

  • 送る情報を暗号化し、他の人に情報が盗聴されても読めないこと。
  • 送信者が想定している送信者であると断定できること。

送られている情報が見られても、その内容が読めない文字列で書かれていたら悪用することは難しくなります。
また、送信者を断定することができれば「10万円振り込んでください。Aより」とAさんを装ってBさんが送信していても騙されることはありません。

このような通信の秘匿性、真正性のためには公開鍵暗号方式電子証明書という技術が使用されています。

まずは公開鍵暗号方式の説明からしていきますね。

公開鍵暗号方式

公開鍵暗号方式の説明の前に 共通鍵 の説明をさせてください。(公開鍵の理解を助けるはず。)

共通鍵での通信

とは暗号化したり、復号(暗号化されたのを元に戻す)するときに使うデータのことです。
この鍵を使って、文書を暗号化したり、復号する流れを説明していきますね。

image.png

共通鍵では送信側と受信側で同じ鍵を使用します。
暗号化するときと、復号する時で同じ鍵を使うということですね。

太郎くん(送信者)は手元の鍵で文書を暗号化し、花子ちゃん(受信者)に送信します。
花子ちゃんも手元の鍵で文書を復号することで内容を確認することができます。

★共通鍵とは1つの鍵で暗号化し、同じ鍵で復号する仕組みです。

ここで難しいのは鍵をどのように渡すかという問題です。( 鍵配送問題
物理的に渡すのでは、海外の人との交換などは非常に手間がかかりますし、
100人と鍵を交換するとなると、その管理も爆発的に増えてしまって嬉しくないですね。。

もし鍵が漏れてしまうと以下のようになってしまいます。
image.png

盗聴者が同じ鍵を持っているので、文書を復号できてしまい、内容を読むことができてしまいます。

この鍵配送問題を解決する手段として 公開鍵暗号方式 の仕組みがあります。

公開鍵暗号方式での通信

先ほどは1種類の鍵を使用して通信していました。
公開鍵暗号方式では2種類の鍵(公開鍵秘密鍵)を使用して通信を行います。

image.png

花子ちゃんは公開鍵と秘密鍵を作成し、名前のとおり公開鍵を公開します。
秘密鍵は他の人には知られないように自分で管理します。
太郎くんは花子ちゃんの公開鍵を使って文書を暗号化し、花子ちゃんに送信します。
花子ちゃんは自分の秘密鍵を使って、文書を復号することで、内容を確認します。
盗聴者は花子ちゃんの秘密鍵を持っていないので、文書を復号することができません。もちろん、公開鍵を使って復号することもできません。

★公開鍵暗号方式とは公開鍵で暗号化し、秘密鍵で復号するという仕組みです。

おお〜これにて一件落着!
っといきたいところですが、この公開鍵が花子ちゃんのでなかったら。。

image.png

花子ちゃんの公開鍵だと思っていたものが、実は盗聴者の公開鍵だったという状態です。
太郎くんは花子ちゃんの公開鍵だと思って暗号化したつもりが、実はこの公開鍵は盗聴者のものでした。
盗聴者の公開鍵で文書を暗号化したので、盗聴者は自分の秘密鍵を使って文書を復号することができてしまいます。

残念。。
これでは安心して通信することができませんね。。
ではどうすれば良いのでしょうか?

使いたい公開鍵が花子ちゃんのだと断定することができれば安心ですよね。
その仕組みが 電子証明書 です。

電子証明書

電子証明書で必要になる、電子署名の説明を先にさせてください。

電子署名

この技術は
★このメッセージを書いたのは誰かを特定するためのものです。

実物の証明書や契約書の場合、その文書が本当にその人によって作成されたものであるかは、その文書に付されたその作成者の署名や印によって証明されますね。

同様に、電子署名はデジタルデータに送信者を証明できる印を付与するものです。これにより相手の確認、なりすましの防止、改ざんの防止が行われ、インターネットを安心して利用することができるようになります。

image.png

電子署名は送信者の太郎くんが行うものです。
太郎くんは自分の秘密鍵を使って、電子署名を作成し、文書に付与します。
電子署名の作成は「私はこのメッセージの内容を認めます」という印です。
電子署名を検証するのは、受信者の花子ちゃんが行います。
花子ちゃんは太郎くんの公開鍵を使って、電子署名の検査を行います。
検査とは「このメッセージのデジタル署名は確かに太郎くんのものかどうか」を調べる行為です。

何だか、どこかで見た流れに似ていますね。
そうです。
電子署名は、公開鍵暗号方式の流れを「逆に使う」ことで実現できる技術です。
秘密鍵を使って電子署名を作成し、公開鍵を使って電子署名を検査するんですね。

電子証明書の仕組み

では電子証明書の説明に入りたいと思います。

電子証明書とは認証局が公開鍵の作成者を確認し、公開鍵に電子署名をして作成されるものです。
認証局とは、電子証明書を発行する機関であり、ここで電子証明書の管理を行なっています。

認証局はなかなかイメージしにくいですが、「この公開鍵は確かにこの人のものである」と認め、電子署名を作成し、電子証明書を発行できる人や組織のことですね。

image.png

花子ちゃんは公開鍵に電子署名をしてもらうため、認証局に自分(花子ちゃん)の公開鍵を渡します。
認証局は自分(認証局)の秘密鍵を使用して、花子ちゃんの公開鍵に電子署名をして、電子証明書を作成します。
太郎くんは認証局から電子証明書を取得します。
そして、認証局の公開鍵を使って、電子証明書に含まれている電子署名を検証します。
検証の結果、間違いなく認証局の電子署名だと確認でき、電子証明書に付いている公開鍵が花子ちゃんのものであることが確認できました。

★電子証明書とは公開鍵への電子署名の仕組みです。

ふ〜色々な技術が出てきましたね。
最後に電子証明書を使用した流れを整理します。(ちょっと複雑になりましたので番号をふって説明します。)

image.png

①花子ちゃんは公開鍵と秘密鍵を作成し、認証局に公開鍵を登録します。
花子ちゃんは自分の公開鍵を認証局に送ります。

②認証局は花子ちゃんの公開鍵に電子署名をして、電子証明書を作成します。
認証局は自身の秘密鍵を使って、花子ちゃんの公開鍵に電子署名をして電子証明書を作成します。

③太郎くんは認証局から電子証明書を入手します。
太郎くんは花子ちゃんに暗号文を送りたいと思い、認証局から電子証明書を入手します。
電子証明書には、花子ちゃんの公開鍵が含まれ、公開鍵には認証局の電子署名が付いています。

④太郎くんは、認証局の公開鍵を使って電子署名が正しいことを検証し、公開鍵が花子ちゃんのであることを確認します。
太郎くんは、認証局の公開鍵を使って、電子証明書に含まれている電子署名を検証します。
この検証に成功すれば、公開鍵は確かに花子ちゃんのものであると確認でき、花子ちゃんの公開鍵を入手したとこになります。

⑤太郎くんは花子ちゃんの公開鍵で文書を暗号化し、花子ちゃんに送信します。
正しいと確認済みの公開鍵を使用ですね。

⑥花子ちゃんは、暗号文を自分の秘密鍵で復号し、太郎くんの文書を読みます。
太郎くんが花子ちゃんの公開鍵を使用して文書を暗号化したので、花子ちゃんは自分の秘密鍵で復号することができました。
盗聴者は自分の秘密鍵を使用して復号を試みますが、花子ちゃんの公開鍵で暗号化されているため文書を復号することはできません。

めでたく安全な通信が完了しましたね!

まとめ

イラストを多用して、公開鍵暗号方式と電子証明書を使った通信の流れを整理したことで、安全な通信の理解を深められました。(でもまだまだですね)
ここでは解説できていない暗号化のアルゴリズムや電子署名の作り方、電子証明書の種類など非常に多くの技術が存在しています。
今回の記事が今後の暗号技術を調査される際の手助けになれば嬉しいです!

「実践ドメイン駆動設計」を読んだので、実際にDDDで設計して作ってみた!

こんにちは、クラウドワークスの新規事業のエンジニアとして仕事をしている高梨です!
最近、「実践ドメイン駆動設計」という本を読みました!

500ページ近くもある技術書で、なかなか量は多かったのですが、DDDがどんなものなのか一通り大枠を掴めた気がします。

ただ読み終わった後にこんな疑念や不安をいだきました。

「たしかにかなり面白そうだけど、実際にやるとどれだけ工数かかるんだろう...?」

「設計の話は全然出てこなかったけど、DDDで作るとなるといったい何から始めればいいんだ?」

「戦術についての知識はついたけど、実際に書こうとしたらできなそうだな...」

そこで、そういった疑念や不安を解決するために、実際にDDDでサンプルプロダクトを作ってみようと思ったわけです。
実際に作ってみるのが、結局一番理解が進みますしね。

今回は、そのプロダクトがリリースされるまでの過程や感想を、作成した設計書やソースコードを公開しつつ、お話ししていければと思います。

この記事の対象者

「DDDについてなんとなく理解したけど、実際に作れるかはちょっとわからない。」

「実践ドメイン駆動設計は全部読んだ!実際にDDDで何か作ってみたいと思っている。」

「チームでDDDを取り入れるか迷っている。DDDに関する記事はいくつか読んでみた。」

本記事では、主に上記のような方を対象にして話を進めていければと思います。

今回は、DDDで出てくる基本的な用語の説明はあまりせずに、それらをある程度理解した上で、実際にDDDを採用してみたいと考えている方に参考にしていただけるような話をしていこうと思っています。

なので、まだDDDについてあまり理解できていないという方は、先に下の参考記事から読んでいただいた方が頭に入りやすいかもしれません。

DDDの理解が進む参考記事

1. そもそもドメイン駆動設計(DDD)とは何か

ドメイン駆動設計入門
https://www.slideshare.net/TakuyaKitamura1/ddd-29003356

ドメイン駆動設計の基礎知識
https://logmi.jp/tech/articles/310424

2. コード(実装)側からDDDを理解してみる(ボトムアップ)

ボトムアップドメイン駆動設計
https://nrslib.com/bottomup-ddd/

ボトムアップドメイン駆動設計2
https://nrslib.com/bottomup-ddd-2/

ここからの話の流れ

ここからは以下の順番で話を進めさせていただければと思います。

  1. 今回作ったプロダクトの内容説明
  2. DDD勉強開始〜プロダクトリリースまでの流れ
  3. 今回登場するユビキタス言語
  4. 設計過程
  5. 実装過程
  6. やってみた感想

今回作ったプロダクトの内容説明

今回は適当なテーマを1つ決めてから、プロダクトを作りました。

そのテーマは「名言」です。

テーマを「名言」に決めたのに大きな理由はなく、単にTwitterを見ていた時に名言Botというアカウントを見て、そこそこフォロワー数が多かったので、これでいいやと思ったという軽い理由です。
(あくまで勉強目的だったので、特別需要があるようなものでないことは承知しています。)

注意点として、今回作成したプロダクトはあくまで業務時間外に作成した個人のサンプルプロダクトであるため、特に社内でレビューを通したりはしていません。

サービスの特徴としては、以下のような感じになります。

  • 経営者, エンジニア, ドラマ, 学問, スポーツ, アニメ, ビジネスなど様々なジャンルの名言が見れる。
  • 名言に対して、いいね, お気に入り登録, コメントなどのアクションを取れる。
  • 名言の登録, 修正, 削除を申請することができる。
  • 名言の登録, 修正, 削除の申請の承認・非承認は他ユーザーの多数決(かそれに似通ったルール)で判断される。
  • 名言の生まれたドラマやアニメなどの詳細(イメージ画像や紹介文, HuluやAmazon Prime対応かなど)が見れる。
  • アプリである。

上のサービスの特徴もまた、今回設計過程で決めていったことなので、ここではその過程を説明していければと思います。
(あくまで設計開始時点では「名言」というテーマだけが決まっている状態で、サービスを作りました。)

技術的には、Spring Boot(Kotlin)を採用して、アプリはReact Nativeで作成しました。

IOSとAndroidの両方で公開中ですので、下のURLからアクセスするまたはアプリの名前が「Phrase Art」になっていますので、App StoreまたはGoogle Playで検索しても出てくるかと思います。

IOS Android
App Store Google Play
スクリーンショット 2018-12-17 11.23.08.png Android.png

一応、アプリのイメージとしてはこのようなものになりますね。

名言一覧 名言詳細 サブカテゴリー詳細 申請一覧 申請詳細 設定 会員登録
a1.png a2.png a3.png a4.png a5.png a6.png a7.png

DDD勉強開始〜プロダクトリリースまでの流れ

前提

もともとチーム内では、DDDを利用して(新規事業の)プロダクトを作り直すという話がでていました。

そのため、先月(2018年11月)の1日あたりから、チーム内でもDDDの読書会が開始され、そこから1週間に1, 2回の頻度で1回1時間のDDD読書会を開いている状況です。

そんな中、参考資料として、本「実践ドメイン駆動設計」がいいという話を聞き、実際に読んでみると作りたくなったというのが今回プロダクトを作ろうと思った理由でした。

チーム内読書会で読んだこと(2018年12月17日 現在)
第1, 2回
本「わかる!ドメイン駆動設計 ~もちこちゃんの大冒険~」

第2, 3, 4回目
記事「ボトムアップドメイン駆動設計」
https://nrslib.com/bottomup-ddd/
https://nrslib.com/bottomup-ddd-2/

第5, 6, 7, 8回目
本「実践ドメイン駆動設計(1, 2, 3, 4章)」

時系列 (2018年)

11月01日 : チーム内でDDD勉強会開始

11月04日 : 本「実践ドメイン駆動設計」をGET & 読書開始

11月10日 : 本「実践ドメイン駆動設計」を読破

11月11日 : Spring Bootの勉強開始

11月12日 : 本「モデルベース要件定義テクニック」読書開始

11月16日 : 本「モデルベース要件定義テクニック」読破

11月17日 : 「名言」というテーマが決定 & 設計開始

11月24日 : 設計完了 & 実装開始

12月17日 : 本記事公開 (実装途中 8割完)

12月26日 : 実装完成 (最終盤 IOSリリース)

12月28日 : Androidリリース

今回登場するユビキタス言語

設計の話に入る前に今回登場するユビキタス言語の説明を少しさせていただければと思います。
登場するユビキタス言語を軽く知っておいた方が、より設計書自体も理解しやすいかなと思い、設計の話の前にお話しすることにしました。

主に登場するユビキタス言語とその説明は以下の通りです。

ユビキタス言語 英名 説明
名言 Phrase そのままの意味。
英名の候補が色々あり、決めるのが難しかったが、結果的にサービス名にもあり、わかりやすいという理由でPhraseにした。
作者 Author 名言の作者を示す。
ユーザー User ユーザーのこと。
アクターであるユーザーのことを示す。
プロフィール Profile ユーザーの保有する認証関係の情報以外のこと。
カテゴリー Category 名言を分類するもの。
管理者のみが変更を行えるもので、ビジネス, 学問, ドラマ, スポーツ, アニメ, その他などがここに属する。
サブカテゴリー Subcategory カテゴリーをさらに分類したもの。
ユーザーも内容を修正することができる。
例えば、ビジネスのサブカテゴリーとして経営者, エンジニア, 営業などが属している。
(ドラマなどでは、そのドラマのタイトルが入ることになる。)
コメント Comment 名言または申請に対してユーザーがつけることのできるコメント。
いいね Like 名言に対してできるアクション。
Twitterなどと同様。
お気に入り Favorite 名言に対してユーザーができるアクションの1つ。
お気に入りとして登録した名言を別の一覧で閲覧することができる。
動画配信サービス Video On Demand ドラマまたはアニメのサブカテゴリーにのみ属するもの。
HuluやAmazon Prime Videoが入ってくる。
更新申請 Update Request 名言登録申請, 名言修正申請, 名言削除申請, サブカテゴリー修正申請の総称。
名言登録申請 Phrase Registration Request ユーザーが名言の登録を申請する際に提出(submit)するもの。
名言修正申請 Phrase Modification Request ユーザーが名言の修正を申請する際に提出(submit)するもの。
名言削除申請 Phrase Deletion Request ユーザーが名言の削除を申請する際に提出(submit)するもの。
サブカテゴリー
修正申請
Subcategory Modification Request ユーザーがサブカテゴリーの内容を修正を申請する際に提出(submit)するもの。
判定 Decision 他ユーザーの更新申請に対してユーザーが行うもの。
承認(approve)または否認(reject)がある。
最終判定結果 Final Decision 更新申請に対してユーザー(複数)が行った判定を元に出された更新申請を認めるかどうかの最終的な判定結果。
承認(approve)または否認(reject)がある。
(更新申請の)有効期限 Expires Datetime 提出された更新申請をユーザーが判定できる期限。
期限を過ぎるとユーザーは判定を行うことができなくなる。
有効期限を過ぎてから一定時間経過すると最終判定結果が出される。
(よく考えると有効期限ではなく判定期限とした方が適切な気がする。)

上記は簡単にユビキタス言語とその英訳、説明を表にしたものですが、チームでやるときは実際にそのユビキタス言語を利用した時の文の例ぐらいまではまとめておくと、より言葉が揃いやすくなると思います。

例えば、名言登録申請であれば、
「ユーザーが名言登録申請を提出(submit)する。」
「提出された名言登録申請の最終判定結果がでた。」
のような文になるでしょうか。

特にチームでやるときなどは、このユビキタス言語とその使い方を統一しておかないと、実装時に人によってメソッド名の動詞が変わったりしてしまったりするので、早い段階でチーム全員が見れるところに記録しておいて、違和感や足りない言葉があれば都度チームメンバーに合意を取った上で更新していくのがよさそうです。

設計過程

どのように設計内容を決めたのか?

本「実践ドメイン駆動設計」には、戦略の大切さや戦術面の具体的な話などは出てくるのですが、具体的な設計についてはほとんど出てきませんでした。

そこで、本を読み終わった後に色々と調べてみると、どうやらDDDをやる際の設計については、別の本を参考にするのが良いという意見が多く見つかります。

DDDの設計内容を考える際に、参考になる良書として、よく以下の2冊があげられています。

モデルベース要件定義テクニック(RDRA)
a1.png

ユースケース駆動開発実践ガイド(ICONIX)
a2.png

実際に、本「モデルベース要件定義」の方は自分でも 本「実践ドメイン駆動設計」を読んだ後に読みました。
DDDの設計内容を考える上では、この2つを基軸に、自分たちで必要な設計を取捨選択して決めるのが良さそうです。

本の中でも言われていますが、必ずしも紹介されている全ての設計をやる必要はありません。
設計それぞれに目的があるので、それらを理解した上で、今回どの設計を自分たちはやるべきか、チームで話し合って決めるというのがいいみたいですね。

今回採用した設計 & 採用しなかった設計

採用した設計

  1. コンテキストモデル
  2. 要求モデル
    • 要望の洗い出し
    • 要求の洗い出し
    • 要件の洗い出し
  3. ドメインモデル
  4. データモデリング
    • ER図のみ
  5. ユースケースモデリング
    • ユースケース図
    • ユースケース記述
  6. 画面設計
    • 画面遷移図
    • ワイヤーフレーム(WF)
    • (簡易的な)UI設計
  7. ロバストネス分析
  8. クラス図

採用しなかった設計

  1. 業務フローモデル
    • 業務フロー的なものはほとんど存在しない想定だったので、除外しました。
  2. 利用シーンモデル
    • やってもよかったのですが、今回は規模的にユースケース図をいきなり描き始めても問題ないと判断しました。
      チームで新しいプロダクトを作るときなんかは、まずはプロダクトのイメージを揃えていくために採用してみるのがいいかもしれません。
  3. 画面・帳票モデル
    • 今回はワイヤーフレームも自分で作成することになっていたので、ワイヤーをいきなり書くことで画面・帳票モデルの役割をカバーしました。
  4. イベントモデル
    • イベント駆動設計にするつもりはなかったのと、そもそもそれほどイベントが多いわけではなかったので除外しました。
  5. 機能モデル
    • 機能がそんなに多いわけではなかったので、他のユースケースモデル等で十分と判断しました。
      機能が多いプロダクトの場合は、それらを整理する意味で採用した方がいいかもしれません。
  6. シーケンス図
    • 設計開始当初は書く想定だったのですが、Spring Boot自体の理解も浅かったこともあり、シーケンス図を書いても有用なものにならなそうだと判断したため、ロバストネス図、クラス図、データモデルを元に実装をして、シーケンス図は書かないことにしました。(本来は書いた方がいいです。)

採用した設計の取り掛かった順番

  1. コンテキストモデル
  2. 要求モデル(要望の洗い出し)
  3. 要求モデル(要求の洗い出し)
  4. 要求モデル(要件の洗い出し)
  5. ドメインモデル
  6. ユースケースモデリング(ユースケース図)
  7. 画面設計(画面遷移図)
  8. 画面設計(ワイヤーフレーム)
  9. ユースケースモデリング(ユースケース記述)
  10. ロバストネス分析
  11. クラス図
  12. データモデリング(ER図)
  13. UI設計
  14. 実装に入る...

基本的には、本や他の参考記事に載っている通りの順番で進めています。

1箇所だけ、ユースケース図を描いた後、ユースケース記述に進む前に画面遷移図、ワイヤーフレームの設計を行なっているのは、ユースケース記述をしていく際に、画面名やボタン名、入力項目が決まっていないと都合が悪かったので、ユースケース記述の前に画面遷移図とワイヤーフレームの設計を行いました。

あくまで画面名やボタン名、入力項目を定めるのが目的なので、代わりに画面・帳票モデルを入れるのでも問題ない気がします。
(今回はワイヤーフレームも自分で作成する必要があったので、このようにしました。)

設計内容詳細

コンテキストモデル

コンテキストモデルとは?
コンテキストモデルは以下の問いに対する答えを明確にするために作成するものです。

  • 要求の元となる関係者にはどのような人や組織がいるか?
  • どのような外部システムと連携するのか?
  • このシステムのは目的はなんなのか?

実際の設計

Version 1

(利用ツール : draw.io)

今回は複雑なシステムではないので、コンテキストモデルはかなりシンプルになりました。

他社と連携している場合などは、この図の中にその他社のことが入ってきたり、他のチームと連携している場合はそのチームのことが入ってきたりします。
また、チームメンバーがPO、PM、Designer、Engineerなどに別れている場合は、そこも線で関係性を見える化しておいて、チーム全体でイメージを統一しておくといいかもしれません。

また、今回以下のようなシステム間の関係性を描くコンテキストマップは作成しませんでした。

本来は作成するべきなのですが、今回は最初どのタイミングで作成するべきなのか分かっておらず、結局ドメインモデルのタイミングでコンテキストをどう分けるか一緒に考えてしまいました。
コンテキストマップについても、このタイミングでコンテキストを分けるかやどう分けていくか決めていくべきだと思います。

要求モデル

要求モデルとは?
要求モデルとは、コンテキストモデルで出てきたアクター(役割)にヒアリングを行い、聞き出した要望を元に、要望 → 要求 → 要件 の順に考えていくことで、アクター(役割)が望むものを考慮した要件を定義していけるというものです。

  • 要望
    • 利用者(アクタ)からヒアリングしたもの。思いつきレベルのもの。
  • 要求
    • 要望を整理して構造化し、粒度を合わせたもの。機能要求、非機能要求がある。
  • 要件
    • 最終的に開発で実施すると決めた重要なもの。上位の利害関係者との確認用項目として整理する。

実際の設計

要望 要求 要件

(利用ツール : esa)

今回は以下のようなルールで要望・要求・要件をそれぞれ洗い出していきました。

  • 要望・要求それぞれの語尾はそれぞれ以下の通りにする。
    • 要望「〜したい」
    • 要求「〜できること」「〜すること」
  • 書式はマークダウンで書く。
  • 機能要求・非機能要求の差異については考えないでよい。

要望については、本来ペルソナを決めたり、ユーザーに直接ヒアリングするべきなのですが、今回はあくまで勉強用だったので、要望は自分で勝手に考えたものを洗い出しています。

要求モデルは作るものの大枠を決めるものなので、最初は出来るだけ要望・要求を洗い出して、要件でスコープをきるという流れで進めました。

今回は普通のMDを扱えるツールを利用しましたが、チームでやるときはリアルタイムで同じ画面を編集できる方がやりやすいと思うので、その場合はHackMDなんかを使う方がよさそうですね。
別の手段として、実際にホワイトボードに付箋をどんどん出していくというのでもよさそうです。

正解がなく、時間を決めないと無限にできてしまうため、
「1時間で要望全部出し切る」
という具合に時間制限を設けるのがとても大切だと思います。

また、このタイミングで他の類似サービスをリサーチすると、要望や要求を洗い出しやすく、抜け漏れも出にくいので、洗い出しをやりつつリサーチをしっかり行うのが大切だなと思いました。

上の要求・要望・要件については、それぞれリサーチ込みで1時間で作成するようにしました。

最後の感想でも言おうと思っていますが、DDDの設計はとにかく期限を決めて前に進むのがすごく大切だと個人的には思っています。
正直、正解はやってみないとわからないので、使おうと思えばいくらでも時間を使えてしまいます。

それよりも期限内に出来るだけ質高く設計して、形になったら次にいく。
その上で次の設計で違和感に気づいたり、よりより方法を発見したりすると思うので、そのタイミングでまた前の設計を見直す方が圧倒的に効率がいい気がします。

(もちろん、いい加減にやってどんどん進めるという意味ではないので、どれだけ納得感あるものになったら次にいくかはチームのメンバーで決めるべきことになると思います。)

ドメインモデル

ドメインモデルとは?
システムの鍵となる概念と用語を文書化するために作成されるものです。
ドメインモデルでは、システムの主要な実体とそれらの間の関係性を明らかにして、場合によってはそれらの重要なメソッドと属性も洗い出します。

実際の設計

Version 1 Version 2 Version 3 Version 4

(利用ツール : draw.io)

上の実際の設計を見るとわかるように、ドメインモデルは何度も更新した設計書になります。
それぞれ以下のタイミング・理由で更新された設計になります。

version タイミング 理由
Version1 要求モデル作成後、一番最初に作成。 -
Version2 ユースケースモデル作成時 もともと名言管理コンテキストにユーザー関係のドメインを置いていたが、ユースケースを洗い出しているタイミングで、申請コンテキストにはないユーザーのドメインが名言管理コンテキストにだけあるのに違和感があったので、新たにユーザー管理コンテキストを作成して切り出すようにした。
Version3 ユースケースモデル作成時 サブカテゴリーの登録申請をなくす仕様に変更したため、そのためのサブカテゴリー登録申請ドメインを削除。
(サブカテゴリーは名言が登録されるタイミングで、未登録のサブカテゴリーが入力されていた場合は、自動的に登録される仕様にした。)
また、もともと「名言を書いた人」を著作者(author)としていたが、書物を書いた人という意味が強い著作者は妥当な言葉でないと判断して、日本語名を作者に修正した。(英語の命名は変わらずauthorを採用)
Version4 ロバストネス分析時 もともと分けていた名言管理コンテキストと申請コンテキストが、かなり互いに作用し合うことがわかったので、コンテキストを分けるメリットが少ないと判断して、全体を1つのコンテキストに統合。
(ユーザーコンテキストも統合したのも、今回の場合ユーザーコンテキストだけ切り出してもメリットが少ないと判断したため。)

全設計書の中で、もっとも高頻度で更新をしたのが、このドメインモデルになります。
また、今回はドメイン内のプロパティ(プロフィールの画像など)はドメインモデルに書いていませんでしたが、できれば書いていった方がいいかもしれません。

これもユビキタス言語の範疇だと思いますが、プロフィールにある画像を「画像」と呼ぶのか「写真」と呼ぶのかでも人によって違うかもしれませんし、名言の文章を「文章(テキスト)」と呼ぶのか「内容(コンテキスト)」と呼ぶのかも人によるでしょう。

また、メソッド(振る舞い)についても同様で、申請1つとっても申請を「送信する(post)」「申請する(request)」「提出する(submit)」... など、色々な言い方ができます。
こういった表記をチーム全体で統一するために、気づいたことがあれば早い段階でチーム全体で納得のいくものに定めて、ドメインモデルに記述していく方が、あとあとになって表記揺れが多発することもなくなると思います。

ちなみに、今回最初にそういったプロパティなどを書かなかったのは、自分自身がまだデータ中心に考える癖が抜けていなかったので、プロパティなどを記述しちゃうとER図に似通ってきて、またデータ中心に考えちゃいそうだったという個人的な理由ですね。

なので、本来はドメイン内のプロパティの名前も決まっているものがあれば書いた方がいいと思います。
(どちらにしろ実装時までには決める必要が出てくるので。)

それと、上の設計書を見るとわかるのですが、途中のVersionまで緑の線でコンテキストを分割しています。

Version1の設計書を見ると緑の線でコンテキストごとに分けられていると思います。

分けたのは、名言を管理するのに必要なドメインと名言を申請する際に必要なドメインで、作用し合うタイミング(依存)は少ないと思い、それぞれ別コンテキストにして独立性を高める方針で進めるようにした方が、余計な依存が発生しにくくなると思ったからですね。
(Version1の時点では、作用し合うのは申請が完了したタイミングで、その申請内容の名言を登録するタイミングぐらいだと考えていました。)

ただ、ロバストネス分析をしたタイミングで、思ったよりも互いに作用し合う必要があることがわかり、コンテキストを分割するメリットが少ないと判断して、最終的には全体を1つのコンテキストとして扱うことにしました。

そもそも本来は、サービスが大規模だったり1つのプロダクトを複数チームで開発するときなんかに、境界づけられたコンテキストを設けることを考えるべきなので、Version1の時点で分けようとしたのが正しい判断ではなかった気もします。

正直、今回最初にコンテキストを分けようと思った一番の理由は、勉強目的で「分けてみたかったから」というのが一番な気もしますしね。
(結果的には分けられませんでしたが、、)

コンテキストを分けて、マイクロサービス化をすると、それはそれでアーキテクチャーが複雑になったり、結果的に管理が大変になるというのはよく聞く話なので、コンテキストを分けるかどうかは慎重に決めるのがよさそうです。

ユースケースモデリング

ユースケースモデリングとは?
ユーザーがシステムで何をするのか?何をできるのか?何をできないのか?といったことを明確化するためのもの。

ユースケース図

実際の設計

Version 1 Version 2

(利用ツール : draw.io)

それぞれ以下のタイミング・内容で更新された設計になります。

version タイミング 理由
Version1 ドメインモデル作成後、一番最初に作成。 -
Version2 ユースケース図作成中 名言管理コンテキストに本来関係のないユーザー自身の情報に関するユースケースがあるのに違和感があり、別コンテキストとしてユーザー情報コンテキストを作成して、そちらに切り出すようにした。

今回は1アクションで済むものが多かったので、かなりシンプルなユースケースになっていると思います。
EC系のサービスであれば、「物を購入する」といったユースケースの中に「商品を探す」「商品をカートに入れる」「住所情報を入れる」など様々な依存があると思うのですが、今回はそういった依存はほぼありませんでした。

ちなみに、ユースケース図はロバストネス分析以降、更新していません。

というのも、ユースケース図はロバストネス分析を行うための準備の役割を果たすところが大きいので、ロバストネス分析で見つかった違和感や修正すべき点などは、ロバストネス分析で全て補完するようにしました。

なので、先ほどドメインモデルでは最終的に1つのコンテキストに統合された設計書になっていたと思うのですが、ユースケース図については、Version2の3つのコンテキストに分割されている状態で止まっていますね。
(コンテキストを1つに統合することに決めたのが、ロバストネス分析のタイミングだったためです。)

ユースケース記述

実際の設計

Version 1

(利用ツール : esa)

ユースケース記述についてもユースケース図同様、ロバストネス分析の準備の役割を果たすところが大きいので、ロバストネス分析以降に見つかった違和感や修正すべき点などは、ロバストネス分析の方で全て補完して、ユースケース記述の更新はしませんでした。

ちなみに、実際にあったロバストネス分析の段階で気づいた修正すべき点の例を1つ紹介させていただきます。
コメントに関するユースケースで、メインフローに以下のようなフローがあります。

「システムはコメントが入力されたことを確認し、「送信」ボタンをdisabled状態からenable状態にする。」

ページを開いたタイミングではボタンをdisabledにしておいて、入力を確認してからenableに変更するというのが今回の仕様なので、そのことについて記述した文ですね。

ただ、ロバストネス分析で気づいたのは、あくまで上記のフローはフロント(今回でいうとアプリ側)の話で、ドメインには関係のない話でした。
なので、ドメインの関心事や振る舞いを表現するロバストネス分析の準備として書くユースケース記述には、書く必要のないものということになります。

このことについて、今回はロバストネス分析で上記の1文を無視するだけで、ユースケース記述の方は特に直していません。

このように別にユースケース記述に書かなくてもよかったものや書いた方がよかったもの、抜けていた考慮事項はいくつかありましたが、それらは全てロバストネス分析で補完するようにしました。

このタイミングで気づいたのは、DDDの設計をやる上で、前にやった設計書(ドメインモデルなど)の更新は必須になると思いますが、どの設計書を更新するべきかはチームで都度話し合って決める必要があると思いました。

例えば、ドメインモデルはシステム全体に存在する概念を表したとても大切な図になりますので、更新するべきだと思いますが、ユースケース記述についてはロバストネス分析ができてしまえば、その後はほぼ不要になると思うので更新する必要はないなど、更新することによるメリットとコストを加味して、どの設計書を都度更新するものにするか決める必要があるみたいです。

また、ユースケース記述のフォーマットについてですが、特別決まったフォーマットがある訳ではないみたいですね。
上のユースケース記述の書き方も他の記事を参考に考えたものです。

最低限、基本コースと代替コース(エラーのパターン)について記述されていて、その流れが順を追って(叙述的な文章で)主語・動詞共に明確に書かれているなどのユースケース記述の基本を押さえていれば、細かいフォーマットは自分たちで見やすいものを考えるのがいいかもしれません。

画面設計

画面遷移図

実際の設計

Version 1 Version 2
a1.png a2.png

(利用ツール : draw.io)

ユースケース記述を書くにあたり画面名が必要になったので、必要な画面を洗い出すために作ったのが、上記の画面遷移図になります。
画面・帳票モデルではないので、あくまで画面名と遷移可能な画面との関係性のみを表しています。

ここまでの設計でユースケースは見えていても、実際に必要な画面やその名前は定まっていないので、後々になって矛盾等が生じないように、ユースケース図が書き終わったタイミングぐらいには、こういった設計で必要な画面を洗い出しておいた方がいい気がしました。

ちなみに、ロバストネス分析のタイミングで数画面追加で必要なことがわかったのですが、それについてはUI設計で補完するようにして、画面遷移図は更新していません。

あくまでユースケース記述を書くために作成した設計書になりますね。
(本来は、UXを考える時などに利用するなら、画面遷移図は更新した方がいいかもしれません。)

ワイヤーフレーム

実際の設計

名言関係 申請関係 設定関係
a1.png a2.png a3.png

(利用ツール : Figma)

画面遷移図のすぐ後に作成したのが、ワイヤーフレームになります。

本来は画面・帳票モデルでもいいのですが、ある程度ページごとの雰囲気を定めておいた方が抜け漏れが発生しにくいと思ったのと、今回ワイヤーも自分で作るということだったので、画面・帳票モデルの代わりにワイヤーを引くことにしました。
(本来はページごとに必要な情報をもう少し吟味して、情報の優先順位などもちゃんと考えた方がいいです。)

ただ実際にワイヤーをこのタイミングで引いて思ったのは、単純に画面ごとに必要な情報をリストアップするような設計に比べるとイメージは断然つきやすいので、新しいプロダクトをチームで作っていくときも間違いなくプロダクトのイメージを統一させるのにかなり役立つと思います。

ここまでは文字や図がベースだったので、実は人によって思っていたものと違ったというのは少なからず避けられないと思いますが、ワイヤーを見ればかなりチーム全体でイメージを統一できるので、より会話もスムーズに進められるのかなと思いました。

なので、チームで話し合いながらデザイナーの方にリアルタイムでワイヤーを引いていってもらうみたいな進め方もできるかもしれませんね。

UI設計

実際の設計

名言関係 申請関係 設定関係
a1.png a2.png a3.png

(利用ツール : Figma)

UIの設計については、デザイナーにお願いする仕事になってくるので、ここでは詳細は省きます。

ロバストネス分析

ロバストネス分析とは?
システムを「バウンダリ」「エンティティ」「コントロール」の3つに分けて分析し、要件の振る舞いを整理することで、実装すべき点を明確にできるというものです。
参考資料
ロバストネス図を活用したシステム設計

実際の設計

Version 1
loba.png

(利用ツール : esa & Visual Studio Code)

ここまでは draw.io を利用して、図を作成してきましたが、ロバストネス図からはUMLを利用して書いています。

というのも、今までの図と違い湾曲した線が多くなったり、線の横に文字を多くおいていたりするため、これらを draw.ioで作成してドラック&ドロップで修正するというのはかなり辛くなると思ったので、コードでUMLを書いていくことにしました。

実際コードを書いていくときは下画像のような雰囲気になりますね。
最近だとqiitaやesaなどのMDでもumlは対応しているみたいです。
最初作るのは少し大変ですが、慣れると簡単に書けますし、更新するのはかなり容易になりますね。
(Visual Studio Codeなどのエディタでは、コードをプレビューを見ながら修正できたりもします。)

a1.png

書いていて気づいた点としては、当たり前ですがロバストネス図が複雑なところはコードも複雑になります。
DDDでは特にユースケース、ロバストネス図のような仕様をできる限りそのままコードで表現することを試みるため、ロバストネス図が複雑である場合、ほぼ間違いなくそのあとの実装でコードも複雑になります。

なので、ロバストネス図を書いてみた上で、明らかに複雑になるようなところがあれば、先に進む前に仕様を改善できないか検討してみるのもいいかもしれません。
もし仕様をよりシンプルに改善できるならユーザーにとっても理解しやすいものになりますし、コードも間違いなくシンプルになるので、このタイミングで仕様が複雑だとわかったらシンプルな仕様にできないか検討してみるのが良さそうですね。

また、今回シーケンス図は書いていないのですが、本来はクラス図などを作成した後にロバストネス図とクラス図を利用して、シーケンス図を作成するのが一般的だと思います。

特にチームで開発する時などは、こういったシーケンス図などで細かく認識を合わせておかないと、実装した時に人によってかなりやり方にズレが出てきたりするので、シーケンス図までしっかり書いた方がいいかもしれません。

クラス図

実際の設計

Version 1 Version2 Version 3
a1.png a2.pnga3.png a4.pnga5.png

(利用ツール : esa & Visual Studio Code)

それぞれ以下のタイミング・内容で更新された設計になります。

version 更新タイミング 更新内容
Version1 一番最初に作成。 -
Version2 クラス図 Version1 作成直後 Userのもつメソッド(振る舞い)が明らかに多かったことに違和感があり、他の記事を参考に振る舞いを定義するクラスを変更した。
また、取得処理をQueryに切り出すように変更した。
Version3 実装途中(前半) 実装中に必要になったメソッド、不要になったメソッドが多数出てきたので更新した。

個人的に今回一番大きい発見だったのは、Version1 → Version2の変更のところですね。
メソッドの置き場所の考え方を改めて考えさせられました。

例えば、「ユーザーが申請を出す」というユースケースをどのように表現するか考えたときに、そのまま英語に直せば
「A user submit a request.」
のようになると思うので、メソッド的には

user.submit(request)

とするのがそのまま読めて綺麗なのかな?というのがVersion1時点での考え方でした。

しかし、Version1のクラス図を見てわかる通り、この考え方でメソッドの置き場所を定義していくと、Userクラスにかなりのメソッド(振る舞い)が集中してしまいます。
(ユースケースの主語になるのは、ユーザーが主であるため。)

逆に、他のクラスのメソッド(振る舞い)が必然的に激減してしまうので、いわゆるドメイン貧血症になってしまうように思えました。

このことに違和感を覚えて、メソッドの置き場所について色々な記事を読んでみたのですが、下の記事が改善の大きなヒントになりました。

ドメインオブジェクトの責務について
https://qiita.com/j5ik2o/items/a64007c6d7a89ec2e086

結論からいうと、上の例で出した「ユーザーが申請を出す」という振る舞いについて定義するのは申請クラスにするようになります。

今までユーザー(アクター)が英文的に主語になる場合は、その主語になるユーザーのクラスに置くべきだと考えていたのですが、そもそもユーザークラスというのは、ユーザーという概念の関心事・振る舞いを定義する場所で、別にそのユーザー自身を示しているわけではないという理由です。

つまり、「申請を出す」というのは、あくまで申請の関心事であり振る舞いであるというのが、上記の記事で言われていることだと思っています。
(上記の記事の場合は、「Customerが銀行口座にお金を預ける」というユースケースについて、同様の例が記述されています。)

当たり前の人にとっては当たり前の事かもしれないのですが、今までここまで深く考えていなかった自分としては、初めてメソッドの定義場所をちゃんと理解したような気がします。

そして、Version2→Version3についてもかなり構造を変更していますが、もちろん1回でここまで大きく改善できたわけではなく、何度も何度も違和感を感じる度に改修を加えていき、最終的にVersion3の形になりました。
(それこそ、実装途中でも5回や6回では済まない回数改修を加えたと思います。)

実際にDDDをやってみて思うのは、やはりこの違和感がある度に細かく改修・改善していくことがとても大切なのかなと感じました。
最初からいい形を作るのはとても難しいと思いますし、それを目指そうとして設計に時間をかけるというのも時間の割に成果が出にくいと思っています。

そのため、チームが最低限納得のいく形になったら、あとは実装中にどんどん話し合って、どんどん改修・改善していく。
これが、DDDにおいては特に大切なのかなと実際にやってみて感じました。

また、
「実装を開始したら、もう設計書の更新はしなくてもいい。」
「コードが設計書の役割を果たす。」

というのも、DDDだとよく言われていることだと思いますが、修正が大きい場合は設計書も見直した方がいいと思っています。

実装を開始して初めて気づく点なども多いと思いますし、特に新しいフレームワークを利用する場合などは見えてない部分も多いと思いますので、実装を開始してからでも設計書を見直した方が、設計書が意味のあるものになる気がしています。

特にチームで開発をする時などは、設計書をみながら開発陣で、
「違和感のある箇所はないか?」
「修正した方が良さそうな箇所はないか?」
定期的に話し合うようにした方が、問題が大きくなる前にどんどん修正していけるので、かなり大切なのかなと思いました。

データモデリング(ER図作成)

実際の設計

Version 1 Version 2
a1.png a2.png

(利用ツール : esa & Visual Studio Code)

それぞれ以下のタイミング・内容で更新された設計になります。

version 更新タイミング 更新内容
Version1 一番最初に作成。 -
Version2 クラス構成を変更した時 クラス構成を変更したのに合わせて、テーブル構成も最適化するために更新した。

今回、データモデルは設計の一番最後に行ったのですが、その理由は単に自分がDDDに不慣れなので、出来るだけデータ中心に設計を考えないようにするためというだけで、もう少し早いタイミングで行っても問題ないと思います。

今回はER図だけを作成したのですが、本来はテーブル定義(データ型やIndexなどの定義)なども行う方がいいでしょう。

ちなみに、データモデリングについても実装時に大きく修正しました。

少し大きめの変更もあったのですが、思い切って労力を気にせず変更してみた結果、かなりデータ構造をシンプルに改善できたので途中から実装もものすごくやりやすかったと思っています。
実際、Version1とVersion2を比較すると視覚的にもスッキリしているようにみえます。

定期的に開発陣で違和感のある箇所や修正した方がよさそうな箇所がないか話し合った方がよさそうですね。
実装が完了してリリースした後になると、ユーザーのデータなどもあり修正コストも大きくなってしまうので、そうなる前に少しでも違和感のある箇所は全員で話してすぐに対応するのがいいと思います。

実装過程

はじめに

今回は依存関係逆転の原則を取り入れつつ、CQRSのデザインパターンを取り入れました。

CQRSを取り入れた理由は、サービス的に取得時のロジックが少し複雑なので、その複雑さをDomainに持ち込みたくなかったからというものです。
(名言一覧などでは、名言の内容 + 名言に付いているコメント数 + ログイン中のユーザーがその名言にいいね, お気に入りをしているかといった情報を返す必要があります。)

ただし、DBはDomainとQueryで共通のものを利用しています。
(時間がなかったためというのと、DB分けるほど複雑ではないと判断したため。)

また、ORMについてもDomainとQueryで同じものを利用していますが、互いに依存はしないようにしているため、Domain部分だけ別のORMに変更するのは容易な状態にしました。

ソースコード

本番で利用しているものは仕様追加によって改修されてしまうことがあるので、リポジトリは別のものにしてあります。
この記事に記載している設計内容と差が出ないように、設計したところまでの実装を公開しています。
(そのため、最新のアプリの実装とは異なる場合があります。)

https://github.com/APPLE4869/phrase-art-web-sample

利用した技術

API側 言語
Kotlin
(チームで次利用する言語として使う可能性が高かったため採用)

API側 フレームワーク
Spring Boot
(チームで次利用するフレームワークとして使う可能性が高かったため採用)

ORM
Domain : MyBatis
Query : MyBatis
(SQLベースのORMで直感的に使いやすいと思ったので採用。)

アプリ側 言語
React Native
(普段の開発で利用しているというのと、IOS, Android個々に開発するだけの余力がなかったため採用。)

サーバー
Heroku
(未経験の設計手法を未経験の言語・フレームワークで進めていくコストが高かったため、インフラは手を抜く意味でHerokuを採用)

DB
MySQL
(個人的に一番利用することの多いDBだったため採用)

参考にさせていただいたコード

今回開発をするにあたり、以下のコードをかなり参考にさせていただきました。
特に実践ドメイン駆動設計のサンプルである、IDDD_Samplesはかなり参考になったと思っています。

DDDについては、他にも色々なサンプルコードがGithubにありますので、実装を考えていく際はぜひ参考にしてみてください。

IDDD_Samples (Java with Spring Boot)
https://github.com/VaughnVernon/IDDD_Samples

spetstore (Scala with Play Framework)
https://github.com/j5ik2o/spetstore

gyst (Kotlin with Spring Boot)
https://github.com/suusan2go/gyst

ディレクトリー構造

- appllication : Application層
- batch : バッチ
- domain : Domain層
- infrastructure : Infrastructure層
    - domain
    - query
- presentation : Presentation層
    - api
        - controller
        - form
    - pages : 普通のWebページ
        - controller
- query : CQRSのQuery
    - dto
- support : AWSの画像アップロード処理や認証処理など

基本的にはDDDで一般的に採用されるディレクトリー構成なのですが、1点だけ、QueryをDomainと並列の階層におくか、Application層の中に置くかは迷いました。
(今回はQueryをDomainと並列の階層に置いています。)

というのも、Queryはほぼデータを取得するだけの処理なので、Application層に渡しても特に何もせずそのままPresentation層に返すことが多いためです。

ただ、Queryの内部ではデータアクセスを行うため、依存関係逆転の原則の中ではその技術的な実装部分はInfrastructure層に置き、QueryはInterfaceとして定義するのが正しいと考えた時に、Application層にクラス定義とインターフェース定義が混ざることに違和感があったため、今回はDomainと同じ階層にQueryというフォルダーを作成することにしました。
(また、個人的にQueryはApplication Serviceと並列の関係というより、Domainと並列の関係というイメージだったのも、今回の判断の要因になりますね。)

ちなみに、実践ドメイン駆動設計のサンプルコードでは、今回とった方法とは別のApplication層の中にQueryを置くようにしているみたいです。
クラス名のSuffixをApplicationServiceにするかQueryServiceにするかで区別するようにしているみたいですね。

今回適用した主な実装ルール

概要

  1. Infrastructure層でDBから取得した値は、必ずDAOに一度格納してから扱う。
  2. Queryで取得した値は必ずDTOに格納してからApplication層に渡す。
  3. Entityの識別子にはそれぞれ専用のValueオブジェクト設ける。
  4. Domainに関するValidationは基本的にSetterで全て行う。(FormのValidationは利用しない。)
  5. 全てのEntityの識別子は早期生成する。(識別子にはUUIDを利用する。)
  6. EntityでRepositoryをDIしてはいけない。
  7. DomainのServiceクラスからはRepositoryを参照してもよい。
  8. Factoryは集約にcreateメソッド(クラスメソッド)を用意するようする。

詳細

1. Infrastructure層でDBから取得した値は、必ずDAOに一度格納してから扱う。

今回はDBから取得した値を、例外なく全てDAOに一度格納するようにしました。
なので、Domain層のオブジェクトを生成する際は、DBに問い合わせたデータを一度DAOに格納して、DAOのデータを利用してDomain層のクラスをインスタンス化する流れになりますね。

ちなみに、ここでいうDAOというのは以下のようなデータクラスになります。

data class PhraseDao(
    val id: String,
    val categoryId: String,
    val subcategoryId: String,
    val authorName: String,
    val content: String,
    val createdAt: Timestamp,
    val updatedAt: Timestamp
)

全てDAOに一度格納するようにしたのは、DBのスキーマの情報がDomainのオブジェクト生成に出来るだけ影響しないようにしたかったからです。
必要に応じて、一部だけDAOを利用するようにするのもありなのですが、全部DAOを利用するようにした方がわかりやすいと思い、今回は全てDAOを利用するようにしました。

2. Queryで取得した値は必ずDTOに格納してからApplication層に渡す。

こちらも上と似通った理由で、Queryには振る舞いなどはないのですが、DAOはあくまでDBから取得したスキーマをそのままの形式で格納しているので、アプリ側に返却するデータ形式とは異なることがあるため、QueryではDAOをDTOに変換してApplication層に返すような実装にしました。

3. Entityの識別子にはそれぞれ専用のValueオブジェクト設ける。

今回はDomainオブジェクトのプロパティのうちEntityの識別子のみValueオブジェクトにするようにしました。

他のDDDのコードを見ると、全てのプロパティにそれぞれValueオブジェクトを定義しているものもあったりしますが、今回はそうしませんでした。

というのも、実践ドメイン駆動設計のサンプルコードでも基本的に識別子しかValueオブジェクトにしていなかったということと、実践ドメイン駆動設計の本の中でも必ずしも全てのプロパティに対してValueオブジェクトを定義する必要はないといった趣旨のことが書かれていたので、今回は基本的に識別子のみをValueオブジェクトにして、他のプロパティは標準のStringやIntにするようにしました。

例えば、Domain層のSubcategoryクラスの場合、以下のような定義になりますね。

class Subcategory : Entity {
    val id: SubcategoryId
    var categoryId: CategoryId
    var name: String
    var imagePath: String?
    var introduction: String?
    var videoOnDemands: MutableList<VideoOnDemand>? // ← VideoOnDemandのみ識別子以外のValueオブジェクトになる。
...

4. Domainに関するValidationは基本的にSetterで行う。(FormのValidationは利用しない。)

Validationについては、DomainのクラスのSetterで定義するようにしました。
実践ドメイン駆動設計(IDDD)で紹介されていたセーフティープログラミングというやつですかね?

IDDDのサンプルでもそのように実装してあったので、それを参考にさせてもらいました。

具体的には以下のようなコードになりますね。
(assert~ というメソッドは別の場所で定義している値検証用のメソッドです。)

class Phrase : Entity {
    val id: PhraseId
    var categoryId: CategoryId
    var subcategoryId: SubcategoryId?
    var content: String
        set(value) {
            this.assertArgumentNotEmpty(value, "内容を入力してください") // ← contentに値を入れたタイミングでこれらの検証処理が実行される。
            this.assertArgumentLength(value, 500, "内容は500文字以内にしてください") // ← 〃
            field = value
        }
    var authorName: String
        set(value) {
            this.assertArgumentNotEmpty(value, "作者を入力してください") // ← 〃
            this.assertArgumentLength(value, 36, "作者は36文字以内にしてください") // ← 〃
            field = value
        }
...

仮に不正な値が入った場合は、IllegalArgumentException が発生する仕様になりますね。

IllegalArgumentExceptionが発生した場合は、自身で作成したExceptionControllerで例外をcatchしてエラー内容をレスポンスに含めて返すように実装してあります。

このやり方だと、Presentation層のFormやApplication層でいちいちValidationを書く必要もないので、不正な値が混入しにくい上、個人的にはかなり安心感があったのがよかったです。

ただ、1箇所で例外が発生すると他の箇所は見ずにそのままレスポンスを返してしまうので、入力値が複数ある場合などでは、いずれか1つのエラーのみしか返せないのがデメリットですね。
そのため、基本的にはフロント側でValidationまたは条件を満たすまではsubmitできない仕様にする必要がありそうです。

ちなみに、
「他に同じ内容の名言がないか?」
「すでに同じ内容の申請が出されていないか?」
といったDBを参照するようなValidationについては、Application層でRepositoryを利用して行うようにしてあります。

なので、Domainで行うValidationはあくまで値に関するValidationのみですね。

5. 全てのEntityの識別子は早期生成する。(識別子にはUUIDを利用する。)

Entityに識別子がない可能性があることを考慮してくなかったこともあり、今回、識別子に関しては遅延生成ではなく早期生成を採用しました。

早期生成でも、DBを利用して連番を識別子に振っていく方法もあるのですが、都度DBに問い合わせるコストがあるのと、その実装コストを加味して、今回はそのままUUIDを識別子に利用することにしました。

わかってはいたのですが、やはりUUIDだと普通の連番のIDに比べるとDBのデータを見た時に相当見にくかったですね。

UUID例

280D1D4D-9582-4C39-BE61-4D3480B474C1

識別子については、Timestampを最後につけたり、テーブルによってプリフィックスをつけていったりなど、要件によっても色々やり方が分かれるところだと思うので、実際にやるときはチームで考えていくのがよさそうです。

ちなみに、実践ドメイン駆動設計のサンプルコードだと、全て遅延生成でやっているコンテキストと基本はUUIDを利用した早期生成で、イベントに関するテーブルはDBの自動採番を利用するようなコンテキストという具合に、コンテキストによって別の方針で実装されていた気がします。

6. EntityでRepositoryをDIしてはいけない。

色々な記事を見ると、EntityにRepositoryをDIして利用するのはよくないパターンだという意見が多かったです。
なので、今回はEntityではRepositoryをDIしないように実装しました。

例えば、名言登録申請をDBに登録するような処理ではこのようなコードになりますね。
(下のコードはApplication層に書かれているコードになります。)

val request = PhraseRegistrationRequest.create(
    updateRequestRepository.nextIdentity(),
    user.id,
    categoryId,
    subcategory?.id,
    form.subcategoryName,
    form.phraseContent,
    form.authorName
)

updateRequestRepository.store(request) // ← ここでrequestをDBに保存している。

実際に、EntityにRepositoryをDIしないようにすると、Application層を見ただけで、どのタイミングでどのオブジェクトが保存, 削除されているのかがわかりやすかったのと、1つのEntityに様々な種類のRepositoryが入ってきて、複雑になるといったこともなかったので、かなりやりやすかった印象です。

ただし、RepositoryをEntityにDIしてはいけないので、Entity内で行う処理に必要な情報は、事前に全て読み込んでおく必要があります。

例えば、今回だと更新申請(Update Request)内に、最終判定結果(Final Decision Request)を出すような処理を設ける場合は、Entityメソッド内でRepositoryを使うことはできないため、予めひもづく全ての判定(Decision)を、UpdateRequestオブジェクトで持っておく必要があります。

コードでいうとこのような状態になりますね。

abstract class UpdateRequest(
    val id: UpdateRequestId,
    val userId: UserId,
    val type: UpdateRequestType,
    var finished: Boolean,
    val expiresDatetime: LocalDateTime,
    var finalDecisionResult: FinalDecisionResultType?,
    var decisions: MutableSet<Decision> // ← DecisionをMutableSet型で全て持っている。
) : Entity() {
...
}

UpdateRequestのdecisionsというプロパティにDecisionオブジェクトを持っているという構成になりますね。

UpdateRequestを生成する際にひもづくdecisionsを全て読み込んでおけば、あとはメソッド内でそれらを見て最終判定結果出してを返すだけなので、RepositoryをEntity内で使う必要は無くなります。

ただし、decisionsが1000や2000もひもづくようなら、かなりのメモリをくってしまう上、DBの負荷も高くなってしまうので、その場合はApplication層でRepositoryを利用してDecisionを取得し、取得したDecisionをEntityのメソッドの引数に渡してあげるというやり方の方が良さそうです。

どれだけひもづく可能性があるかでEntityに読み込んでおくべきか、後から読み込むかの判断は分かれますね。

この話は実践ドメイン駆動設計の本の中の、SprintにひもづくBacklogItemを事前に読み込んでおくべきかという話と同様になります。

7. DomainのServiceクラスからはRepositoryを参照してもよい。

EntityではRepositoryをDIしないようにしたのですが、DomainのServiceではやってもいいことに今回はしました。

というのも、DomainのServiceでやるような処理は、DBのデータを参照することを必須とするものが多かったからです。

例えば、今回だと名言の登録申請をする際に、その申請を登録できるかどうかのチェック項目として以下のようなものがあります。

  1. 全く同じ内容の名言が登録されていないこと。
  2. 全く同じ内容の名言登録申請が提出されていないこと。
  3. 全く同じ内容の名言修正申請が提出されていないこと。

このチェックをApplication層で行うと可読性が落ちるというのと、名言登録申請の細かい仕様をApplication層に書くのに抵抗があったという理由で、DomainのServiceクラスを使うことにしたのですが、チェック項目の全てがDB参照を必要とするものでした。

今回の実装だと、DomainのServiceではこのような既存データを参照する処理が多かったので、Entityとは違い、RepositoryのDIを許すようにしました。

8. Factoryは集約にcreateメソッド(クラスメソッド)を用意するようする。

Factoryはconstructorで定義したり、複雑である場合は別クラスに切り出すという手段を取ることが多い気がしますが、今回はEntityにクラスメソッドとして定義するようにしました。

理由としては、Constructorに定義するのだと名前がついていないので目的がわかりにくいと思ったからです。
また、数も量も多くなかったので、別クラスに切り出すほどではないと思い、Entity内に定義するようにしました。

具体的なコードでいうと以下のようになります。

class Phrase : Entity {
    // ↓ Kotlinの場合は、このcompanion objectの中に定義したメソッドがクラスメソッドのように扱える。
    companion object {
        // 登録申請を元に名言を作成する
        fun createFromRegistrationRequest(phraseId: PhraseId, request: PhraseRegistrationRequest): Phrase {
            return Phrase(
                phraseId,
                request.requestedCategoryId,
                request.requestedSubcategoryId,
                request.requestedPhraseContent,
                request.requestedPhraseAuthorName
            )
        }
    }
...
}

上のコードは名言登録申請を元にPhraseエンティティを生成するFactoryメソッドです。

個人的には、constructorに定義するよりも、このようにクラスメソッドとして定義した方がわかりやすいと思い、今回はそのようにしました。

よかった事 3つ

1. CQRSはかなりやりやすかった

取得処理(Query)をDomainと完全に切り離せるのはかなり良かったと個人的に思っています。
QueryをいじってDomainがバグることはまず間違いなくないので、影響範囲も把握しやすいですし、かなり安心して作業することができました。

今回はまだ良かったのですが、もう少しデータ構成が複雑になるようなら、Query部分だけElasticsearchを利用するなど、データ構造もDomainの影響を受けないようにした方いいのかなというのが感想ですね。

2. DTOやDAOをきっちり使うのは修正がやりやすかった

これについても、修正した時の影響範囲が把握しやすいのがかなりよかったです。

あとは、DAOはDBから取得するスキーマを表現、DTOはアプリ側に渡すデータ構成を表現という具合に、明確に役割が分かれていたので、取得した時のSQLがめちゃめちゃ複雑になるみたいなこともなく、DAOからDTOに変換するところでフォーマットを変換できたりするので、部分的に見ればシンプルになりやすいのかなと思いました。

このぐらいの規模だと、普通よりたくさんのクラスを作る必要があるので、めんどくさい気もしてしまいますが、規模が大きくなるほど細かくクラスを作って、シンプルにしていくのが大切になりそうだなと感じました。

3. QueryにSQLベースのORM(MyBatis)を利用するのはやりやすかった

普通のオブジェクトベースのORMだと、そのORMの仕様に引きづられて、思ったようにデータが取れなかったりするのですが、SQLベースのORMだと、そのままSQLを書いてデータ取得ができるので、素直に取りたいデータをそのまま取れるのがすごくやりやすかったです。

ただ今回の場合、自分のORM(MyBatis)の理解が浅かったこともあり、より簡単に書けるところを愚直に書いてしまった箇所が多かったので、もう少しMyBatisの使い方を理解しないといけないなというのが反省ですね。

よくなかった事 3つ

1. ファイル名やディレクトリー名の命名規約をちゃんと決めておけばよかった

今回、ファイル名やディレクトリー名の命名規約を全く決めないまま実装に入ってしまったのが、一番後悔した点です。

Ruby on Railsのような事前に命名規約が決められているものと違い、今回はほぼ全て自分で命名を決める必要があったので、事前に命名規約を決めておくべきでした。

決めなかったことで、なんとなくで進めてしまい、いくつか修正が追いついておらず、統一感のない箇所が出てきてしまっています。

一人で開発する時ならまだ最小限で収まりますが、チームで開発する時は必ずディレクトリー構成やその命名、ファイルの命名の規約はきっちり決めておいた方がいいと思いました。

2. URLの命名規約もちゃんと決めておくべきだった

これについても、普段利用しているRuby on RailsだとほぼURLが自動生成されるので、それと同じ感覚でURLについて何も考えず実装に入ってしまい、統一感のない構成になってしまっています。

特にSpring Bootなどの自由度が高いフレームワークを利用する場合は、こういった規約を実装前にしっかり決めておいた方がいいみたいですね。

3. Domainの方のORMはMyBatis以外にしたいかも

Query側でSQLベースのORM(MyBatis)を利用したのは、すごくやりやすかったのですが、Domain側で行うようなInsertやUpdate, Deleteといった処理やシンプルなSelect処理についてはオブジェクトベースのORMを利用した方がやりやすいのかなというのが、今回MyBatis(SQLベースのORM)をDomain側でも使ってみた感想です。

最初はHibernateなどのオブジェクトベースのORMを利用する予定だったのですが、時間的にQuery側と同じORMを利用することにしました。
機会があれば、Domain側だけHibernateを利用するように改修しようと思います。

最後に

まだ本「実践ドメイン駆動設計」を読み終わってから1ヶ月ちょっとですが、実際にDDDでプロダクトを1つ作ってみて、かなり理解が進んだ気がします。

もちろん、まだまだ理解があまいところも多々あるとは思いますが、どうにか1プロダクト形にできるところまではできたので、ここからより理解を進めていければと思っています。

また今回、使ったことのなかったSpring Bootもどうにか使えたというのと、初めてアプリを自分で0から作れたということで、DDD以外にも色々な経験を得ることができたのはすごくよかったです。

この記事に関して疑問点や「自分だったらこうする」「ここは違うんじゃないか?」といった意見などありましたら、ぜひコメントください!

この記事が少しでもあなたの参考になればうれしいです!

それではここまでお読みいただき、ありがとうございました。

CircleCI Workflow + Orbs & Reusing Config実践ガイド

はじめに

クラウドワークス Advent Calendar 2018、18日目となりました🎁

先日、CircleCI Advent Calendar 2018に「CircleCI Orbsをテストする」を投稿しましたが、今回もCircleCIネタです。

本記事ではRailsアプリケーションであるCrowdWorksのCircleCI設定でWorkflowの活用を実践し、設定の可読性を高めつつ、CIのメイン処理であるRSpec実行のオーバーヘッドを減らすことで並列数引き上げの効果を高めたお話をします。

Rails 4.2におけるお話となりますが、Workspaceの仕様など言語・フレームワークを問わない内容もありますので、ぜひご覧ください。

Motivation

  • 従量課金であるPerformance Planを利用しているため、少ないコンテナ数で長く実行するよりも、並列数を上げて短い時間で終わらせた方がうれしい。それをほぼ同じ料金で行うには並列数を上げるジョブのオーバーヘッドを減らす必要がある。
  • 1つのジョブでたくさんのことを実行しているため、設定ファイルの見通しが悪い。
  • YAMLのアンカー&エイリアスだらけになっているため、CircleCI 2.1の機能でスッキリさせたい。

Workflowの活用

まずはWorkflowを使ってジョブをどのように分割していったかについてと、Workflowと切っても切れない関係にあるWorkspaceについて解説します。

意味のある単位でジョブを分割

改善前は処理がほぼ1つのジョブに入っており、「とりあえずWorkflowとして動くようにした」という状態でした。

そのため、buildジョブにいろいろなものが詰め込まれていました。これだと中で何が行われているかわかりません。

Before
workflows:
  version: 2
  build:
    jobs:
      - build
      - validate_factory

そこで、次のように意味のある単位でステップの集合をジョブに抽出していきました。

Workflow

これにより、rspecジョブのオーバーヘッドが減り、並列数を上げる効果が高まりました。

また、改善後はWorkflowの定義を見るだけで概要がわかるようになりました。

After
workflows:
  version: 2
  continuous-integration:
    jobs:
      - static-checks
      - bundle-install
      - yarn-install
      - assets-precompile:
          requires:
            - bundle-install
            - yarn-install
      - setup-database:
          requires:
            - bundle-install
      - rspec:
          requires:
            - assets-precompile
            - setup-database
      - validate-factory:
          requires:
            - bundle-install
            - setup-database
      - validate-dwh-tag:
          requires:
            - bundle-install
            - setup-database

config.ymlのサンプルではよくWorkflowの定義がファイルの末尾に書かれていることが多いですが、YAMLとしてはここの順序は関係ないため、概要が先にわかるようにWorkflowの定義はファイルの先頭に記述することをおすすめします。

Workspaceについて

ジョブを分けていくと、bundle installで入ったGemファイルやyarn installで入ったNodeモジュール、assets:precompileで生成されたAssetを、それらを必要とするジョブに渡してあげなければなりません。

そのために、Workspaceを利用します。

Workspaceを利用するには、データを永続化するpersist_to_workspaceと、データを取り出すattach_workspaceの2つのステップを使います。

Workspaceの特徴として以下が挙げられます。

  • 実行されるWorkflowごとに1つのWorkspaceが確保される
  • Workspaceにはファイルが相対的なパスで保存される
  • Workspaceに永続化された時点で格納されたデータが他のジョブから参照可能となる

実行されるWorkflowごとに1つのWorkspaceが確保される

WorkspaceのライフサイクルはWorkflowごとです。

また、Workflowの再実行の場合は元のWorkspaceが継承されてデータを参照することができます。同様に失敗したジョブのみ再実行するときや、SSHを有効にして再実行するときも元のWorkspaceが継承されるため、上流のジョブを再実行しなくても問題ありません。

ただし、データの保存期間は最大30日間となっているため、ジョブを個別に再実行するとWorkspaceのデータを必要とするステップでエラーになります。逆に、この保存期間を過ぎるまでは自分で削除することもできない点には注意です。

Workspaceにはファイルが相対的なパスで保存される

データをWorkspaceに永続化するpersist_to_workspaceステップには2つのプロパティが存在します。

1つはrootプロパティで、絶対パスかworking_directoryからの相対パスが指定できます。これをどう指定するかは2つ目のpathsプロパティによって決まります。

pathsプロパティではWorkspaceに永続化したいファイルを、rootプロパティで指定したパスからの相対パスで指定します。同時にWorkspace内にはこのpathsプロパティで指定されたパスで保存され、rootプロパティで指定されたパスの情報はWorkspaceに残りません。

データを取り出すときにはどのようになるかというと、attach_workspaceステップのatプロパティで指定されたパスからの相対パスでファイルが展開されます。

job-a
- persist_to_workspace:
    root: /home/ruby/project
    paths:
      - vendor/bundle

例えば上記のように記述した場合、job-a/home/ruby/project/vendor/bundleがWorkspaceのvendor/bundleに永続化されます。

job-b
- persist_to_workspace:
    root: /home/ruby/project
    paths:
      - node_modules

次にjob-bでは/home/ruby/project/node_modulesがWorkspaceのnode_moduleに永続化されます。

job-c
- attach_workspace:
    at: /var/tmp/workspace

そして、job-c/var/tmp/workspace/vendor/bundle/var/tmp/workspace/node_modulesに展開されます。1回のattach_workspaceステップで、Workspace内に永続化されているものがすべて展開されます。

また、pathsプロパティで指定するパスにはGo言語のfilepath.Matchの書式を用いて記述することができます。

Workspaceに永続化された時点で格納されたデータが他のジョブから参照可能となる

persist_to_workspaceステップが呼び出されるごとに、データがレイヤーとして積み重ねられていきます。attach_workspaceステップで取り出す際は、その時点までに格納されたデータが指定されたディレクトリ配下にすべて展開されます。

Workspaceのイメージ

この図のsetup-databaseジョブの時点では、依存しているbundle-installジョブが完了しているので、「Gemファイル」を取り出すことができます。

注意点として、persist_to_workspaceステップが完了した時点でWorkspaceに永続化したものが参照できるようになるため、もしyarn-installジョブがbundle-installジョブよりも早く終わっていた場合はsetup-databaseジョブ内のattach_workspaceステップで「Gemファイル」だけでなく、「Nodeモジュール」も展開されます。

あくまで「その時点で永続化されているものが見える」という仕様なので、Workflowの設定でrequiresに定義されているジョブで永続化されたものは順序が保証されているため確実に参照できますが、そうでないものは不定となっています。

OrbsとReusing Configの活用

もともとYAMLのアンカーとエイリアスで記述の重複を回避していたのですが、せっかくOrbsReusing Configが使えるようになったため、このタイミングで置き換えました。

YAMLのアンカー&エイリアスの問題点はステップ1つにしかアンカーが張れないことが大きかったです。Reusing Configであれば、意味のある複数ステップの塊を1つのCommandにまとめることができます。

具体例

ここでは実際にOrbsとReusing Configを使っている箇所をピックアップして紹介します。

ソースコードのチェックアウト

CrowdWorksのリポジトリは約1.3GBあり、そのままgit cloneすると非常に時間が掛かります。そのためShallow Cloneを使っていたのですが、これをOrb化したので参照するようにしました。

orbs:
  git: ganta/git@1.2.0

...

commands:
  ...
  https-shallow-clone-checkout:
    description: Gitのユーザー設定を行い、リポジトリをHTTPS経由のShallowでチェックアウトします。
    steps:
      - run:
          name: git config
          command: |
            git config --global user.email "**********"
            git config --global user.name "*****"
      - git/shallow-clone-checkout:
          use-https: true
          github-access-token: ${GITHUB_TOKEN}

実際には直接ジョブ内で呼び出さずに、Commandを作ってラップしています。これはブランチを切ってコミットするステップが存在するため、Gitのユーザー設定を行っておきたかったのと、shallow-clone-checkoutコマンドのパラメーターを毎回指定するのを避けるためです。

今回はチェックアウトを1回にしてソースコードをWorkspace経由で渡さず、各ジョブの先頭で行うようにしました。Workspaceのサイズが大きいとattach_workspaceでOOM Killerされる事例を聞いていたため、ちょっとサイズが大きめなリポジトリを渡すのは避け、優先順位を下げました。

Bundlerの場合

sue445さんのruby-orbsを上記同様Commandにラップして使っています。

orbs:
  ...
  ruby: sue445/ruby-orbs@1.3.0

...

commands:
  ...
  bundle-install:
    description: BundlerでGemをインストールします。
    parameters:
      persist-to-workspace:
        description: インストールしたGemをWorkspaceに永続化するかを指定します。
        type: boolean
        default: false
    steps:
      - ruby/bundle-install:
          cache_key_prefix: '{{ .Environment.COMMON_CACHE_KEY }}-gems-v1'
          # GemfileとGemfile.lockの不一致を検出するため、--deploymentオプションを付ける。
          bundle_extra_args: "--deployment"
      - run:
          name: Bundlerの設定確認
          command: bundle config
      - run:
          name: vendor/bundleのサイズ確認
          command: du -hs vendor/bundle
      - when:
          condition: << parameters.persist-to-workspace >>
          steps:
            - persist_to_workspace:
                root: .
                paths:
                  - vendor/bundle
                  - .bundle/config

Workspaceを利用しないジョブからも使えるように、Workspaceへの永続化は選択できるようにしておきました。

    parameters:
      persist-to-workspace:
        description: インストールしたGemをWorkspaceに永続化するかを指定します。
        type: boolean
        default: false

whenステップを使ってパラメーターの値がtrueになるときだけWorkspaceへ永続化するようにしています。

      - when:
          condition: << parameters.persist-to-workspace >>
          steps:
            - persist_to_workspace:
                root: .
                paths:
                  - vendor/bundle
                  - .bundle/config

bundle-installのパラメーターはcache_key_prefixbundle_extra_argsを指定しています。

      - ruby/bundle-install:
          cache_key_prefix: '{{ .Environment.COMMON_CACHE_KEY }}-gems-v1'
          # GemfileとGemfile.lockの不一致を検出するため、--deploymentオプションを付ける。
          bundle_extra_args: "--deployment"

cache_key_prefixパラメーターには全キャッシュをまとめて飛ばせるように{{ .Environment.COMMON_CACHE_KEY }}と、Commandをチューニングしているときにキャッシュを飛ばせるようバージョンを付けています。

bundle_extra_argsパラメーターにはGemfileGemfile.lockの不一致を検出できる--deploymentを渡しています。

CrowdWorksではRubyのオフィシャルイメージをベースとしたDockerイメージを使っているのですが、環境変数BUNDLE_PATHBUNDLE_APP_CONFIGが定義された状態になっています。

オフィシャルRubyイメージの初期状態
$ bundle config
Settings are listed in order of priority. The top value will be used.
path
Set via BUNDLE_PATH: "/usr/local/bundle"

app_config
Set via BUNDLE_APP_CONFIG: "/usr/local/bundle"

...

ところが、bundle installコマンドに--deploymentを渡すと、--path vendor/bundleオプションも暗黙的に設定され、Gemの保存先となるpath設定がBUNDLE_PATHのデフォルトの/usr/local/bundleと異なる場所に変わります。

--deployment付きでbundleコマンドを実行した状態
$ bundle config
Settings are listed in order of priority. The top value will be used.
frozen
Set for your local app (/usr/local/bundle/config): true

path
Set for your local app (/usr/local/bundle/config): "vendor/bundle"
Set via BUNDLE_PATH: "/usr/local/bundle"

app_config
Set via BUNDLE_APP_CONFIG: "/usr/local/bundle"

...

Workspaceのアタッチ先をworking_directoryに統一しているため、カレントのvendor/bundleディレクトリに保存されるのは都合がよいのですが、そのままattach_workspaceするだけでは--deploymentオプションによって書き換えられたpath設定が保持されず、BUNDLE_PATHの値である/usr/local/bundleを参照してしまい、bundle execでエラーになってしまいます。

そこで環境変数BUNDLE_PATHをExecutorのenvironment設定でvendor/bundleに書き換えてしまう方法を思いつきますが、環境変数BUNDLE_PATHによる指定と--pathオプションによる指定ではディレクトリ構造が変わってしまうためうまく動きません。1

そのため、Executorのenvironment設定にはBUNDLE_PATHではなく、BUNDLE_APP_CONFIGworking_directory直下の.bundleディレクトリに設定し、pathが設定されたconfigを一緒にWorkspaceに永続化します。

BUNDLE_APP_CONFIGに/usr/src/app/.bundleを設定した状態
$ bundle config
Settings are listed in order of priority. The top value will be used.
path
Set via BUNDLE_PATH: "/usr/local/bundle"

app_config
Set via BUNDLE_APP_CONFIG: "/usr/src/app/.bundle"

...

すると、attach_workspaceするだけで--path vendor/bundleが設定された状態にすることができます。

attach_workspaceによって.bundle/configが展開された状態
$ bundle config
Settings are listed in order of priority. The top value will be used.
frozen
Set for your local app (/usr/src/app/.bundle/config): true

path
Set for your local app (/usr/src/app/.bundle/config): "vendor/bundle"
Set via BUNDLE_PATH: "/usr/local/bundle"

app_config
Set via BUNDLE_APP_CONFIG: "/usr/src/app/.bundle"

まとめ

Workflowを活用してジョブを意味のある塊に分割することで可読性を高めるだけでなく、ジョブのオーバーヘッドを減らし、並列化の効果も高めることができます。

改善前は10〜12分ほどで完了していたWorkflowが、8〜9分で完了するようになりました:tada:

改善前後

実はもっとrspecジョブの並列数を上げれば速くなるのですが、Pull Requestのチェックを行う他のサービスを待つ状態になったことと、まだジョブのDockerのイメージのpullに時間が掛かるというオーバーヘッドが残っているため、いったん並列数を上げるのを止めています。

また、CircleCI 2.1のOrbsとReusing Configも活用することで、より可読性を高めることができます。

これらの機能を活用してメンテナンス性の高いconfig.ymlにしていきましょう。

明日のAdvent Calendarは@k-waragaiさんの社内でのVR活用についてです:santa_tone1:
残り7日、引き続きクラウドワークス Advent Calendar 2018をよろしくおねがいします:christmas_tree:

チーム内1on1をVR上で行った結果、対話が苦手な自分でも本音を話せた件について

この記事は クラウドワークス Advent Calendar 2018 の19日目の記事です。

昨日は、@ganta によるCircleCI Workflow + Orbs & Reusing Config実践ガイド でした!

✍はじめに

今年の3月から玉露という名のチームでエンジニアとして働いている @k-waragai です٩(๑´0`๑)۶

玉露のチームがどういう事を行っていたかは以下のリンクをご確認ください。
- 6万行の大規模リファクタリングを完遂する上でPOとしてやってよかった5つのこと
- 混沌を極める jQuery のコードをいかにして vue.js に頼らずに整理したか、あるいは ayasuda さんへのアンサーソング

今回は、最近何かと話題になっているOculusを活用したVR(Virtual Reality)のお話をしようと思います。

🍤チーム内1on1を対面ではなくVR空間で行ってみた

oculus_rooms.png
(※1on1ではなくチーム全員でOculus Roomsを体験した時の写真です)

HMDはOculus Goを利用しました。

使ったアプリケーションはOculus Roomsになります。
無料だし話せるし十分ですね٩(๑´0`๑)۶
スクリーンショット 2018-12-11 12.16.21.png

● チーム内1on1を始めたきっかけ

チームビルディングの一環で
メンバー間の壁をもっと薄くし対話が生まれやすい状態を作ろうと始まったのがチーム内1on1でした。

● なぜそれをVR上で?

始めはやってみたかった。ただそれだけです。

やってみたかったと言ってもちゃんと理由があります。

具体的には
1. VR空間でアバター越しに対話を行うと、心理的にどういう状態になるのか?
2. 現実空間で行う1on1との差異はどんなものか?
3. VR空間で行うメリットデメリットは何か?
という事を知りたかった為提案し行動してみました。

1つずつ分かった事について説明していきます。

● VR空間では心理的安全性が担保される?

ぶっちゃけ上司との1on1って緊張しませんか?

自分は少なくとも、対面で話すのが苦手なのでハードルは高いなと感じています:(´◦ω◦`):

対面で話すのが苦手な理由としては、
目を見て話しなさいとか、はっきり喋りなさいとか小さい頃から言われてきたのが原因かと...(ㆁωㆁ*)

それに比べて、VR空間上ではアバターという1枚皮を被った状態で対話することが出来ます。

相手ももちろんアバターなので、見られていても全然怖くありません。

表情を伺われることも無い為、ちゃんとアバターを見て話す事ができます。

アバターという自分の分身を通すことで、目を見て話すが楽になりました。

また声の問題を抱えている人も中にはいるのではないでしょうか?
(通りにくい声や大きい声を出すことが出来ないなど)

Web会議ツールなどを使うと、
小さい声でもマイクの位置や機材次第でブーストさせる事が可能なのでそれも解決することができます。

こうしていろいろな問題が解決され対話という面においては、心理的安全な状態になる感じました。

● 現実空間とVR空間での1on1の違いは何か?

VR空間では、対話をする為に最適な環境を一瞬で整える事が可能です。

気持ちを落ち着けるBGMや、ゆったりとした空間
現実世界では自宅などから接続している為かなりリラックスした状態で望めます。

また、非現実的な空間が備わっている為アイスブレイクも捗ります。

対話を始めるまでの場作りにおいては、VR空間の方が優れていると感じました。

これだけ聞くと「VR空間での1on1めっちゃ良いじゃん!」と思うのですが、
VR空間では声意外の情報はあまり使えない為、
ノンバーバルコミュニケーションがしにくいというデメリットに気づきました。

相手の表情や視線などの情報を得ることが出来ない為、
気持ちを読み取るのが少々難しくなるのではと考えています。

またアバターについてですが、
心理的安全性が担保されている為か安心して個人をさらけ出しやすくなるという気がしました。

突然ですが皆さん『だてマスク』をご存知でしょうか?

メールやSNSなどネット上のコミュニケーションに慣れた若者が“だてマスク”をするようになっている。人間のコミュニケーションは本来、言葉つきや相手の表情を含んでとられるものだったが、携帯やパソコン上の文字だけのコミュニケーションでは、そのような要素がないため、互いに本音を隠したままでことを進めることができる。それに慣れてしまった若者たちは、まず自分の本音を他人に知られることが怖い。そして自分の弱みを知られることを嫌うのではないか。

上記のwikiの一部を抜粋しています。

簡単に言うと「自己防衛本能」が働いてる状態になります。

つまり普段マスクをしている人が、マスクを外した状態で対話を行うと警戒心ビンビンで本音を話す事ができません。

それをカバーしてくれるのがVR空間なのではと思いました。

アバターというマスクを被り対話を行うことで警戒心が緩和され『本音』が出やすい状態になると自分は感じました。

ちなみに、幼少期のお話とかエンジニアになる前になりたかった職業とかそういった話をしていました。

● 1on1をVR空間で行うメリットとデメリットをまとめる

チームビルディングや関係性向上の為にVR空間を活用すると
より本音で会話することが出来るため向いていると言える。

目標設定などのしっかりとした会議などでは
ノンバーバルコミュニケーションがしにくい為VR空間は向いていないと言える。

📌最後に

実際にOculus Goをチームビルディング等で利用してみたところいろいろな知見を得ることが出来ました。

VR空間を利用することで会議室を無限に作ることが出来るようになったり、
オンライン上のコワーキングスペースやオフィスが出来るようになったりしてくるんじゃないかなー
なんて毎日妄想しています。

また、会社としてもVRには可能性を感じているようで、積極的に支援してくれています。

例えば...
デザイナーの田村さんが「VRのスクールに通いたい」と分報で呟く
すぐに副社長の成田さんから「通いましょう!」と返信が来たり
8b9f75d1-b72a-e3fd-3039-91d30cbe54cc.png

Questの発表があった日はこんな発言も
8534cdcc-a1b4-b369-7a84-720ce298c915.png

こんな感じで VR に対して積極的に取り組みを見せています。

良い会社だ...٩(๑´0`๑)۶

Next 👉

明日は マーケティングチームによる
【夜も眠れない】マーケットプレイス型プロダクトが直面する3つの課題
についてのお話だそうです。

引き続き クラウドワークス Advent Calendar 2018 をよろしくお願いします!👋

【夜も眠れない】マーケットプレイス型プロダクトが直面する3つの課題

この記事はCrowdWorks Advent Calendar 2018の20日目の記事です。

はじめまして、古田(フルタ)と申します。
クラウドワークスというプロダクトのプロダクトマネージャーを務めています。

f9f774f9-8c73-652e-8cdd-2017f582c708.png

クラウドワークスをご存知無い方向けに説明すると、クラウドワークスは、

  • 「役務」を媒体として
  • クライアントとワーカーが
  • オンラインでお仕事マッチングをすることができる
  • マーケットプレイス型のプロダクト

です。

「マーケットプレイス」という言葉に耳馴染みの無い方は、

  • 国内:メルカリ・ラクマ・minne
  • 海外:Uber・Airbnb

といったプロダクトをイメージしていただくと良いかもです。

マーケットプレイスでは、売り手と買い手のツーサイドが存在し、各々が合理的経済人(経済合理性の最大化を意図して動く人)として市場の中で自由に振る舞います。

そのため、市場の運営者たる企業であっても、完全にはユーザーの動きを制御することは不可能です。そして、一度市場が動き始めてしまうと方向の修正が非常に困難な代物です。

また、国内であるとマーケットプレイス型プロダクトに関する情報は数が少なく、難易度に対して、ノウハウが公開されていないことが実情ではないでしょうか。

そこで、「マーケットプレイス型プロダクトが直面する、夜も眠れない3つの課題」と題して、クラウドワークスを実例に、過去そして現在もなお直面している3つの課題を紹介していきたいと思います。最後の見出しに「情報源」に関するおまけも書きました。

夜も眠れない3つの課題とは?

ずばり、以下の3つです。

1a636f9a-5508-4700-1698-dba9fdf91917.png

  • 市場の厚み
  • 混雑の解消
  • 安心・安全な取引

それぞれについて、「とは?」「クラウドワークスの場合」といった形で話を進めていこうと思います。

ざっくりイメージとしては、以下の通りです。

9cd9a1eb-5a95-6a0e-6f0a-d630edc5a4d7.png

  • 市場のユーザー数が増えることで(市場の厚み)
  • 情報の探索コストの増加、マッチングの非効率が発生するとともに(混雑の解消)
  • 市場をハックして悪用してくるユーザーが現れる(安心・安全な取引)

では、行きます。

1.市場の厚み 〜鶏と卵とレモン〜

b28477e0-cc86-048b-71d3-a122f7c3ab2a.png

1-1.市場の厚みとは?

市場が成立するためには、

  • 需要を満たす供給
  • 供給を満たす需要

が必要です。

この需給のバランスを満たすことで、

  • より多くの売り手が、より多くの買い手にアクセスできる
  • より多くの買い手が、より多くの売り手にアクセスできる

ようになり、

  • より多くの売り手がいるところに、買い手が集まる
  • より多くの買い手がいるところに、売り手が集まる

といった好循環が生まれ、市場に「厚み」が生まれます。

「需給のバランスを満たす」と簡単に書いたものの、これがマーケットプレイス型プロダクトにおける最初にして最大の関門 です。

関門たる所以は2つ。

まず、所謂「鶏が先か、卵が先か」の問題。
「需要が無いところに、供給は起こらない」「供給が無いところに、需要は起こらない」ため、(多くの場合)人力で鶏(需要)か卵(供給)かのどちらかを集めてくる必要があります。

次に「レモン市場」の問題。
仮に「鶏を先に集めるぜ!」と決めて、量だけ集めたとしても、質が伴わない場合、所謂レモン市場に陥ります。詳細は後ほど触れます。

1-2.鶏と卵の問題とクラウドワークス

クラウドワークスで置き換えるなら以下の通りです。

  • 仕事があるから、ワーカーが集まる
  • ワーカーがいるから、仕事が集まる

結論として、 クラウドワークスの場合「ワーカーがいるから仕事が集まる」 と考え、プロダクト開始当時はワーカーを「フリーランスエンジニア」に絞って集客を開始しました。

理由としては、プロダクト開始当時は2011年であり、現在と同様にエンジニアは売り手市場でした。そこで、初期では「フリーランスのエンジニアの集客を優先的に行い、仕事は後から付いてくる」という考え方でした。

結果的に、「フリーランスエンジニア」という市場に「厚み」が生まれ、「働く」というドメインをコアにしながら、在宅ワーカー→副業サラリーマンと市場を広げています。

1-3.レモン市場とクラウドワークス

まず、レモン市場とは、以下の通りです。

経済学において、財やサービスの品質が買い手にとって未知であるために、不良品ばかりが出回ってしまう市場のことである。

引用:レモン市場 - Wikipedia

クラウドワークスで置き換えるなら以下の通りです。

0b1b5cee-8e35-0b95-dcca-aca9f6e11a78.png

  • 誰が良いワーカーであるか分からないため、クライアントは高額な仕事依頼を避ける
  • どれが良い仕事であるか分からないため、ワーカーは積極的な応募や契約を避ける

つまり、「取引相手のことが分からない」 がために、良いワーカーや仕事が市場から去ってしまうような状態の市場のことです。

対策としては、「情報の非対称を無くす」こと であり、まずは以下のような状態にまで持っていく必要があります。

  • 誰が良いワーカーであるのか、クライアントが分かる
  • どれが良い仕事であるのか、ワーカーが分かる

クラウドワークスをより健全な市場として成長させるためには、市場への参加者を増やすという「量」の観点だけではなく、情報という「質」の観点とのバランスがポイントです。

特に、クラウドワークスの場合、後者の課題解決がまだまだ十分ではなく、目下取り組んでいます。

2.混雑の解消 〜探索コストとマッチング効率〜

d484d039-f79b-e6b3-b772-a01034ef5246.png

2-1.混雑の解消とは?

厚みを獲得した市場は、混雑を引き起こします。

混雑には種類があり、主に以下の通りです。

  • 情報が多くなったことで、探索コストが高くなる
  • 一部の売り手(買い手)に、買い手(売り手)が集中し、マッチング効率が悪化する

具体例で説明した方が良いと思うので、早速クラウドワークスを例に説明したいと思います。

2-2.混雑による「探索コスト」の高まり

まず、「情報が多くなったことで、探索コストが高くなる」ことに関して。

クラウドワークスでは常時、数多くのワーカーと仕事が市場に存在しており、取引を行っています。

以前であれば、今よりも数がもっと少なかったため、

  • ワーカーは、自分に合う仕事を探すことができる
  • クライアントは、仕事に合うワーカーを探すことができる

と、多少使い勝手が悪くとも、探索コストが低く済む状態でした。

しかし、市場が厚みを獲得することで、市場に存在するワーカーと仕事が増えるため、探索コストが高まります。

e68a6383-83b8-8c88-f76d-107b5b6ee017.png

ワーカーとしては「報酬を得ること」ことが目的です。仕事の探索コストが高まってしまうと、トータルなコスト(仕事をするために必要なあらゆるコスト=取引コスト)で見た際に、費用対効果が悪いように感じ、市場から離脱をしてしまいます。

2-3.混雑による「マッチング効率」の悪化

そして、もう1つ悩ましい問題が「一部の売り手(買い手)に、買い手(売り手)が集中し、マッチング効率が悪化する」ことです。

クラウドワークスであれば以下のような事象です。

  • 一部の人気の仕事に、ワーカーの応募が集中する
  • 一部の人気のワーカーに、スカウトが集中する

結果的に何が起こるかと言うと、ワーカー視点では以下の通りです。

bf015eae-24fb-6e38-ca77-2ad9e004b0f9.png

  • 仕事の探索コストが高いことで
  • 他にも良い仕事があるにも関わらず
  • 一部の良い仕事しか見つけることができないので
  • その仕事に応募が集中することで契約倍率が高まり
  • 結果として契約を得ることができないワーカーが数多く発生し
  • そのまま離脱してしまう

2-4.情報の整理と見せ方で対策

市場の厚みを獲得した結果、混雑を引き起こしてしまい、クライアントやワーカーの離脱を促すような構造となってしまいます。

対策としては「情報の整理」と「情報の見せ方」です。

2-4-1.情報の整理

まず、「市場が厚みを獲得したことで、ワーカーや仕事が増え、探索コストが高まる」というのは 「市場に情報が無秩序に溢れてしまっている」と考えることができます。

したがって、まずはその情報を整理する必要があります。その整理を怠ると、上述のようなレモン市場に陥ったり、探索コストが高まったりします。

そして、情報の整理ができると、以下のような施策を打つことができます。

  • 高精度なキーワード検索
  • 条件フィルタリング
  • 協調フィルタリング

クラウドワークスでは、まだ全てに取り組むことができているわけではありませんが、「混雑」という課題に対しては「探索コストを押し下げる」ことは有効ではないでしょうか。

2-4-2.情報の見せ方

情報の整理をし、探索コストを押し下げることができると、ワーカー視点では、より素早く自分に合う仕事を見つけることができます。

しかし、これでは「一部の人気の仕事に、ワーカーの応募が集中する」という問題を解決できていません。

この問題を解決する際に、 重要なポイントは「ワーカーにとって価値の高い仕事とは何か」を明らかにすること です。

まず、仕事の価値の高低はワーカーによって異なるため、「仕事への応募」の原資となる「仕事の閲覧」に関係する「仕事一覧画面」では、各々の嗜好性を反映させた結果順に表示をさせたいところです。

次に、もう1つ重要なのは「ワーカーにとって、応募が集まっている仕事は価値が低減している」と考えることです。なぜなら、ワーカーの目的は「報酬を得ること」ことであり、応募が少ない、つまり契約倍率が低い仕事の方が、契約に至ることができる確率が高まり、その結果として報酬を得ることができるので、価値が高いと考えられます。

したがって、まとめると以下の通りです。

16cc9fac-fb7d-14fd-bd0c-e08cd08b2eff.png

  • まずは「嗜好性」に合う仕事一覧画面を表示したい
  • 次に「契約倍率」といった条件をバランスさせる
  • その上で、仕事一覧画面の表示→閲覧→応募をワーカーにしてもらうことで
  • 「一部の人気の仕事に、ワーカーの応募が集中する」という問題の解決に近づける

クラウドワークスでは、仕事一覧画面の科学に取り組んでおり、試行錯誤を繰り返しています。

3.安心・安全な取引 〜悪用ユーザーとの闘い〜

233c279c-4ecb-8c4f-36ff-27449b4e0905.png

3-1.安心・安全な取引とは?

市場が厚みを獲得し、混雑をしながらも拡大をしていくと、市場を悪用しようとするユーザーが現れます。

具体例は後述しますが、 取り締まりをせず悪用ユーザーが市場を跋扈してしまうと、市場で安心・安全な取引をすることがままならなく なり、真っ当なユーザーは市場を離脱してしまいます。

悪用ユーザーはあの手この手で市場に対するハックを仕掛けてくるため、運営者としても継続的な対応が必要となります。

3-2.安心・安全な取引とクラウドワークス

クラウドワークスの場合、クライアントが悪用ユーザーとして不正な仕事を投稿することです。不正な仕事とは具体的に以下の通りです。

  • MLMへの勧誘を目的としたもの
  • 自社メディアに誘導し、広告の誤クリックを誘うもの
  • その他、お仕事ガイドラインに違反するもの

クラウドワークスでは目視チェックでこういった不正な仕事への対処を行っていましたが、さすがに増え続ける不正な仕事に対して、人力での対処では追いつかなくなってしまいました。

そこで、メールのスパム判定に用いられる「ベイジアンフィルタ」を活用した検知システムの構築をすることで対策を講じました。

技術的な内容は専門では無いため割愛しますが、以下のような大きな成果を得ることができました。

  • 検出精度91%(当時)
  • 不正な仕事を最盛期の12%まで削減

具体的な内容を知りたい方は、ぜひ以下をご覧ください。

しかし、まだまだクラウドワークス上で不正な案件がユーザーの目に触れてしまっていることは事実で、あの手この手でハックを仕掛けられています。

わずかではあるものの、影響力を世の中に対して持ち始めた市場であるからこそ、ベースとして大事になるのは安心・安全に取引ができること。「ここまで対策すれば大丈夫だろう」という発想ではなく、継続的に粘り強くクラウドワークスでは取り組んでいます。

複雑さこそが「やりがい」であり「成長の伸び代」

「夜も眠れない3つの課題」と題して、以下3つの課題をクラウドワークスの文脈に乗せてお話ししました。

  • 市場の厚み
  • 混雑の解消
  • 安心・安全な取引

恐らく、マーケットプレイス型のプロダクトに取り組んでいる方であれば、共感していただける部分もあったかなと思います。

最後に、 これら3つの課題はお互いに密接に結びついており、状況変化に応じて、生き物のように動き続けています。

そのため、「現状がどうなっているのか」を把握するだけで一苦労です。そして、仮に把握できたとしても、互いが密接に結びついているため、「どこかを動かせば、どこかも動く」状態となっており、微妙なバランスを取るような打ち手を考えることも一苦労です。

しかし、この複雑さこそが運営者としてのやりがいであり、解決した際の大きな成長の伸び代に繋がると確信しています。

まだまだ未熟ではありますが「人々の働き方を変える」そんな世界を夢見て、真摯に運営を続けていきたいと思います。

長文となりましたが、ご覧いただきありがとうございました。
明日は@tmc28が「GASへの愛」を語ります。引き続きCrowdWorks Advent Calendar 2018をお楽しみください。

おまけ:マーケットプレイス型プロダクトの情報源

cc9db874-276e-ae2e-fde0-ce3b8bb3efec.png

最後に、おまけとしてマーケットプレイス型プロダクトのことを調べる際に、参考とした情報源をまとめておきたいと思います。

他にもおすすめがありましたら、ぜひコメントください!&積極的に情報交換したいなと思っています。

書籍編

まずは、おすすめの書籍です。

Who Gets What (フー・ゲッツ・ホワット) ―マッチメイキングとマーケットデザインの新しい経済学

このエントリで出てきた「夜も眠れない3つの課題」は、決して私が独自で考えたものではなく、マーケットデザインという分野で頻出するものです。このWho Gets Whatという書籍はノーベル経済学賞を受賞したロス氏による書籍で、現実の臓器移植や学校選択といった実例を引き合いに出しながら「マッチメイキング」という切り口で学ぶことができます。

最新プラットフォーム戦略 マッチメイカー

マーケットプレイス型プロダクトのモデルに関してかなり体系的に学ぶことができます。守破離の「守」、型を身につけるためにはもってこいの一冊だと思います。

プラットフォーム革命

「最新プラットフォーム戦略 マッチメイカー」と同じく、体系的に学べます。また、個人的には、書籍内で紹介されている「ネットワーク効果のはしご」という「いかにネットワーク効果を構築するか」といった考え方が非常に面白かったです。「ネットワーク効果」と言えば、以下の「資料編」でも紹介するような資料でもよく学ぶことができます。

資料編

次に、おすすめの資料です。

マーケットプレイス・ガイドブック

まさにガイドブックで、マーケットプレイス型プロダクトの指南書のような位置付けの資料。ビジネス的なノウハウも合わせて、学ぶことができます。

The Network Effects Bible

分かるようで分からないネットワーク効果に関して、非常に体系的にまとめられています。特に、「ネットワーク効果には13の種類ある」という整理の仕方は俊逸で、BEENEXTの前田ヒロ氏が日本語で参入障壁を生み出す「13種類のネットワーク効果」とまとめてくれています。

All about Network Effects

シリコンバレーを代表するVCの1つであるAndreessen Horowitzのネットワーク効果に関する資料です。FacebookやAirbnb、Mediumなどの企業事例に、どのようにネットワーク効果を構築したのかを学ぶことができます。

ブログ編

最後に、おすすめ記事です。

Work Hard!

フリル(現・ラクマ)の創業者である堀井翔太氏のブログです。非常に実践的かつリアルなノウハウを学ぶことができます。ちなみに、前田ヒロ氏のブログで公開された堀井氏のPodcastマーケットプレイスの立ち上げと拡大方法、そしてネットワーク効果の考え方。〜 Fablic 堀井 翔太は必見です。

Four Questions Every Marketplace Startup Should Be Able to Answer

Mediumに投稿されていたAirbnbのPMを勤めていたJonathan Golden氏の記事です。この記事では「ネットワーク効果には13の種類ある」という切り口とは別に「密度の高いネットワーク効果」と「グローバルなネットワーク効果」という観点での話が面白かったです。

また、Mediumではマーケットプレイス型プロダクトに関する記事がいくつもあり、以下はおすすめです。

Required reading for marketplace startups: The 20 best essays

最後に紹介するのは、Uberでグロースハッカーとして活躍し、現在はAndreessen Horowitzに所属しているAndrew Chen氏のブログです。上記でブログをいくつか紹介しましたが、ここで紹介されている記事を一旦読んでおけば間違いないかと思います。

青春GAS野郎は作業効率化の夢しか見ない

はじめまして。クラウドワークスの西村と申します。
iOS/Androidアプリと、ワーカー検索周りのPOをやっています。

クラウドワークス Advent Calendar 2018 の21日目になりました。
昨日の @kokkokokouya による 【夜も眠れない】マーケットプレイス型プロダクトが直面する3つの課題に引き続き、非エンジニアによるアドベントカレンダーが続いております。

今回は、POになる以前から扱っていたGoogle Apps Script(以下、GAS)を久々に触ったところ愛が止まらなくなってしまったので、情熱の赴くままにこのテーマでブログを書くことにしました。
(途中で愛が醒めかけて、執筆直前にGAS書くお仕事をなんとか取ってきて愛を取り戻したのはここだけの話にさせてください)

POになる前まではCWコンシェルジュやコンサルティンググループに所属していましたが、エンジニアが一人もいなかったので、クラウドワークスを使う企業様向けのコンサルティングや新規サービスのフィジビリティ、チーム内の業務効率化などなどあらゆるところでGASを活用していました。

自分自身は開発の業務経験は一切なく、高校生の頃にモバスペというHP作成サイトでHTMLもどきを書いて自分のブログをカスタマイズしていた程度です(しかもガラケーで!!!)。1
よって、非エンジニアという立場からGASの魅力と、GASを活用してみたい人のためのアイディア集をお届けしたいと思います。

GASのどこが愛おしいのか

1. 開発環境構築がいらない

これ、ほんとでかい。
Googleアカウントさえあれば始められるのです。
これだけで始めるハードルがひとつなくなりますね。

2. ドキュメントが豊富

公式APIドキュメントも充実していますが、日本語の記事やブログも検索してみるとたくさんヒットします。

私がGASを触り始めた時には、先に当時の上司がある程度書いてくれたコードを修正、カスタマイズするところから始めました。
なかなか上司に質問できる機会も多くなかったので、よくわからないところは自分でググるしかなかったのですが、非常に助かったのを覚えています。
ググるついでに新しいライブラリを見つけたときには機能追加してみたり、自分のスキルも広がったと思います。

3. Googleのアプリケーションに関わることは基本なんでもできる

非エンジニアで業務改善に課題を抱いている方々から「GASって何ができるの?」とよく聞かれます。
そんな時私は「Googleのアプリケーションに関わることは、なんでもできる」と答えるのです。

なんでもできすぎて、何ができるかイメージできないと思いますので、それに関してはこの後でアイディアをいくつか提案したいと思います。

GASを始めたい人にありがちなこと

「GAS教えて欲しい!」とよく言われるものの「こういうことを自動化したい!」が明確になっていなくてどう教えるのがいいか悩ましい経験が多々ありました。
詳しく聞いてみると、往往にして「それ、スプレッドシートのimportrange関数でできるよ...」みたいなことも多く。。。

結構ありがちなんですが、このツールが良さそう、このツールを使えば世界が変わりそう、というツールありきで考えるより、実現したいことベースで手段を考えるのが大事なんじゃないかなと思います!

これからGASによる業務効率化を始めてみたい方は、まずは「こういうことを自動化したい」ところから考えてみることをおすすめします。

GASでできることアイディア

PO以前の業務経験をもとに、営業やコーポレート系職種の方々にありがちな作業の中でこんなものが自動化できるよ、という例をいくつか挙げてみたいと思います。
是非みなさんも、自分の業務で自動化できる作業はないか考えながら読んでみてください。

Googleスプレッドシートで管理されている売上進捗をGmailで通知する

毎朝スプレッドシートを開くのでもいいかもしれませんが、普段一番使っているツールで通知が届くとより便利ですよね。
この機能では、「いちいちスプレッドシートを開かなければならない」手間を削減しています。

我々はSlackに通知することが多いですが、ここではメールで通知すると想定してみます。
※メール送信は短時間に大量に送ると迷惑メール送ってる輩だとGoogle先生に認識されて怒られてしまいますので(メール送信に限らずですが)常識の範囲内で使うようにしましょうね!!!

sample.gs
function sendKPIMail() {
  //KPIが記録されているシートとセルを指定し、値を取得する
  var ss = SpreadsheetApp.getActiveSheet();
  var value = ss.getRange("b2").getValue(); 

  var today = Moment.moment().format("YYYY年M月D日"); 

  var to = "hogehoge@gmail.com"; //送り先アドレス。gmail以外でも可
  var subject = today + "[テスト]KPI通知メール";//メールの件名
  var body = today + "の売り上げ報告:" + value + "万円" ;//メールの本文

  //メールを送信する
  MailApp.sendEmail(to, subject, body);
}

todayを定義している箇所は違う書き方もあるのですが今回はラクをするために、Moment.jsというライブラリを入れてます。
おすすめです。
:link: 日付&時刻の便利ライブラリ「Moment.js」をGoogle Apps Scriptで使う方法
:link: GAS版Moment.jsライブラリで超簡単に日時の比較をする方法

Utilities.formatDate(date, timeZone, format) という、Google先生が用意してくれている日付フォーマットのメソッドもあるのですが、これはあくまでも文字列に変換するだけなので、日時比較をしたい場合などはやはりMoment.jsが便利だなと思います。

ライブラリには他にもいろんなものがあるみたいですので、積極的に使ってみるとまた一段上のGAS体験ができます。

Gmailで届いたCSVデータをスプレッドシートに貼り付ける

これも、自動化できます。

この場合は
・Gmailで届いたCSVをダウンロードする
・CSVを展開する
・特定のスプレッドシートにコピペする
という作業が発生しています。これらの工数を全て削減することができます。

ただのコピペ作業を手作業でやるのは時間の無駄ですし、 機械に任せた方がエラーが少ない です。

sample.gs
var SEARCH_TERM = "subject:通知メール"; //Gmailフォルダの検索条件
var FOLDER_ID = "hogehoge"; //添付されたCSVを格納するフォルダ

var SS = SpreadsheetApp.getActiveSpreadsheet(); //転記先のSpread Sheetを定義


//添付ファイルの取得
function fetchFile(){

  var myThreads = GmailApp.search(SEARCH_TERM, 0, 1); //条件にマッチしたスレッドを検索して取得
  var myMessages = GmailApp.getMessagesForThreads(myThreads); //スレッドからメールを取得し二次元配列で格納


  for(var i in myMessages){
    for(var j in myMessages[i]){

      var attachments = myMessages[i][j].getAttachments(); //添付ファイルを取得

      if(attachments != ""){
        for(var k in attachments){
          var attachmentsName = attachments[k].getName();

          //同名のファイルがなければフォルダに保存する
          saveToGmailFolder(attachments);

          //フォルダからCSVファイルを読み込み、スプレッドシートに書き込む
          import(attachmentsName);
        }
      }
    }
  }
}

//保存したいフォルダに、nameと同名のファイルがあるか確認する
function isInGmailFolder(name) {

  //nameと同名のファイルをGoogleDrive全体から探す
  var files = DriveApp.getFilesByName(name);


  //ヒットしたファイルの親のフォルダが保存したいフォルダと同じ場合はtrueを返す
  while (files.hasNext()) {
    var file = files.next();
    var folders = file.getParents();

    while (folders.hasNext()) {
      var folder = folders.next();
      if (folder.getId() === FOLDER_ID) {
        return true;
      }
    }

  }

  /*
    同名のファイルがGoogleDrive全体から見つからない場合や
    保存したいフォルダには同名のファイルが存在しない場合にはfalseを返す
   */
  return false;
}



//添付ファイルを指定のフォルダに保存する(添付ファイルが複数ある場合を想定)
function saveToGmailFolder(attachments) {

  //保存先フォルダを指定
  var folder = DriveApp.getFolderById(FOLDER_ID);

  //同名のファイルがすでに保存されていなければ、フォルダにファイルを保存する
  for (var i = 0; i < attachments.length; i++) {
    if (!isInGmailFolder(attachments[i].getName())) {
      var data = DriveApp.createFile(attachments[i]);
      folder.addFile(data);
    }
  }
}



//CSVファイルの内容をスプレッドシートに転記する
function import(attachmentsName) {

  // 対象のCSVファイルのファイル名と置かれているフォルダを定義
  var fileName = attachmentsName;
  var folder = DriveApp.getFolderById(FOLDER_ID);


  //フォルダとファイルの検索
  var files = DriveApp.getFilesByName(fileName);
  while (files.hasNext()) {
    var file = files.next();
    if (file.getName() == fileName) {

      //文字化け対策
      var data = file.getBlob().getDataAsString("Shift_JIS"); 


      var csv = Utilities.parseCsv(data);

      var sheetName = fileName

      //同名のシートが存在しているかどうか確認
      if( !isInSpreadsheet(sheetName)){

        //転記するシートを新たに追加する
        insert(sheetName);
        var sh = SS.getSheetByName(sheetName);

        //↑で作ったシートに、セルA1からCSVの内容を書き込んでいく
        sh.getRange(1,1,csv.length,csv[0].length).setValues(csv);
        return;
      }
    }
  }

}


//テンプレートシートを複製し、シート名を特定のsheetNameにする
function insert(sheetName){
  var templateSheet = SS.getSheetByName("シート1");
  SS.insertSheet(sheetName, {template: templateSheet});
}


//スプレッドシートに、同名のシートがすでに存在しているかどうか確認する
function isInSpreadsheet(sheetName){

  var sheets = SS.getSheets();

  //スプレッドシート内にあるシート数をカウント
  for( var i in sheets){
    i++;
  }


  for(var v=0; v < i; v++){

    if( sheets[v].getName() === sheetName ){

      //同名のシートがすでに作られていれば、trueを返す
      return true;
    } 
  }
  //同名のシートが作られていなければfalseを返す
  return false;
}

結構魔改造感ある...

:link: Gmailの添付ファイルをGoogleドライブに自動保存する
このスクリプトは、こちらの記事を参考に作りました。

営業メンバーから、オペレーションを回すコンサルタントへ商談情報を引き継ぐ

営業メンバー→コンサルタントに商談内容を引き継ぎ、即座にコンサルタントの対応を開始する、という状況を想定しています。

弊社ではGoogleフォーム、Googleスプレッドシート、Trelloを用いてフローを構築しました。

この機能では、人から人への情報連携をスムーズにすることのほかにも、クライアントの対応進捗状況を可視化するためのTrello登録作業が自動化される、という効果もありました。

実際の流れは以下のようになっていました。

  1. 商談終了次第、営業メンバーがGoogleフォームに用件を入力し送信する
  2. フォームの入力内容がSlackチャンネル上に通知され、同時にTrelloに新規カードが作られる
  3. コンサルタントが即座にクライアントに連絡し対応を開始する
  4. 以降の対応ステータスはTrello上で管理される

2番のところが自動化ポイントですね。
※実際のコードは大人の事情もあり今回は割愛させていただきます。

TrelloAPIが鬼のように難しかったので結構つらかったことを記憶していますw

参考

【保存版】初心者向け実務で使えるGoogle Apps Script完全マニュアル
こちらのサイトはかなり網羅的に書かれているので、本当に初めての方は上から順番にやってみるといいかも。

GASで文字コード指定してファイルを書き出す
CSVを読み込むなどテキストを処理する際に、文字化けしたり改行がちゃんと認識されなかったりすることがあります。

日付&時刻の便利ライブラリ「Moment.js」をGoogle Apps Scriptで使う方法
GAS版Moment.jsライブラリで超簡単に日時の比較をする方法
日時のフォーマットはよく使いますが、特に日時の差分をとりたい場合などはこれ入れとくと便利です。

formatDate(date, timeZone, format)
単純に日時を文字列に変換したい場合にはこちらの公式メソッドで十分です。

Gmail で使用できる検索演算子
Gmailでメール検索する際の検索方法は、GmailApp.search()でメール検索するときの検索クエリとしてそのまま使えます。

おわりに

営業組織やコーポレート系部署には専属のエンジニアがいないことも多いと思うのですが、GASを使うことで非エンジニアでも業務の自動化・効率化ができます。
無駄だなあと感じる作業のほとんどはGASで解決できるかもしれません。

初めたばかりの方は、最初から自分でGASを書こうとすると心が折れる可能性もありますので、まずはインターネット上のあちこちに転がっているスクリプトをコピペしカスタマイズするところから始めることをおすすめします!

ちなみに、「青春ブタ野郎はバニーガール先輩の夢を見ない」はまだ観てないのですがこんなタイトルにしてしまったのでこの年末年始の宿題にしようと思っています。


  1. あの頃モバスペを触っていた女子高生たちはみんなHTML/CSSに抵抗ないんじゃないかななんて仮説を持っています。 

ビジネス・デザイン・テクノロジーが融合した、他職種連携の話

クラウドワークスマーケティングチームの玉木です。
クラウドワークス Advent Calendar 2018 の22日目となる本日は「他職種連携」のお話をします。

この話をする理由

クラウドワークスでは半年に一回、全社でキックオフが行われます。
前回のキックオフの際に、他職種連携をテーマに掲げたグロースハック部が表彰をされました。
そこでグロースハック部がどんな動きをし、表彰されるまでに至ったのかを書きたいと思っています。

※弊社ではユーザーグロースという名前でしたが、世の中に浸透しているであろうグロースハックという名前を本記事では使います。

他職種連携とは

他職種連携とは、 ビジネス・デザイン・テクノロジーの三位一体となり、相互扶助をすること です。
他職種連携.png

職種は、マーケ・データ分析・デザイナー・エンジニアが該当します。

クラウドワークスでは、2018年4月から "他職種連携" を掲げたグロースハック部が発足し、約半年間活動をしていました。

なぜ発足をしたのか

発足した理由を一言でまとめるとこんな感じです。

ビジネス職とデザイナー職・エンジニア職の人が一緒の方向を向いて仕事をし、事業の成長スピードを加速させたい。

では、他職種連携をする前は、どんな課題があったのかを次にお話しようと思います。
私はマーケティングを担当しているので、マーケターから見た「Only ビジネスチーム」をあるある形式で事例をお話します。

Only ビジネスチームのあるある

Onlyビジネスチームの課題を事例でお話します。

fusagikomu_businesswoman.png

1. SEO改善をやりたいけど、ユーザービリティを損ねる

SEO担当者なら一度は以下のような経験があるかもしれません。

Googleの検索流入を増やすために、順位をあげたい。順位を上げるためには、コンテンツを増やさなければいけない。

「そうだ、コンテンツを増やせばいいんだ!」と安易な考えをすると、ユーザービリティを毀損してしまいます。

そのため、SEOの施策ができない。しかし、現状のままでは検索流入が増えず、集客面での課題も残る。

こんな悩みを抱えたことが私もあります。

2. 広告クリエイティブの制作が難航する

広告担当者にとってクリエイティブ作成は死活問題です。
特にSNS系の広告の運用には、常に新たなクリエイティブが求められます。

そんな中で、もちろんクオリティが高いものはつくりたいけど、それ以上に数とスピードが重要。
何が一番刺さるメッセージなの?と聞かれても、わかんないから試すんだよ!というかそれもいろいろ必要なんだよ!と議論は平行線に。
かといって、外注するとクオリティやメッセージ性に問題があるんじゃない?と議論が起きたり・・・。

お互いにいいものを作りたいという思いは同じなはずなのに、なぜかいがみ合ったり、バナー作成は社内で誰もやりたがらない作業になっている、なんてことはないでしょうか。

3. 施策をやりたいけど、エンジニア・デザイナーに助けを求めにくい

弊社のマーケターは、ユーザーの集客だけではなく、サービスの改善の役割も担っています。
データから改善するアイデアをだし、施策を検討するのもマーケターの仕事です。
しかし、サービスの改善となると、エンジニアやデザイナーの力が必須です。

グロースハック部が誕生する前は、エンジニア・デザイナーとは別のチームでした。

そんな中、エンジニア・デザイナーの力を借りる際に、 コミュニケーションのコストが重くなったり、マーケターが考えた施策の優先順位が下がってしまう こともありました。

グロースハック部誕生

改めてクラウドワークスのグロースハック部は、ビジネス(マーケティング&データ分析)とデザインとテクノロジーが一体となった部です。

グロースハック部として、どのように動いていったのかを簡単に紹介します。

1. KGI/KPIの数字の認識を揃える

■どんな状態だったか

  • なんのために存在しているチームなのかの認識が揃っていない
  • 重要視したい指標がバラバラで本日的な議論ができない

■やったこと

  • 業績から逆算したKPIツリーの作成する
  • 定期的にチームで集まり、数字の振り返りを行う

■その結果どうなったか

  • 相互理解が進み、困っていることや悩んでいることが他職種間で伝わりやすくなる
  • チームの目標となる数字の意識が揃っているために、他職種間で本質的な議論ができる

2. 施策の検討から他職種間で議論をする

■どんな状態だったか

  • ビジネスチームは企画と設計をするだけ。デザイン・エンジニアチームに検討している施策を依頼する。という状態
  • ビジネスチームのみで検討をするので、「本当にユーザーの課題を解決する施策なのか」という不安が残ったまま施策を動かす状態

■やったこと

  • 施策を他職種間で議論をしながら検討する
  • 施策のKPI設計をデータ分析チームと合同で行う

■どうなったか

  • 自分の職種とは違った観点でアドバイスをもらえるために、施策の精度が上がる
  • 他職種間で設計から議論をしたために、コミュニケーションのロスやコストが軽減され、施策のリリースまでのスピードが上がる
  • データ分析チームが設計したKPIにより、リリースした施策に対して最適な改善ができる

Onlyビジネスチームの変化

先ほどご紹介した、Only ビジネスチームのあるある の変化を紹介します。

1. ユーザービリティを意識したSEO改善ができる

SEOは ユーザーの検索意図が考えられたコンテンツを提供し、サイト内のユーザー体験(UX)を追求 していかなければなりません。

直近のGoogleアルゴリズムの変動は、より上記の特性が現れています。
そんな状況下の中、SEO改善は、SEO担当者だけで最適化できるのでしょうか?

グロースハック部は、ユーザーの検索意図に答えるコンテンツとサイト内でのユーザー体験を意識した改善施策を実施しています。

今では、 SEOとUXは切っても切り離せない関係性にあるのです。

2. 広告クリエイティブのクオリティ・スピードが上がる

広告のPDCAを回すためには、クリエイティブの数とスピードが重要です。しかし、社内のデザイナーに制作してもらうには負担が大きい業務でした。

そこでグロースハック部は、ユーザーに届けたい訴求とクリエティブの構成だけデザイナーに助けてもらっています。
できあがった構成を外注し、デザイナーにフィードバックをもらいながら修正し、クリエイティブが完成します。

デザイナーの力を借りながら、クオリティとスピード、数を意識したクリエイティブが完成し、広告の最適化が実現できます。

3. デザイナーとエンジニアに助けを求めやすい

施策の立案はビジネスサイドの人間だけで考えることはできても、他職種の意見や考えを取り入れなければ ユーザーの求めている価値として本当に最適な施策なのか という点に不安が残ります。

「2. 施策の検討から他職種間で議論をする」 に記載をしましたが、他職種間で施策の設計から議論をすることにより、施策の最適化やリリースまでのスピード、リリース後の改善を実施することができました。

43226028_1180632528741430_6680108492558172160_o.jpg

最後に

私が感じた感想としては、やりたい施策を他職種の観点でアドバイスが聞けること、施策を動かす時の連携が取りやすくなったことがメリットして大きかったと感じています。
Only ビジネスのチームでは進めづらかった施策もこの半年間でスピード感を持って進めることができました。

今後は、クラウドワークス全体で他職種連携ができたらいいなーと思っています。


次回のクラウドワークス Advent Calendar 2018 もぜひご覧ください!

Railsで「ViewからカジュアルにSQLを叩いている」ところを列挙する

まえおき

そこそこ大規模なRailsアプリを触っているといろんなモデルの状態を参照する魔のpartialを目にすることが稀によくあります。

Slice.png

_employees.html.erb というファイル名なので、Employeeを表示するだけ……かとおもいきや、評価だったりプロフィールだったり契約数だったり、たくさんのモデルを参照して頑張って表示してる!みたいなやつ。

一覧画面ではN+1問題がワンチャン起きてて、呼び元も呼び先もまぁまぁ規模があったりすると

EmployeesController#index
 employees/index.html.erb
 → employees/_search_result.html.erb (600行くらい)
  → each { shared_partials/_employee.erb } (300行くらい)
   →その先で読んでいる EmployeeHelperのメソッドたち... (200行くらい)

結局なにをpreload/eager_loadすればN+1が回避できるの???ってのを調べるのにかなり苦慮します。
(というか、ここ1ヶ月くらいそれでかなり苦慮していました :sweat_smile:

その際に「ViewからカジュアルにSQLを叩いている場所を列挙する」っていうことをやると、割と効率的に調べることができたので、なんとなくその方法を書いておきます。

「おー、これは役に立つわ!」って人は多分ほとんど居ない気がするけど、Railsでこんなことできるんだ、くらいで思っていただければ幸いです。

シンプルな例で説明する

魔のパーシャルはサンプルコードを書くのもつらいので、おもちゃみたいなシンプルな例で説明します。

N+1を起こす例

モデル
class User < ApplicationRecord
  has_one :profile
end

class Profile < ApplicationRecord
  belongs_to :user

  def full_name
    "#{first_name} #{last_name}"
  end
end
app/controllers/users_controller.rb
class UsersController < ApplicationController
  def index
    @users = User.order(created_at: :desc)
  end
end
app/views/users/index.html.erb
<% if @users.present? %>
<ul>
<%= render partial: 'shared_partials/user', collection: @users %>
</ul>
<% end %>
app/views/shared_partials/_user.html.erb
<li><%= user.profile.display_name %></li>

image.png

イメージ、こんな感じで名前をずらーーっと出すだけ。

Railsコンソールを見てみると

Processing by UsersController#index as HTML
  User Load (0.4ms)  SELECT `users`.* FROM `users`  ORDER BY `users`.`created_at` DESC
  Profile Load (0.3ms)  SELECT  `profiles`.* FROM `profiles` WHERE `profiles`.`user_id` = 20 LIMIT 1
  Profile Load (0.2ms)  SELECT  `profiles`.* FROM `profiles` WHERE `profiles`.`user_id` = 12 LIMIT 1
  Profile Load (0.2ms)  SELECT  `profiles`.* FROM `profiles` WHERE `profiles`.`user_id` = 13 LIMIT 1
  Profile Load (0.2ms)  SELECT  `profiles`.* FROM `profiles` WHERE `profiles`.`user_id` = 14 LIMIT 1
  Profile Load (1.3ms)  SELECT  `profiles`.* FROM `profiles` WHERE `profiles`.`user_id` = 15 LIMIT 1
  Profile Load (0.3ms)  SELECT  `profiles`.* FROM `profiles` WHERE `profiles`.`user_id` = 16 LIMIT 1
  Profile Load (0.3ms)  SELECT  `profiles`.* FROM `profiles` WHERE `profiles`.`user_id` = 17 LIMIT 1
  Profile Load (0.2ms)  SELECT  `profiles`.* FROM `profiles` WHERE `profiles`.`user_id` = 18 LIMIT 1
  Profile Load (0.3ms)  SELECT  `profiles`.* FROM `profiles` WHERE `profiles`.`user_id` = 19 LIMIT 1
  Profile Load (0.2ms)  SELECT  `profiles`.* FROM `profiles` WHERE `profiles`.`user_id` = 11 LIMIT 1
  Profile Load (0.1ms)  SELECT  `profiles`.* FROM `profiles` WHERE `profiles`.`user_id` = 10 LIMIT 1
  Profile Load (0.1ms)  SELECT  `profiles`.* FROM `profiles` WHERE `profiles`.`user_id` = 9 LIMIT 1
  Profile Load (0.3ms)  SELECT  `profiles`.* FROM `profiles` WHERE `profiles`.`user_id` = 8 LIMIT 1
  Profile Load (0.3ms)  SELECT  `profiles`.* FROM `profiles` WHERE `profiles`.`user_id` = 7 LIMIT 1
  Profile Load (0.2ms)  SELECT  `profiles`.* FROM `profiles` WHERE `profiles`.`user_id` = 5 LIMIT 1
  Profile Load (0.1ms)  SELECT  `profiles`.* FROM `profiles` WHERE `profiles`.`user_id` = 6 LIMIT 1
  Profile Load (0.2ms)  SELECT  `profiles`.* FROM `profiles` WHERE `profiles`.`user_id` = 4 LIMIT 1
  Profile Load (0.2ms)  SELECT  `profiles`.* FROM `profiles` WHERE `profiles`.`user_id` = 3 LIMIT 1
  Profile Load (0.2ms)  SELECT  `profiles`.* FROM `profiles` WHERE `profiles`.`user_id` = 2 LIMIT 1
  Profile Load (0.3ms)  SELECT  `profiles`.* FROM `profiles` WHERE `profiles`.`user_id` = 1 LIMIT 1
  Rendered shared_partials/_user.html.erb (23.2ms)
  Rendered users/index.html.erb within layouts/application (26.1ms)
Completed 200 OK in 29ms (Views: 22.5ms | ActiveRecord: 5.8ms)

(ノ∀`)アチャー N+1が起きてます。

パーシャルViewから叩かれているSQLはどれ??

先述のおもちゃな例だと

  • Userのロード User Load (0.4ms) SELECTusers.* FROMusersORDER BYusers.created_atDESC × 1回
  • Profileのロード Profile Load (0.1ms) SELECTprofiles.* FROMprofilesWHEREprofiles.user_id= 7 LIMIT 1 × Userの数

のSQL呼び出しがされています。このうち、パーシャルViewから叩かれているのはどれでしょう?

・・・正解は、全部です。

ActiveRecordは遅延評価が基本なので、 @users = User.order(created_at: :desc) の時点ではSQLは叩かれず、ビュー側で必要となったときに初めてSQLが叩かれます。

ViewからカジュアルSQL叩いている場所を見つける

ようやく本題です。
ビューからSQLが叩かれている、ということを見つけ出すにはどうしましょう?という話です。

実は、単純にSQL叩いている場所を見つけるだけなら、ActiveRecord::Causeを入れることで、

  User Load (0.4ms)  SELECT `users`.* FROM `users`  ORDER BY `users`.`created_at` DESC
  User Load (ActiveRecord::Cause) caused by /home/ubuntu/workspace/app/views/users/index.html.erb:1:in `_app_views_users_index_html_erb__2451733280735124226_22222060'
  Profile Load (0.3ms)  SELECT  `profiles`.* FROM `profiles` WHERE `profiles`.`user_id` = 20 LIMIT 1
  Profile Load (ActiveRecord::Cause) caused by /home/ubuntu/workspace/app/views/shared_partials/_user.html.erb:1:in `_app_views_shared_partials__user_html_erb___725293523527224828_69998977673900'
      :
      :
  Profile Load (ActiveRecord::Cause) caused by /home/ubuntu/workspace/app/views/shared_partials/_user.html.erb:1:in `_app_views_shared_partials__user_html_erb___725293523527224828_69998977673900'
  Profile Load (0.1ms)  SELECT  `profiles`.* FROM `profiles` WHERE `profiles`.`user_id` = 2 LIMIT 1
  Profile Load (ActiveRecord::Cause) caused by /home/ubuntu/workspace/app/views/shared_partials/_user.html.erb:1:in `_app_views_shared_partials__user_html_erb___725293523527224828_69998977673900'
  Profile Load (0.1ms)  SELECT  `profiles`.* FROM `profiles` WHERE `profiles`.`user_id` = 1 LIMIT 1
  Profile Load (ActiveRecord::Cause) caused by /home/ubuntu/workspace/app/views/shared_partials/_user.html.erb:1:in `_app_views_shared_partials__user_html_erb___725293523527224828_69998977673900'

のようなログ出力からSQLを叩いている場所をある程度特定することはできます。

しかし今回は

  • コントローラからSQL叩いているところは別にいい。ビューから叩いているところだけ見たい
  • 全部の処理じゃなくて、特定のコントローラの特定のアクションに絞って、ビューからSQL叩くのを検出したい
    • #show アクションのようにN+1がそもそも起きない(起きづらい)ところでは無理してSQL叩くのを抑制しなくてもいい
    • 知らないコントローラのSQLログは出したくない

という要件です。

 

で、「上記の要件を満たすようなGemを新たに作ったぜ、ほい」だと記事として全然おもしろくないので、この記事では、車輪の再発明なところはあるかもしれないけど、それっぽいのを実現する仕組みを作った様子を順を追って書いていきます。

ActiveRecord::CauseがSQLを叩いている場所を特定している仕組みを探る

https://github.com/joker1007/activerecord-cause/blob/master/lib/activerecord/cause.rb を見ると ActiveRecord::LogSubscriber という文字が見えるので、「ActiveSupport::Notificationのサブスクリプションで実現しているんだなー」ってのがなんとなくわかります。

ためしに

config/initializers/hogefuga_subscription.rb
class HogeSubscriber < ActiveSupport::Subscriber
  def sql(event)
    Rails.logger.debug("sql: #{event.payload}")
  end
end

class FugaSubscriber < ActiveSupport::Subscriber
  def start_processing(event)
    Rails.logger.debug("start_processing: #{event.payload}")
  end

  def process_action(event)
    Rails.logger.debug("process_action: #{event.payload}")
  end
end

HogeSubscriber.attach_to :active_record
FugaSubscriber.attach_to :action_controller

のようにサブスクライバをinitializerで雑にアタッチさせてアプリケーションを実行すると・・・

Started GET "/users" for 115.69.238.232 at 2018-12-18 10:26:07 +0000
Cannot render console from 115.69.238.232! Allowed networks: 127.0.0.1, ::1, 127.0.0.0/127.255.255.255
sql: {:sql=>"SHOW TABLES LIKE 'schema_migrations'", :name=>"SCHEMA", :connection_id=>21111960, :statement_name=>nil, :binds=>[]}
  ActiveRecord::SchemaMigration Load (0.2ms)  SELECT `schema_migrations`.* FROM `schema_migrations`
sql: {:sql=>"SELECT `schema_migrations`.* FROM `schema_migrations`", :name=>"ActiveRecord::SchemaMigration Load", :connection_id=>21111960, :statement_name=>nil, :binds=>[]}
sql: {:sql=>"SHOW FULL FIELDS FROM `schema_migrations`", :name=>"SCHEMA", :connection_id=>21111960, :statement_name=>nil, :binds=>[]}
start_processing: {:controller=>"UsersController", :action=>"index", :params=>{"controller"=>"users", "action"=>"index"}, :format=>:html, :method=>"GET", :path=>"/users"}
Processing by UsersController#index as HTML
  User Load (0.3ms)  SELECT `users`.* FROM `users`  ORDER BY `users`.`created_at` DESC
sql: {:sql=>"SELECT `users`.* FROM `users`  ORDER BY `users`.`created_at` DESC", :name=>"User Load", :connection_id=>21111960, :statement_name=>nil, :binds=>[]}
sql: {:sql=>"SHOW FULL FIELDS FROM `users`", :name=>"SCHEMA", :connection_id=>21111960, :statement_name=>nil, :binds=>[]}
sql: {:sql=>"SHOW TABLES ", :name=>"SCHEMA", :connection_id=>21111960, :statement_name=>nil, :binds=>[]}
sql: {:sql=>"SHOW CREATE TABLE `users`", :name=>"SCHEMA", :connection_id=>21111960, :statement_name=>nil, :binds=>[]}
sql: {:sql=>"SHOW FULL FIELDS FROM `profiles`", :name=>"SCHEMA", :connection_id=>21111960, :statement_name=>nil, :binds=>[]}
  Profile Load (0.2ms)  SELECT `profiles`.* FROM `profiles` WHERE `profiles`.`user_id` IN (20, 12, 13, 14, 15, 16, 17, 18, 19, 11, 10, 9, 8, 7, 5, 6, 4, 3, 2, 1)
sql: {:sql=>"SELECT `profiles`.* FROM `profiles` WHERE `profiles`.`user_id` IN (20, 12, 13, 14, 15, 16, 17, 18, 19, 11, 10, 9, 8, 7, 5, 6, 4, 3, 2, 1)", :name=>"Profile Load", :connection_id=>21111960, :statement_name=>nil, :binds=>[]}
sql: {:sql=>"SHOW CREATE TABLE `profiles`", :name=>"SCHEMA", :connection_id=>21111960, :statement_name=>nil, :binds=>[]}
  Rendered shared_partials/_user.html.erb (23.2ms)
  Rendered users/index.html.erb within layouts/application (27.7ms)
process_action: {:controller=>"UsersController", :action=>"index", :params=>{"controller"=>"users", "action"=>"index"}, :format=>:html, :method=>"GET", :path=>"/users", :status=>200, :view_runtime=>34.6481842715888, :db_runtime=>3.6058969999999997}
Completed 200 OK in 51ms (Views: 34.6ms | ActiveRecord: 3.6ms)

いろいろログに出力されます。

ActiveRecord::Cause は sql.active_record を利用してSQLクエリ発行イベントを受けた際に Kernel#caller_locations というコールスタックを得るメソッドを使って、それっぽい(あらかじめconfigしておいたパスにマッチする)呼び出し箇所を抽出する&ログ出力する、ということをやっています。

caller_locationsには具体的にどんな値が入っているか見てみる

ActiveRecord::Causeのコードでは caller_locationsに対して正規表現でマッチしたものを取得する、という処理があります。

  def get_locations
    return [] if ActiveRecord::Cause.match_paths.empty?
    caller_locations.select do |l|
      ActiveRecord::Cause.match_paths.any? do |re|
        re.match(l.absolute_path)
      end
    end
  end

でも、せっかくなので、そもそもこのcaller_locationsにはどういう値が入っているのか見ておこうと思います。

これも、とりあえず雑にイニシャライザでサブスクライバを仕掛けてみます。

config/initializers/hogefuga_subscription.rb
class HogeSubscriber < ActiveSupport::Subscriber
  def sql(event)
    return if ["SCHEMA", "EXPLAIN"].include?(event.payload[:name])

    caller_locations.each do |loc|
      Rails.logger.debug("caller_location:#{loc}")
    end
    Rails.logger.debug("sql: #{event.payload}")
  end
end

HogeSubscriber.attach_to :active_record
sql: {:sql=>"SELECT `users`.* FROM `users`  ORDER BY `users`.`created_at` DESC", :name=>"User Load", :connection_id=>29621900, :statement_name=>nil, :binds=>[]}
  Profile Load (0.5ms)  SELECT `profiles`.* FROM `profiles` WHERE `profiles`.`user_id` IN (20, 12, 13, 14, 15, 16, 17, 18, 19, 11, 10, 9, 8, 7, 5, 6, 4, 3, 2, 1)
caller_location:/usr/local/rvm/gems/ruby-2.4.5/gems/activesupport-4.2.10/lib/active_support/subscriber.rb:100:in `finish'
caller_location:/usr/local/rvm/gems/ruby-2.4.5/gems/activesupport-4.2.10/lib/active_support/notifications/fanout.rb:102:in `finish'
 〜(中略)〜
caller_location:/usr/local/rvm/gems/ruby-2.4.5/gems/activerecord-4.2.10/lib/active_record/relation.rb:243:in `to_a'
caller_location:/usr/local/rvm/gems/ruby-2.4.5/gems/activerecord-4.2.10/lib/active_record/relation.rb:622:in `blank?'
caller_location:/usr/local/rvm/gems/ruby-2.4.5/gems/activesupport-4.2.10/lib/active_support/core_ext/object/blank.rb:24:in `present?'
caller_location:/home/ubuntu/workspace/app/views/users/index.html.erb:1:in `_app_views_users_index_html_erb___1313782717592081990_69842311333420'
caller_location:/usr/local/rvm/gems/ruby-2.4.5/gems/actionview-4.2.10/lib/action_view/template.rb:145:in `block in render'
caller_location:/usr/local/rvm/gems/ruby-2.4.5/gems/activesupport-4.2.10/lib/active_support/notifications.rb:166:in `instrument'
 〜(中略)〜
caller_location:/usr/local/rvm/rubies/ruby-2.4.5/lib/ruby/2.4.0/webrick/httpserver.rb:140:in `service'
caller_location:/usr/local/rvm/rubies/ruby-2.4.5/lib/ruby/2.4.0/webrick/httpserver.rb:96:in `run'
caller_location:/usr/local/rvm/rubies/ruby-2.4.5/lib/ruby/2.4.0/webrick/server.rb:308:in `block in start_thread'
sql: {:sql=>"SELECT `profiles`.* FROM `profiles` WHERE `profiles`.`user_id` IN (20, 12, 13, 14, 15, 16, 17, 18, 19, 11, 10, 9, 8, 7, 5, 6, 4, 3, 2, 1)", :name=>"Profile Load", :connection_id=>29621900, :statement_name=>nil, :binds=>[]}
  Rendered users/index.html.erb within layouts/application (38.3ms)
Completed 200 OK in 63ms (Views: 45.6ms | ActiveRecord: 3.9ms)

Railsのバージョンによって細かいところは違うと思いますが、

caller_location:/home/ubuntu/workspace/app/views/users/index.html.erb:1:in `_app_views_users_index_html_erb___1313782717592081990_69842311333420'

app/viewsから呼ばれているなー、ってのが改めてわかります。

じゃあ、仮にビューからSQLが叩かれないようなコードにした場合はどうなるでしょう。

app/view_models/shared_partial_user_view_model.rb
# app/views/shared_partials/_user.html.erbで使用する変数をまとめた、単なるValue Object
class SharedPartialUserViewModel
  def initialize(full_name:)
    @full_name = full_name
  end

  attr_reader :full_name
end

のように、パーシャルビューに必要なデータをまとめたビューモデルクラスをActiveRecord非依存な形で定義し

app/controllers/users_controller.rb
class UsersController < ApplicationController
  def index
    users = User.order(created_at: :desc).includes(:profile)
    @user_view_models = users.map{|u| SharedPartialsUserViewModel.new(full_name: u.profile.full_name }
  end

のように変えてからログを見てみます。

  Profile Load (0.6ms)  SELECT `profiles`.* FROM `profiles` WHERE `profiles`.`user_id` IN (20, 12, 13, 14, 15, 16, 17, 18, 19, 11, 10, 9, 8, 7, 5, 6, 4, 3, 2, 1)
caller_location:/usr/local/rvm/gems/ruby-2.4.5/gems/activesupport-4.2.10/lib/active_support/subscriber.rb:100:in `finish'
caller_location:/usr/local/rvm/gems/ruby-2.4.5/gems/activesupport-4.2.10/lib/active_support/notifications/fanout.rb:102:in `finish'
 〜(中略)〜
caller_location:/usr/local/rvm/gems/ruby-2.4.5/gems/activerecord-4.2.10/lib/active_record/relation/delegation.rb:46:in `map'
caller_location:/home/ubuntu/workspace/app/controllers/users_controller.rb:6:in `index'
caller_location:/usr/local/rvm/gems/ruby-2.4.5/gems/actionpack-4.2.10/lib/action_controller/metal/implicit_render.rb:4:in `send_action'
 〜(中略)〜
caller_location:/usr/local/rvm/rubies/ruby-2.4.5/lib/ruby/2.4.0/webrick/httpserver.rb:140:in `service'
caller_location:/usr/local/rvm/rubies/ruby-2.4.5/lib/ruby/2.4.0/webrick/httpserver.rb:96:in `run'
caller_location:/usr/local/rvm/rubies/ruby-2.4.5/lib/ruby/2.4.0/webrick/server.rb:308:in `block in start_thread'
sql: {:sql=>"SELECT `profiles`.* FROM `profiles` WHERE `profiles`.`user_id` IN (20, 12, 13, 14, 15, 16, 17, 18, 19, 11, 10, 9, 8, 7, 5, 6, 4, 3, 2, 1)", :name=>"Profile Load", :connection_id=>29621900, :statement_name=>nil, :binds=>[]}
  Rendered users/index.html.erb within layouts/application (0.2ms)
Completed 200 OK in 34ms (Views: 6.5ms | ActiveRecord: 5.4ms)

お。期待通り

caller_location:/home/ubuntu/workspace/app/controllers/users_controller.rb:6:in `index'

になりました。app/viewsのファイルからの呼び出しが無くなってることがわかります。

以上のことから、ビューからSQLを叩いているかどうかは

  • sql.active_record のサブスクリプション
  • caller_locations の中に app/views/... がある?

を組み合わせることで判別できそうです。

特定のコントローラの特定のアクションでのみ、ビューからのSQL叩きを検出したい

ビューからSQLを叩いている箇所だけをログ出力するということはできました。
しかし、単純に attach_to で登録する方法だと、全部のコントローラの全アクションが対象になってしまいます。

app/controllers/users_controller.rb
class UsersController < ApplicationController
  def index
    @users = ...
  end

  def show
    @user = User.find(params[:id])
  end
end

のようなコントローラが有って「showアクションは対象外にしたい・・・」みたいなケースも大いにあるので、まずは特定のコントローラの特定のアクションだけをSQL監視対象にするようにしてみます。

またしても、雑にイニシャライザで・・・ :sweat_smile:

config/intializers/hogefuga_subscription.rb
class HogeSubscriber < ActiveSupport::Subscriber
  def sql(event)
    return if ["SCHEMA", "EXPLAIN"].include?(event.payload[:name])

    if caller_locations.any?{|loc| loc.absolute_path.include?("app/views") }
      Rails.logger.debug("SQL in view: #{event.payload}")
    end
  end
end

class ActionControllerSubscriber < ActiveSupport::Subscriber

  def start_processing(event)
    payload = event.payload
    if payload[:controller] == UsersController.to_s && payload[:action] == 'index'
      subscriptions[key_for(payload)] ||= ActiveSupport::Notifications.subscribe("sql.active_record", HogeSubscriber.new)
    end
  end

  def process_action(event)
    payload = event.payload
    if payload[:controller] == UsersController.to_s && payload[:action] == 'index'
      subscriptions.delete(key_for(payload)).try do |s|
        ActiveSupport::Notifications.unsubscribe(s)
      end
    end
  end

  private

  def key_for(payload)
    "#{payload[:controller]}##{payload[:action]}"
  end

  def subscriptions
    @__subscriptions ||= {}
  end
end

ActionControllerSubscriber.attach_to :action_controller

今回は、action_controllerのサブスクリプションをメインに使ってみました。
start_processing.action_controllerprocess_action.action_controller が各リクエスト・レスポンスのトランザクションの開始と終了になることを利用し、start_processingでsubscribe, process_actionでunsubscribeするようにしました。

Started GET "/users/1" for 115.69.238.232 at 2018-12-23 13:51:17 +0000
Cannot render console from 115.69.238.232! Allowed networks: 127.0.0.1, ::1, 127.0.0.0/127.255.255.255
Processing by UsersController#show as HTML
  Parameters: {"id"=>"1"}
  User Load (0.3ms)  SELECT  `users`.* FROM `users` WHERE `users`.`id` = 1 LIMIT 1
  Profile Load (0.3ms)  SELECT  `profiles`.* FROM `profiles` WHERE `profiles`.`user_id` = 1 LIMIT 1
  Rendered users/show.html.erb within layouts/application (75.3ms)
Completed 200 OK in 106ms (Views: 36.1ms | ActiveRecord: 54.3ms)


Started GET "/users" for 115.69.238.232 at 2018-12-23 13:51:21 +0000
Cannot render console from 115.69.238.232! Allowed networks: 127.0.0.1, ::1, 127.0.0.0/127.255.255.255
Processing by UsersController#index as HTML
  User Load (0.3ms)  SELECT `users`.* FROM `users`  ORDER BY `users`.`created_at` DESC
SQL in view: {:sql=>"SELECT `users`.* FROM `users`  ORDER BY `users`.`created_at` DESC", :name=>"User Load", :connection_id=>20179940, :statement_name=>nil, :binds=>[]}
  Profile Load (0.2ms)  SELECT  `profiles`.* FROM `profiles` WHERE `profiles`.`user_id` = 20 LIMIT 1
SQL in view: {:sql=>"SELECT  `profiles`.* FROM `profiles` WHERE `profiles`.`user_id` = 20 LIMIT 1", :name=>"Profile Load", :connection_id=>20179940, :statement_name=>nil, :binds=>[]}
  Profile Load (0.3ms)  SELECT  `profiles`.* FROM `profiles` WHERE `profiles`.`user_id` = 12 LIMIT 1
SQL in view: {:sql=>"SELECT  `profiles`.* FROM `profiles` WHERE `profiles`.`user_id` = 12 LIMIT 1", :name=>"Profile Load", :connection_id=>20179940, :statement_name=>nil, :binds=>[]}
  Profile Load (0.1ms)  SELECT  `profiles`.* FROM `profiles` WHERE `profiles`.`user_id` = 13 LIMIT 1
SQL in view: {:sql=>"SELECT  `profiles`.* FROM `profiles` WHERE `profiles`.`user_id` = 13 LIMIT 1", :name=>"Profile Load", :connection_id=>20179940, :statement_name=>nil, :binds=>[]}
    :
    :
  Profile Load (0.1ms)  SELECT  `profiles`.* FROM `profiles` WHERE `profiles`.`user_id` = 1 LIMIT 1
SQL in view: {:sql=>"SELECT  `profiles`.* FROM `profiles` WHERE `profiles`.`user_id` = 1 LIMIT 1", :name=>"Profile Load", :connection_id=>20179940, :statement_name=>nil, :binds=>[]}
  Rendered shared_partials/_user.html.erb (23.6ms)
  Rendered users/index.html.erb within layouts/application (26.9ms)
Completed 200 OK in 30ms (Views: 24.8ms | ActiveRecord: 4.4ms)

期待通り、UsersControllerのindexのみを対象として、ViewからカジュアルにSQLを叩いているのをログ出力できました。

コントローラ側で「このアクションをSQL監視したい」と書けるようにする

「さすがにイニシャライザで各コントローラのことを書くのはちょっと・・・」と誰しもが感じるでしょう。
コントローラ側で

app/controllers/users_controller.rb
class UsersController < ApplicationController
  observe_sql_in_view only: [:index]

  def index
    @users = User.order(created_at: :desc).includes(:profile)
  end

  def show
    @user = User.find(params[:id])
  end
end

のように明示的に書けたら理想的ですね。ということでいざトライ

config/initializer/hogefuga_subscription.rb
# HogeSubscriberの定義は前と同じなので省略

class ActionControllerSubscriber < ActiveSupport::Subscriber
  class << self
    def target
      @@target ||= {}
    end

    def add_target(controller:, action:)
      target[key_for(controller, action)] = 1
    end

    def target?(controller:, action:)
      target[key_for(controller, action)] == 1
    end

    def key_for(controller, action)
      "#{controller}##{action}"
    end
  end

  def start_processing(event)
    payload = event.payload
    if ActionControllerSubscriber.target?(controller: payload[:controller], action: payload[:action])
      subscriptions[key_for(payload)] ||= ActiveSupport::Notifications.subscribe("sql.active_record", HogeSubscriber.new)
    end
  end

  def process_action(event)
    payload = event.payload
    subscriptions.delete(key_for(payload)).try do |s|
      ActiveSupport::Notifications.unsubscribe(s)
    end
  end

  private

  def key_for(payload)
    ActionControllerSubscriber.key_for(payload[:controller], payload[:action])
  end

  def subscriptions
    @__subscriptions ||= {}
  end
end

class ActionController::Base
  def self.observe_sql_in_view(only:)
    only.each do |action|
      ActionControllerSubscriber.add_target(controller: self.to_s, action: action)
    end
  end
end

ActionControllerSubscriber.attach_to :action_controller

対象とするコントローラ・アクションはクラス変数で保持し、ベタで文字列比較していたところを置き換えました。

ログは変わらずです。

SQL in view のログ出力を少しだけまともにする

SQL in view: {:sql=>"SELECT `users`.* FROM `users`  ORDER BY `users`.`created_at` DESC", :name=>"User Load", :connection_id=>16084600, :statement_name=>nil, :binds=>[]}

はちょっと雑すぎるので、少しだけまともにしましょう。HogeSubscriberを

  • color を使って目立たせる
  • SQL以外の情報やJSON構造はログ出力しない
  • クラス名をまともにする :sweat_smile:

のように、少し改変する。

class SqlInViewSubscriber < ActiveSupport::LogSubscriber
  IGNORE_PAYLOAD_NAMES = ["SCHEMA", "EXPLAIN"]

  def sql(event)
    payload = event.payload
    log_sql_in_view(payload) unless IGNORE_PAYLOAD_NAMES.include?(payload[:name])
  end

  private

  def log_sql_in_view(payload)
    location = caller_locations.find{|loc| loc.absolute_path.include?("app/views/")}
    if location.present?
      logger.info("#{color("SQL in view", RED)}: #{payload[:sql]} caused by #{location}")
    end
  end
end

実際のログ出力はこんな感じになります :point_down:

image.png

それっぽい感じになったぞー\(^o^)/わーい

まとめ

config/initializers/sql_in_view_subscription.rb
class SqlInViewSubscriber < ActiveSupport::LogSubscriber
  IGNORE_PAYLOAD_NAMES = ["SCHEMA", "EXPLAIN"]

  def sql(event)
    payload = event.payload
    log_sql_in_view(payload) unless IGNORE_PAYLOAD_NAMES.include?(payload[:name])
  end

  private

  def log_sql_in_view(payload)
    location = caller_locations.find{|loc| loc.absolute_path.include?("app/views/")}
    if location.present?
      logger.info("#{color("SQL in view", RED)}: #{payload[:sql]} caused by #{location}")
    end
  end
end

class SqlInViewControllerSubscriber < ActiveSupport::Subscriber
  class << self
    def target
      @@target ||= {}
    end

    def add_target(controller:, action:)
      target[key_for(controller, action)] = 1
    end

    def target?(controller:, action:)
      target[key_for(controller, action)] == 1
    end

    def key_for(controller, action)
      "#{controller}##{action}"
    end
  end

  def start_processing(event)
    payload = event.payload
    if SqlInViewControllerSubscriber.target?(controller: payload[:controller], action: payload[:action])
      subscriptions[key_for(payload)] ||= ActiveSupport::Notifications.subscribe("sql.active_record", SqlInViewSubscriber.new)
    end
  end

  def process_action(event)
    payload = event.payload
    subscriptions.delete(key_for(payload)).try do |s|
      ActiveSupport::Notifications.unsubscribe(s)
    end
  end

  private

  def key_for(payload)
    SqlInViewControllerSubscriber.key_for(payload[:controller], payload[:action])
  end

  def subscriptions
    @__subscriptions ||= {}
  end
end

class ActionController::Base
  def self.observe_sql_in_view(only:)
    only.each do |action|
      SqlInViewControllerSubscriber.add_target(controller: self.to_s, action: action)
    end
  end
end

SqlInViewControllerSubscriber.attach_to :action_controller

Railsのロード時に :point_up_2: のようにパッチを当てて

app/controllers/users_controller.rb
class UsersController < ApplicationController
  observe_sql_in_view only: [:index]

  def index
    ...
  end
end

のように observe_sql_in_view で、「ViewからカジュアルにSQLを叩いている」ってのを検出したいアクションを指定すると

image.png

こんな感じでログに出て、べんり。

【2018年版】クラウドワークスで起きた変化とその感想まとめ8つ

External article

プロダクト開発を取り巻く愛と文化について

この記事は クラウドワークス Advent Calendar 2018 の12月25日の記事です.

co_creation.png

はじめまして.プロダクトDiv. エンジニアリング部でポエマーをしております @lemtosh469 です.

私(わたくし),社内日報では日頃考えていることを少しだけ放出させていただくことがあるのですが,ありがたいことにたまに反響をいただくことがあり,自分はエンジニアよりもポエマーに向いているのではないかと調子に乗って自負しております.

僭越ながらそんな私が,クラウドワークス Advent Calendar 2018 の最終日を飾らせていただけることになりました.クラウドワークスの名に恥じないように精一杯取り組ませていただきます.

※注 以下,エッセイを読むような温かい気持ちでご笑覧いただけますと幸いです.

プロダクト開発を取り巻く愛と文化について

目次

第1章  愛の発生装置
- Slackというアプリケーション
- カスタム絵文字機能
- 社内文化醸成ツールとしてのSlack
- 「愛の○○」
- 愛の定量化

第2章  文化の伝播
- コミュニケーションに関する真面目なお話

第3章  組織における文化的側面からの解釈
- アジャイルであれ
- アジャイルな時間を過ごす

最終章
- おわりに

あとがき

第1章 愛の発生装置

Slackというアプリケーション

さて,そんな私が執筆させていただく内容は,

社内チャットツールの活用から見えてきた組織文化の醸成に関する考察

です.

皆様のなかには日常的に Slack というアプリケーションに触れられている方も多いのではないでしょうか.

Slack とは

近年ベンチャー界隈を中心として急速に普及している,コミュニケーション用のチャットツールです.Slackにはコミュニケーションを促進させるための様々な機能がありますが,今回はそのなかでも私が特に注目している機能についてご紹介させてください.

カスタム絵文字機能

Slackには,発言に対して絵文字でリアクションできる機能があります.

please_take_me_crowdworks_logo.png

さらにその絵文字は カスタム絵文字を追加する
という機能によってオリジナル絵文字を追加していくことができます.

こんなふうに,既存の絵文字セット以外に自分たちで作成した絵文字(画像など)を追加しておくと,誰かの発言に対して気軽にリアクションをとることができます.弊社でもたくさんのカスタム絵文字が追加され,日々活用されております.

「Slack 絵文字」などで検索すると弊社以外にも様々な組織で,楽しげに活用されていることを知ることができます.

え,そんなものは知っているし既に使っている?
そんなことを思う方は多いのではないでしょうか.

はい,おっしゃる通りですが,
もう少しだけお付き合いいただければと思います.

社内文化醸成ツールとしてのSlack

2018年4月,私が入社した当時もカスタム絵文字は活用されていました.しかし,個人的に既存のカスタム絵文字を使ったリアクションの表現の幅に対して, なんかちょっと痒いところに手が届かない.(もっといい感じにリアクションしたい....) という課題感を勝手に感じていました.

・・・

そんな悶々とした日々を過ごしていたある日,弊社GM(ジェネラルマネージャー)の日報を見ていたときに,

愛だよ愛

という言葉が目に飛び込んできました.

(その言葉の意図を スナックのあ1 に立ち寄ったときに聞いてみると,全ては愛に帰結するということ,愛があれば大抵のことは解決するんだ.ということを教えていただきました.)

コ レ だ !!!

いまコミュニケーションに足りていない表現は 愛なんだ!
が足りていなかったんだ!!

そう受け取った私は,これはなんとしても布教しなければならないという謎の使命感に駆られ, 愛シリーズ (愛のカスタム絵文字) 記念すべき第1号である,

aidayoai.png

を作成して,こっそりと 社内Slackのカスタム絵文字 にリリースしたのです.

それからというもの,徐々に という言霊が頭のリソースを占めるようになり,
なにかに取り憑かれたように,ことあるごとに愛のカスタム絵文字を作成して追加するようになっていきました.そんな隠密活動を通じて私は,組織の中でカスタム絵文字職人として生きる道を見出したのです.

※利用している絵文字作成ツール( 絵文字ジェネレーター - Slack 向け絵文字を無料で簡単生成 ).慣れると10秒もかからないでSlackにカスタム絵文字を追加することができます.

「愛の○○」

この半年の間にコツコツと追加された愛の数は,総計 64 個!!

ainokatamari.png

気がつけば嬉しいことに 自分以外の人も愛の絵文字を追加してくれる ようになっていました.拡散ゼロで愛の仲間が増えていた! ←※ココ重要

愛の定量化

ここで終わってしまうと,ただの自己満足になってしまう(愛の押し付けですね).なので,エンジニアらしく愛の利用率の定量化を試みることにしました.

love_programmer.jpg

愛の利用率の集計

発言のリアクション集計には,Slack APIを利用しました.

(今回作成したコードは,大変恐縮なのですが文章の都合上割愛させていただきます.)

調査対象チャンネル

すべてのチャンネルを網羅したわけではなく,主に雑談が行われるチャンネルを対象としました.そのため,リアクション数は全体をイメージしたものに比べると少なく見える数となっております.また今回は,「2017年4月1日〜2017年12月24日」と「2018年4月1日〜2018年12月24日」(1年前と1年後)をそれぞれ集計して比較することとしました.

愛の利用率

ドンッ!!!

13位に, 愛はそこにあった  がランクインしている...!!!
そして社内の共通言語である,「 」や,「 BeAgile 」といったの絵文字の利用が増えていることがリアクション数からわかりました.

リアクション数の集計処理は,なかなか大変でしたが,やってみる価値はあったと思います.

・・・

(想定では下記のように多かった場合と少なかった場合で,書く内容を変えようと考えていたのですが,なんとも絶妙にコメントしにくい結果となっておりました.せっかく作った画像ですのでそのまま掲載させてください.)

多かった場合

love_is_fun.jpg

愛が溢れていたね!やってよかった!これからも増やしていきたい!

少なかった場合

love_is_cry.jpg

まだまだ愛が足りていない.愛とはなんなのか.これからも増やしていきたい.

・・・

このようにカスタム絵文字1つとはいえ,その機能は個人の感情や価値観を提示する1つの手段,共通言語の醸成のための機能としての側面を持っていると考えられます.

ただのチャットツールとして使うだけではなく,組織のコミュニケーションに愛が溢れる仕組みを取り入れてみませんか.

ここに,愛の塊.zipを置いておきます.

ぜひ皆様も愛の伝道師に(違

第2章 文化の伝播

コミュニケーションに関する真面目なお話

近年,チャットツールの普及によって日々の業務では,メールよりも的確かつスピード感のあるコミュニケーションが求められるようになってきています.

ただここで今一度,そのプロダクト名でもある Slack(弛み) という単語に込められた意味について考えてみたい.

僕は思うのです, コミュニケーションにおいて本当に大切なことは何なのか

言語化能力や深く考える力,ロジカルな思考はもちろん大事ですが,それは自分の価値観から湧き出てくる言葉を整理してより明確に伝えるための手段でしかなく,

本質的に大事なことは,自分とは全く異なる価値観があることを意識しながら,想像力を働かせて相手を受け入れる こと.

もっと簡単に言うと,ひとりひとりの大切にしている価値観がコミュニケーションを取る上で障害にならない こと.そしてそれが 尊重される こと.

ここで,僕の尊敬する文化評論家の言葉を引用させていただきます.

「近所のおばあさんが、毎朝、家の前を竹ぼうきで掃除してるとする。そういう行為をすると、いいよねって思う人がいる。もちろん、何とも思わない人もいるんだけど。で、自分もやってみようとか思って、掃除したりする。つい、忙しいとサボっちゃうんだけど。それはそれでいいんだよ。結局やらなくてもいい。でも、『毎朝家の前を掃除する』という習慣を受け取ったわけだ。それをいいよねとか、古臭いなとか、暇なんだなとか考える。その結果、自分も毎朝掃除してみたり、無理だから日曜だけでも掃除してみたりする人がいる。それが真似するということ。で、真似することによって、それを見た人がまたいいよねって思ったり、若いのに偉いねって思ったりする。」

僕は,こんなふうに 良い習慣が小さくても伝播して ,少しずつ少しずつ人々の間に連鎖しながら拡がっていく( 受け取って,考えて,真似して,行動する )ことが, 文化を作っていく んじゃないかと考えています.

だから, 誰かの良い習慣を見つけたら言葉にして発信 しなきゃいけないし, リアクションをしたほうが絶対にいい ,もしくは 真似をして自分の行動にも反映 してみて,それがまた 他の誰かに伝わる 必要があると思うのです.それが 誰に規定されるわけでもない組織の意志 であり,組織の文化になっていくんだ考えています.(余談ですが,それが組織が生き物と言われる所以なんじゃないかと考えていたりします.)

誤解を恐れずにあえて言うならば,現在のクラウドワークスでは,マネージャーでもない,テックリードでもない,POでもない,チームリーダーでもない,社長に名前や顔すら覚えられていないような末端の一社員である立場の僕でも,このようなことを考えていたりします.

もっと言うと,クラウドワークスには,こんなふうに考えることに意識が向くような環境や大きな変化を受容する文化の器が存在しているのではないかと思います.それは,これまで クラウドワークスを作ってきてくれた人たちの意志 によって少しずつ形成されてきたのではないでしょうか.組織について考えることに上も下も内も外もない.これは, 組織に関わる全員がフラットに考えることができるテーマ だと思います.

第3章 組織における文化的側面からの解釈

アジャイルであれ

2018年12月現在,株式会社クラウドワークスでは 「Be Agile」 というスローガンが掲げられています.(参考:Don't Do Agile. Be Agile

組織から 「アジャイルであれ」という問い を与えられ, ひとりひとりがそのテーマについて考えながら仕事に取り組んでいる状態に向かっている ように感じます.あくまで, 向かっている状態 です.その状態であることが大事なんだと個人的には解釈をしています.事実としてまだまだ大変なことは多いけど,組織の中にいる一人として,文化をつくる一人として,着実に変化する大きな流れの中にいることをピリピリと肌に感じています.

空を見て,雨を受けて,濡れているひとがいたら傘を差しのべる.
傘を閉じて一緒に雨宿りをしながら話すでもいい.
これからジョインしてくれる人は,一体どんなアイデアを考えるだろうか.

こんなふうに,いまのクラウドワークスでは ,ひとりひとりの振る舞いが文化になっていくひとりひとりが文化をつくる過程のど真ん中にいる
それが今期のスローガンである,

be_agile.png

という言葉に込められていると考えています.

アジャイルな時間を過ごす

いまの情報化社会には,生き急がないとまるで負け組かのように煽る記事や解釈が無限に溢れています.そんな中でイケてる自分の演出や肩書きに惑わされることなく,声の大きい人に左右されない気持ちであることはなかなか難しい.

ただ,ひとりひとりが自分の物語の主人公のはずで,そこは 誰の解釈にも揺るがされることはない事実 だと思います.

でも,ひとりだと心が折れてしまいそうな状況はたくさん訪れる.惰性という気持ちも湧いてくる.だから, 身近な誰かとスクラムを組んでお互いに刺激し合いながらアジャイルな状態を維持 しなければならない.

せっかく出会えたのだから,人生を振り返ったときに大きな成果や立派な肩書よりも少しだけ先に, 仲間と共にプロダクト開発をしていたアジャイルな時間 を思い出すほうが,僕はひとりのエンジニアとして人生が豊かだったと言えると思うのです.健全だと思うのです.

残念ながらこの世の中には 絶対的に良い場所は存在しない けれど,自分の世界のなかで過去を振り返ったときに 良い時間だったと解釈する ことはできる.

クラウドワークス Advent Calendar 2018 のエントリーを見ていただくとわかる通り,今年のアドベントカレンダーは エンジニア だけではなく, PM(プロジェクトマネージャー)PO(プロダクトオーナー)デザイナーマーケター ,様々な分野が混じり合いながら一つの組織(プロダクト)を作っていることが垣間見れると思います.

ダイバーシティがあると言うと尤もらしいけど,これからももっとたくさんの人が集まってより複雑な状態になっていく中でも, アジャイルであることでその複雑性を受け入れながらプロダクト開発をしていく ,これがクラウドワークスの文化と言えるんじゃないか,と思いたい.

最終章

おわりに

いかがでしたでしょうか?
今回はご紹介しませんでしたが,Slackにはローディングメッセージ機能や,小さなインタラクションの数々など,文化を作るための機能がたくさん仕込まれています.(こんな素敵な機能を使い倒さない手はない!)

こうして文章としてまとめていくなかで単なるチャットツールではなく,文化を醸成する機能を持つプロダクトとしてのSlackの偉大さを改めて感じたのでした.

愛が溢れるプロダクト,作っていきたいですね!!

長々とご拝読いただきまして,誠にありがとうございました.
クラウドワークス Advent Calendar 2018 を楽しみにしてくださっていた皆様,
弊社にご興味を持っていただき感謝申し上げます.

この場を借りて深くお礼申し上げます.

あとがき

あとがきから読む派の方,僕も一緒です.はじめまして.プロダクトDiv. エンジニアリング部でポエマーをしております @lemtosh469 です.

本記事は,単なるSlackの機能紹介とその活用事例ではなく,プロダクト開発を取り巻くコミュニケーションに関する考察と,現在のクラウドワークス文化のほんの一部の雰囲気を紹介している記事です.そして,テクノロジーを使うとこんなふうに文化を作れるんだという一つの事例としても見ていただけるかもしれません.

技術に注力した記事ではなく,個人的な解釈の強い内容ではありますが,ぜひ冒頭から読んでいただけると何か新しい発見があるかもしれません.(なかったらすみません!!)

えらく大きなことを書いてしまったような,偉そうなことを書いてしまったような,そんな気もします.ただ,エンジニアとして駆け出しの僕が,いまこんなふうにのびのびと記事を書けていることも,こういうこっ恥ずかしい内容を書いて公開できることも,

気の良い仲間たちと適切な心理的安全が作られ,日々前進している実感があるからだと思います.悩んだり,怠けそうになったりしても互いの振る舞いによって刺激し合いながら,楽しくいれたり,笑いあえたり,そんなふうに関われている周りの人たちに,改めて感謝です.ありがとう.




それでは,みなさま!「働くを通して人々に笑顔を!」


クラウド・ワー!!(※弊社の写真撮影時の掛け声です.)

w680_感謝祭.jpg


  1. 「スナックのあ」とは,不定期に開催される @noa_design51 をママとした飲み会.社外イベントとして開催されることもあります. 

Browsing Latest Articles All 25 Live