Pro Yearly is on sale from $80 to $50! »

Vue.js___デザインパターン_によるコンポーネント実装_v2.pdf

B8403d102456248570005ee7fb2ba0f7?s=47 philomagi
October 24, 2019

 Vue.js___デザインパターン_によるコンポーネント実装_v2.pdf

Vue.js + デザインパターンによるコンポーネント実装。
2019/10/17 Yumemi.vue で発表した内容の一部差し替え版です。

Notification の代わりに Strategy が入っています。

B8403d102456248570005ee7fb2ba0f7?s=128

philomagi

October 24, 2019
Tweet

Transcript

  1. Vue.js + デザインパターン によるコンポーネント実装 1 @Philomagi 2019/10/24@俺得フロントエンド (2) LT会 #oretoku_front

  2. 発表者 @Philomagi • 主にフロントエンド主体のWEB系エンジニア • ScalaとTypescriptとRubyが好き ◦ Rubyは最近、公私共に若干疎遠 • PHPは中々縁が切れない悪友

    ◦ 最近は、「然程悪いやつでもないな」と思い始めてる 2
  3. Vue.js 3

  4. 4

  5. Single File Component (Vue.js) 5

  6. 6 <template> <div class="home"> <img alt="Vue logo" src="../assets/logo.png"> <HelloWorld msg="Welcome

    to Your Vue.js + TypeScript App"/> </div> </template> <script lang="ts"> import { Component, Vue } from 'vue-property-decorator' import HelloWorld from '@/components/HelloWorld.vue' @Component({ components: { HelloWorld } }) export default class Home extends Vue {} </script>
  7. デザインパターン 7

  8. 「デザインパターン」とは 8 多くのオブジェクト指向システムには、クラスやオブジェクト群の有る 種のパターンが繰り返し用いられている。 (中略) このようなパターンに精通した設計者は、それらを再発見する必要が ないので、これらのパターンを設計問題に速やかに適用することがで きる。 日本語版 Design

    Patterns Elements of Reusable Object-Oriented Software (SB Creative) p.13
  9. 「デザインパターン」とは 9 プログラミングを行っていると、以前と同じことを繰り返しているなぁ、 と気づくときがあります。経験が増すにつれ、そのような「パターン」が 自分の心の中に数多く蓄積されます。 (中略) そのような開発者の「経験」や「内的な蓄積」としてのパターンを「デザ インパターン」という形に整理しました。 結城浩 Java言語で学ぶデザインパターン入門

    (SB Creative) p.ⅲ
  10. ここではザックリ システム設計でよく使われる 定番の構造/組み合わせのパターン ぐらいの理解でいきます 「デザインパターン」とは 10

  11. ここではザックリ システム設計でよく使われる 定番の構造/組み合わせのパターン ぐらいの理解でいきます ※「デザインパターン」それ自体の定義は、今回余り掘り下げません 「デザインパターン」とは 11

  12. Vue.js + デザインパターン 12

  13. Vue.js + デザインパターン 13 ありがちな問題 • Vue.jsのSingle File Component(SFC)は便利だが、 何かとベタ書きしがち/されがち

    ◦ Smart UI アンチパターン ◦ FatなPHPテンプレート、ERBテンプレート etc…
  14. Vue.js + デザインパターン 14 Atomic Designは? • UI設計に対する方法論 ◦ デザイン上の視点がメイン

    ◦ 細かく作ることでデザイン上の再利用性は上がる ◦ 状態管理や振る舞いの設計はスコープ外 コンポーネントの状態/振る舞いを設計/実装するには また別の方法を用意する必要がある
  15. Vue.js + デザインパターン 15 あらゆる処理をSFCに組み込むと辛い ◦ 実装が肥大化する ◦ 読み込まないと意図が分からない ◦

    テストが大変 SFCには具体的なコンポーネントの振る舞いを記述し、 振る舞いの詳細は別に管理した方が良い
  16. Vue.js + デザインパターン じゃあ、ロジックをSFCから分離しよう! 16

  17. Vue.js + デザインパターン じゃあ、ロジックをSFCから分離しよう! → そのロジックはどうやって組み立てる? → そのロジックにはどんな構造が適切? 17

  18. Vue.js + デザインパターン じゃあ、ロジックをSFCから分離しよう! → そのロジックはどうやって組み立てる? → そのロジックにはどんな構造が適切? → デザインパターンを利用する

    18
  19. Vue.js + デザインパターン 19 • デザインパターン=システム設計の 定番な構造/組み合わせパターン • バックエンド/フロントエンド問わず使えるはず

  20. Vue.js + デザインパターン 20 • デザインパターン=システム設計の 定番な構造/組み合わせパターン • バックエンド/フロントエンド問わず使えるはず

  21. Vue.js + デザインパターン 21 • デザインパターン=システム設計の 定番な構造/組み合わせパターン • バックエンド/フロントエンド問わず使えるはず →

    「ありがちな問題」の解消にも利用できるはず!
  22. 実際に使ってみた 22 • Stateパターン ◦ コンポーネントの状態をオブジェクト化する • Strategyパターン ◦ ロジックを丸ごと交換可能にする

    • First Class Collectionパターン ◦ コレクションをラップしたオブジェクトを作る
  23. Stateパターン 23

  24. Stateパターン • コンポーネントの内部状態が変化した時に、 振る舞いも連動して変わるようにする • 状態変化をオブジェクトの切り替えで実現する 24

  25. 25 例) TODOリスト

  26. 26

  27. 27

  28. タスク毎の状態によって、 背景色や文字色を切り替え 28

  29. 29

  30. 30 タスク毎の状態によって、 追加テキストを表示

  31. 31

  32. 32 <template> <li class="todo-task" :style="taskState.style" > <div> {{ content }}&nbsp;{{

    taskState.notification }} </div> <div class="todo-task__limit"> <div> 期限:&nbsp; {{ limit }} </div> </div> </li> </template>
  33. 33 <template> <li class="todo-task" :style="taskState.style" > <div> {{ content }}&nbsp;{{

    taskState.notification }} </div> <div class="todo-task__limit"> <div> 期限:&nbsp; {{ limit }} </div> </div> </li> </template>
  34. 34 <template> <li class="todo-task" :style="taskState.style" > <div> {{ content }}&nbsp;{{

    taskState.notification }} </div> <div class="todo-task__limit"> <div> 期限:&nbsp; {{ limit }} </div> </div> </li> </template> TODOタスクの表示調整内容を taskStateオブジェクトから取得
  35. 35 <template> <li class="todo-task" :style="taskState.style" > <div> {{ content }}&nbsp;{{

    taskState.notification }} </div> <div class="todo-task__limit"> <div> 期限:&nbsp; {{ limit }} </div> </div> </li> </template> TODOタスクの通知テキストを taskStateオブジェクトから取得
  36. 36 get taskState (): TaskState { const today = new

    Date() const limit = new Date(this.limit) const diff = limit.getTime() - today.getTime() const oneDay = 1000 * 60 * 60 * 24 if (diff > oneDay * 3) { return new NormalState() } if (diff >= 0) { return new CloseToLimitState() } return new LimitOverState() }
  37. 37 get taskState (): TaskState { const today = new

    Date() const limit = new Date(this.limit) const diff = limit.getTime() - today.getTime() const oneDay = 1000 * 60 * 60 * 24 if (diff > oneDay * 3) { return new NormalState() } if (diff >= 0) { return new CloseToLimitState() } return new LimitOverState() }
  38. 38 get taskState (): TaskState { const today = new

    Date() const limit = new Date(this.limit) const diff = limit.getTime() - today.getTime() const oneDay = 1000 * 60 * 60 * 24 if (diff > oneDay * 3) { return new NormalState() } if (diff >= 0) { return new CloseToLimitState() } return new LimitOverState() } 条件に応じて、 適切なStateオブジェクトを選択
  39. 39 Stateパターンの効果 • 状態のパターンが増えたら、それに応じてStateの種類とState の選択方法を変更すれば良い = htmlは変更無し • 複数のフィールド/プロパティを管理する必要が無くなる ◦

    関連するものは全てStateオブジェクトに統合される ◦ 異なる状態には、また別のStateオブジェクトを用意
  40. Strategyパターン 40

  41. Strategyパターン 41 • ロジックを動的に交換可能にする • ロジックの切り替えを、分岐ではなく オブジェクトの交換で実現する

  42. 42 例) ボタンの挙動切り替え

  43. 43

  44. 44 Loading中は クリックしても反応しない

  45. 45

  46. 46

  47. 47 Loading後は クリックするとポップアップ

  48. 48

  49. 49 Loading後は クリックすると再読み込み

  50. 50

  51. export interface ButtonBehavior { readonly label: string onClick (): void

    } export class OnLoading implements ButtonBehavior { onClick () { // ignore } } export class OnSuccess implements ButtonBehavior { onClick () { alert('Success!!') } } export class OnFailed implements ButtonBehavior { onClick () { location.reload() } } 51
  52. export interface ButtonBehavior { readonly label: string onClick (): void

    } export class OnLoading implements ButtonBehavior { onClick () { // ignore } } export class OnSuccess implements ButtonBehavior { onClick () { alert('Success!!') } } export class OnFailed implements ButtonBehavior { onClick () { location.reload() } } 52
  53. export interface ButtonBehavior { readonly label: string onClick (): void

    } export class OnLoading implements ButtonBehavior { onClick () { // ignore } } export class OnSuccess implements ButtonBehavior { onClick () { alert('Success!!') } } export class OnFailed implements ButtonBehavior { onClick () { location.reload() } } 53 ・ボタンの振る舞いをIFで定義 ・クリック時の処理をメソッドとして定義
  54. export interface ButtonBehavior { readonly label: string onClick (): void

    } export class OnLoading implements ButtonBehavior { onClick () { // ignore } } export class OnSuccess implements ButtonBehavior { onClick () { alert('Success!!') } } export class OnFailed implements ButtonBehavior { onClick () { location.reload() } } 54
  55. export interface ButtonBehavior { readonly label: string onClick (): void

    } export class OnLoading implements ButtonBehavior { onClick () { // ignore } } export class OnSuccess implements ButtonBehavior { onClick () { alert('Success!!') } } export class OnFailed implements ButtonBehavior { onClick () { location.reload() } } 55
  56. export interface ButtonBehavior { readonly label: string onClick (): void

    } export class OnLoading implements ButtonBehavior { onClick () { // ignore } } export class OnSuccess implements ButtonBehavior { onClick () { alert('Success!!') } } export class OnFailed implements ButtonBehavior { onClick () { location.reload() } } 56 ボタンクリック時の振る舞いを バリエーション毎に定義
  57. 57 export default class Loading extends Vue { selected: Selector

    = this.selectors[0] // (略) get buttonBehavior (): ButtonBehavior { switch (this.selected.value) { case 'loading': return new OnLoading() case 'success': return new OnSuccess() default: return new OnFailed() } } }
  58. 58 export default class Loading extends Vue { selected: Selector

    = this.selectors[0] // (略) get buttonBehavior (): ButtonBehavior { switch (this.selected.value) { case 'loading': return new OnLoading() case 'success': return new OnSuccess() default: return new OnFailed() } } } 状態に応じた ボタンの振る舞いを選択
  59. 59 <template> <div class="loading"> <h1>Loading Button</h1> <loading-state-selector-list :selectors="selectors" :selected="selected" @click="onClick"

    /> <div class="button" @click="buttonBehavior.onClick" > {{ buttonBehavior.label }} </div> </div> </template>
  60. 60 <template> <div class="loading"> <h1>Loading Button</h1> <loading-state-selector-list :selectors="selectors" :selected="selected" @click="onClick"

    /> <div class="button" @click="buttonBehavior.onClick" > {{ buttonBehavior.label }} </div> </div> </template>
  61. 61 <template> <div class="loading"> <h1>Loading Button</h1> <loading-state-selector-list :selectors="selectors" :selected="selected" @click="onClick"

    /> <div class="button" @click="buttonBehavior.onClick" > {{ buttonBehavior.label }} </div> </div> </template>
  62. 62 <template> <div class="loading"> <h1>Loading Button</h1> <loading-state-selector-list :selectors="selectors" :selected="selected" @click="onClick"

    /> <div class="button" @click="buttonBehavior.onClick" > {{ buttonBehavior.label }} </div> </div> </template> クリック時の具体的な振る舞いは buttonBehaviorに移譲
  63. 63 Strategyパターンの効果 • コンポーネントの振る舞いをオブジェクトにすることで動的に処 理を切り替えることができる • 「どの状況でどのように振る舞うか」を選ぶことに  集中でき る • 条件分岐で肥大化しにくい

  64. First Class Collection パターン 64

  65. • 各コレクションをそれぞれ独自のクラスにラップする • コレクションに対する操作を、そのクラスの振る舞いとして定義する ことができる • プログラム上の意図を明示したIFを用意できる First Class Collection

    パターン 65
  66. 66 例) 商品カート

  67. 67

  68. 68

  69. 69 カート内には 複数の商品が存在する

  70. 70

  71. 71 ・カートから削除 ・削除せず後で買う といった設定が可能

  72. 72

  73. 73 カート内の ・購入する商品数 ・購入時の合計金額 を表示 ただし、後で買う商品は除外

  74. 74

  75. 75 export class CartItemList { private constructor (private readonly cartItems:

    CartItem[]) { } // (中略) onlyWillPurchase (): CartItemList { return this.filterInner(cartItem => cartItem.state.willPurchase) } add (item: Item): CartItemList { // 省略 } remove (item: Item): CartItemList { return this.filter(itemInCart => itemInCart.id !== item.id) } buyLater (item: Item): CartItemList { return this.changeState(item, { willPurchase: false }) } // 省略 }
  76. 76 export class CartItemList { private constructor (private readonly cartItems:

    CartItem[]) { } // (中略) onlyWillPurchase (): CartItemList { return this.filterInner(cartItem => cartItem.state.willPurchase) } add (item: Item): CartItemList { // 省略 } remove (item: Item): CartItemList { return this.filter(itemInCart => itemInCart.id !== item.id) } buyLater (item: Item): CartItemList { return this.changeState(item, { willPurchase: false }) } // 省略 }
  77. 77 export class CartItemList { private constructor (private readonly cartItems:

    CartItem[]) { } // (中略) onlyWillPurchase (): CartItemList { return this.filterInner(cartItem => cartItem.state.willPurchase) } add (item: Item): CartItemList { // 省略 } remove (item: Item): CartItemList { return this.filter(itemInCart => itemInCart.id !== item.id) } buyLater (item: Item): CartItemList { return this.changeState(item, { willPurchase: false }) } // 省略 } ・コレクションに対する操作に  メソッドとして名前を付けられる ・「どのように」ではなく  「〜をしたい」に集中できる
  78. 78 export default class Cart extends Vue { cartItems =

    CartItemList.initialize() .add({ // 省略 }) get cartItemsWillPurchase (): CartItemList { return this.cartItems.onlyWillPurchase() } remove (item: Item) { this.cartItems = this.cartItems.remove(item) } buyLater (item: Item) { this.cartItems = this.cartItems.buyLater(item) } buyNow (item: Item) { this.cartItems = this.cartItems.buyNow(item) } }
  79. 79 export default class Cart extends Vue { cartItems =

    CartItemList.initialize() .add({ // 省略 }) get cartItemsWillPurchase (): CartItemList { return this.cartItems.onlyWillPurchase() } remove (item: Item) { this.cartItems = this.cartItems.remove(item) } buyLater (item: Item) { this.cartItems = this.cartItems.buyLater(item) } buyNow (item: Item) { this.cartItems = this.cartItems.buyNow(item) } } Vueコンポーネントでの実装は ・コレクションの初期化 ・イベントと操作の紐付け ・用途に応じた絞り込み のみで良い
  80. 80 First Class Collectionパターンの効果 • コレクションに対する操作を抽象化できる ◦ 直接配列をVueコンポーネントで持つと、配列操作でコード が肥大化しがち、意図が埋もれがち ◦

    「何をしたいか」をメソッドやプロパティとして 明示的に記述できる
  81. 81 First Class Collectionパターンの効果 • (オマケ) immutableな実装にしやすい ◦ 添字参照で配列を更新しても、vueでは検知されない ▪

    破壊的/暗黙的な実装が入りやすい ◦ first class collection 自体をimmutableにする ▪ コレクションの更新 = 再代入になるので確実 ◦ 性能的に問題が有り、オブジェクトを再利用したい ▪ 破壊的であることを明示したメソッドを定義
  82. まとめ 82

  83. 実際に使ってみた 83 • Stateパターン ◦ コンポーネントの状態をオブジェクト化する • Strategyパターン ◦ ロジックを丸ごと交換可能にする

    • First Class Collectionパターン ◦ コレクションをラップしたオブジェクトを作る
  84. Vue + デザインパターン 84 • デザインパターン = システム設計の定番パターンなので、フロ ントのコンポーネント設計/実装でも活用可能 •

    複雑なロジックはコンポーネント内部に押し込まず、 専門のモジュールに任せる • 専門のモジュールを組み立てるにあたって、デザインパターン が活用できる
  85. 考え方 85 • 「パーツの分解」だけに集中しない • 「パーツの状態/振る舞い」にも意識を向ける • 複雑な状態/振る舞いが有れば、 それ自体をオブジェクト化する •

    デザインパターン = レシピ集(not マニュアル)
  86. サンプルアプリ 86 掲載のアプリ + コード全体はこちら https://github.com/tooppoo/sample-for-vue-with-design-patterns

  87. 参考資料 87 • ThoughtWorksアンソロジー ―アジャイルとオブジェクト指向によるソフト ウェアイノベーション • 増補改訂版Java言語で学ぶデザインパターン入門 • オブジェクト指向における再利用のためのデザインパターン

    • martinFowler.com ◦ https://www.martinfowler.com/eaaDev/Notification.html
  88. 参考資料 88

  89. ご清聴ありがとうございました 89