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

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

B8403d102456248570005ee7fb2ba0f7?s=47 philomagi
October 17, 2019

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

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

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

B8403d102456248570005ee7fb2ba0f7?s=128

philomagi

October 17, 2019
Tweet

Transcript

  1. Vue.js + デザインパターン によるコンポーネント実装 1 @Philomagi 2019/10/17@Yumemi.vue #2 #yumemi_vue

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

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

  4. 4

  5. デザインパターン 5

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

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

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

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

  10. Vue + デザインパターン 10

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

    UI アンチパターン ◦ FatなPHPテンプレート、ERBテンプレート etc…
  12. Vue + デザインパターン 12 Atomic Designは? • UI設計に対する方法論 ◦ 「UIパーツをどう分解するか」が焦点

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

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

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

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

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

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

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

    「ありがちな問題」の解消にも利用できるはず!
  20. 紹介するパターン 20 • Stateパターン ◦ コンポーネントの状態をオブジェクト化する • Notificationパターン ◦ 通知する情報(ex.

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

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

  23. 23 例) TODOリスト

  24. 24

  25. 25

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

  27. 27

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

  29. 29

  30. 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>
  31. 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>
  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> TODOタスクの表示調整内容を taskStateオブジェクトから取得
  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> TODOタスクの通知テキストを taskStateオブジェクトから取得
  34. 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() }
  35. 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() }
  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() } 条件に応じて、 適切なStateオブジェクトを選択
  37. 37 Stateパターンの効果 • 状態のパターンが増えたら、それに応じてStateの種類とState の選択方法を変更すれば良い = htmlは変更無し • 複数のフィールド/プロパティを管理する必要が無くなる

  38. Notificationパターン 38

  39. • 処理中のエラー情報等を収集するオブジェクトを作 り、表示側でのユーザ通知に使用する • 処理中の複数のエラーが発生した場合に、 全てのエラー情報をまとめて表示できる Notificationパターン 39

  40. 40

  41. 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 ] } }
  42. 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 ] } }
  43. 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 ] } } エラーを配列で保持
  44. 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
  45. 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
  46. 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
  47. 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に追加
  48. 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を返す
  49. 49 Notificationパターンの効果 • エラーをなるべく詳しくユーザーに伝えられる ◦ 例えば処理失敗 => 例外throwだと、 他にエラーが発生し得ても一つしか残らない •

    バリデーションや並行処理の時に便利
  50. First Class Collection パターン 50

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

    パターン 51
  52. 52 例) 商品カート

  53. 53

  54. 54

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

  56. 56

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

  58. 58

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

  60. 60

  61. 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 }) } // 省略 }
  62. 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 }) } // 省略 }
  63. 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 }) } // 省略 } ・コレクションに対する操作に  メソッドとして名前を付けられる ・「どのように」ではなく  「〜をしたい」に集中できる
  64. 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) } }
  65. 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コンポーネントでの実装は ・コレクションの初期化 ・イベントと操作の紐付け ・用途に応じた絞り込み のみで良い
  66. 66 First Class Collectionパターンの効果 • コレクションに対する操作を抽象化できる ◦ 直接配列をVueコンポーネントで持つと、配列操作でコード が肥大化しがち、意図が埋もれがち ◦

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

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

  69. 紹介したパターン 69 • Stateパターン ◦ コンポーネントの状態をオブジェクト化する • Notificationパターン ◦ 通知する情報(ex.

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

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

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

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

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

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