$30 off During Our Annual Pro Sale. View details »

自分がやった設計反省会 / Evaluation of my Architecture

自分がやった設計反省会 / Evaluation of my Architecture

社内勉強会用

数年前は最適な選択をしたつもりだったが、時が経つにつれツラミが増してきた設計について反省する会。
※ 誰かが悪いとかそういうのではなく、反省して次に活かそうという話です

ダーシノ

July 21, 2022
Tweet

More Decks by ダーシノ

Other Decks in Programming

Transcript

  1. 自分でやった設計反省会 その節は、誠に申し訳ございませんでした。 ダーシノ / @bc_rikko

  2. 祇 園 精 舎 の 鐘 の 声   諸

    行 無 常 の 響 き あ り 娑 羅 双 樹 の 花 の 色   盛 者 必 衰 の 理 を あ ら は す
  3. 失敗を共有する理由 成功パターンはさまざまな要因が重なり再現性が低い 失敗パターンはだいたい似ていて再現性が高い

  4. 目次 TypeScript/JavaScript Enumの濫用 型定義の誤った共通化 大きすぎるライブラリ(lodash)の導入 フレームワーク 中途半端なコンポーネント化 SFC内はJavaScriptで書く

  5. TypeScript/JavaScript

  6. Enumの濫用

  7. 当時の背景 コード内にマジックナンバーや文字列リテラルがあった JavaScript → TypeScript の移行期だった if文、switch文などでtypoに気づけない 定数クラスの代わりにString Enumsを採用した

  8. 何が悪かったのか 定数を扱うために String Enums を採用 冗長的で Enum 本来の使い方ではなかった Enum名がつくためコードが長くなった enum

    MyStatus { Waiting = 'waiting', Running = 'running', Finished = 'finished' } if (res.data.status === MyStatus.Running) { // ... }
  9. どうすればよかったのか Literal Union Types を使う マジックナンバーなど意味が伝わらりづらいときだけ Enum を使う as const

    でもOK // Literal Union Types type MyStatus = 'waiting' | 'running' | 'finished'; if (status === 'running') { /***/ } // String Enums enum StatusCode { OK = 200, Forbidden = 403, NotFound = 404 } if (res.code === StatusCode.NotFound) { /***/ }
  10. 型定義の誤った共通化

  11. 当時の背景 JavaScriptで実装されたプロジェクトをTypeScriptに移行しようとして いた APIレスポンスの型定義を作りたかった ID , Name , Description ,

    ... などはほとんど同じだった コード量を減らすために共通化した
  12. 何が悪かったのか APIの仕様上、エンドポイントによりスキーマが変わる ひとつにまとめたため、 Optional Properties が大量発生した interface CommonResponse { id:

    string scope?: '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 undefined if (res.status.availability === 'xxx') { /** */ } if (res.status?.availability === 'xxx') { /** */ } // Optional Chaining が必要
  13. どうすればよかったのか 共通化するときは本当に同じモノなのか検討する Union Types を使う( Tagged Union Types も有効) type

    ItemResponse = { id: string scope: 'public' | 'private' } type StatusResponse = { id: string status: { availability: MyAvailable } } const res = await fetch<StatusResponse>(`/status/${id}`) if (res.status.availability === 'available') { /** */ }
  14. 大きすぎるライブラリの導入

  15. 当時の背景 もともと使われていた Prototype.js の代用を探していた IE11やSafariの一部ブラウザで使えないコレクション操作メソッドがあった babel導入前でpolyfillという選択肢がなかった polyfillではなくlodashの導入を選択した

  16. 何が悪かったのか 大きすぎてバンドルサイズがモリモリ増えた 今となっては lodash がなくても実装できるようになった 脆弱性報告がときどき来た import * as _

    from 'lodash' _.includes(list, 'xxx') _.chain(list).map(...).flat(...).filter(...).values() _.get(item, 'a.b.c')
  17. どうすればよかったのか 「あると便利だけどなくてもなんとかなる」ライブラリは徹底排除 標準仕様を優先する 過去のコードの真似をせず、最新の仕様で考える list.includes('xxx') list.map(...).flat(2).filter(...) item.a?.b?.c

  18. フレームワーク

  19. 中途半端なコンポーネント化

  20. 当時の背景 Atomic Design の厳密性より、スピードと柔軟性を求めていた コンポーネント粒度が細かいほどバケツリレーになりがちだった よく使うコンポーネント以外は各ページで実装する

  21. 何が悪かったのか コンポーネント化する基準が曖昧なままチームメンバーが増えた 同じスタイルを異なるページで使うようになった <!-- Foo.vue --> <div class="item">...</div> <p> Lorem

    ipsum dolor sit amet <span class="mute">consectetur</span> </p> <style> .mute { /***/ } </style> <!-- Bar.vue --> <header class="header">...</header> <p> consectetur adipiscing elit <span class="mute">sed do eiusmod</span> </p> <style> /** Foo.vueの .mute と同じスタイル */ .mute { /***/ } </style>
  22. どうすればよかったのか 明確なガイドラインを用意する Atomic Design を採用する痛みはあれどプロジェクトは安定化する Atomic Design でなくてもチーム全体で合意がとれたガイドラインを用意す る <!--

    Foo.vue --> <Item>...</Item> <Message> Lorem ipsum dolor sit amet <MuteText class="mute">...</MuteText> </Message> <!-- Bar.vue --> <Header>...</Header> <Message> consectetur adipiscing elit <MuteText class="mute">...</MuteText> </Message>
  23. VueのSFCはJavaScriptで書く

  24. 当時の背景 Nuxt@1 系( vue@2.5 )の頃は、TypeScritpのサポートが十分でなかった Class Components 化するよりはJavaScriptで書いたほうが悩みが少な いと思った JSDoc

    である程度型をつけられると思った SFC内はJavaScriptで実装した
  25. 何が悪かったのか Store 層はTypeScript化して dispatchs / getters にも型をつけた mapヘルパー や computed

    を使うことで Store 層の型が全部死んだ JSDoc を書かなかった export default { computed: { items() { return this.$store.getters['items'] || [] } }, methods: { doSomething() { const items = this.items // any } } }
  26. どうすればよかったのか TypeScriptのサポートが充実してきたころにJS→TSに移行する mapヘルパー の利用や getters を computed でラップしない export default

    Vue.extend({ methods: { doSomething(): Promise<void> { const items = this.$store.getters['items'] // ^? Item[] } } })
  27. まとめポエム 誰も悪くない。 「最良の方法」だと信じていたし、実際そうだった。 でも、時間が経つにつれ痛みが、 It's so painful. そして、過去の自分の設計を振り返ってこう思う。 お前のいう「絶対」って60%くらいだよな、と。