社内勉強会用
数年前は最適な選択をしたつもりだったが、時が経つにつれツラミが増してきた設計について反省する会。 ※ 誰かが悪いとかそういうのではなく、反省して次に活かそうという話です
自分でやった設計反省会その節は、誠に申し訳ございませんでした。ダーシノ / @bc_rikko
View Slide
祇園精舎の鐘の声 諸行無常の響きあり娑羅双樹の花の色 盛者必衰の理をあらはす
失敗を共有する理由成功パターンはさまざまな要因が重なり再現性が低い失敗パターンはだいたい似ていて再現性が高い
目次TypeScript/JavaScriptEnumの濫用型定義の誤った共通化大きすぎるライブラリ(lodash)の導入フレームワーク中途半端なコンポーネント化SFC内はJavaScriptで書く
TypeScript/JavaScript
Enumの濫用
当時の背景コード内にマジックナンバーや文字列リテラルがあったJavaScript → TypeScript の移行期だったif文、switch文などでtypoに気づけない定数クラスの代わりにString Enumsを採用した
何が悪かったのか定数を扱うために String Enums を採用冗長的でEnum 本来の使い方ではなかったEnum名がつくためコードが長くなったenum MyStatus {Waiting = 'waiting',Running = 'running',Finished = 'finished'}if (res.data.status === MyStatus.Running) {// ...}
どうすればよかったのかLiteral Union Types を使うマジックナンバーなど意味が伝わらりづらいときだけEnum を使うas const でもOK// Literal Union Typestype MyStatus = 'waiting' | 'running' | 'finished';if (status === 'running') { /***/ }// String Enumsenum StatusCode {OK = 200,Forbidden = 403,NotFound = 404}if (res.code === StatusCode.NotFound) { /***/ }
型定義の誤った共通化
当時の背景JavaScriptで実装されたプロジェクトをTypeScriptに移行しようとしていたAPIレスポンスの型定義を作りたかったID ,Name ,Description , ... などはほとんど同じだったコード量を減らすために共通化した
何が悪かったのかAPIの仕様上、エンドポイントによりスキーマが変わるひとつにまとめたため、Optional Properties が大量発生したinterface CommonResponse {id: stringscope?: 'public' | 'private' // GET /item のときだけ有効status?: { availability: string } // GET /status のときだけ有効}const res: CommonResponse = await fetch(`/item/${id}`)console.log(`scope: ${res.scope}`) // scope: public// Cannot read properties of undefinedif (res.status.availability === 'xxx') { /** */ }if (res.status?.availability === 'xxx') { /** */ } // Optional Chaining が必要
どうすればよかったのか共通化するときは本当に同じモノなのか検討するUnion Types を使う(Tagged Union Types も有効)type ItemResponse = {id: stringscope: 'public' | 'private'}type StatusResponse = {id: stringstatus: { availability: MyAvailable }}const res = await fetch(`/status/${id}`)if (res.status.availability === 'available') { /** */ }
大きすぎるライブラリの導入
当時の背景もともと使われていたPrototype.js の代用を探していたIE11やSafariの一部ブラウザで使えないコレクション操作メソッドがあったbabel導入前でpolyfillという選択肢がなかったpolyfillではなくlodashの導入を選択した
何が悪かったのか大きすぎてバンドルサイズがモリモリ増えた今となってはlodash がなくても実装できるようになった脆弱性報告がときどき来たimport * as _ from 'lodash'_.includes(list, 'xxx')_.chain(list).map(...).flat(...).filter(...).values()_.get(item, 'a.b.c')
どうすればよかったのか「あると便利だけどなくてもなんとかなる」ライブラリは徹底排除標準仕様を優先する過去のコードの真似をせず、最新の仕様で考えるlist.includes('xxx')list.map(...).flat(2).filter(...)item.a?.b?.c
フレームワーク
中途半端なコンポーネント化
当時の背景Atomic Design の厳密性より、スピードと柔軟性を求めていたコンポーネント粒度が細かいほどバケツリレーになりがちだったよく使うコンポーネント以外は各ページで実装する
何が悪かったのかコンポーネント化する基準が曖昧なままチームメンバーが増えた同じスタイルを異なるページで使うようになった...Lorem ipsum dolor sit ametconsectetur<br/><br/>.mute { /***/ }<br/><br/>...consectetur adipiscing elitsed do eiusmod<br/><br/>/** Foo.vueの .mute と同じスタイル */<br/><br/>.mute { /***/ }<br/><br/>
どうすればよかったのか明確なガイドラインを用意するAtomic Design を採用する痛みはあれどプロジェクトは安定化するAtomic Design でなくてもチーム全体で合意がとれたガイドラインを用意する...Lorem ipsum dolor sit amet......consectetur adipiscing elit...
VueのSFCはJavaScriptで書く
当時の背景Nuxt@1 系([email protected] )の頃は、TypeScritpのサポートが十分でなかったClass Components 化するよりはJavaScriptで書いたほうが悩みが少ないと思ったJSDoc である程度型をつけられると思ったSFC内はJavaScriptで実装した
何が悪かったのかStore 層はTypeScript化してdispatchs /getters にも型をつけたmapヘルパー やcomputed を使うことでStore 層の型が全部死んだJSDoc を書かなかったexport default {computed: {items() {return this.$store.getters['items'] || []}},methods: {doSomething() {const items = this.items // any}}}
どうすればよかったのかTypeScriptのサポートが充実してきたころにJS→TSに移行するmapヘルパー の利用やgetters をcomputed でラップしないexport default Vue.extend({methods: {doSomething(): Promise {const items = this.$store.getters['items']// ^? Item[]}}})
まとめポエム誰も悪くない。「最良の方法」だと信じていたし、実際そうだった。でも、時間が経つにつれ痛みが、 It's so painful.そして、過去の自分の設計を振り返ってこう思う。お前のいう「絶対」って60%くらいだよな、と。