Share
Embed
Vue.js + デザインパターンによるコンポーネント実装
Vue.js と デザインパターンを活用したコンポーネント開発について
サンプルアプリは↓
https://github.com/tooppoo/sample-for-vue-with-design-patterns
philomagi
October 17, 2019
Transcript
75 slides
-
発表者 @Philomagi • 主にフロントエンド主体のWEB系エンジニア • ScalaとTypescriptとRubyが好き ◦ Rubyは最近、公私共に若干疎遠 • PHPは中々縁が切れない悪友
◦ 最近は、「然程悪いやつでもないな」と思い始めてる 2 -
「デザインパターン」とは 6 多くのオブジェクト指向システムには、クラスやオブジェクト群の有る 種のパターンが繰り返し用いられている。 (中略) このようなパターンに精通した設計者は、それらを再発見する必要が ないので、これらのパターンを設計問題に速やかに適用することがで きる。 日本語版 Design
Patterns Elements of Reusable Object-Oriented Software (SB Creative) p.13 -
Vue + デザインパターン 11 ありがちな問題 • Vue.jsのSFCは便利だが、 何かとベタ書きしがち/されがち ◦ Smart
UI アンチパターン ◦ FatなPHPテンプレート、ERBテンプレート etc… -
Vue + デザインパターン 12 Atomic Designは? • UI設計に対する方法論 ◦ 「UIパーツをどう分解するか」が焦点
◦ パーツの状態/振る舞いはスコープ外 コンポーネントの状態/振る舞いを設計/実装するには また別の方法を用意する必要がある -
Vue + デザインパターン 13 あらゆる処理をSFCに組み込むと辛い ◦ 実装が肥大化する ◦ 読み込まないと意図が分からない ◦
テストが大変 SFCには具体的なコンポーネントの振る舞いを記述し、 振る舞いの詳細は別に管理した方が良い -
Vue + デザインパターン 19 • デザインパターン=システム設計の 定番な構造/組み合わせパターン • バックエンド/フロントエンド問わず使える手法 →
「ありがちな問題」の解消にも利用できるはず! -
紹介するパターン 20 • Stateパターン ◦ コンポーネントの状態をオブジェクト化する • Notificationパターン ◦ 通知する情報(ex.
エラー)をオブジェクト化する • First Class Collectionパターン ◦ コレクションをラップしたオブジェクトを作る -
30 <template> <li class="todo-task" :style="taskState.style" > <div> {{ content }} {{
taskState.notification }} </div> <div class="todo-task__limit"> <div> 期限: {{ limit }} </div> </div> </li> </template> -
31 <template> <li class="todo-task" :style="taskState.style" > <div> {{ content }} {{
taskState.notification }} </div> <div class="todo-task__limit"> <div> 期限: {{ limit }} </div> </div> </li> </template> -
32 <template> <li class="todo-task" :style="taskState.style" > <div> {{ content }} {{
taskState.notification }} </div> <div class="todo-task__limit"> <div> 期限: {{ limit }} </div> </div> </li> </template> TODOタスクの表示調整内容を taskStateオブジェクトから取得 -
33 <template> <li class="todo-task" :style="taskState.style" > <div> {{ content }} {{
taskState.notification }} </div> <div class="todo-task__limit"> <div> 期限: {{ limit }} </div> </div> </li> </template> TODOタスクの通知テキストを taskStateオブジェクトから取得 -
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 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 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オブジェクトを選択 -
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 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 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 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 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 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 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 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を返す -
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 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 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 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 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 First Class Collectionパターンの効果 • コレクションに対する操作を抽象化できる ◦ 直接配列をVueコンポーネントで持つと、配列操作でコード が肥大化しがち、意図が埋もれがち ◦
「何をしたいか」をメソッドやプロパティとして 明示的に記述できる -
67 First Class Collectionパターンの効果 • immutableな実装にしやすい ◦ 添字参照で配列を更新しても、vueでは検知されない ▪ 破壊的/暗黙的な実装が入りやすい
◦ first class collection 自体をimmutableにする ▪ コレクションの更新 = 再代入になるので確実 ◦ 性能的に問題が有り、オブジェクトを再利用したい ▪ 破壊的であることを明示したメソッドを定義 -
紹介したパターン 69 • Stateパターン ◦ コンポーネントの状態をオブジェクト化する • Notificationパターン ◦ 通知する情報(ex.
エラー)をオブジェクト化する • First Class Collectionパターン ◦ コレクションをラップしたオブジェクトを作る -
Vue + デザインパターン 70 • デザインパターン = システム設計の定番パターンなので、フロ ントのコンポーネント設計/実装でも活用可能 •
複雑なロジックはコンポーネント内部に押し込まず、 専門のモジュールに任せる • 専門のモジュールを組み立てるにあたって、デザインパターン が活用できる -
考え方 71 • 「パーツの分解」だけに集中しない • 「パーツの状態/振る舞い」にも意識を向ける • 複雑な状態/振る舞いが有れば、 それ自体をオブジェクト化する •
デザインパターン = レシピ集(not マニュアル) -
参考資料 73 • ThoughtWorksアンソロジー ―アジャイルとオブジェクト指向によるソフト ウェアイノベーション • 増補改訂版Java言語で学ぶデザインパターン入門 • オブジェクト指向における再利用のためのデザインパターン
• martinFowler.com ◦ https://www.martinfowler.com/eaaDev/Notification.html