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

Vue.js + デザインパターンによるコンポーネント実装

philomagi
October 17, 2019

Vue.js + デザインパターンによるコンポーネント実装

Vue.js と デザインパターンを活用したコンポーネント開発について

サンプルアプリは↓
https://github.com/tooppoo/sample-for-vue-with-design-patterns

philomagi

October 17, 2019
Tweet

More Decks by philomagi

Other Decks in Programming

Transcript

  1. 4

  2. Vue + デザインパターン 12 Atomic Designは? • UI設計に対する方法論 ◦ 「UIパーツをどう分解するか」が焦点

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

    テストが大変 SFCには具体的なコンポーネントの振る舞いを記述し、 振る舞いの詳細は別に管理した方が良い
  4. 紹介するパターン 20 • Stateパターン ◦ コンポーネントの状態をオブジェクト化する • Notificationパターン ◦ 通知する情報(ex.

    エラー)をオブジェクト化する • First Class Collectionパターン ◦ コレクションをラップしたオブジェクトを作る
  5. 24

  6. 25

  7. 27

  8. 29

  9. 30 <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. 31 <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. 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> TODOタスクの表示調整内容を taskStateオブジェクトから取得
  12. 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> TODOタスクの通知テキストを taskStateオブジェクトから取得
  13. 34 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. 35 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. 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() } 条件に応じて、 適切なStateオブジェクトを選択
  16. 40

  17. 41 export class Notification { private _errors: JobError[] = []

    get hasErrors (): boolean { return this._errors.length > 0 } get errors (): JobError[] { return this._errors } hasError (error: JobError): boolean { return this._errors.find(e => e.equals(error)) !== null } addError (error: JobError): void { this._errors = [ ...this._errors, error ] } }
  18. 42 export class Notification { private _errors: JobError[] = []

    get hasErrors (): boolean { return this._errors.length > 0 } get errors (): JobError[] { return this._errors } hasError (error: JobError): boolean { return this._errors.find(e => e.equals(error)) !== null } addError (error: JobError): void { this._errors = [ ...this._errors, error ] } }
  19. 43 export class Notification { private _errors: JobError[] = []

    get hasErrors (): boolean { return this._errors.length > 0 } get errors (): JobError[] { return this._errors } hasError (error: JobError): boolean { return this._errors.find(e => e.equals(error)) !== null } addError (error: JobError): void { this._errors = [ ...this._errors, error ] } } エラーを配列で保持
  20. 44 export class Notification { private _errors: JobError[] = []

    get hasErrors (): boolean { return this._errors.length > 0 } get errors (): JobError[] { return this._errors } hasError (error: JobError): boolean { return this._errors.find(e => e.equals(error)) !== null } addError (error: JobError): void { this._errors = [ ...this._errors, error ] } } エラー追加用のIF
  21. 45 const notification = new Notification() if (this.handleError('conflict')) { notification.addError(jobConflictedError)

    } if (this.handleError('forbidden')) { notification.addError(jobForbiddenError) } if (this.handleError('not-enough')) { notification.addError(jobNotEnoughParameterError) } if (this.handleError('not-found')) { notification.addError(jobNotFoundError) } if (this.handleError('timeout')) { notification.addError(jobTimeoutError) } this.notification = notification
  22. 46 const notification = new Notification() if (this.handleError('conflict')) { notification.addError(jobConflictedError)

    } if (this.handleError('forbidden')) { notification.addError(jobForbiddenError) } if (this.handleError('not-enough')) { notification.addError(jobNotEnoughParameterError) } if (this.handleError('not-found')) { notification.addError(jobNotFoundError) } if (this.handleError('timeout')) { notification.addError(jobTimeoutError) } this.notification = notification
  23. 47 const notification = new Notification() if (this.handleError('conflict')) { notification.addError(jobConflictedError)

    } if (this.handleError('forbidden')) { notification.addError(jobForbiddenError) } if (this.handleError('not-enough')) { notification.addError(jobNotEnoughParameterError) } if (this.handleError('not-found')) { notification.addError(jobNotFoundError) } if (this.handleError('timeout')) { notification.addError(jobTimeoutError) } this.notification = notification エラーが有れば Notificationに追加
  24. 48 const notification = new Notification() if (this.handleError('conflict')) { notification.addError(jobConflictedError)

    } if (this.handleError('forbidden')) { notification.addError(jobForbiddenError) } if (this.handleError('not-enough')) { notification.addError(jobNotEnoughParameterError) } if (this.handleError('not-found')) { notification.addError(jobNotFoundError) } if (this.handleError('timeout')) { notification.addError(jobTimeoutError) } this.notification = notification 最終的に、 諸々抱えたNotificationを返す
  25. 53

  26. 54

  27. 56

  28. 58

  29. 60

  30. 61 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 }) } // 省略 }
  31. 62 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 }) } // 省略 }
  32. 63 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 }) } // 省略 } ・コレクションに対する操作に  メソッドとして名前を付けられる ・「どのように」ではなく  「〜をしたい」に集中できる
  33. 64 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) } }
  34. 65 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コンポーネントでの実装は ・コレクションの初期化 ・イベントと操作の紐付け ・用途に応じた絞り込み のみで良い
  35. 67 First Class Collectionパターンの効果 • immutableな実装にしやすい ◦ 添字参照で配列を更新しても、vueでは検知されない ▪ 破壊的/暗黙的な実装が入りやすい

    ◦ first class collection 自体をimmutableにする ▪ コレクションの更新 = 再代入になるので確実 ◦ 性能的に問題が有り、オブジェクトを再利用したい ▪ 破壊的であることを明示したメソッドを定義
  36. 紹介したパターン 69 • Stateパターン ◦ コンポーネントの状態をオブジェクト化する • Notificationパターン ◦ 通知する情報(ex.

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

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