Slide 1

Slide 1 text

Vue.js + デザインパターン によるコンポーネント実装 1 @Philomagi 2019/10/24@俺得フロントエンド (2) LT会 #oretoku_front

Slide 2

Slide 2 text

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

Slide 3

Slide 3 text

Vue.js 3

Slide 4

Slide 4 text

4

Slide 5

Slide 5 text

Single File Component (Vue.js) 5

Slide 6

Slide 6 text

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

Slide 7

Slide 7 text

デザインパターン 7

Slide 8

Slide 8 text

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

Slide 9

Slide 9 text

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

Slide 10

Slide 10 text

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

Slide 11

Slide 11 text

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

Slide 12

Slide 12 text

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

Slide 13

Slide 13 text

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

Slide 14

Slide 14 text

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

Slide 15

Slide 15 text

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

Slide 16

Slide 16 text

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

Slide 17

Slide 17 text

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

Slide 18

Slide 18 text

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

Slide 19

Slide 19 text

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

Slide 20

Slide 20 text

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

Slide 21

Slide 21 text

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

Slide 22

Slide 22 text

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

Slide 23

Slide 23 text

Stateパターン 23

Slide 24

Slide 24 text

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

Slide 25

Slide 25 text

25 例) TODOリスト

Slide 26

Slide 26 text

26

Slide 27

Slide 27 text

27

Slide 28

Slide 28 text

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

Slide 29

Slide 29 text

29

Slide 30

Slide 30 text

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

Slide 31

Slide 31 text

31

Slide 32

Slide 32 text

32
  • {{ content }} {{ taskState.notification }}
    期限:  {{ limit }}
  • Slide 33

    Slide 33 text

    33
  • {{ content }} {{ taskState.notification }}
    期限:  {{ limit }}
  • Slide 34

    Slide 34 text

    34
  • {{ content }} {{ taskState.notification }}
    期限:  {{ limit }}
  • TODOタスクの表示調整内容を taskStateオブジェクトから取得

    Slide 35

    Slide 35 text

    35
  • {{ content }} {{ taskState.notification }}
    期限:  {{ limit }}
  • TODOタスクの通知テキストを taskStateオブジェクトから取得

    Slide 36

    Slide 36 text

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

    Slide 37

    Slide 37 text

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

    Slide 38

    Slide 38 text

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

    Slide 39

    Slide 39 text

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

    Slide 40

    Slide 40 text

    Strategyパターン 40

    Slide 41

    Slide 41 text

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

    Slide 42

    Slide 42 text

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

    Slide 43

    Slide 43 text

    43

    Slide 44

    Slide 44 text

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

    Slide 45

    Slide 45 text

    45

    Slide 46

    Slide 46 text

    46

    Slide 47

    Slide 47 text

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

    Slide 48

    Slide 48 text

    48

    Slide 49

    Slide 49 text

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

    Slide 50

    Slide 50 text

    50

    Slide 51

    Slide 51 text

    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

    Slide 52

    Slide 52 text

    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

    Slide 53

    Slide 53 text

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

    Slide 54

    Slide 54 text

    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

    Slide 55

    Slide 55 text

    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

    Slide 56

    Slide 56 text

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

    Slide 57

    Slide 57 text

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

    Slide 58

    Slide 58 text

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

    Slide 59

    Slide 59 text

    59

    Loading Button

    {{ buttonBehavior.label }}

    Slide 60

    Slide 60 text

    60

    Loading Button

    {{ buttonBehavior.label }}

    Slide 61

    Slide 61 text

    61

    Loading Button

    {{ buttonBehavior.label }}

    Slide 62

    Slide 62 text

    62

    Loading Button

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

    Slide 63

    Slide 63 text

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

    Slide 64

    Slide 64 text

    First Class Collection パターン 64

    Slide 65

    Slide 65 text

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

    Slide 66

    Slide 66 text

    66 例) 商品カート

    Slide 67

    Slide 67 text

    67

    Slide 68

    Slide 68 text

    68

    Slide 69

    Slide 69 text

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

    Slide 70

    Slide 70 text

    70

    Slide 71

    Slide 71 text

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

    Slide 72

    Slide 72 text

    72

    Slide 73

    Slide 73 text

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

    Slide 74

    Slide 74 text

    74

    Slide 75

    Slide 75 text

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

    Slide 76

    Slide 76 text

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

    Slide 77

    Slide 77 text

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

    Slide 78

    Slide 78 text

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

    Slide 79

    Slide 79 text

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

    Slide 80

    Slide 80 text

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

    Slide 81

    Slide 81 text

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

    Slide 82

    Slide 82 text

    まとめ 82

    Slide 83

    Slide 83 text

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

    Slide 84

    Slide 84 text

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

    Slide 85

    Slide 85 text

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

    Slide 86

    Slide 86 text

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

    Slide 87

    Slide 87 text

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

    Slide 88

    Slide 88 text

    参考資料 88

    Slide 89

    Slide 89 text

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