おめー。
中の人ありー
おめー。
中の人ありー
やりたいことがいっぱいありすぎるので一覧を作る。
5箇年計画と書いたが、実際には何年かかるか全く見当がつかない。
30才までに終わるかな。
今、デベロッパー向けにこういうサービスあったらいいなーと思っているものがある。
その中でSSH接続を受け付けたいなと思ったから実現方法を考えた。
WordPressをホスティングしてvimで記事を編集できるようなものをイメージして欲しい。
ルール
・1人1Docker コンテナ
・コンテナを実行するEC2インスタンスを運営側がいつでも変更できるようにする
・サーバーの状態の保持は別問題
・HTTPも別問題
・ユーザー名は全ユーザーで1意、ホストは共通
サインアップしたユーザーにDockerコンテナを用意し、そこにSSH接続を受け付けるようなものを作りたい。
せっかくなので、1Dockerに複数のユーザーを用意できるようにしたかった。それができると「管理者」「編集者」「記者」等のロールを作れる。しかし、難易度が上がりそうなので今回は諦め。
コンテナを実行するEC2インスタンスを運営側がいつでも変更できるようにしたい。
特定のホスト名、ポート番号で待ち受け続けないといけないと運用がきつくなる。
例えばもっと安いEC2インスタンスが発表されたらこっそりコンテナを移動したい。
また、使われてないコンテナを勝手に落としておき、次にアクセスが来たときに空いているインスタンスで立ち上げる仕組みも入れられるようにしたい。
そのため、生EC2アドレスをユーザーに渡すのではなく、SSHプロキシのアドレスを渡してそこからEC2にブリッジする。
インスタンスを立ち上げなおしたときにユーザーが変更したファイルの保持をどうするかは、ネットワーク設計とは別問題なので今日は気にしない。それはアプリケーション設計の話だ。
大方 /var/hoge/fuga 以外を変更しても保存は保障しませんよといっておけばいいだろう。
HTTPでも受け入れられるようにしたいが、そっちはそっちで話がでかすぎるので別で考える。
ユーザー名は全ユーザーで一意にする。
さっきも書いたが本当はホストごとに名前空間を区切って
- admin@user1.some-service.com
- author@user1.some-service.com
- editor@user1.some-service.com
- junior-staff@user1.some-service.com
という風にロールわけをできるようにしたい。
しかし、後述のライブラリがまだHostに対応していないので諦めた。
とりあえず
- user1@some-service.com
- user2@some-service.com
というユーザー名体系にする。
実現方法
これはまだ試していなくて、こうすれば大丈夫だろうと僕が今想像していることである。
用意するもの
・ELB
・セキュリティゾーン
・EC2を3台以上
・内2台はSSHプロキシ
・もう一台はDockerのせるインスタンス(アプリケーションインスタンスと呼ぶ)
ユーザーが増えてきたらこのインスタンスを増やしていけば良い
・DockerImage
まず、ELBから2台のSSHプロキシサーバーに大してTCPロードバランスを設定
外向きのホスト名はssh.some-service.comで統一する。
https://github.com/tg123/sshpiper
アプリケーションインスタンスでDockerを起動
このとき、内向きのSSHポートは22とする。
外向きは可変にする。
docker -p 3333:22 等
これで準備は完了。
新しくユーザーがサインアップしたらどこかのアプリケーションインスタンスでDockerを起動する。
鍵ペアを生成し、Docker内に渡すと同時にSSHプロキシにそのインスタンスのホスト名、ポート名、鍵を設置。
ユーザーに秘密鍵を渡す。
これでできるんじゃないかな?
ビジネスロジックのコードを解析することでこのようなことができるのではないか?
という疑問だ。
僕はビジネスロジック内でDBにアクセスするのではなく、その前にデータを落としてきておいて、そのデータを引数に取る事にしている。
// こうではない
func getUserSalesSum(req Request, db DB) Response {
sales := db.QuerySalesByUser(req.UserID)
return sum(sales)
}// こう
func onRequest(req Request, db DB) Response {
sales := db.QuerySalesByUser(req.UserID)
return getUserSalesSum(req, sales)
}
func getUserSalesSum(req Request, sales []Sale) Response {
return sum(sales)
}
これはgetUserSalesSumがスローテストにならないようにするためだ。
また、DBへのクエリが正しく書けたかということとこのビジネスロジックが正しく書けたかということは本来別問題なのに、同時にテストしなければいけなくなる。だからビジネスロジックに入る前に必要なデータは全部落としておくべきだ。
前述のこのコードは悪いコードの例である。よく「バッドコードの例」として紹介される。
func getUserSalesSum(req Request, sales []Sale) Response {
return sum(sales)
}
暗黙の前提が存在することについてだ。
このコードでsalesという値はあるユーザの購買履歴だけを含む配列ということになっている。しかし、このコードにそのメッセージ性がない。このコードを読んだ人は普通「このsalesはすべてのsaleが入った配列だな」と感じると思う。
そこで、いくつかの手を打つ。例えば引数名や関数名を工夫することがある。また、Assertを書くというのもただしい選択の一つだろう。しかし、僕はビジネスロジックにもフィルターを書くのがいいと思う。
func getUserSalesSum(req Request, sales []Sale) Response {
sales = filterUserSales(sales, req.UserID)
return sum(sales)
}
こうしたほうがビジネスロジックの1行1行が仕様と1:1で対応するようになるのでわかりやすい。
また、前者はこの配列が特定の条件でフィルターされていることに依存していたコードだが後者は必要なデータが入っていればそれ以外のデータが入っていても正常に動くコードになる。後者のほうがキャッシュなどとの相性が良くなる。
またクエリを書くことを手抜く余地が出る。例えば国コード一覧というのがあったとする。それは高々200個くらいの値しかないはずだ。そうであれば、毎回DBに取りに行くより手元のインメモリに持って置きたいかもしれない。そういう時に特別なコードを書かなくてよくなる。
このコードの問題点の一つとして使うデータの種類が増えれば引数が多くなってしまう点がある。ユーザー情報や購買情報や位置データやあれこれ……
そこで、DBのテーブルを全部一つのオブジェクトツリーに含めてしまうといいと思う。
type Table struct {
Users []User
Products []Product
Sales []Sale
}
func getUserSalesSum(req Request, table Table) Response {
sales := filterUserSales(table.Sales, req.UserID)
return sum(sales)
}
さて、「ビジネスロジックの前段階で必要なデータを落としてくる」でビジネスロジックが参照透過性を手に入れた。そして、「ビジネスロジックでも同じフィルターを書く」でテーブルが特定の条件に依存するコードから。さらに一つのオブジェクトツリーにまとめることによって、テーブル全体の関数に変わった。
ここで一つの結論に到達した。
HTTPハンドラとは、HTTPリクエストとDB上のすべてのデータに対してHTTPレスポンスを返す純粋関数である。
POSTメソッドなどのこともあるので、HTTPレスポンスとDB書き込みコマンドを返す関数であるということにしておく。
おさらいだが、このビジネスロジックは引数でDB上のすべてのデータを受け取った前提で書いてある。しかし、実際に必要なのはその一部分である。その部分は必ず入っていなければいけないが、それ以外の部分にデータが入っていても入っていなくても動作は変わらない。逆に混ざっているとまずいデータというのはありえない。
たとえば上記のこのコードの場合だ。
type Table struct {
Users []User
Products []Product
Sales []Sale
}
func getUserSalesSum(req Request, table Table) Response {
sales := filterUserSales(table.Sales, req.UserID)
return sum(sales)
}func filterUser(sales []Sale, id interface{}) []Sale {
var results []Sale
for _, s := range sales {
if s.UserID == id {
results = append(results, s)
}
}
return results
}
このコードを見れば誰でもUsersとProductsにデータが入っていても入っていなくても同じ動きをすることはわかる。
また、Salesにreq.UserIDのすべてのSalesが入っていなければならないが、UserBさんやUserCさんのデータはあってもなくても同じということもわかる。
僕の問題意識は、「人間がこのコードを読めば必要なデータと不要なデータを判定できるならコンピューターに計算させることができないか?」である。
UsersとProductsが不要であるということはASTを見ていけば結構簡単にできそうだ。しかし、req.UserIDのSalesだけが必要だ、ということはどうすれば判定できるのだろうか。
今のところ、すべての「値」に対して「その値を作るのに必要な値」を挙げていき、それをツリー上にすればある程度可能かと思ったが自信がない。
誰か教えてくれ。
サーバーサイドもDartで書いてきた。
ワンパターン化してきたのでフレームワーク化する。
とりあえず構想を発表する。
認証を楽にするため、HTTPサーバーはなくFirebaseサーバーとして振舞うものにする。
サポートする機能3つだ。
FIrebaseはJSONでデータをやりとりするため、エンコーダとデコーダが必要になる。
Firebase特有の機能があるので一般的なDartのJSONエンコーダーライブラリでは機能不足だ。サーバー側とクライアント側で微妙にユースケースが違うこともネックになる。
だから、型定義ファイルからサーバー・クライアントのコードを自動生成する。
定義ファイルの書式はDartにする。型定義ファイルのフォーマットとして、JSONは書きたくない。yamlも辛い。yamlは序盤は快調だが行が増えてくると辛くなる。
結局、型定義にどのフォーマットが適しているか。プログラム言語が一番いい。だからDartにした。これはそのDartを実行したりリフレクションで値を入れたりするものではない。DartのASTをとって解釈してそれを元にコードを生成する。Dartの書式で型を定義すれば読みやすいし各自のエディタのシンタックスハイライトや補完がきく。
将来はマイグレーション機能もつけたいと思っている。
Firebaseを使っているとDBのようにAlter Tableが打てないとめんどくさくなる局面がある。
型定義の変更履歴から既存のデータベースを変更できるコマンドを作れるはずだ。
2.view
FirebaseにDBのViewの機能が欲しい。もしかしたらこれは本家が対応するかもしれない。して欲しい。
例えばユーザーがいろんなグループに参加できるグループチャットアプリを作っているとする。ユーザーごとに取得したい書き込みはバラバラである。これを頑張ってクエリで表現するよりも
A チャットの書き込みを全部一箇所にまとめたデータ
B 誰がどのグループに所属しているかまとめたデータ
C ユーザーごとに所属するグループの書き込みをまとめたデータ
を別々に保持したい。
だから、AとBが変更された時にリアクティブにCを再作成したい。
どうするかというと、AとBの全データを引数にCの全データを作成する純粋関数を一個定義すると、A、Bが変更された時に毎回その関数が実行され、結果をFirebaseの所定の場所に書き込むものが欲しい。
いくつか最適化の方法を考えている。
DBにならってAとBをテーブル、Cをビューと呼ぶ。
第一に、テーブルの全データを毎回Firebaseから読みだすのは悪いので、テーブルのコピーをローカルファイルにおいておきたい。
第二に、作り直すたびにビューの全データをFirebaseに書き込むのは申し訳ないので、前回作成したビューをローカルに持っておき、差分をチェックしパッチを送る形にしたい。
第三に、どのテーブルが更新された時にどのビューを更新するのか、どのビューを更新するためにどのテーブルを読み出さないといけないのかを自動で判断する。ソースコードを解析することで可能だ。
第四に、実現方法がわからないので夢物語だが、ビューの1行がテーブルのどの行に依存しているかを判断できたら、毎回テーブルの全データを読み込まなくてよくなる。
以前設計の逆流問題の話をした。
この最後の部分にinterfaceを自動生成していってくれればいいということを書いた。
それを含めたい。
僕が設計の逆流問題と呼んでいる概念の話をしたい。
僕がコードを書いていて一番疲れることの一つだ。
一般に下レイヤーのコードが上レイヤーのコードに感知しないのがいい設計ということになっている。
例えば、WebAPIを開発しているとしよう。APIのハンドラ内でDBからデータを取得し、それを加工してレスポンスするとする。
Web APIを書くディレクトリの中にDBアクセスのコードを書くと汚くなるので、DBアクセスは別のディレクトリに書いてインポートすることにした。
こんな感じのコードになる。
/db/user.dart
User getUser(String userID) {
...
}
import “../db/users.dart” as db;
HttpResponse getUser(HttpRequest httpRequest) {
return new HttpResponse()..body = db.getUser(httpRequest.param["user_id"]);
}
このとき、/db/users.dartから書くか、/web_api/get_users.dartから書くか、それが問題だという話だ。
善悪の観点から言えばdbアクセサを書いてからWebAPIハンドラを書くのが正しい。
なぜなら依存関係を1方向にし、依存関係の後ろ側から書いていく方が自然だからだ。
しかし、ここで設計の逆流問題が起こる。
上レイヤーで何をするかわからないと下レイヤーがどんな機能を公開するか決められないのだ。
例えば/db/users.dartにはどんな処理が必要か?
などなど。
これは/web_api/**のすべてのコードがわからないとと決めようがない。
では/web_api/から書いていくべきか?
そうでもない。
上レイヤーから書いていくと終始コンパイルエラーがでて目障りだ。
しかも、コード補完やリファクタリングなどIDEの機能が使えなくなる。
ここで二つの方法が考えられる
一つは深さ優先探索だ。
WebAPIハンドラを書きながら必要に応じてDBアクセサを書いていく。
些細なことならこれが一番楽だ。
しかし、あっちこっちいろんなファイルを触ることになるので、僕のように脳内ワーキングメモリが乏しい人は「あれ? 今何してたんだっけ」状態になりやすいのが難点だ。
そうでなくとも、要領が悪い方法なのは確かだ。
もう一つ、事前リストアップ作戦がある。
先に脳内でWebAPIハンドラのコードを書き、必要な下層の機能をメモ帳に書いていく。
この方法はかなり時間短縮になる。特に大きい作業ではかなり効いてくる。
しかし、まず疲れる。
第二に不正確になりがちである。
第三に、そもそもプログラミング言語が一番得意そうな領域のことをわざわざメモ帳に自然言語で書くせいで消耗するのは筋が悪くないか?
結局どうしようもないというのが僕の結論だ。
そこで僕は考えた。要するにリストアップする専用の書式か何かがあればいいのではないか?
単にリストアップする書式よりも、下層が上層からどのように使われるかを擬似コードで書いていけばその使われ方を集約してくれるものがあればいい。
それなら、プログラム言語をそのまま使った方がいい。
そして至った暫定解がこれだ。
まず、WebAPIハンドラを先に書く。
バックグラウンドプロセスがそのファイルを監視し、そのコードのコンパイルが通るようにinterface文を自動生成していく。
例えば、WebAPIハンドラに
var user = db.users.getUser(httpRequest.useID)
というコードがあれば
abstract class DB {
SomeInterface users;
}
abstract class SomeInterface {
dynamic getUser(String arg1);
}
というinterfaceを自動で生成すればいい。
すると、まずコンパイルエラーが消える。もちろん、本当に間違った処理を書いていたらエラーを消して欲しいので、例えば「db.」で始まるステートメントとその子どもっぽいものだけ自動生成すればい。
さらに、一回使ったメンバーは次回からIDEが自動補完してくれる。リファクタリングもIDEから直接実行可能だ。
上レイヤーを書き終わったら、interfaceを実装するように下層を書いていけばいい。
僕は今、具体的な実装方法やより詳細なかた情報をバックグラウンドプロセスに伝える手続きなどを色々考えてる。
僕がモデリング関数と呼んでいる概念の話をしたい。
DBから落としてきたデータをもとにビジネスロジックを書くと、関数の引数が増えすぎてうざくなる経験はないだろうか。
例えばTwitterを作っているとして、AさんがBさんをフォローしているか判定する関数はこうだ。
(followsはFollowテーブルのレコードの配列だ)
if(isFollow(toUserID, fromUserID, follows)){
// 処理
}
投稿数が5以上のユーザーをフィルターするのはこうだ。
users = filterPostCount(users, posts, 5);
どうも汚い。
オブジェクト指向言語で書く以上こういう風に書きたい。
if(user.isFollow(toUserID)){
// 処理
}
users = users.filterPostCount(5);
これを実現するためにはそもそも生データ用のクラスとビジネスロジックで使う用のクラスを分けるといい。僕は前者をEntity、後者をModelと呼び、EntityをModelに変換する関数をmodeling関数と呼んでいる。
EntityはDBのスキーマと1:1で対応し、ORMがインスタンスを作ってくれるものだ。
// エンティティ
class UserEntity {
String userID , name;
}
class FollowEntity {
String to , from;
}
// モデル
class UserModel {
String userID , name;
List<FollowEntity> follows;
bool isFollowTo(String to) {
return follows.where((f ) => f.to == to).isNotEmpty;
}
}
// モデリング関数
List<UserModel> modeling(List<UserEntity> userEntity , List<FollowEntity> follows ){
// ここにいろんなフィルターとかマップとか
}
これで目的が達成できた。
欲を言えばEntityとModelを一つのオブジェクトツリーにぶら下げることで、全てのmodeling関数を共通化することができる。そうすれば、どんな時もとりあえずEntityを取得して一種類のmodeling関数にかければいいということになりわかりやすい。
class Entities {
List<UserEntity> users;
List<FollowEntity> follows;
// その他いろんなエンティティ
}
class Model {
List<UserModel> users;
// その他いろんなモデル
}
// モデリング関数
List<Model> modeling(Entities entities ){
// ここにいろんなフィルターとかマップとか
}
この方法のメリットは「そのオブジェクトに関することは、そいつに聞けばなんでもわかる」状態になりコードが綺麗になることだ。
デメリットは、そのビジネスロジックに必要のないところまでモデリングするためリソースが無駄になることだ。
注意点は、Modelに生やすメソッドはそのModelの状態を書き換えてはいけないということだ。単に聞かれたことに答えるだけの関数を書こう。
このような反論が想定される。クラス自体を分けずにEntityにメソッドを生やすだけではダメなのか。それをしようとするといつどのタイミングで関連するEntityをそのEntityに配ればいいのかわからないからダメだ。
今回はサーバーサイドを例に説明したが、本質的にデスクトップアプリでもWebアプリでも使えるテクニックだ。僕はReduxでmapStateToPropsとReducerの先頭でStateをModelに変換している。
考え方の背景を説明しよう。データを保持するということとそのデータをもとに何かを判断するということでは、求められるデータ構造が異なる。
前者は重複がなく、極力1操作で状態を更新でき、矛盾しづらいデータ構造だ。
後者はそのオブジェクトに関することはなんでも知っている方がよく、データの重複は問題ではない。Twitterのフォロワー・フォロイーの話なら、AさんオブジェクトがBさんをフォローしているという情報を持っておりBさんオブジェクトもAさんにフォローされているという情報を持っていてもかまわない。
f(保持用のデータ) = 判断用のデータ
の関係になっており、判断用のデータは毎回保持用のデータから作成されそして使い捨てになっているといい。
Active Recordとかの話は長くなりそうなので省略した