アプリ内の日付変更線をズラす系の実装

例えば日記を書くときに、午前 2 時に書いたものは前日分としたいことがある。またユーザがメチャクチャ多いサービスでは、0:00 を回ったら翌日のログインボーナスを配る、としていると、まだユーザが多い時間にサーバの処理が要求されて大変なので、28:00 を日付変更線にしたいことがある。

こういうときには

module AppTime
  def self.beginning_of_day(time)
    t = time.change(hour: 4)
    t <= time ? t : t - 1.day
  end
end

を作って、

AppTime.beginning_of_day(Time.current)

を使うと「アプリ内の日付変更線では何日なのか」が取れる。

# 02:00 は前日扱い
time = Time.zone.parse("2021-01-31 02:00")
AppTime.beginning_of_day(time)
# => Sat, 30 Jan 2021 04:00:00.000000000 JST +09:00

僕は Date 苦手なので何でも Time で処理したい派だけど、こういうメソッドがあっても便利だと思う。

module AppTime
  def self.today
    to_date(Time.current)
  end

  def self.to_date(time)
    beginning_of_day(time).to_date
  end
end
# 02:00 は前日扱い
time = Time.zone.parse("2021-01-31 02:00")
AppTime.to_date(time)
=> Sat, 30 Jan 2021

また、「今日日記を書いたか?」や「今日ログインボーナスを受け取ったか?」を以下のようなメソッドを用意することで表現できる。

module AppTime
  # AppTime.all_day(Time.current) は以下の Range を返す
  # => Sun, 31 Jan 2021 04:00:00.000000000 JST +09:00..Mon, 01 Feb 2021 03:59:59.999999999 JST +09:00
  def self.all_day(time)
    beginning_of_day(time)..end_of_day(time)
  end

  def self.end_of_day(time)
    (beginning_of_day(time) + 1.day - 1.second).change(usec: Rational(999999999, 1000))
  end
end
class User < ApplicationRecord
  has_many :diaries
end

class Diary < ApplicationRecord
  belongs_to :user
  scope :of_day, ->(time) { where(published_at: AppTime.all_day(time)) }
end
user.diaries.of_day(Time.current).exists?
# Dairy Exists? (2.2ms)  SELECT 1 AS one FROM `diaries` WHERE `diaries`.`user_id` = 1 AND `diaries`.`published_at` BETWEEN '2021-01-30 19:00:00' AND '2021-01-31 18:59:59' LIMIT 1

肝は「DB には UTC で保存して、アプリ内では JST で扱う、というのは変えない」「日付変更線に沿った Time や Range を返すモジュールを用意することで、保存時に 04:00 を入れるし、検索時には 04:00〜04:00 (UTC では 19:00〜19:00) で検索する」です。

アプリケーション側で変換するのだと限界もあって、日記だと Habit Tracker っぽい (いわゆる「草」) 見せ方をしたいし、その実装に groupdate gem を使うことが多いと思うけど、そういうのはアプリ内日付変更線に基づいた日付カラムを DB に入れてしまうと楽だと思います。

エクスキューズ

ここから始めてリファクタリングして育てていきましょう、です。 この記事ではモジュールにしたけど、クラスにしてアプリ内日付変更線を持ったインスタンスを作っていくこともあるだろう。

all_day 難しい

ActiveSupport の実装に寄せて end_of_dayall_day03:59:59.999999999 を返して書いたけど、時間を区切るというのは半開区間でないと困るんですよ。

↑↑でも

`diaries`.`published_at` BETWEEN '2021-01-30 19:00:00' AND '2021-01-31 18:59:59'

SQL として見えてるけど、BETWEEN では表現したくなくて、

WHERE '2021-01-30 19:00:00' <= published_at AND published_at < '2021-01-31 19:00:00'

であって欲しい。

時間は連続なので、切断点はどちらかのみに所属しなければならない=閉区間だと表現できないのだ。詳しくはデデキントカットを見てくれ。何故かニコニコ大百科が詳しい。

dic.nicovideo.jp

じゃあどう実装すれば良いかで言うと、終端を含まない ... で作られた Range を渡すといい感じになります。

module AppTime
  def self.all_day(time)
    beginning_of_day(time)...beginning_of_day(time + 1.day)
  end
end
user.diaries.of_day(Time.current).exists?
# Diary Exists? (2.0ms)  SELECT 1 AS one FROM `diaries` WHERE `diaries`.`user_id` = 1 AND `diaries`.`published_at` >= '2021-01-30 19:00:00' AND `diaries`.`published_at` < '2021-01-31 19:00:00' LIMIT 1

おうちのスマートホーム化メモ

現状

各部屋のグッズはこんなモン。

  1. 玄関
    • 天井に人感センサー電球
    • 外には Netatmo Weather の室外モジュール
  2. 寝室
  3. 仕事部屋
  4. LDK

