Slide 1

Slide 1 text

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

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

デザインパターン 5

Slide 6

Slide 6 text

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

Slide 7

Slide 7 text

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

Slide 8

Slide 8 text

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

Slide 9

Slide 9 text

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

Slide 10

Slide 10 text

Vue + デザインパターン 10

Slide 11

Slide 11 text

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

Slide 12

Slide 12 text

Vue + デザインパターン 12 Atomic Designは? ● UI設計に対する方法論 ○ 「UIパーツをどう分解するか」が焦点 ○ パーツの状態/振る舞いはスコープ外 コンポーネントの状態/振る舞いを設計/実装するには また別の方法を用意する必要がある

Slide 13

Slide 13 text

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

Slide 14

Slide 14 text

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

Slide 15

Slide 15 text

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

Slide 16

Slide 16 text

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

Slide 17

Slide 17 text

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

Slide 18

Slide 18 text

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

Slide 19

Slide 19 text

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

Slide 20

Slide 20 text

紹介するパターン 20 ● Stateパターン ○ コンポーネントの状態をオブジェクト化する ● Notificationパターン ○ 通知する情報(ex. エラー)をオブジェクト化する ● First Class Collectionパターン ○ コレクションをラップしたオブジェクトを作る

Slide 21

Slide 21 text

Stateパターン 21

Slide 22

Slide 22 text

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

Slide 23

Slide 23 text

23 例) TODOリスト

Slide 24

Slide 24 text

24

Slide 25

Slide 25 text

25

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
  • {{ content }} {{ taskState.notification }}
    期限:  {{ limit }}
  • Slide 31

    Slide 31 text

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

    Slide 32 text

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

    Slide 33

    Slide 33 text

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

    Slide 34

    Slide 34 text

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

    Slide 35

    Slide 35 text

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

    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() } 条件に応じて、 適切なStateオブジェクトを選択

    Slide 37

    Slide 37 text

    37 Stateパターンの効果 ● 状態のパターンが増えたら、それに応じてStateの種類とState の選択方法を変更すれば良い = htmlは変更無し ● 複数のフィールド/プロパティを管理する必要が無くなる

    Slide 38

    Slide 38 text

    Notificationパターン 38

    Slide 39

    Slide 39 text

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

    Slide 40

    Slide 40 text

    40

    Slide 41

    Slide 41 text

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

    Slide 42

    Slide 42 text

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

    Slide 43

    Slide 43 text

    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 ] } } エラーを配列で保持

    Slide 44

    Slide 44 text

    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

    Slide 45

    Slide 45 text

    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

    Slide 46

    Slide 46 text

    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

    Slide 47

    Slide 47 text

    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に追加

    Slide 48

    Slide 48 text

    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を返す

    Slide 49

    Slide 49 text

    49 Notificationパターンの効果 ● エラーをなるべく詳しくユーザーに伝えられる ○ 例えば処理失敗 => 例外throwだと、 他にエラーが発生し得ても一つしか残らない ● バリデーションや並行処理の時に便利

    Slide 50

    Slide 50 text

    First Class Collection パターン 50

    Slide 51

    Slide 51 text

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

    Slide 52

    Slide 52 text

    52 例) 商品カート

    Slide 53

    Slide 53 text

    53

    Slide 54

    Slide 54 text

    54

    Slide 55

    Slide 55 text

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

    Slide 56

    Slide 56 text

    56

    Slide 57

    Slide 57 text

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

    Slide 58

    Slide 58 text

    58

    Slide 59

    Slide 59 text

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

    Slide 60

    Slide 60 text

    60

    Slide 61

    Slide 61 text

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

    Slide 62

    Slide 62 text

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

    Slide 63

    Slide 63 text

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

    Slide 64

    Slide 64 text

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

    Slide 65

    Slide 65 text

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

    Slide 66

    Slide 66 text

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

    Slide 67

    Slide 67 text

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

    Slide 68

    Slide 68 text

    まとめ 68

    Slide 69

    Slide 69 text

    紹介したパターン 69 ● Stateパターン ○ コンポーネントの状態をオブジェクト化する ● Notificationパターン ○ 通知する情報(ex. エラー)をオブジェクト化する ● First Class Collectionパターン ○ コレクションをラップしたオブジェクトを作る

    Slide 70

    Slide 70 text

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

    Slide 71

    Slide 71 text

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

    Slide 72

    Slide 72 text

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

    Slide 73

    Slide 73 text

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

    Slide 74

    Slide 74 text

    参考資料 74

    Slide 75

    Slide 75 text

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