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. Vue.js + デザインパターン
    によるコンポーネント実装
    1
    @Philomagi
    2019/10/[email protected]俺得フロントエンド (2) LT会
    #oretoku_front

    View Slide

  2. 発表者
    @Philomagi
    ● 主にフロントエンド主体のWEB系エンジニア
    ● ScalaとTypescriptとRubyが好き
    ○ Rubyは最近、公私共に若干疎遠
    ● PHPは中々縁が切れない悪友
    ○ 最近は、「然程悪いやつでもないな」と思い始めてる
    2

    View Slide

  3. Vue.js
    3

    View Slide

  4. 4

    View Slide

  5. Single File Component
    (Vue.js)
    5

    View Slide

  6. 6






    <br/>import { Component, Vue } from 'vue-property-decorator'<br/>import HelloWorld from '@/components/HelloWorld.vue'<br/>@Component({<br/>components: {<br/>HelloWorld<br/>}<br/>})<br/>export default class Home extends Vue {}<br/>

    View Slide

  7. デザインパターン
    7

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  13. Vue.js + デザインパターン
    13
    ありがちな問題
    ● Vue.jsのSingle File Component(SFC)は便利だが、
    何かとベタ書きしがち/されがち
    ○ Smart UI アンチパターン
    ○ FatなPHPテンプレート、ERBテンプレート etc…

    View Slide

  14. Vue.js + デザインパターン
    14
    Atomic Designは?
    ● UI設計に対する方法論
    ○ デザイン上の視点がメイン
    ○ 細かく作ることでデザイン上の再利用性は上がる
    ○ 状態管理や振る舞いの設計はスコープ外
    コンポーネントの状態/振る舞いを設計/実装するには
    また別の方法を用意する必要がある

    View Slide

  15. Vue.js + デザインパターン
    15
    あらゆる処理をSFCに組み込むと辛い
    ○ 実装が肥大化する
    ○ 読み込まないと意図が分からない
    ○ テストが大変
    SFCには具体的なコンポーネントの振る舞いを記述し、
    振る舞いの詳細は別に管理した方が良い

    View Slide

  16. Vue.js + デザインパターン
    じゃあ、ロジックをSFCから分離しよう!
    16

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  21. Vue.js + デザインパターン
    21
    ● デザインパターン=システム設計の
    定番な構造/組み合わせパターン
    ● バックエンド/フロントエンド問わず使えるはず
    → 「ありがちな問題」の解消にも利用できるはず!

    View Slide

  22. 実際に使ってみた
    22
    ● Stateパターン
    ○ コンポーネントの状態をオブジェクト化する
    ● Strategyパターン
    ○ ロジックを丸ごと交換可能にする
    ● First Class Collectionパターン
    ○ コレクションをラップしたオブジェクトを作る

    View Slide

  23. Stateパターン
    23

    View Slide

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

    View Slide

  25. 25
    例) TODOリスト

    View Slide

  26. 26

    View Slide

  27. 27

    View Slide

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

    View Slide

  29. 29

    View Slide

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

    View Slide

  31. 31

    View Slide

  32. 32

    class="todo-task"
    :style="taskState.style"
    >

    {{ content }} {{ taskState.notification }}



    期限:  {{ limit }}




    View Slide

  33. 33

    class="todo-task"
    :style="taskState.style"
    >

    {{ content }} {{ taskState.notification }}



    期限:  {{ limit }}




    View Slide

  34. 34

    class="todo-task"
    :style="taskState.style"
    >

    {{ content }} {{ taskState.notification }}



    期限:  {{ limit }}




    TODOタスクの表示調整内容を
    taskStateオブジェクトから取得

    View Slide

  35. 35

    class="todo-task"
    :style="taskState.style"
    >

    {{ content }} {{ taskState.notification }}



    期限:  {{ limit }}




    TODOタスクの通知テキストを
    taskStateオブジェクトから取得

    View Slide

  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()
    }

    View Slide

  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()
    }

    View Slide

  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オブジェクトを選択

    View Slide

  39. 39
    Stateパターンの効果
    ● 状態のパターンが増えたら、それに応じてStateの種類とState
    の選択方法を変更すれば良い = htmlは変更無し
    ● 複数のフィールド/プロパティを管理する必要が無くなる
    ○ 関連するものは全てStateオブジェクトに統合される
    ○ 異なる状態には、また別のStateオブジェクトを用意

    View Slide

  40. Strategyパターン
    40

    View Slide

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

    View Slide

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

    View Slide

  43. 43

    View Slide

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

    View Slide

  45. 45

    View Slide

  46. 46

    View Slide

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

    View Slide

  48. 48

    View Slide

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

    View Slide

  50. 50

    View Slide

  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

    View Slide

  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

    View Slide

  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で定義
    ・クリック時の処理をメソッドとして定義

    View Slide

  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

    View Slide

  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

    View Slide

  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
    ボタンクリック時の振る舞いを
    バリエーション毎に定義

    View Slide

  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()
    }
    }
    }

    View Slide

  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()
    }
    }
    }
    状態に応じた
    ボタンの振る舞いを選択

    View Slide

  59. 59


    Loading Button
    :selectors="selectors"
    :selected="selected"
    @click="onClick"
    />
    class="button"
    @click="buttonBehavior.onClick"
    >
    {{ buttonBehavior.label }}



    View Slide

  60. 60


    Loading Button
    :selectors="selectors"
    :selected="selected"
    @click="onClick"
    />
    class="button"
    @click="buttonBehavior.onClick"
    >
    {{ buttonBehavior.label }}



    View Slide

  61. 61


    Loading Button
    :selectors="selectors"
    :selected="selected"
    @click="onClick"
    />
    class="button"
    @click="buttonBehavior.onClick"
    >
    {{ buttonBehavior.label }}



    View Slide

  62. 62


    Loading Button
    :selectors="selectors"
    :selected="selected"
    @click="onClick"
    />
    class="button"
    @click="buttonBehavior.onClick"
    >
    {{ buttonBehavior.label }}



    クリック時の具体的な振る舞いは
    buttonBehaviorに移譲

    View Slide

  63. 63
    Strategyパターンの効果
    ● コンポーネントの振る舞いをオブジェクトにすることで動的に処
    理を切り替えることができる
    ● 「どの状況でどのように振る舞うか」を選ぶことに  集中でき

    ● 条件分岐で肥大化しにくい

    View Slide

  64. First Class Collection
    パターン
    64

    View Slide

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

    View Slide

  66. 66
    例) 商品カート

    View Slide

  67. 67

    View Slide

  68. 68

    View Slide

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

    View Slide

  70. 70

    View Slide

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

    View Slide

  72. 72

    View Slide

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

    View Slide

  74. 74

    View Slide

  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
    })
    }
    // 省略
    }

    View Slide

  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
    })
    }
    // 省略
    }

    View Slide

  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
    })
    }
    // 省略
    }
    ・コレクションに対する操作に
     メソッドとして名前を付けられる
    ・「どのように」ではなく
     「〜をしたい」に集中できる

    View Slide

  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)
    }
    }

    View Slide

  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コンポーネントでの実装は
    ・コレクションの初期化
    ・イベントと操作の紐付け
    ・用途に応じた絞り込み
    のみで良い

    View Slide

  80. 80
    First Class Collectionパターンの効果
    ● コレクションに対する操作を抽象化できる
    ○ 直接配列をVueコンポーネントで持つと、配列操作でコード
    が肥大化しがち、意図が埋もれがち
    ○ 「何をしたいか」をメソッドやプロパティとして
    明示的に記述できる

    View Slide

  81. 81
    First Class Collectionパターンの効果
    ● (オマケ) immutableな実装にしやすい
    ○ 添字参照で配列を更新しても、vueでは検知されない
    ■ 破壊的/暗黙的な実装が入りやすい
    ○ first class collection 自体をimmutableにする
    ■ コレクションの更新 = 再代入になるので確実
    ○ 性能的に問題が有り、オブジェクトを再利用したい
    ■ 破壊的であることを明示したメソッドを定義

    View Slide

  82. まとめ
    82

    View Slide

  83. 実際に使ってみた
    83
    ● Stateパターン
    ○ コンポーネントの状態をオブジェクト化する
    ● Strategyパターン
    ○ ロジックを丸ごと交換可能にする
    ● First Class Collectionパターン
    ○ コレクションをラップしたオブジェクトを作る

    View Slide

  84. Vue + デザインパターン
    84
    ● デザインパターン = システム設計の定番パターンなので、フロ
    ントのコンポーネント設計/実装でも活用可能
    ● 複雑なロジックはコンポーネント内部に押し込まず、
    専門のモジュールに任せる
    ● 専門のモジュールを組み立てるにあたって、デザインパターン
    が活用できる

    View Slide

  85. 考え方
    85
    ● 「パーツの分解」だけに集中しない
    ● 「パーツの状態/振る舞い」にも意識を向ける
    ● 複雑な状態/振る舞いが有れば、
    それ自体をオブジェクト化する
    ● デザインパターン = レシピ集(not マニュアル)

    View Slide

  86. サンプルアプリ
    86
    掲載のアプリ + コード全体はこちら
    https://github.com/tooppoo/sample-for-vue-with-design-patterns

    View Slide

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

    View Slide

  88. 参考資料
    88

    View Slide

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

    View Slide