以上で

  • 照明
    • Philips Hue、Nature Remo、SwithBot 指ロボットを駆使して、すべて声が届くようになった
    • ほとんど Google Next Mini/Hub 経由で声で操作している
      • 物理スイッチの場所に移動する必要が無いのは Life Changing
  • エアコン
    • 付属のリモコンは収納の奥にしまって、スマートスピーカー経由か、スマホの Nature Remo アプリ経由で操作している
      • 最高に便利。リモコン探さなくて良いし、布団の中から操作できるのは Life Changing
    • Nature Remo で測った温度を Mackerel に送ることで、付いているかどうかをいつでも把握できるようになった
  • ホットカーペット
    • スマートスピーカー&スマートプラグ経由で操作している
      • 「ねぇグーグル、カーペット消して」
    • だいたい手を伸ばした方が早いが、布団に入った後に声で消せるのはかなり便利
  • テレビ
    • 「ねぇグーグル、おやすみ」で以下が発動するようにしている

とそれぞれを操作するようになった。

特に照明とエアコンは、スマホ or 声で操作できない状態には絶対に戻りたくないぐらい便利で良い。

ルーティンで操作するのもピタゴラスイッチを眺めているかのような気持ち良さがある。発話の必要すらなく全自動になるともっと便利なんだが、例えばベッドに感圧センサー仕込むとか、スマートウォッチの睡眠をトリガーにするとかは独り暮らしじゃないと上手くいかない。。2 人以上の生活空間では人感センサーは無価値だと思う。

次にやっておきたいこと

  • Philips Hue のシーンをスマートスピーカー経由で部屋ごとに設定したい
    • 「ねぇグーグル、月の光に設定して」と言うと 2 部屋とも暗くなる
    • 細かく調整するときはスマホ開いてる
  • 寝室、仕事部屋にスマートスピーカーが 1 つしか無い
    • 寝室の Google Nest Mini のみで操作しているので、仕事部屋のものを操作するのは少し面倒
      • 「ねぇグーグル、仕事部屋の照明を消して」的な操作になる
    • 部屋ごとにスマートスピーカーある状態にする方が自然だよなぁ。買い足すか。とはいえ置く場所とケーブル周りが……。壁に埋め込みたい
  • スマートロックもやりたかったんだけど、新型コロナウイルスの影響で外出すること自体が減ったのでメリットが少ない
    • GPS 連動で家の何かを弄ることも無い
  • タイマー等を用いて何かをプログラミングすることは今のところ需要がない……?
    • 規則正しい生活をしていない問題がな。。
  • 隣の部屋との会話をスマートスピーカー経由で行うのは便利そうだけど、まだ使いこなしていない
    • 移動してノックするよりも声かけやすい、はず
  • Wi-Fi を必要とする子が多すぎ問題
    • 30 台弱が 1 AP にぶら下がっている
    • そろそろメッシュ Wi-Fi 導入するか……?

センサー

Nature Remo 3 には温度、湿度、人感、照度センサーが付いているので、せっかくだし有効活用したいよね。

ということで雑に Mackerel に投げている。Lambda を EventBridge でスケジュール実行。

値を取得しだしたら CO2 濃度も測りたくなったので Netatmo WeatherStation も購入した。

CO2 濃度も、気圧変化も、アラートだけは投げてるんだけど自分の体は鈍感なのでそもそも影響が分かっていないが、測ること自体が目的なのでそれでいいのだ。GARMIN の body battery をライフログとして投げているので、そのうち突き合わせて眺めてみよう。

今年買ったもの2020

去年 に引き続き、今年買ったものコーナー。

はてなに入社して明らかに変わったのが「日常をブログに残すようになった」ことで、その中でも「今年買ってよかったもの」はついタグを追ってしまうし追ったら買っちゃうし経済がどんどん回ってしまう。よくない。

だいたい買った順です。

といった感じで現在の机はこんな。

まったく飾らずに今写真撮った。缶コーヒーは正月の巣籠もり用

机周りが一通り揃った (モニタ、椅子、キーボード、マウス) ので、来年は IYH することは減るはず、きっと、多分。

って去年言ったけど、モニタも椅子もキーボードもマウスも買ったね。そしてスマートスピーカーを起点にリモコンが全部発話で動かせるように変わった一年だった。

GitHub の新規リポジトリ作成時にサジェストされる名前

GitHub で新規リポジトリを作ろうとすると、リポジトリ名をサジェストされる。

f:id:onk:20201229005510p:plain
bug-free-robot めっちょ良い名前を引いた

docker contianer の命名規則を思い出した。

deeeet.com

せっかくなのでどんな名前が出てくるのか集めてみよう。HTML に含まれているので、スクレイピングの出番。

