2020年も残り数日。
昨年に続き今年もいろいろ環境が変わり、ちょっと開発は離れめだった昨年に比べると、今年はがっつりコードを書く時間も増えた。
初めて使った技術もそこそこあり、また改めてこれまで使ってきた技術を見直すきっかけにもなったので、取り留めなく書き残しておきたい。
Reactとフロントエンド開発
React は良いものだ。
自分はもともと C#/WPFでGUIアプリを作っていた時間が長く、そこからWebのフロントエンド実装をかじり始めたとき、Knockout.js の双方向バインドの仕組みはわりと抵抗なく受け入れることができた。
そこからもう少しモダンなフレームワークを使おうとした際、React の jsx記法にどうにも抵抗があり、最初は React を避けて Vue.js を覚えることを選択した。
今回、改めて技術選定をするにあたって、観念して React を学ぶことに決めたが、これがすこぶる開発体験が良かった。
MVCからのパラダイムシフト
React の素晴らしさを今さら自分なんぞが語る必要はないと思うが、そもそも jsx のどこに抵抗を感じていたかというと、もともと自分の中にあったMVC的観念(WPF MVVM の影響を多く含む)でいくと、View と Model を疎結合で分離することが正義であり、そのために html は見た目のみを担当、そこに変数を binding で埋め込んで、ロジックを js に分離して扱う、という発想はすんなり理解できた。
これに対し、React の jsx では、js が html もどきのタグを生成するという。
これは明らかにロジックとViewの密結合じゃないか。
いまにしてググってみると、最初にこういう反応を示した人は自分だけでなくそこそこ居たようだ。
Fluxのアーキ図を見てもいまいちピンときていなかったが、これはMVCの固定観念の中で扱うべき代物ではなく、DOMそのものをアプリケーションの状態としてロジックから宣言できるというパラダイムシフトなのだというのが、使ってみて完全に腑に落ちた。
View と Model の分離、ではなく、View を完全に Model で表現できるでしょという発想。
js で書けるということは、 js で検証できる(テストを書ける)というのも素晴らしい。
しかも気がつくと TypeScript との相性が抜群になっていて、十分にタイプセーフにコーディングできる。React なら flowtype でしょみたいな選択も今は昔か。
もうちょっと感覚的な言い方をすれば、html の DOM って、結局ただの「テキスト」なんだよなーって思う。
foo.html って名前でテキストファイル作って、中に hello
とだけ書けば、 htmlタグすらなくても大抵のブラウザはとりあえず「hello」って表示してくれるし。
.NET の場合、Forms にせよ WPF にせよ、「バイナリ」っていう感覚が強かった。
単にコンパイル言語かどうかの違いなのかもしれない。
JSONの文字列をそのまま js オブジェクトに変換できるのとかもコンパイル言語の感覚からするとちょっとギョッとする。
TypeScript
TypeScriptは良いものだ。
好きなのは以前から変わりないが、以前にも増して TypeScript の素晴らしさは向上している。
そもそもが JavaScript というのは本当におもちゃみたいな言語で、一定以上の規模の開発で使う気になるものではなかった。このオレ評価を180度ひっくり返した TypeScript は本当に素晴らしい。
漸進的にしか型付けできないというのが、今となってはデメリットではなく、柔軟性と堅固さを兼ね備えた武器であるようにさえ思う*1。
たとえばリテラル型の使い勝手が良い。
type ZeroOrOne = 0 | 1; let val: ZeroOrOne = 0; val = 1; //OK val = 2; //コンパイルエラー
const hello = (dogOrCat: "dog"|"cat") => { if (dogOrCat === "dog") { console.log("hello, dog!"); } else { console.log("hello, cat!") } } hello("dog"); //OK hello("dug"); //コンパイルエラー
型によって値の正しさを保証できるというのは Coq とかの考えに通じるものがあるんじゃないかとか、例えば「偶数型」を type Even = (n:number) => n % 2 === 0
みたいにして定義できるようになると嬉しいんじゃないかとか思うんだけども、このへん型システムというものについてちゃんと勉強したいなという気持ちがある。
型システムといえば TypeScript は Structural Subtyping を採用しているところも気に入っている。メンバが同じなら同じ型として扱いましょうというやつだ。
Java や C# での感覚からすると一見乱暴そうにみえるが、今のところそのせいでひどいめにあった経験などもなく、むしろおんなじようなクラスを量産して ModelMapper みたいなので詰め替えるよりはよっぽど安全であると思う。
しいて思いつくデメリットは、IDEでメンバの参照を追いにくくなるくらいか。
あとは、js の厳密比較の演算子 ===
は今見てもヤケクソすぎるので、TypeScriptでは ==
での比較をトランスパイルですべて ===
に変えちゃうくらいでもいいのにと思っている。*2
JavaScript のダメなところのひとつがこのへんの比較の緩さで、 !isValid()
と書くべきところを !isValid
と書いてもエラーにならないのとかも、便利さより害のほうがずっとでかいと思う。
「ゆるく導入しましょう」という風潮
JavaScript → TypeScript の移行にあたっては、最初からあんまりガチガチにせず、たとえば noImplicityAny
とかやめましょうみたいな風潮があるように思うんだけど、個人的にはそんな時代はもう終わっているというか、そもそもゆるく導入するって下手すると全く型がないより危険なんじゃないかと思っているところ。
なんせ TypeScript の型ってただの「ラベル」なので、 number
と書いてあっても、実際の値が number である保証がない。
なので、どっかで間違ってラベル付けしてしまうとかえってそれを信じ込むことによるバグのリスクが生まれる。
だからできるだけ途中で any とか as Foo
とかせずに、最初から最後まで正しい・保証された型のまま扱うことが、TypeScriptで書く際のひとつのポイントであると思う。
いっこ例。
ユーザ情報のオブジェクト、またはnullが入っているリストがあり、
type User = { name: string; age: number; }; const userOrNullList = [ { name: "sato", age: 20 }, null, { name: "suzuki", age: 30 }, ];
ここからnullでないものだけを抽出したいとする。
const users = userOrNullList.filter(u => u !== null);
このとき、users は Array<User>
型になってほしいところだが、残念ながら現時点では Type Guard がそこまで賢くなく Array<User|null>
のままになる。
この users を Array<User>
にしたい場合、↓こんなような書き方をしがち。
const users = userOrNullList.filter(u => u !== null) as User[]; //または const users = userOrNullList.filter((u): u is User => u !== null);
しかし、この書き方をしてしまった時点で、リスト内の object は User型( { name: string, age: number }
)であることを盲目的に信じることになるので、たとえば後から age がなくったりしても気づくことができない。
type User = { name: string; age: number; }; const userOrNullList = [ { name: "sato" }, null, { name: "suzuki" }, ]; const users = userOrNullList.filter(u => u !== null) as User[]; const ages = users.map(u => u.age); //エラーにならない
できるだけ型アサーションではなく、Type Guard でうまくやる。この場合は↓のように。
const notNull = <T>(array: Array<T|null>): T[] => { return array.filter((t): t is T => t !== null); } const users = notNull(userOrNullList); const ages = users.map(u => u.age); //コンパイルエラーになる
似た話で、せっかく null安全な言語なのに foo?.bar
とか foo!.bar
とか多用されているときは危険信号。
let val: string | undefined; if ((val ?? "") !== "") { const trimmed = val!.trim(); ... }
↑こういうシーンとか、 val
が undefinedでないのわかるでしょ!って言いたくなるところではあるが、こういうのはこれくらい狭いスコープでの利用に留めておくべし。
どっか途中で、型付けされているラベルと実際の値が正しいことを検証できる手段があるといいのかなと思うのだけど。
特に API 経由で受信した JSON なんかは、受け取った側で勝手に型付けしたところで、実際にそのとおりの値が入っているかどうかはひとつひとつ調べてみないと分からないからな。
Java
ある言語が好きだとか嫌いだとか言うとき、自分の場合は半分くらい IDE の評価が混ざっていることに気づいた。
そういう意味で、 Eclipse を使っているときは Java のことが嫌いであったが、IntelliJ IDEA を使うようになってからはそこまで嫌いではなくなった。
しかし IntelliJ は、メモリが足りなくなったらスッとあっさり落ちるのとか、キャッシュが狂ってメンバの参照検索が簡単に壊れるのとか、IDEとしてそこだけは死守してくれ!って思う部分があるが…。
さて言語の話。
以前までのイメージとして、Java は柔軟でない分、硬くて安全な言語であると思っていたが、実は全然安全じゃないなと思えてきた。
最たるものは null の扱い。
Java に限らず、歴史ある言語が null 安全でないのは仕方ないのだろうが、それにしても List などにプリミティブ型を格納することができず、かつアンボクシングを暗黙的にやろうとしてぬるぽを投げるっていうのはちょっと終わってるかなと思う。
null の発明は何億ドルの損失だった的な話もあるが、TypeScript と Java を比べるとよくわかる。悪いのは null ではなくて、それを安全に扱う仕組みがないことだ。
後からそこを考えて作ったはずの Optional も、メソッドの戻り値に使うためのもので、引数に利用するのは非推奨だという。
理由はそのメソッドを呼び出すたびに foo(Optional.of(bar))
みたいな冗長な書き方になるから。なんでそんな設計にするかな。
Optional型への変換はそれこそ暗黙的にやるようにしたって問題ないはず。
引数では Optional じゃなくてオーバーロードを使えという、その冗長さが Java っぽいなーと思う。
以前は、Javaってのは必要最小限の重要な機能を組み合わせて拡張していく哲学なんだ的な説明で納得していたが、標準API が貧弱な結果、Utilメソッド生やしまくってカオスじゃねぇかとつっこみたくなってしまう。
Stream API の冗長さとかを見ても、Java言語そのものを進化させるのは互換性の問題もあってかどうにも難しいのかなという反面、その言語的な危うさを IDEやLombokやUtilメソッドでどうにか補いつつやっていく感じなのかなというのが、今年感じた Java というものの印象。
C#, WPF
翻って WPF。
最近久しぶりに WPF で MVVM をやろうとしたら、Prism を使ってもなお記述量の多さにうんざりした。
前はあんまり気にしていなかったが、React x TypeScript をやった後だと、形式的に要求されるコードの多さが際立つかも。
そしてこれも改めて考えるとなのだけど、XAMLの記述複雑すぎ。
良くできてはいるが、真面目に流行らす気ないだろ。
1年ぶりくらいにいじったら、コアな部分をめっちゃ忘れてしまっていて、いちいち書き方がわからず苦労した。
そして MVVM。
これも Web のフロントエンドに触れた後だと、Modelの変更をトリガにダイアログを表示するのをどうやろうか問題とか、何でそんなとこで悩んでんだって気がする。本当にそこの複雑さは必要なの?と思えてきたというか。
いちばん html / js で楽だなーと思ったのは、スレッドを意識する必要がないところ。
.NET の STA モデルでは、UIを非UIスレッドから操作してはいけないという鉄の掟があって苦しめられる。
MVVM アーキでは View と ViewModel - Model の分離をといいつつも、たとえば DataGrid にバインドしたコレクションの CollectionChanged をバックグラウンドスレッドから発火したりするとエラーになるし、Model 側でのDBアクセスなどをメインスレッドで実行するとそのせいで画面がフリーズしてしまうので、どうしても Model 側で UI のスレッドを意識する必要があり、分離といいつつこの掟からフリーになれなかったのが惜しい。
MVVMが実現した疎結合
そしてこれは前から思っていたことだが、はたしてその「疎結合」は正しかったのかという問題。
従来の Windows Forms では、UIを構成するコードとそのロジック(コードビハインド)とは、別ファイルに分かれてこそいるが実際は同一クラスであった。
デザイナ側にボタンを配置したら、そのクリックイベントハンドラをコードビハインドに記述する必要があり、どちらか一方だけではコンパイルが通らない。
WPFでも同様の書き方は可能であったが、WPFではこれに加えて Binding の機能を実装し、(おそらくは)こちらを本命とした。
UIは XAML で定義し、
<Grid> <Button Content="クリック!" Command="{Binding HelloCommand}" /> </Grid>
コードビハインドにイベントハンドラを書く代わりに、 DataContext に設定したオブジェクト(=ViewModel)に同名のコマンドプロパティを用意する。
class MainViewModel : INotifyPropertyChanged { public event PropertyChangedEventHandler PropertyChanged; public ICommand HelloCommand { get; } public MainViewModel() { HelloCommand = new HelloCommand(); } } public class HelloCommand : ICommand { public event EventHandler CanExecuteChanged; public bool CanExecute(object parameter) { return true; } public void Execute(object parameter) { //ここにコマンドの処理を書く } }
XAML側のUI部品は DataContext のオブジェクトが無くても表示することができるし、また DataContext となる ViewModel の型が何であるかを意識しないので、同名のコマンドプロパティを持つモックに差し替えることも容易である。
一方で ViewModel 側のオブジェクトも、UIクラスに依存しないので、単独でテストが可能である。
呼び名が「クリックイベント」ではなく「コマンド」なのもポイントで、つまり ViewModel は、このハンドラがクリック操作によって発生するものであるかどうかを意識しない。
ボタンクリックでも、キー押下でも、なんでもいいはず、という思想である。
View と ViewModel とを繋ぐものは "HelloCommand" というプロパティ名のみ、というのが WPF の MVVM における疎結合である。
さてここからが本題だが、型を意識しないということは、コンパイラの型チェックが効かない、ということである。 View の DataContext に設定されるオブジェクトに "HelloCommand" という名前のコマンドプロパティがあるかどうかは、アプリケーションを起動してみないとわからない*3。
総じて見れば、Binding ベースの開発は、イベントハンドラベースの開発に比べれば十分に快適であったし、新しくものを作る際は規模の小さいものであっても基本的に Binding ベースで作るつもりだが、ふと手を止めて考えたときに、はたしてこの「疎結合」は、タイプセーフを捨ててまで手に入れたかったものなのか、という思いが頭をよぎる。
例えばデザイナーとプログラマーの分業みたいなケースで、画面にはメールアドレス・パスワードの入力欄と送信ボタンがあるとなったら、プログラマーからデザイン担当者へ、この画面のバインド先は Email
, Password
, SubmitCommand
でお願いします、と伝える。
それを受けてデザイン担当は、Blend を使ってXAMLで画面を組み立て、その各部品に Text="{Binding Email, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
のようにプロパティ名を埋め込んでいく。
同時進行でプログラマーは ViewModel以下のコードを作っておき、両者出揃った時点でくっつければ完成!みたいな開発フローの話を聞くことがあるが、本当にそんな分業の仕方で成り立っている現場があるんだろうかというのはとても気になる。
個人的にはできる気がしない。
先にも書いたとおり、XAMLの記述は複雑で、しかもそこには多分にロジックが含まれていると思う。
bool 型のプロパティによって Visibility を切り替えたいと思ったら ValueConverter の実装が必要だし、画面内の各エリアに対して DataContextオブジェクトをどの単位で分割するかも判断が必要だ。
添付プロパティだ EventTrigger だとかも XAML に書くものだが、およそデザインの関心事からは離れすぎており、ロジックの実装と同時に発生するはずのものだ。
だからデザインとロジックを分業するとしたら、実際にはまずデザイン担当がデザインのみをXAMLで作り、それをロジック担当に渡して、はい、あとここにロジックを入れ込んでください、という形になるはずで、であればそこにコードビハインドがあろうがなかろうが関係ない。
もちろん先にも挙げたとおり、ViewModel 以下を単独で検証できるメリットはあるが、それだけなら必ずしも今のような形である必要はなかったのかもなどと、そんなことを考えていたところに、「いや、DOMってロジックでしょ」と言わんばかりの React に触れたものだから、ですよねー!という気持ちになっているのがいま。
.NET 5 のめざす世界
.NET 5、その次の .NET 6 のめざす「One .NET」の世界では、 .NET Core やら .NET Standard やらのカオスな標準化が統合され、いよいよひとつのフレームワークで、Windows でも Linux でも Webでも、さらにはモバイルでも動くアプリケーションが作れるようになり、C# だけで勝てる世界がやってくる。
これ、マイクロソフトはずっとやりたかった(そしてやっていた)がうまくいかなかったことで、いよいよかと期待する気持ちも、ほんとかと懐疑的な気持ちもある。
まあこればっかりはMSのお気持ちだけではどうにもならないというか、Windows Phone およびストアアプリは全く流行らなかったじゃねーかというのをMSに言っても仕方がない。Silverlight なんてのもあったな。
また、同じフレームワークとはいえ、Blazor と Xamarin がワンソースにはなるわけではないだろうと思っているが、どうなんだろう。
C# の言語的な人気は根強いと思うし、WebAssembly も盛り上がってきたように思うので、来年の動向が気になるところ。
その他
UIテスト(e2eテスト)について
Puppeteer や Playwright なんかでUIの自動化テストを書く際、よく言われるのが「壊れにくいテストを書きましょう」ってなことで、たとえば DOM の id や class 属性なんかでセレクタを書くよりは data-testid
みたいな専用の属性で書くとか、あるいはユーザ目線での目印となるものと同じもの(ボタンテキストなど)をセレクタとするとか、適切に抽象化することで、内部構造に改修が入っても壊れにくいテストにすることが大事とされる。
これ自体は至極真っ当だと思う一方で、壊れないテストなんてのは幻想だなとも思えてきた。
テストが壊れないようにと気を使いながらDOMを改修するよりは、むしろ「壊れても簡単に直せるテスト」を意識するほうが現実的ではないかと思う。
具体的には、同じ目的のふるまいをテストコードでも適切に共通化すること。
「メニューから管理画面を開く」という自動コードがあっちこっちのテストケースで実装されていると、メニューをいじったときに、軒並みテストが壊れて片っ端から直す必要ができるが、共通メソッドになっていれば1か所直すだけで良い。
結局プロダクトコードでやっていることを真面目にテストコードでもやることが大事という結論。
OpenAPI Generator
openapi-generator、これもよかった。
Open API という共通仕様に則ったサーバ実装であれば、そこからフロント用の API 利用コードを自動生成できる。
フロントエンドの実装が現状ほぼ js 一択である以上、API 経由でそこと同じ I/F を使うバックエンドも本当は node.js で実装すればソースを共通化できるのにっていうのはずっと思っていたが、言語が違ってもコードを生成できればいいじゃんっていう、こんな単純な問題だったのかという感想。
これに限らずだが、なんとなくコードの自動生成的なものに良いイメージを持っていなかったのは、なぜだろうな。
余計なものがゴチャゴチャくっついてくるっていうイメージと、(ボイラープレート的なものを)自動生成されると、中で何やっているのか理解できないまま道具に使われる感覚があったからか。
ともあれ、これは良いものでした。
まとめ
その他今年は、データ設計やプロジェクトの進行なんかについても学ぶ部分が多かった。
今年のうちに気が向いたら続きを書くかもしれない。
フロントエンドのエコシステム&フレームワーク戦争が落ち着いたと思ったのも束の間、相変わらず年を追うごとに、新しい技術が登場しシェアを集めるスピードも速くなっている。
技術を選び、学びつづけるのは、もはやキャリア戦略なんて話じゃない。
問題解決の武器をどう選び、磨いていくか、そしてエンジニアとしての生涯をどう全うするかという人生設計だ。
コロナなんかに負けないで来年もいい年にしましょう。