人間同士の通信プロトコルを見直し、ユビキタス言語で会話しよう
【Mentor Ver.】TechTrain Advent Calendar 2019 18日目の記事として書いています。
はじめまして。 @bussorenre と申します。主に業務では Scalaを利用してサーバーサイドアプリケーションを書いたり、時々terraform スクリプトを書いたりしてAWSでインフラを整えたりしています。
TechTrain の説明を少しだけさせていただくと、なんと、「無料でエンジニアになんでも相談出来る」というすごいサービスです。 メンターの方々も、メルカリやLINE 等、第一線で活躍されているエンジニアさんばっかり。
相談出来る内容も
- 具体的な実装の相談(ReactであれこれしたいがAという問題に詰まって悩んでる)
- キャリアの相談(副業とかどうしてるのか、就職先をどうやって決めたか)
- 起業相談(←代表の小澤さんが趣味でやってるだけ?笑)
などなど、幅広く、むしろ私が相談にのってほしい。(ダメ?笑)
はじめに - コミュニケーションロスと向き合う
業務としてプログラムを書き始めるようになると、それまで趣味でやってた時に比べて明らかに圧倒的に「他者とのコミュニケーション量」が増えます。「この実装いつまでに終わりそうですか?」といった簡単なほうれん草から、「この仕様を実現するために、どういう設計にしようか」といった複雑な議論まで、一人で仕事が簡潔しません。
チームに人数が増えれば増えるほど、物事を伝えるということに工数が増えていき、実作業に取られる時間が減っていくなんてことが起こりがちです。
人間同士の通信プロトコルを定義しよう
人間同士で「今質問していいですか?」「いいですよ」「ありがとうございます。Aということについてなんですが…」 と会話を始めるように、TCP コネクションを張るときは 「SYN」「SYN ACK」「ACK」パケットを送信しあって、通信を始めます。
我々が普段仕事で当たり前のように使っているインターネットがこんなに広く普及してるのは、早期に「通信規約(=プロトコル)」が標準化されたからです。
インターネットの通信方法がRFCで標準化されているように、人間と人間の間にも通信のプロトコルを明示的に示すと非常に便利です。 特に、「常識」と言われていることほど、「お互いにとって常識かどうか」を確認しあうと便利でしょう。
例えば、リモートワークなどをするときは、
- 始業時に「リモートワークを開始します」とSlackで宣言する。
- 昼食などで離席するときは「離席しています」とSlackで宣言する。
- 就業するときは「リモートワークを終了します」とSlackで宣言する。
というプロトコルを共有しておくと、今誰が仕事をしているのか、今誰に話しかけても問題ないのかなどがわかって便利です。
大事なのは、プロトコルのルールの中身よりも
- 「ルールをみんなで共有している状態」
- 「ルールの是非をみんなが納得している状態」
- 「柔軟にルールを変更できること」
が大事です。これらができていないと、せっかくプロトコルを規定しても、それに従わないメンバーが一定数出てきてしまい形骸化してしまうからです。 形骸化したルールを早く修正しないと、プロトコルは腐ってしまいます。
ユビキタス言語を定義して、言葉の齟齬を減らそう
例えば「アイテム」という言葉を使う時、そのアイテムって一体何なんでしょうか?
- ゲームでプレイヤーが入手できる道具?
- 課金して購入できる商品のこと…?
- テーブルビューの一つの要素…?
英語的にはおそらくどれも正しいですが、チームメンバーが「アイテム」という時の意味は一体なんなんでしょうか?
ユビキタス言語はドメイン駆動設計において非常に重要な概念の一つで、チーム内で共有する固有の言語です。 今から作ろうとしているシステムの仕様や動作を伝えたりする時に使用します。
何が、何と関連しているのか。何が何を操作するのか。ソフトウェアが解決すべき問題(=ドメインモデル)を定義する時に使います。
日々の人間同士のコミュニケーションでも、今話している話が「ゲーム内のアイテム」なのか「商品」のことなのか等、共通の認識があると スムーズに議論が進むことができます。
私の本業のチームでは「○○チーム用語集」という共同編集可能なmarkdown で書かれたページが有り、そこにドメイン用語を記載しています。特に、「新しいチームメンバー」が入ったときほどユビキタス言語を見直す良い機会です。
「AdminUser と OrganizationAdminUser と CustomerAdminUser の違いってなんですか?」とか「NormalPermissionとGeneralPermission って意味かぶってませんか?」等、普段使ってて感覚が麻痺している古株のチームメンバーのいい刺激になります。
先程の、コミュニケーションプロトコルの話と同じですが、チームメンバーみんなが共通認識を持っており、理想状態に近づけるために柔軟に変更できる状態が最も望ましいです。
ドメインモデルでプログラムを表現できるようにしよう
ユビキタス言語と、アプリケーションの中での表現が一致していれば、これ以上便利なことは有りません。
これは、人間同士のコミュニケーションロスを減らすだけでなく、人間←→プログラムという変換ログを少なくすることもできます。
ドメインモデルだけでビジネスロジックを表現できるようなアーキテクチャを採用すると、技術的関心事に左右されることなく、 「このソフトウェアが実現しようとしている事はなにか」ということにのみ注力することができます。
具体的には「レイヤードアーキテクチャ」「クリーンアーキテクチャ」などと呼ばれる構造を取り入れたりすることで解決に向かうことができます。
この辺を語りだすと adventcalendar 毎日書いても終わらないので、参考になったリンクを張っておきます(下部に参考書籍も載せておきます)
まとめ
業務エンジニアに必要なスキルでかつアマチュアエンジニアではそこまで必要ないスキルの特徴として、コミュニケーションという課題があるなと思い、 こんな感じで記事を書きましたが、中途半端なDDDの紹介に行ってしまった中途半端な記事になってしましました…。猛省。
まぁ、実際通信を実装するのも、アプリケーション内での通信の定義がほとんどだったりしますし、自分がコミュニケーションで使ってる「プロトコル」を見直すと 良いのかなと思いました。以上です。
以下に、参考書籍のリンクを張っておくので、ぜひよかったらどうぞ。
Google Chrome で「サードパーティーCookieが無効になっています」と表示され、はてなブログにログインできなくなる問題とその対応
対処方法
アドレスバーに chrome://flags/ と入力して、試験運用版の機能をオンにしたりオフにする必要があります。
その中の「SameSite by default cookies」を「Disable」にする必要があります。 (変更後は、再起動する必要があります)
どうして
SameSite Updates - The Chromium Projects を読むと、 Chrome 78 から、一部のユーザーで 「SameSite by default cookies」の Enable がデフォルトになったとのこと。
経緯は Chromium Blog: Developers: Get Ready for New SameSite=None; Secure Cookie Settings に書いてあります
要するにセキュリティアップデートですね。
技術的な解説は HTTP クッキーをより安全にする SameSite 属性について (Same-site Cookies) | ラボラジアン が参考になります
今後、SameSite=None を指定した場合(クロスオリジンであってもクッキーを送信させたい場合)は、Secure属性の付与も必須になります。
この辺は、サービスを提供している側が対応する必要があるので、現段階でエンドユーザーに出来ることはありません(カスタマーサポートに連絡するくらい?)
早くはてなに対応してほしい所ですが、結構根深い問題なのでM80 (全ユーザーにこの変更が適応される予定のバージョン)までに対応されるかは不明です。 (今の所、はてな公式からアナウンスはないです)
Scala で Functor っぽいものを実際に実装して挙動を追ってみる
こんにちは。 @bussorenre です。今日はScala です。 定型句となって恐縮ですが、「ここは表現がおかしい」とか「間違ってる」とか「もっとこうしたほうがいい」等ありましたら、Twitter で罵倒する前にコメントをいただけると凄く助かります。。。
はじめに
Functor を自分なりの理解で書いてみます。「実際実装してみて挙動を追ってみるか」と言うスタンスの補足解説的な記事です。 eed3si9n さんの独習Scalaz のページを読み解きながら書いたので、先にこちらをご覧になることをオススメします。
また、そもそもFunctor とはなんぞやという解説記事は以下の翻訳ページが参考になりました。
このページでは、上述のページを読んでいて「ん?」ってなったところを、可能な限りScalaで書いて理解を深めていこうと思います。
map
Functor
は日本語で関手(かんしゅ)と呼ばれます。Scala で書くとこんな感じです。
trait Functor[F[_]] { def map(fa: F[A])(f: A => B): F[B] }
Functor
は F[A]
を F[B]
に変換するmap
関数を定義します。要するにmap が定義できれば Functor
ということかな…?。入力値 F[A]
と A => B
に変換する関数を定義します。A と Bは同型でも問題なさそう。F
だと分かりづらいので、具体的な型で実際に例を見てみましょう。以下の例は、F = Option の場合の実装です。
def map[A, B](fa: Option[A])(f:A => B): Option[B] = { fa match { case Some(a) => Some(f(a)) case None => None } }
Option
の中身が Some
なら中の値に対して関数fを適応し、中身が None
ならそのまま None
を返しています。簡単ですね。
F = Either の場合はこんな感じになりますかね…?
def map[A, B, C](fa: Either[A, B])(f:B => C): Either[A, C] = { fa match { case Left(a) => Left(a) case Right(b) => Right(f(b)) } }
この例では右側だけをmap していますが、左側だけをmap する leftMap
などもScalaz.\/
には定義されています。
関数へのFunctor
さて、独習Scalaz を読むと Scalaz は Function1 に対する Functor のインスタンスも定義する。
という記述があります。下記コードもそこからの抜粋。
scala> ((x: Int) => x + 1) map {_ * 7} res30: Int => Int = <function1> scala> res30(3) res31: Int = 28
さてどういうことだろうか?と思って、実際にScalaz のFunction.scala
を見に行きます。
override def map[A, B](fa: () => A)(f: A => B) = () => f(fa())
Scalaz のどこかに, ()
を 「なんでも引数指定可な関数」みたいな意味の拡張があるんだろうか…?それともどこかに別の定義があるんだろうか?とにかく、関数のFunctor は、関数の合成を表すことが出来る。
A → B → C という順番で処理を実行してくれる A => C という関数を定義出来るみたいだ…。
lift
「持ち上げ」などと言うが、コレは何をやっているんだろうか? 実際に定義を見に行くとこんな感じになっています。
/** Lift `f` into `F`. */ def lift[A, B](f: A => B): F[A] => F[B] = map(_)(f)
A => B
と引数にとって、 F[A] => F[B]
という関数を返しています。先程のOption の具体例を引き続き使うと、以下のような感じ
val f = (x: Int) => x + 100 def map[A, B](fa: Option[A])(f:A => B): Option[B] = { fa match { case Some(a) => Some(f(a)) case None => None } } def lift[A, B](f: A => B): Option[A] => Option[B] = map(_)(f) lift(f) res0: Option[Int] => Option[Int] = $$Lambda$910/314622131@5a97b17c res0(Some(100)) res1: Option[Int] = Some(200)
Int => Int
という関数を Option[Int] => Option[Int]
という関数に変換されました。これにより、先程定義した Int => Int
という変換が、Option
という文脈で包まれて登場しても問題なく利用できるということになります。
これ、渡すのがOption ではなく関数gだった場合、関数g が一つの文脈 という事になるんだろうか…?
まとめ
Functor の実装を追いながら、実際にOption という具体例で動作を追ってみた。
def map(fa: F[A])(f: A => B): F[B]
override def map[A, B](fa: () => A)(f: A => B) = () => f(fa())
def lift[A, B](f: A => B): F[A] => F[B] = map(_)(f)
個人的にOption がわかりやすいのでOption を使ったけど、 Either でも List でも何でも良いと思う。
TypeScript で Option っぽいものを作ってみる
こんばんは。 @bussorenre です。趣味でTypeScript をはじめました。
Option っぽいもの
Scala には、値が無いことを許容する仕組みとして Option
という仕組みがあります。Some(A)
は値がある状態。 None
は値がない状態を示します。
中の値を取り出すには、get
というメソッドを用いますが、None
に対してget
を実行してしまうとエラーになってしまします。
値があるか無いかわからないけど中の値を操作したい時、 map
を使います。
以下に例を示します。
val a: Option[Int] = Some(100) // or None // a が Some でも None でも実行できる a.map(x => x + 100) a match { case Some(a) => println(a) // 200 case None => println("None") // None }
これと似たような物をTypeScript で再現します。あくまで「Option っぽいもの」なので、厳密にはOption ではありません。
案1: 共用体を使う
最初に思いついた方法です。共用体を使います。共用体は代入可能型を絞ることができます。
let a: number | null = 100 a = null // 問題なし a = "hogehogehogehoge" // エラーになる
これを用いて、nullable な変数は全部 A | null
にしてしまえばいいなじゃないかと思いつきます。しかし、毎回値が有効か、null かをif 文でチェックする必要があり、面倒です。
let a: number | null = 100 // or null if ( a === null) { // error } console.log(a)
案2: もうちょっといい感じに共用体を使う。
interface
を用いて、Some型とNone 型を定義し、それらを type
で 共用型にします
interface Some<A> { type: 'some'; value: A; } interface None { type: 'none'; } type Option<A> = Some<A> | None;
これは比較的良さそうです。Option<A>
は Some<A>
か None
を持ちません。しかし、Option.map
みたいな芸当ができません。無理やりmap 関数を作るとすればこんな感じになりそうです。
function map<A, B>(obj: Option<A>, f: (obj: A)=> B): Option<B> { if (obj.type === 'some') { return { type: 'some', value: f(obj.value), }; } else { return { type: 'none', }; } }
案3: 素直にクラスを使ってみる。
Option<A>
というインターフェースを用意し、そこから派生させたインターフェース Some<A>
と None
を定義します。また、それぞれ Some<A>
を継承したクラスSome<A>
を定義し、実際のmap
や get
の挙動も定義します。None
も同様です。
interface Option<A> { get(): A | null map<B>(f: (a: A) => B): Option<B> } interface Some<A> extends Option<A> { value: A get(): A } interface None extends Option<null> { get(): null } class Some<A> implements Some<A> { constructor(value: A) { this.value = value } get(): A { return this.value } map<B>(f: (a: A) => B): Some<B> { return new Some(f(this.value)) } } class None implements None { constructor() { } get(): null { return null } map<A, B>(f: (a: A) => B): None { return new None } } let a: Option<number> = new Some(100) console.log(a.map( x => x + 100)) // Some(200) let b: Option<number> = new None console.log(b.map( x => x + 100)) // None console.log(new Some(100).map(x => x + 100)) // 200 console.log(new None().map( (x: number) => x + 100)) // new None.map はできない。x の型を明示しないとコンパイルエラー
get ができて、map が出来る、Option っぽいものができました。めでたしめでたし。
最後に
決り文句的で恐縮ですが、「もっとこうしたらいい」とか「ここ間違っているよ」等があれば、ご指摘いただけると幸いです。
技術書典7で定価よりお安く頂いた「実践TypeScript - BFF と Next.js & Nuxt.js の型定義 吉井健文著」が非常にわかりやすくて勉強になっています。良書をありがとうございます。
追記
プロトタイプを使うことにより、更にScala のOption っぽいものに近づけることが出来るみたいです。勉強になりました…。 https://github.com/AlexGalays/spacelift/blob/master/src/option/index.ts
ISUCON9 に参加し惨敗しました
お久しぶりです。 @bussorenre です。
前々から存在は知っていたものの、なかなかプライベートの予定が合わず参加出来なかったISUCON にようやく初参加出来ました!! 結果は、最終スコア2910 (初期スコア 2100くらい)で、まぁものの見事に惨敗です 😇
やったことの振り返りと、次回参加とかに向けての反省をば
チーム構成
会社のメンバーと組みました。と言っても普段業務で一緒に仕事をしているメンバーではなく、全く別のプロダクト開発に従事ししているメンバー、つまりほぼほぼ「はじめまして」のメンバーでチームを組みました。 メンバー全員にruby 知見があるので、使用言語はruby に即決。それぞれの担当としては以下の通りです。
- @Yoshimaru46
- @Miyamizu
- アプリケーションの改善
- @bussorenre
- 計測環境の整備やデプロイの自動化。ミドルウェアのチューニング等
@Miyamizu 氏が9月入社のメンバーで、@Yoshimaru46 氏がそのメンターということもあり、二人のペアプロ体制が最初に出来上がり、そこに私が乗っかった形のチーム構成です。
事前準備
チームが決まったのが8月最終日くらいだったので、事前準備にあまり時間を割けなかったのですが、就業後の時間等を利用し、物理的に集まって作戦会議等をしていました。
作戦会議で決めたことは以下のとおりです。
- 目標
- isucon に挑戦する(0次予選を突破する = 起床して課題と向き合う)
- 0点を回避する(必ずスコアが出る状態で終了する)
- 出来ることを確実にやる
初挑戦から優勝を狙いに行く強者もいますが、我々の実力的に(準備期間的にも)まずそれは難しいだろうということで、「わからないことに挑戦すること」を最も重要な目的としました。 雑に「絶対優勝すっぞ!!」とか言うと、圧があってかえって挑戦するマインドを萎縮させかねないので、「まぁ楽しもうぜ」ということを、共有しました。
一方で、出来ることは必ずやるようにしようという目標も設定しました。具体的には以下の事を必ずやるようにしようと事前に決めました。
- アプリケーション系
- index をしっかり貼る
- N+1問題の解消
- SQLの並列化
- 画像をバイナリでDBに持っているのをやめる
- 可読性低いコードをやめる
- 計測系
- スロウクエリの自動検出
- デプロイの自動化
- nginx のログをalp で統計する
- ミドルウェア
- nginx じゃないhttpサーバーが来たらnginx にする(わからないから)
- DBロックの調査
- Cache Control
- タイムアウト/キャッシュのの設定を適切に
- Worker数を適切な数に
また、alibaba Cloud という普段使い慣れないクラウドが舞台なので、確実にインスタンスを立ててssh 出来るようにする練習を行いました。
予選開始前から、解くべき課題を見つけるくらいまで
予選当日は9時過ぎくらいに会社に集合し、設営を行い、9:50分ごろから事前ブリーフィングを行いました。
まずは午前中にやることを共有し、私は以下のことを担当することになりました。
- 起動しているサービスを確認する
service --status-all
- 全ての情報をgithub に乗せる
- application (全実装)
- conf(nginx や mysql など)
- ↑で載せた情報をシンボリックリンクにする
- デプロイ自動化
- alp の構築
- 全体的に基本的なnginx.conf の見直し
私がこれらの基本的な設定に取り組んでいる間に、他二人のチームメンバーには課題を読み込んでもらい、N+1等、解決できそうな問題を発見してもらうことにしました。
諸々の準備が整ったのが11:52分。その頃には「どうやら /users/transactions.json あたりがN+1でヤバそう」という情報がアプリケーションコードからも、alp の統計からもう読み解くことが出来ました。
昼食。とりあえず寿司を発注する
メインイベント このあたりで、絶対集中力が尽きるだろうと思っていたのと、前日のチームが寿司を発注しているのをTLで観測したのを受け、「これはもう寿司頼むしかねーな!」という感じで頼みました。
発注から1時間足らずで届いたのにはびっくり。銀のさら様様です。
昼食を取りながら、お互い次に取り組むことを決めますが、どうもアプリケーション側にボトルネックが多いと判断し、私も午後からアプリケーション側を見ることになりました。 予定ではnginx の最適化とか、せっかくインスタンス三台使えるんだからDBとapp 分けるかとか考えていたのですが、アプリケーションを改善しないとインスタンス分離してもそもそもスコアが出ないだろうという判断の元決定しました。
午後:地獄のN+1潰し
このあたりで普段の業務を如何にライブラリや既存の仕組みにに乗っかかってるだけか反省することが多かったです…… 😇 join を使ってN+1を解消するのに結構時間がかかりました。普段如何にORMに頼っているかわかりますね。
あーでもないこーでもないと3人で言いながら /users/tranzaction.json
と /new_items
のアクセスを早くした頃にはもう16 時でした……。
「N+1潰したからベンチ上がるやろ!」と思い意気揚々とベンチを回すも、上述の「2910」までしかスコアが出ず 😇
改めてalpを確認すると、たしかにそこはかなり改善しているのですが、今度は /buy
のレスポンスが遅く、更に殆どのリクエストが4xx系のエラーを返していることに気が付きます。
このとき今更「そういえば、今回のスコアは商品の売上合計だったよな」ということに気が付き、よりたくさん商品を売りさばくチューニングをしなければ行けなかったことに気が付きます。問題の趣旨の理解が出来たのが16:30頃
必死にbuy の改善に取り組むも、「外部APIの仕様わからん」「なんでこんな403ばっかり返しているのかわからん」等と言ってるうちに18時となりゲームセット。 (※ 最後はポータルの不具合で18:10 まで時間が延長になりましたが…)
最終スコアは2910でした。
反省
大きく2つで、
- 焦らずもっと課題とドキュメントを読み込む事
- 環境構築系のスクリプトをもっと事前に用意しておけばよかった事
が課題でした。特に後者は、「本当に解くべき課題がなにか」を理解するのに遅れたという痛恨のミスです。
作問担当がメルカリということで、メルカリかメルペイっぽいアプリが来るだろうなーという予想を立てていたんだから、メルカリっぽいサービスを適当に作ってみるとかをしても良かったかなと思います。 この辺は実装経験が差をつけるかなと。
後者に関しては↑の課題理解をより早く行うためにある課題だと思っていて、構築系の自動化・高速化はもうちょっと備えていきたいかなという所です。
今回、チームメンバー全員がisucon 初挑戦という非常にフレッシュなチームで、個人的にはかなり楽しくさせていただきました。 チームメンバーの Yoshimaru46氏 と Miyamizu氏には改めて感謝を。そしてまたぜひ来年一緒に挑戦して予選突破しましょう!!笑
最後になりましたが、isucon 運営の皆様、非常に楽しい大会を提供していただきありがとうございました!!
tmux 2.9 にバージョンアップしたときのconf の変更等
普段使っているMac の brew
を更新し、いくつかのパッケージを最新にしたら、tmux から以下のような警告が出るようになった。
.tmux.conf:65: invalid option: status-attr .tmux.conf:68: invalid option: window-status-fg .tmux.conf:69: invalid option: window-status-bg .tmux.conf:73: invalid option: window-status-current-fg .tmux.conf:74: invalid option: window-status-current-bg .tmux.conf:78: invalid option: pane-border-fg .tmux.conf:79: invalid option: pane-active-border-fg .tmux.conf:82: invalid option: message-bg .tmux.conf:83: invalid option: message-fg
調べてみると、tmux 2.9
から option の指定が一部変更になったらしい。
以下のように変更すると治った
# default statusbar colors -set-option -g status-bg colour236 #base02 -set-option -g status-fg colour136 #yellow -set-option -g status-attr default +set-option -g status-style bg=colour236,fg=colour136 +#set-option -g status-style default # default window title colors -set-window-option -g window-status-fg colour244 #base0 -set-window-option -g window-status-bg default +set-window-option -g window-status-style fg=colour244,bg=default #set-window-option -g window-status-attr dim # active window title colors -set-window-option -g window-status-current-fg colour166 #orange -set-window-option -g window-status-current-bg default +set-window-option -g window-status-current-style fg=colour166,bg=default #set-window-option -g window-status-current-attr bright # pane border -set-option -g pane-border-fg colour235 #base02 -set-option -g pane-active-border-fg colour240 #base01 +set-option -g pane-border-style fg=colour235 +set-option -g pane-active-border-style fg=colour240 #base01 # message text -set-option -g message-bg colour235 #base02 -set-option -g message-fg colour166 #orange +set-option -g message-style bg=colour235,fg=colour166