while :; do
  curl -H 'Cookie: user_session=xxxx' 'https://github.com/new' | ggrep -oP '(?<=<strong class="reponame-suggestion js-reponame-suggestion">).+(?=</strong>)'
  sleep 10
done

grep -oPワンライナーの頻出テクニック *1Macgrep-P に対応していないので brew install grep して使う。

これを動かすと以下のような名前が取れる。

didactic-engine
expert-potato
solid-giggle
furry-succotash
supreme-doodle
psychic-guacamole
crispy-doodle
probable-fiesta
sturdy-journey
stunning-octo-journey
glowing-journey
fuzzy-barnacle
laughing-octo-funicular
upgraded-parakeet
jubilant-engine
psychic-goggles
sturdy-octo-couscous
crispy-couscous
psychic-potato
congenial-sniffle

xxx(-yyy)*-zzz で構成されそう。

xxx の部分は super, upgraded, solid, shiny, fuzzy といった形容詞が並ぶ。

(-yyy)* の部分はほとんど octo。時々 palm, rotary, computing, duper, free。稀に super-octo-palm-tree のように 2 個出てくる。

zzz の部分は tree, phone, umbrella, machine, enigma 等、名詞っぽい。 特に何かに関連する名詞かどうかは分かんなかった。

意外と使われている。

ところで何のためにリポジトリを作ろうと思ってたんだっけ……。


300 件ほど集めたのでもうちょっと取れたデータ眺めてみたけど、bug-free は xxx-yyy ではなくこれで xxx のようだ。まぁ bug だけで接頭語にするとバグ量産してしまいそうだしね。また computing-machinepalm-treerotary-phone もこの組み合わせしか無いので yyy-zzz ではなく zzz 側っぽい。

すると yyy で octo 以外はあと duper だけなんだけど、これも supersuper-duper がそれぞれ xxx にありそうだ。

というわけで xxx(-octo)?-zzz という生成ルールなのではないか。

いやそれが分かったから何だって話だけど。

クエリパラメータのデリミタに ; を使うこともできる

本記事は、はてなエンジニア Advent Calendar 2020 の 18 日目の記事です。昨日は id:YaaMaa さんでした。

yaamaa-memo.hatenablog.com

社内チャットではこの話で盛り上がったときにトライ木も作られており、良い頭の体操になっていました。


さて、本題。

Hatena::Let を眺めていて、こんな URL に気づいた。

http://let.st-hatelabo.com/onk/let.iframe?code_id=g5G0uOeEqfcA;key=

クエリパラメータにセミコロン……!

パッと考えるとこれは

{
  code_id => "g5G0uOeEqfcA;key="
}

となりそうで、というか Ruby で実際にパースするとそうなる。

uri = URI("http://let.st-hatelabo.com/onk/let.iframe?code_id=g5G0uOeEqfcA;key=")
URI.decode_www_form(uri.query)
#=> [["code_id", "g5G0uOeEqfcA;key="]]

「こんなカッコ良くもない URL で独自規格作らないでよ」が最初の感想だったんだけど、ソースコードを読むと普通に $r->req->param('code_id')$r->req->param('key') で取得している。

param の実装である Plack::Request::_query_parameters を見に行くと

sub _query_parameters {
    my $self = shift;
    $self->env->{'plack.request.query_parameters'} ||= parse_urlencoded_arrayref($self->env->{'QUERY_STRING'});
}

となっていて、WWW::Form::UrlEncoded::parse_urlencoded_arrayref がパーサ。パーサの実装は

