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/24@俺得フロントエンド (2) LT会
    #oretoku_front

    View full-size slide

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

    View full-size slide

  3. Single File Component
    (Vue.js)
    5

    View full-size slide

  4. 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 full-size slide

  5. デザインパターン
    7

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  21. Stateパターン
    23

    View full-size slide

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

    View full-size slide

  23. 25
    例) TODOリスト

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  26. 32

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

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



    期限:  {{ limit }}




    View full-size slide

  27. 33

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

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



    期限:  {{ limit }}




    View full-size slide

  28. 34

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

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



    期限:  {{ limit }}




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

    View full-size slide

  29. 35

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

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



    期限:  {{ limit }}




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

    View full-size slide

  30. 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 full-size slide

  31. 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 full-size slide

  32. 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 full-size slide

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

    View full-size slide

  34. Strategyパターン
    40

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  40. 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 full-size slide

  41. 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 full-size slide

  42. 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 full-size slide

  43. 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 full-size slide

  44. 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 full-size slide

  45. 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 full-size slide

  46. 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 full-size slide

  47. 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 full-size slide

  48. 59


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



    View full-size slide

  49. 60


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



    View full-size slide

  50. 61


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



    View full-size slide

  51. 62


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



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

    View full-size slide

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

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

    View full-size slide

  53. First Class Collection
    パターン
    64

    View full-size slide

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

    View full-size slide

  55. 66
    例) 商品カート

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  59. 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 full-size slide

  60. 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 full-size slide

  61. 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 full-size slide

  62. 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 full-size slide

  63. 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 full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  71. 参考資料
    88

    View full-size slide

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

    View full-size slide