Upgrade to Pro — share decks privately, control downloads, hide ads and more …

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

philomagi
October 24, 2019

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

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

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

philomagi

October 24, 2019
Tweet

More Decks by philomagi

Other Decks in Programming

Transcript

  1. 4

  2. 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>
  3. Vue.js + デザインパターン 14 Atomic Designは? • UI設計に対する方法論 ◦ デザイン上の視点がメイン

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

    テストが大変 SFCには具体的なコンポーネントの振る舞いを記述し、 振る舞いの詳細は別に管理した方が良い
  5. 26

  6. 27

  7. 29

  8. 31

  9. 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>
  10. 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>
  11. 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オブジェクトから取得
  12. 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オブジェクトから取得
  13. 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() }
  14. 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() }
  15. 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オブジェクトを選択
  16. 43

  17. 45

  18. 46

  19. 48

  20. 50

  21. 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
  22. 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
  23. 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で定義 ・クリック時の処理をメソッドとして定義
  24. 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
  25. 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
  26. 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 ボタンクリック時の振る舞いを バリエーション毎に定義
  27. 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() } } }
  28. 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() } } } 状態に応じた ボタンの振る舞いを選択
  29. 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>
  30. 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>
  31. 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>
  32. 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に移譲
  33. 67

  34. 68

  35. 70

  36. 72

  37. 74

  38. 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 }) } // 省略 }
  39. 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 }) } // 省略 }
  40. 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 }) } // 省略 } ・コレクションに対する操作に  メソッドとして名前を付けられる ・「どのように」ではなく  「〜をしたい」に集中できる
  41. 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) } }
  42. 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コンポーネントでの実装は ・コレクションの初期化 ・イベントと操作の紐付け ・用途に応じた絞り込み のみで良い
  43. 81 First Class Collectionパターンの効果 • (オマケ) immutableな実装にしやすい ◦ 添字参照で配列を更新しても、vueでは検知されない ▪

    破壊的/暗黙的な実装が入りやすい ◦ first class collection 自体をimmutableにする ▪ コレクションの更新 = 再代入になるので確実 ◦ 性能的に問題が有り、オブジェクトを再利用したい ▪ 破壊的であることを明示したメソッドを定義
  44. Vue + デザインパターン 84 • デザインパターン = システム設計の定番パターンなので、フロ ントのコンポーネント設計/実装でも活用可能 •

    複雑なロジックはコンポーネント内部に押し込まず、 専門のモジュールに任せる • 専門のモジュールを組み立てるにあたって、デザインパターン が活用できる