if ( src[i] == '&' || src[i] == ';') {

https://github.com/kazeburo/WWW-Form-UrlEncoded-XS/blob/0.26/lib/WWW/Form/UrlEncoded/XS.xs#L246

&; の両方をクエリパラメータのデリミタとしているし、テストコードにも含まれている!

'a=b&c=d'     => ["a","b","c","d"]
'a=b;c=d'     => ["a","b","c","d"]
'a=1&b=2;c=3' => ["a","1","b","2","c","3"]
'a==b&c==d'   => ["a","=b","c","=d"]
'a=b& c=d'    => ["a","b","c","d"]
'a=b; c=d'    => ["a","b","c","d"]
'a=b; c =d'   => ["a","b","c ","d"]
'a=b;c= d '   => ["a","b","c"," d "]
'a=b&+c=d'    => ["a","b"," c","d"]
'a=b&+c+=d'   => ["a","b"," c ","d"]
'a=b&c=+d+'   => ["a","b","c"," d "]
'a=b&%20c=d'  => ["a","b"," c","d"]
'a=b&%20c%20=d' => ["a","b"," c ","d"]
'a=b&c=%20d%20' => ["a","b","c"," d "]
'a&c=d'       => ["a","","c","d"]
'a=b&=d'      => ["a","b","","d"]
'a=b&='       => ["a","b","",""]
'a=&'         => ["a","","",""]
'&'           => ["","","",""]
'='           => ["",""]
''            => []

https://github.com/kazeburo/WWW-Form-UrlEncoded-XS/blob/0.26/t/01_parse.t#L26

ふえぇぇって思ってググるQUERY_STRING 中のパラメータの区切りは必ずしも '&' ではない - 理系学生日記 に行き着いて、

We recommend that HTTP server implementors, and in particular, CGI implementors support the use of ";" in place of "&" to save authors the trouble of escaping "&" characters in this manner.

https://www.w3.org/TR/1999/REC-html401-19991224/appendix/notes.html#h-B.2.2

という記述を見つけた。

えーーじゃあ Ruby でさっきダメだったのは何!!!って話になったのでまず Rack を見に行ったら id:Nyoho さんが 2015 年に PR 出していてW3C の同じ記述を参照しながら

Fix to use semicolons as separators for GET not for POST.

と GET リクエストのときのパース時のみ ; でも分割できるように修正している。当時 tDiary にも Issue が上がっていて

http://www.tamoot.net/d/?year=2015;month=1Q;category=Ruby

という URL がパースできなくなったトノコト。なるほど、tDiary にもセミコロン区切りがあったのね。CGI 時代の歴史的経緯っぽい香りがする。

閑話休題。「Rack は通すんじゃん」って思いつつ URI.decode_www_form の実装を見に行くと

# This refers http://url.spec.whatwg.org/#concept-urlencoded-parser,
# so this supports only &-separator, and doesn't support ;-separator.

https://github.com/ruby/ruby/blob/v2_7_2/lib/uri/common.rb#L443-L444

とわざわざ「; は対応しないよ」ってコメントが書いてある。

更に blame すると

lib/uri/common.rb (URI.decode_www_form): follow current URL Standard.

- # This refers http://www.w3.org/TR/html5/forms.html#url-encoded-form-data
+ # This refers http://url.spec.whatwg.org/#concept-urlencoded-parser ,
+ # so this supports only &-separator, don't support ;-separator.

https://github.com/ruby/ruby/commit/4a50d44

とのことで、 WHATWGapplication/x-www-form-urlencoded の仕様に則ったらしい。

http://url.spec.whatwg.org/#concept-urlencoded-parser

こちらでは & のみがデリミタと定義されている。

というわけで、application/x-www-form-urlencoded で POST したときは仕様があるけど、URL では query string のパース方法までは決まっていないんじゃないかなぁ。*1

; を使っても良いと解釈できずに URL Escape してしまうクローラがいる等の弊害もあり、今となっては敢えて使う必要はなさそう *2 なのでただの雑学ですが、最近見た 10 年モノの面白コードの話でした。


はてなエンジニア Advent Calendar 2020、明日 19 日目は id:Pasta-K さんです。

*1:この辺りはまだ調べ切れてないんだけど、RFC 3986 には書いてなかった

*2:なので直しました

Smart UI パターンが再評価される世界

設計ナイト2020 を受けて、今どんなアーキテクチャを選ぶべきかという話をしたくなったのだ。

kichijojipm.connpass.com

お前誰よ

をやってきた人間。数百万〜数億行のデータ、月間数千万〜数十億 imp 程度を主戦場にしています。

今日の話

昂ぶって 15,000 字ぐらい書いてしまった。

DDD と PofEAA から学ぶパターン/アンチパターン

DDD も PofEAA も 2002 年出版だけど、ほとんど今でも通用する話なので、まずココを出発点とするのが良い。

利口な UI (Smart UI) アンチパターン

  • エリック・エヴァンスのドメイン駆動設計に書いてある
  • KENT WEB 時代ぐらいの PerlPHPCGI を思い出すと良いはず
    • 画面ごとの .cgi ファイルに、上の方にデータロードのコードが、下の方に UI のコードが書いてある。何なら混ざり合ってる
  • もちろん良いこともある
    • 画面ごとに分かれているので、影響が局所化される

minekoa.hatenadiary.org

これを少し改善したものが トランザクションスクリプト

f:id:onk:20201111020809p:plain

  • 分かりやすいのは ISUCON のコード
    • template とは分離された、依然として手続き的なコード
    • 各 Action 間でのコピペは引き続き横行している

PofEAA でのデータソースのアーキテクチャに関するパターン

第10章 データソースのアーキテクチャに関するパターン

テーブルデータゲートウェイ

https://bliki-ja.github.io/pofeaa/TableDataGateway/

SIにいた人なら「ダオ」のほうが通りが良いのではと思います。テーブルデーゲートウェイなんて現場で聞いたことが無い

より良いトランザクションスクリプトを目指す - enrike3のブログ

  • ほぼ Table 単位
  • すべての CRUD はこのゲートウェイを通る
  • SQL や、クエリビルダの組み立てが書かれるクラス

行データゲートウェイ

https://bliki-ja.github.io/pofeaa/RowDataGateway/

私は、トランザクションスクリプトを使用する場合に、行データゲートウェイを使用する頻度が最も高い。この場合、行データゲートウェイでデータベースアクセスコードを適切に抜き出し、別のトランザクションスクリプトで容易に再使用できるようにする。 私は、ドメインモデルを使用する場合には行データゲートウェイを使用しない。シンプルなマッピングを実行する場合には、コードレイヤを追加しなくても、アクティブレコードが同じ役割を果たす。

...

トランザクションスクリプトを行データゲートウェイとともに使用する場合、複数のスクリプトで繰り返されるビジネスロジックこそが、行データゲートウェイに必要なロジックであることがわかるだろう。ロジックを移動することによって、行データゲートウェイは段階的にアクティブレコードへと変化し、ビジネスロジックの重複を軽減する効果をもたらす。

(PofEAA より

アクティブレコード

https://bliki-ja.github.io/pofeaa/ActiveRecord/

データマッパー

https://bliki-ja.github.io/pofeaa/DataMapper/

  • レイヤー化しようと思うとデータマッパーパターンになりがち
    • だけど、密結合してエイッてやるともっと楽だよというのが Rails が示した道 (後述
  • 各レイヤー間を疎結合にしようとすると DTO による詰め直しが必要になる

この辺りは一昔前 (2000年代前半) に SSH (Struts, Spring, Hibernate) アーキテクチャが流行っていた頃を思い出す。Hibernate は二次キャッシュで DB アクセスを隠蔽して、例えば

# 概念コード
my $entry_1 = EntryRepository->find_all_by_author_id(author_id => 1)->[0]
my $entry_2 = EntryRepository->find_by_id(id => 2)

で読み込む $entry_1, $entry_2 はメモリ上も同じインスタンスであるべきという考え方で、片方に変更を加えると (同じインスタンスであるので) もう片方にも影響する。

  • 同じデータなんだから 1 つなのは当然だし、レースコンディションも絶対に発生しない
  • DB からロード済みだと SQL は発行しない (Identity Map を持つ)、というのをデータマッパー上でできるので、うまく使うとパフォーマンスが向上する
    • 我々の道具で言うと Apollo Client のキャッシュに似たイメージ

PofEAA や DDD で語られたパターンの詳しい話は texta.fm で id:t-wadaid:Yasaichi が話しているのでぜひ聞いてください!

anchor.fm

話はトランザクションスクリプトに戻る

  • Smart UI パターンよりモデル化していきやすいので、何らかのコードをトランザクションスクリプトに抜き出すのは推奨されている
  • どういうレイヤー分けを行うかは正解がない

トランザクションスクリプトをどこに置くかは、レイヤをどのように体系化するかによって異なる。

...

トランザクションスクリプトを複数のクラスに体系化する方法は2つある。 最も一般的な方法は、複数のトランザクションスクリプトを1つのクラスに入れ、各クラスが関連するトランザクションスクリプトの対象エリアを定義する方法である。この方法は最も簡単で一般的な手法である。 もう1つの方法は、トランザクションスクリプトごとに独自のクラスを持たせ、「コマンドパターン」を使うというものだ。

(PofEAA より

ドメインロジックを構築する方法 3 種類

ドメインロジックを構築する方法は以下の 3 つが PofEAA に書かれている。(第2章 ドメインロジックの構築)

それぞれのコストはこう図示される

f:id:onk:20201110231849p:plain

ドメインで扱う概念の中には、1つの機能や処理が単体で存在していて、もの(オブジェクト)として扱うのが不自然なものもある。そうしたものは、サービスという形でユビキタス言語に組み込む。サービスは基本的に状態をもたない(stateless)。

[ 技術講座 ] Domain-Driven Designのエッセンス 第2回|オブジェクトの広場

 

ドメインにおける重要なプロセスや変換処理が、エンティティや値オブジェクトの自然な責務でない場合は、その操作は、サービスとして宣言される独立したインターフェイスとしてモデルに追加すること。モデルの言語を用いてインターフェイスを定義し、操作名が必ずユビキタス言語の一部になるようにすること。サービスには状態を持たせないこと。

...

このコードを見てもらうと、"なんだ ドメインモデルを入出力にとる関数じゃないか" と思うでしょう。そのとおりです。

混乱しがちなサービスという概念について - かとじゅんの技術日誌

ここまでのまとめ

  • もっともコピペが横行していて、その分労働集約的に並列作業できる、他には影響を与えずに改修できるのが Smart UI パターン
  • データロードと Template を分離するトランザクションスクリプト
    • まずは ISUCON コードを思い浮かべると良い
    • 各 Action の中を設計しようと思うと、無限の可能性が広がっている。実装のレールは (PofEAA には) 無い
  • データと振る舞いを 1 箇所に集めようとしているのが「ドメイン

はい。まだ 1/3 ぐらいだよ!

Rails によって発見された、密結合で速く走れるソフトウェア

和田:Ruby on Rails自身は疎結合の設計に対してNoを言っている。密結合にすることによって疎結合な設計以上の開発スピードが生まれる。少なくともスタートアップ企業にとってスピードは本当にクリティカルな力なので、もし密結合の状態でも速く走れるソフトウェアの構造があるのであれば、それはゆっくり安定して継続的に歩いていく疎結合のソフトウェア設計より強いということをRailsはある程度証明していたわけですね。そしていま、その構造のまま大きくなるとすごく大変になるということも証明している。

マニアが潰したテスト駆動開発〜『健全なビジネスの継続的成長のためには健全なコードが必要だ』対談 (5) | by Takeshi Kakeda | 時を超えたプログラミングの道

DHH がどのように密結合を作り上げていったのか、については以下のスライドが詳しい。

speakerdeck.com

  • RESTful ルーティングと ActiveRecord パターンによって、URL で表されるリソースから DB 上のテーブルまでが密結合する構造を作った
  • ActiveRecord パターンとその Validations/Callbacks によって、ビジネスロジックとその組み立て処理を全て Model に書けるようにした

この密結合は、間違いなく最速の設計技法である。じゃないとあんなにスタートアップ界隈で採用されなかったし、十分にワークすることは歴史が証明している。

RailsMVC が標準となっていった歴史は最近だとここでも語られている。

この頃に同時に起こったのがRuby on Railsに代表されるLLの躍進とそれに伴うテンプレートエンジンの簡素化です。

これによりASP.NETJSFは所謂Web界隈と呼ばれるようなコンシューマよりへの拡大はもちろん、主戦場であるエンタープライズ領域すらLL言語にフロントエンド系を中心に浸食されていきました。

またこれらのFWは細かい理由は知りませんが結果的にコンポーネント指向ではなく、シンプルなMVC Model 2を採用しておりテンプレートエンジンはループや条件分岐、変数をバインディングしたりレイアウトを作れる程度の簡素なものでイベントドリブンなどは採用さていません。一部、ClickやWicketなんかは採用していましたが、まあ普及していませんし?

WASMとRustはVue.js/React.jsを打倒するのか? - JSへの侵略の歴史

僕の感覚としても、シンプルに Rails をベースラインにしていると幸せ。

このコードを見てもらうと、"なんだ ドメインモデルを入出力にとる関数じゃないか" と思うでしょう。そのとおりです。最初はその程度の認識でよいと思いますが、ここで一点だけいいたいのは、乱用は禁止ということです。

...

従属するエンティティや値オブジェクトがないということで早期あきらめてしまい、なんでもかんでもドメインサービスにするというのもの違うのです。後者の場合は、振る舞いがあるべきドメインモデルから振る舞いを奪うことになるので、ドメインモデル貧血症の温床になる可能性があるのです。

混乱しがちなサービスという概念について - かとじゅんの技術日誌

素朴な密結合 MVC では限界がある

Rails は最速ではあるが、「その構造のまま大きくなるとすごく大変になる」フレームワークでもある。

素朴な MVC では限界があるというのを皆が発表している。

Rails は少人数スタートアップで小〜中規模 *1 なアプリケーションを作るために最適化されたフレームワークなので、ギャップはある。

ただ僕の体感としては、複雑なのはごく一部 (10%程度) で、ほとんどの要件はシンプルに扱える。

複雑さをベースにしたアーキテクチャは不当に難しい。シンプルな要件のときに大仰に見せたくないので、複雑なところは例外的に見えるようなアーキテクチャであると良い。

本当に複雑なものと、複雑ではあるが工夫で対処できるもの

ActiveRecord を前提として、解決方法はいくつも語られてきた

また、「アプリケーションサービス」はよく導入される(が、間違えやすい)

トランザクションスクリプトの延長にあるこの Service クラス (コマンドパターン) で戦う方法がよく採られている。

間違えないように Operation に持って行く=Trailblazer

そもそものはじまりは、作者のNick Sutterer氏がRailsMVC抽象レイヤーのあり方に疑問を持ったこと。Railsの手軽さを認める一方、ModelやControllerの肥大カオス化により、のちの保守性が下がることを問題視されたそうです(Nickさんの本意訳)。

TrailBlazer概要まとめてみた - Qiita

概要は Rails のアーキテクチャ設計を考える - Qiita を読むと掴みやすいかもしれない。

Trailblazer: Operation Overview

  • Model の validation に当たるものを contract に持っていく
  • Operation の中身は本質的に手続き的なものなので step で処理する
    • このときに step の連続である=state を持っている、という点も扱いやすさに繋がっている
    • クラスメソッドではなく、インスタンスを作って状態を持つと楽になる、というのが PofEAA でも語られている。

この手法のメリットは、スクリプトインスタンスを実行時にオブジェクトとして扱える点であるが、トランザクションスクリプトを使ってドメインロジックを体系化するようなシステムでは、このメリットを活かす必要性はほとんどない。もちろん多くの言語では、クラスを完全に無視してグローバル関数だけを使うこともできる。しかし、新たなオブジェクトをインスタンス化することでスレッドの問題が解決できる場合もある。データの分離が簡単になるからだ。

(PofEAA より

間違えないように Interactor に持って行く=Hanami(=Clean Architecture)

Hanami is based on two principles: Clean Architecture and Monolith First.

Architecture: Overview | Hanami Guides

Architecture: Interactors | Hanami Guides

Overview の次に Interactors の章が置かれる程度には Interactor が主軸なフレームワーク

Interactor も Operation と同じく initializecall のみを持つクラスである。Iterator の考え方は HanamiはRubyの救世主(メシア)となるか、愚かな星と散るのか を読むと良いかな。

イマイチ流行らなかった rectify

https://github.com/andypike/rectify

  • FormObject, Command パターンがあるのはもう前提として、僕は書き味の話をしたい!
  • Controller が Controller に必要な本質だけを追究できる世界

Controller に必要な本質は、これも PofEAA に書かれている。

ドメインロジックの扱いで最も難しいのは、人々が思っているように、何がドメインロジックで何が他のロジックかを見極めることだろう。私が好きな非公式のテストは、Webアプリケーションにコマンドラインインタフェースを追加するときのように、まったく異なるレイヤをアプリケーションに追加することを想像するというものである。この追加を行うときに機能を複製する必要があれば、それはドメインロジックがプレゼンテーションの中にはみ出していることを示している。

(PofEAA より

Web アプリケーションとしてのインタフェース (View 以外) が Controller の役目で、「同機能の CUI コマンドを作るときに重複が無い」という基準で考えると良い。

rectify を使ったときの Controller は

def create
  @form = RegistrationForm.from_params(params)

  RegisterAccount.call(@form) do
    on(:ok)      { redirect_to dashboard_path }
    on(:invalid) { render :new }
    on(:already_registered) { redirect_to login_path }
  end
end

と、ビジネスロジックで起きたイベントを on で捕捉して、redirect や render を行う。これは CUI にするときにまったく重複しないまさしく Controller の役目だし、見た目も美しい。

とまぁ僕は好きなんだけど、ガッツリ実務で使ったわけではないので見た目の美しさにのみ囚われている可能性はある。

いずれも Controller or Action と 1:1 対応する新しい層 という考え方

本当に複雑なもの

以上、だいたいのことは工夫で解決できる程度の複雑性、と置いた。じゃあ本当に複雑なものは何かというと、SoR だろうと思う。

ただ僕の目には SoE=モード2 で十分なものがほとんどに見えている。

すべて SoE 的手法でもいけるんじゃないか

https://speakerdeck.com/naoya/system-of-record-to-system-of-engagement#25

バランスを欠いていたと id:naoya は振り返っているが、p20 の表にあるようなシステム領域はあまり普段の開発=イテレーションを回しながら不確実性に対処していく中には出てこないので、だいたい SoE と捉えていくのが Web アプリケーション開発の実情じゃないかな。

今求められているアーキテクチャ

SoE の戦場はクライアント側に移っている。

  • ビジネスにおいて、(ネイティブ or Web SPA) クライアントの UX が必須な時代なので、 API 開発の手綱もサーバからクライアントに移していくような流れが生まれています
  • GraphQL も BFF も、クライアント側で制御していこうという発想
    • 新しい概念「クエリ」を入れたのが GraphQL。クライアントはともかく、サーバは新規開発になる
    • クライアントごとの中間レイヤーを作ろうというのが BFF。途中からでも導入しやすい

The NEXT of REST - onk.ninja

【エンジニアブログ】ダイニーのエンジニアリング3カ条|dinii(ダイニー)公式|note では Hasura を用いると GraphQL の query 側はほとんど開発が要らないという話をしている。

「フロントエンド領域」を再定義する - Speaker Deck でもフロントエンジニアの領域がサーバ側に広がっている。 GraphQL や BFF をフロントエンジニアが扱うことで、ブラウザがバックエンドからデータを取得する、データソースのアーキテクチャはフロントエンジニアのものになった。

Builderscon Tokyo 2019 で「ビジネスの構造を扱うアーキテクチャとユーザとの接点を扱うアーキテクチャ」というタイトルで登壇してきました #builderscon - assertInstanceOf('Engineer', $a_suenami) では

プレゼンテーション優位な技術駆動アーキテクチャを選択する場合は、むしろ立派な実装パターンであると言える。

 

「DDDのスマートUIアンチパターン」もSoEの目的に合わせて使えばデザインパターンになる

という話をしている。

まさに Smart UI パターンは再評価されるポイントに来ている。クライアントが、表示の都合でクエリを都度書く世界になった。

  • フロントエンドではコンポーネントの作り方が確立されている
  • データマッパーは Apollo Client のキャッシュが上手いことやってくれる

ので、Smart UI の痛みも少なく実装することができる。

そしてバックエンドは、SoE では ActiveRecord で表現できないほど複雑な「ドメイン」は無いと位置づけて良く、サーバサイドの仕事は GraphQL の schema を提供したら loader を考えるだけの仕事になっている、というのがイマココです。

(誤解がありそうなのでちょっとだけ言っておくと、GraphQL の schema を考える仕事は RESTful API の schema を考える仕事とほとんど変わんない感覚です。「DB を露出している」と捉えるのは筋違いかな)

Mutation 側は?

Mutation はアプリケーションサービスと非常に親和性が高く、コマンドパターンの Service クラスを無思考で作ってしまいがち。

ただ今までの話であったように、安易に Service に全てを押し込めるとドメインモデル貧血症に陥ります。ActiveRecord の機能をちゃんと使って、適切に validation, callback を使った上で、それでも複雑だから Service クラスが必要になったというのが歴史です。まずはオブジェクト指向的に育てることを忘れないでください。

シンプルな Muation はシンプルに扱える。もしナイーブに実装し過ぎたとしても、改善方法が確立されているので大丈夫。

なんか言及する隙が無かったものあれこれ

*1:複雑さは色んな指標があると思うけど、雑な一つの基準として、100 テーブル以内ぐらいだと考えると良いと思う

PK/Unique KEY が UUID v4 だと INDEX 作成効率が悪い

zenn.dev

プライマリキーにUUIDを採用した場合のINSERT時間のペナルティ

っていう話があったので、へーって思って見てみた。対象の MySQL はただ docker run mysql:8.0 したヤツです。

string が主キーで、PK があるものと無いもの、UUID v4 と v1 とで比べれば良かろう。(v1 は値が偏るので)

create table bar(id varchar(255) not null, primary key(id));
create table baz(id varchar(255) not null, primary key(id));
create table qux(id varchar(255) not null);
INSERT_SQL = <<~SQL
INSERT INTO %s(id) values
  (?), (?), (?), (?), (?), (?), (?), (?), (?), (?),
  (?), (?), (?), (?), (?), (?), (?), (?), (?), (?),
  (?), (?), (?), (?), (?), (?), (?), (?), (?), (?),
  (?), (?), (?), (?), (?), (?), (?), (?), (?), (?),
  (?), (?), (?), (?), (?), (?), (?), (?), (?), (?),
  (?), (?), (?), (?), (?), (?), (?), (?), (?), (?),
  (?), (?), (?), (?), (?), (?), (?), (?), (?), (?),
  (?), (?), (?), (?), (?), (?), (?), (?), (?), (?),
  (?), (?), (?), (?), (?), (?), (?), (?), (?), (?),
  (?), (?), (?), (?), (?), (?), (?), (?), (?), (?)
SQL

COUNT_NUM = 50_000; # 回 *100 行の INSERT

def execute(table_name, &block)
  COUNT_NUM.times do |i|
    puts "#{table_name}\t#{i}\t#{Time.now.to_i}" if i % 1000 == 0 # 10万行 INSERT ごとに時間を出力
    db.xquery(INSERT_SQL % table_name, *100.times.map{ block.call })
  end
end

uuid = UUID.new
execute("bar") { SecureRandom.uuid } # UUID v4
execute("baz") { uuid.generate }     # UUID v1
execute("qux") { SecureRandom.uuid } # UUID v4 PK なし

100 行ずつ 5 万回 INSERT、1000 INSERT (=10 万行)ごとに時間を取得しました。

結果をグラフにするとこう。横軸が総 INSERT 行数、縦軸は 10 万行の INSERT にかかった秒数。

f:id:onk:20201013163946p:plain

へー。たしかに 100 万レコードを過ぎた辺りから徐々に遅くなってるのが分かる。 もうちょっと右側 (数千万レコード級) でどうなるかも見てみたい……。

UUID 文字列を使うのが間違い (例えば UUID_TO_BIN() 使えよとか) という話もなくはないし、1 行あたりだと 1ms 切るので数千万程度なら大して気にはしないと思うけど、とりあえず。

自分では snowflake 系に慣れきっちゃってるので、コレを見てもなんともだなぁ。

おもしろかった。