前巻の終わりで、帝国からの刺客を返り討ちにしました。 そっから帝国に占領された王都に逆襲しに行って、表紙の通りボスっぽいキャラとの対決まで行きます。 お話がサクサク進んでいきます。 伏線を必然的な演出っぽく埋め込むの上手いです。
人はFat Modelを恐れサービスを求め ドメインモデルは貧血に至る
この文章は祈りです。 主にRuby on Railsアプリケーションを想定した話です。
Ruby on Railsアプリケーションでは、Fat Model問題という問題が起きることがあります。 ドメインオブジェクトが肥大化しメンテナンスしにくくなる問題です。 Fat Model問題に対応するためにサービスレイヤーを導入することがあります。 「ドメインモデル貧血症」と呼ばれているアンチパターンです。
ドメインのロジックをドメインオブジェクトの中に入れないという設計ルールに従っているのでしょう。その代わり、すべてのドメインロジックを含むサービスオブジェクト群が存在しているのです。
Fat Modelを恐れよ
Fat Modelは「単一責任原則」を満たしていないモデルです。
1つのサブシステムやモジュール、クラス、関数などに、変更する理由が2つ以上あるようではいけない
重要なのは「変更する理由が2つあるということは、機能変更のためにあるクラスを変更すると別の機能が壊れる可能性がある」です*1。 Fat Modelの問題は行数の多さではありません。 複数の責務を持つため、あるドメインオブジェクトへの変更が、変更したくない機能にも影響を与える可能性があることです。
Fat Modelがあるとアプリケーションの変更が難しくなります。
地獄への道は善意で舗装されている
サービスを求める
どうすればFat Model問題を解消できるでしょうか? モデルが複数の責務を持っているのが問題です。 責務をモデルの外に出してしまいましょう。 各責務を担当するサービスオブジェクトを定義し、ロジックをサービスオブジェクトに移動します。 1つのサービスオブジェクトは1つの責務を持ち、単一責任原則を満たします。 すべてのFat Modelはなくなります。
レイヤーを得る
人間はサービスオブジェクトを見るとレイヤーだと認識します。 なぜかはわかりません。 サービスオブジェクトの群れとドメインオブジェクトの群れを別のものだと認識し、そこに境界を求めるのかもしれません。 サービスオブジェクトが、コントローラーとドメインオブジェクトに挟まれているのがレイヤーと思わせるのかもしれません。 レイヤーだと認識した人類は「レイヤーは薄くあるべき」と考えます。 サービスオブジェクトからサービスオブジェクトを呼び出すことを避けはじめます。 つまりサービスオブジェクトの処理を他のサービスオブジェクトに「委譲」しなくなります。
抽象を失う
「委譲」しなくなった結果、サービスオブジェクト間の共通の処理はどうやって共通化するでしょうか? 「継承」です。 Ruby on Railsの場合はConcernを使って処理を共通化します。 ConcernはRubyのMix-inという多重継承のための仕組みをRuby on Railsアプリケーションで使いやすくしたものです。 オブジェクト間にis-a関係が無いのに実装上の都合で継承を使います。 人間の認知における抽象-具象の方向と継承の方向が合わなくなります。 複数のサービスオブジェクトを抽象的なサービスオブジェクトとして認識できなくなります。
オブジェクトは去る
レイヤーの認識はコントローラーとの近さからサービスオブジェクトをデータより遠く手続きに近いものだと認識します。 そしてサービスオブジェクトに「○○Service」という名前を与えます。 ひとたび「○○Service」という名前を得れば、それは手続きです。 サービス業が「もの」を売らないように、サービスが「もの」を持つはずがありません。 「サービスオブジェクトは手続きをもつ、ドメインオブジェクトが状態を持つ」ルールを導入します。 データと振る舞いを分割してします。 active recordパターンを使ってオブジェクト指向でモデリング出来るようにしたのに、利点を捨てました。
具象の林 現る
「サービスオブジェクトは手続き」だと認識した人類は、「サービスオブジェクトはただ一つの静的なパブリックメソッドを持つ」ルールを導入します。 サービスオブジェクトは「もの」ではなく「行為」になります。 これがイベントとしてモデリングしてデータベースにしまえるものなら良かったのですが・・・そうではない処理の羅列が生まれます。 サービスオブジェクトに適切な名前を与えて、を抽象化して認識することを難しくします。 あるサービスオブジェクトが何をするオブジェクトなのかを認識するには、ソースコードを読んで処理を追いかける必要があります。 ひたすら具象と向き合います。
林は深く
サービスオブジェクトが具象としてしか認識出来ないのであれば、2つのサービスオブジェクトの共通はどうしたらわかるでしょうか? 一行一行比較していけばわかります。 10あるサービスオブジェクトが共通の処理を持つかどうか探し出せるでしょうか? できないのです。 新しいサービスオブジェクトを追加するときに、既存のサービスオブジェクトが使えるのかどうかわかりません。 このとき大変面白いのは、読みにくいソースコードほどコピペされます。 機能をもとに似たサービスオブジェクトを探します。 そこに読みやすいソースコードが書かれていれば共通化の努力をするでしょう。 読みにくいソースコードが合ったらどうするでしょうか? コピペしてわかるところだけ修正します。
データを覆い隠す
あるサービスオブジェクトがリレーショナルデータベースのどのテーブルを操作しているかわかるでしょうか? 処理を追いかけて、どのドメインオブジェクトを参照しているか確認します。 さらに参照してるドメインオブジェクトのうちどれが参照のみでどれが変更しているか分類していく必要があります。
さて、リレーショナルデータベースのテーブルを変更したくなりました。 どのサービスオブジェクトに影響がでるかわかりますか?
Fat Modelを恐れドメインオブジェクトの責務を「機能」と認識した結果人類は、複数のテーブルに責務をもつサービスオブジェクトを作り出してしまいた。 そこはサービスオブジェクトの群れるサービスレイヤーという新たな地獄でした。
人はどうすれば救われるのでしょうか?
天国は善行で満ちている
実は「単一責任原則」を満たしていなくても、1クラスに2つくらい責務があっても、変更前に影響範囲を調べればなんとかなります。 そうはいってもアプリケーションに機能を追加していくと、クラスの責務は増えていきます。
どういうわけか特定のクラスだけ、責務が増えていく傾向があります。 世の中で挙げられている例では、UserクラスやEmployeeクラスが多いです。 実際のアプリケーションのFat Model問題では、ほとんどのドメインオブジェクトはFatになりません。 特定のドメインオブジェクトだけが肥えます。
Fat Model問題に必要な対策は、Fat Modelになっているドメインオブジェクトを特定し、分割することです。 分割には「委譲」を使います。 Fat ドメインオブジェクトのソースコードをにらみつけます。 すると、小さなデータと振る舞いの塊がうごめいているのが見つかります。
消えていなくならないうちに、さっと捕まえます。 そいつを新しいドメインオブジェクトとして抽出します。 「あとでやろう」と考えたら、Fatドメインオブジェクトの中に溶けていき、消えます。
Ruby on Railsでは、ドメインオブジェクトはリレーショナルデータベースのテーブルと対応していることが多いです。 ドメインオブジェクトからデータを分割しようとしたら、テーブルの分割が必要になります。 ここは歯を食いしばってマイグレーションします。 たいていはデータのマイグレーションも必要です。歯を食いしばってマイグレーションします。
こう書くと、Ruby on Railsがドメインオブジェクトとリレーショナルデータベースのテーブルを対応させているから、サービスレイヤーが生まれるように思えるかもしれません。 2003年に「ドメインモデル貧血症」はすでに存在しています。 Ruby on Railsは2004年生まれです。
参考
- 単一責任の原則(Single responsibility principle)について、もう一度考える | オブジェクトの広場
- 【SOLID原則】単一責任の原則 - SRP
- 単一責任原則で無責任な多目的クラスを爆殺する - Qiita
- よくわかるSOLID原則1: S(単一責任の原則)|erukiti|note
- コードの臭い - Wikipedia
- 委譲 - Wikipedia
- 継承のことは忘れよう - オブジェクト指向プログラミングを極める
*1:「単一責任原則」は良い設計の指標と考えても理解できませんでした。満たして居ないことが「コードの臭い」となる、と理解しました。
Romeは本当に速かった
RomeプロジェクトのJavaScriptフォーマッターがリリースされました。
ちょうどPrettier を使っているプロジェクトがあったので比較してみました。
本当にめちゃくちゃ速い。
— ぎゃばん@手洗い (@ledsun) April 6, 2022
prettierで8秒掛かるのが0.5秒とかで終わる。
prettierを速く動かしたくてparallel-prettierに.prettierignoreを読む修正を加えて動かして速くなったか - Qiita でNode.jsのまま並列化したparallel-prettierを試したときは
やったね1.25倍速くなりました!
でした。 それと比べてるとRomeは10倍以上速くなっています。 驚異的です。 JavaScriptパーサーやASTを扱うライブラリーなどの資産がないrustで書き直すのは、大変な労力に思えます。 それに見合う性能向上がはかれるようです。
今後の成長が期待大なプロジェクトです。
rails ujsを学ぶ
一昔前のRailsではJavaScriptを簡単に使う機能としてrails ujsがありました。 Rails ガイドにも説明があります。
「UJS: Unobtrusive(控えめな)JavaScript」
主な用途は
- JSの処理で確認ダイアログとして「マジでログアウトすんのか?」という表示をする
- JSの処理でログアウトのために「deleteメソッドとして」HTTPのPOSTリクエストをする
なんだかんだ言って、削除ボタンとか確認ダイアログとか簡単に実装できて便利です。
JavaScriptとして動く部分の実装は https://github.com/rails/rails/tree/cfa728478935dc48d9d6e2463dc500ffafee802b/actionview/app/assets/javascripts/rails-ujs/features にあります。 たかだか300行のCoffeeScriptです。 読もうと思えば読めそうです。 今回は読みません。
https://github.com/rails/rails/tree/cfa728478935dc48d9d6e2463dc500ffafee802b/actionview/app/assets/javascripts がプロジェクトのルートです。
npmパッケージを作っているのは、もう少し上の https://github.com/rails/rails/tree/cfa728478935dc48d9d6e2463dc500ffafee802b/actionview です。依存しているGemの定義がactionview.gemspecに書いてあるからだと思います。
ビルドするにはnpm run build
を実行します。
次のように環境を設定します。
git clone git@github.com:rails/rails.git cd rails bundle install --without db cd actionview/
次のようにnpm run build
を実行するとビルドできます。
ledsun@MSI:~/r/actionview[1]►npm run build > @rails/ujs@7.1.0-alpha build > bundle exec blade build Building assets… [created] lib/assets/compiled/rails-ujs.js npm notice npm notice New minor version of npm available! 8.3.0 -> 8.6.0 npm notice Changelog: https://github.com/npm/cli/releases/tag/v8.6.0 npm notice Run npm install -g npm@8.6.0 to update! npm notice
CoffeeScriptのビルドには GitHub - javan/blade を使っています。
manifest.compile(logical_paths)
def manifest @manifest ||= Sprockets::Manifest.new(environment.index, compile_path) end
結局Sprocketsを呼び出しています。 https://github.com/rails/sprockets/blob/3aa96f7499eb3043eb686d918a308152f658bb53/lib/sprockets/manifest.rb#L161 の辺りですが、Sprocketsは訳わからないっす。
https://github.com/javan/blade/blob/main/blade.gemspec#L27
spec.add_dependency "coffee-script"
bladeの依存関係にcoffee-script gemが入っています。 なんらかの魔法の力によって、これが呼び出されているのだと思います。 どうせ最終的に GitHub - jashkenas/coffeescript: Unfancy JavaScript が呼ばれると思うので、npm scriptから直接こいつを呼び出しては?みたいな気分になります。
冒険王ビィト 16
最終巻ぽい集合した表紙です。 最終巻ではありませんでした。
PHPでTODOリストをつくる、追加まで
PHPを勉強しています。 WebアプリケーションとしてPHPを動かすことが出来たので何か作ってみましょう。
公式マニュアルをみていたらsetcookie関数を見つけました。 Cookieに値が保存できるならTODOリストアプリケーションが作れそうです。 ここでイメージしているTODOリストアプリケーションは https://todomvc.com/ のようなブラウザだけで完結したTODOリストアプリケーションです。 というのは、いまのところPHPでDBはおろかファイルを操作する方法すら知らないからです。
というわけで実装してみました。 まずは追加だけを考えます。 というのは、ユーザー操作が追加、達成、削除のどれかを区別する方法を知らないからです。
<!DOCTYPE html> <head> <title>TODOリスト</title> </head> <body> <form method="post"> <input type="text" name="name" autofocus> <button>追加</button> <?php $todos = restore_todos(); $todos = add_todo($todos, $_POST["name"]); save_todos($todos); ?> </form> <ul> <?php foreach (array_reverse($todos) as $todo) { echo "<li>" . $todo . "</li>"; } ?> </ul> </body> <?php function add_todo($todos, $todo) { if ($todo) { $todos = restore_todos(); array_push($todos, $todo); } return $todos; } function restore_todos() { return array_filter(explode(",", $_COOKIE["todos"])); } function save_todos($todos) { $todos_str = array_reduce($todos, function ($carry, $todo) { return $carry . "," . $todo; }); setcookie("todos", $todos_str); } ?>
実装してみると、最初にHTMLでテンプレート書いて、それから振る舞いを実装していきます。 すごくフロントエンド開発っぽいです。 サーバー側でエラーが出たときに500エラーが起きて詳しい情報がわからないのは辛かったです。 Better Errorsって、ありがたいんだなと思いました。
動かすと次の感じです。
次の振る舞いで困りました。
- 追加した新しいタスクがリロードしないと表示されない
- リロードすると前回追加したタスクがまた追加される
前者はグローバル変数をつかって、変更後のリストをレンダリングすることで回避しました。 後者が思いつかなくてググったらPRGパターンを使えって出てきて「そらそうだー!」っておでこをはたきました。 まあ、リダイレクトの仕方がわからないので、次の課題です。
どうやら僕は、PHPの勉強を「縛りプレイ」だと思うと楽しめそうです。 Webアプリケーションを作ること自体は何週目かわからないので新鮮味はないです。 新しい言語で使える物を限定したこと「この道具だけでどうやって実現するか?」を考えるのが面白いです。 それでWebアプリケーションについての理解が深まるのが、また楽しいです。
PHPでエコーするWebアプリケーションをつくる
を参考にしています。 環境構築時にインストールしたMAPMで、Apacheが自動的にインストールされてPHPが実行できます。 環境設定はこれに全部おまかせでPHPファイルだけを書きます。 ソースコードです。
<!DOCTYPE html> <html> <head></head> <title>Echo by PHP</title> <body> <h1>入力</h1> <form> <input type="text" name="comment" autofocus> <button>送信</button> </form> <h1>出力</h1> <p><?= h($_GET["comment"]) ?></p> </body> </html> <?php function h($str) { return htmlspecialchars($str, ENT_QUOTES, "UTF-8"); } ?>
動かしたとろが次の動画です。
一応入力文字列のエスケープ処理も入っています。 動画では入力してません。
PHPのソースコードがそのままHTMLのテンプレートになっているのが驚きです。 PHP: Hypertext Preprocessor というのは伊達じゃないですね。 1ページで完結するようなアプリケーション、例えば掲示板やTODOリストは、割と簡単に作れそうに思えます。
$( document ).ready()
コールバック関数が実行されるタイミング
$( document ).ready()はJavaScriptでDOMを操作するに当たって、DOMの読込完了を待つためのjQueryの便利関数です。 ブラウザにloadイベントしか実装されていなかった時代がありました。 loadイベントは次の2つを待ちます。
- DOMの構築
- 画像、CSSの読込
$( document ).ready()
は2の前に発火します。
画像などの(当時として)重たいアセットの読込を待たなくてよくなるため、JavaScriptの開始を高速化出来ました。
次のように使いました。
$(document).ready(function(){ /// 好きな処理 })
代替え案
scriptタグのdefer属性
scriptタグにdefer属性を設定すると
スクリプトを文書の解析完了後かつ DOMContentLoaded が発生する前に実行する
ので、同様の効果が得られます。ただし、インラインスクリプトには効果がありません。 src属性を使って読み込む外部スクリプトには、defer属性を使うのが良いと思います。
<html> <head> <script defer src="outer.js"></script> </head> <body> 本文 </body>
outer.js
では、$( document ).ready()
を使わずに、いきなり実行したいJavaScriptが書けます。
/// 好きな処理
scriptタグの位置
bodyタグの最後にscriptタグを書いても大体同じ効果が得られます。 確かheadタグにで読み込むCSSを読み込んだ後で、画像を読み込む前だったような・・・うろ覚えです。 O'Reilly Japan - ハイパフォーマンスJavaScript に詳しい説明が載っています。
インラインスクリプトの場合はこちらの方が良いでしょう。
<html> <head></head> <body> 本文 <script> /// 好きな処理 </script> </body>
DOMContentLoaded
他人が使うJavaScriptファイルを提供する場合など、scriptタグの書き方を指定を変更出来ない時、JavaScriptだけで実行タイミングを制御したいことがあります。その場合はDOMContentLoadedイベントが使えます。
document.addEventListener('DOMContentLoaded', () => { /// 好きな処理 })
DOMContentLoadedは一度しか発火しないため、後から追加するJavaScriptでは発火しません。
$( document ).ready()
は次のようにjQuery.Deferred
を使って実装されています。
// The deferred used on DOM ready var readyList = jQuery.Deferred(); jQuery.fn.ready = function( fn ) { readyList .then( fn )
DOM構築後に$( document ).ready()
を読ぶと即座に実行されます。
同じように動作させるには次のようにreadyState
の値を確認します。
function doSomething() { /// 好きな処理 } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', doSomething); } else { doSomething(); }
厳密には$( document ).ready()
は次のイベントループ(?)で非同期に実行します。
doSomething
の実行に長い時間が掛かる場合は、requestAnimationFrameを使うなどの工夫が必要かもしれません。
関連ページ
PHPでFizzBuzz
if文とforeach文を覚えました。 早速FizzBuzzしてみたいと思います。
<?php foreach (range(1, 15) as $i) { if ($i % 15 == 0) { var_dump("FizzBuzz"); } else if ($i % 5 == 0) { var_dump("Buzz"); } else if ($i % 3 == 0) { var_dump("Fizz"); } else { var_dump($i); } }
はまった点
- 変数の頭に$を付け忘れた
range(1, 15) as $i
の順序を逆だと思い込んで$i as range(1, 15)
と書いた
実行してみましょう。
PS C:\Users\led_l\php_playground> php .\fizz_buzz.php int(1) int(2) string(4) "Fizz" int(4) string(4) "Buzz" string(4) "Fizz" int(7) int(8) string(4) "Fizz" string(4) "Buzz" int(11) string(4) "Fizz" int(13) int(14) string(8) "FizzBuzz"
出力方法はvar_dumpと言う関数しか知りません。 var_dump関数は、値の形とサイズも一緒に表示します。
あ、<?php
って必ずしも閉じなくて良いんですね!
PHPを学ぶ
数ヶ月後にPHPを扱う可能性が出てきたのでゆるゆると勉強しようと思います。 もっと近づいてからガッと勉強してもいいんですが、そうすると実利に近づきすぎるというか、遊びがなくなります。 ゆっくりダラダラ学んだほうが周辺情報がわかって良いかもしれません。
ふりがなプログラミング
まずは初心者向けの入門書から入ります。 ふりがなプログラミングシリーズは出た頃に話題になったのですが、それを学ぶ立場で読むって貴重な体験に思えます。 読んでみると、プログラマーにすでに見えている情報をふりがなでアノテーションしているように思いました。 なるほど初心者向けのギブスとしてよさげな感じがします。 特に良いと思ったのがエラーメッセージにふりがなを振っている例です。
知らないプログラミング言語のエラーメッセージって、つい読み飛ばします。 読んでもファイル名と行数くらいで、メッセージの内容を読む気になれません。 ふりがながふってあると、読んでみたくなるのなかなかすごいです。
MAMP
PHPの環境構築には MAMP & MAMP PRO - your local web development solution for PHP and WordPress development というのを使うみたいです。 もとはLAMP(Linux + Apache + MySQL + PHP)をもじってMac向けだったみたいです。 名前はそのままでWindows版もあるので使ってみました。 やけにインストールに時間が掛かるくせに、パスは設定してくれないし、微妙に不便だな?と思いました。
9バージョンインストールされてました。 rbenvみたいに必要に応じて必要なバージョンをインストールするんじゃないんですね。 いきなり全部用意してくれました。 カルチャーの違いにおどろきます。
超一流になるのは才能か努力か?
「一万時間の法則」の根拠として研究を使われたアンダース・エリクソンさん本人の本。
P111「経験豊富な医師は高度な心的イメージを身につけており、そのおかげで一見関係のなさそうな情報も含めてたくさんの事実を同時に考慮することができる」
— ぎゃばん@手洗い (@ledsun) September 26, 2020
偽春菜
20年前に名前が問題になって話題になった、カイル君のもっとウザい感じの常駐アプリケーションです。 なんかすごくインストールしたくなりました。 今もあるのかな?と、調べたら独立伺か研究施設 ばぐとら研究所 で後継アプリケーションが配布されていました。
インストールしてみたら普通に動きます。
今はゴーストという名前でキャラクターを入れ替えられるようになっているようです。
相変わらずのウザさです。 20年間受け継がれています。 すばらしい。
標準入力とはなにか?
雑に言うと、UNIX用のプログラムがキーボードからの文字入力を受けとる口です。 プログラムの標準入力は、親プロセスの標準入力を引き継ぎます。 対話シェル(Bashとかzshとか)は入力をキーボードから受け取ります。 シェルからプログラムを起動すると、これを引き継いで標準入力はキーボードからの入力になります。
UNIX以前は、プログラムの入力装置と出力装置は、プログラムを起動する毎に指定したようです。 おそらくコンピューター毎に入力がパンチカードや磁気テープだったり、出力がプリンターだったりという環境を想定していたのだと思います。 UNIXでは、デフォルトの入力装置をキーボード、出力装置をモニターにして、プログラムを起動する度に、入力装置と出力装置を指定しなくてよくしました。 それで「標準」入力と名付けたようです。 1970年代にキーボードとモニターが普及し、大抵のコンピューターにキーボードとモニターがついていると想定できるようになったのだと思います。
UNIXのバージョンが進むにつれて標準エラー出力が追加され現在の振る舞いになりました。 この動きが多くのOSやプログラミング言語に引き継がれているようです。 いまではLinuxでもMacでもWindowsでも、Javaでも.NETでもPythonでもRubyでもPHPでも標準入力を持つようになりました。
参考
ホームレス転生 7 ~異世界で自由すぎる自給自足生活~
他国の進軍を止めるクエストを受けた主人公たち、無事に時間を稼ぐことが出来るのか? 逃げ場のない状況で迫る危機にどう対処するのか、「家の中のモンスター」です。
表紙の通り女性キャラに囲まれています。 その割には、メインヒロインと子供役以外は主人公に迫ってこなくてハーレム化しない点が読みやすいです。 女性の作画は妙にエロいです。 エロ展開があるとかリアリティがあるとか肉感的であるとかではないのです。 でもエロスがあります。 謎です。