Vue.js + デザインパターンによるコンポーネント実装。 2019/10/17 Yumemi.vue で発表した内容の一部差し替え版です。
Notification の代わりに Strategy が入っています。
Vue.js + デザインパターンによるコンポーネント実装1@Philomagi2019/10/[email protected]俺得フロントエンド (2) LT会#oretoku_front
View Slide
発表者@Philomagi● 主にフロントエンド主体のWEB系エンジニア● ScalaとTypescriptとRubyが好き○ Rubyは最近、公私共に若干疎遠● PHPは中々縁が切れない悪友○ 最近は、「然程悪いやつでもないな」と思い始めてる2
Vue.js3
4
Single File Component(Vue.js)5
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/>
デザインパターン7
「デザインパターン」とは8多くのオブジェクト指向システムには、クラスやオブジェクト群の有る種のパターンが繰り返し用いられている。(中略)このようなパターンに精通した設計者は、それらを再発見する必要がないので、これらのパターンを設計問題に速やかに適用することができる。日本語版 Design Patterns Elements of Reusable Object-Oriented Software (SB Creative) p.13
「デザインパターン」とは9プログラミングを行っていると、以前と同じことを繰り返しているなぁ、と気づくときがあります。経験が増すにつれ、そのような「パターン」が自分の心の中に数多く蓄積されます。(中略)そのような開発者の「経験」や「内的な蓄積」としてのパターンを「デザインパターン」という形に整理しました。結城浩 Java言語で学ぶデザインパターン入門 (SB Creative) p.ⅲ
ここではザックリシステム設計でよく使われる定番の構造/組み合わせのパターンぐらいの理解でいきます「デザインパターン」とは10
ここではザックリシステム設計でよく使われる定番の構造/組み合わせのパターンぐらいの理解でいきます※「デザインパターン」それ自体の定義は、今回余り掘り下げません「デザインパターン」とは11
Vue.js+デザインパターン12
Vue.js + デザインパターン13ありがちな問題● Vue.jsのSingle File Component(SFC)は便利だが、何かとベタ書きしがち/されがち○ Smart UI アンチパターン○ FatなPHPテンプレート、ERBテンプレート etc…
Vue.js + デザインパターン14Atomic Designは?● UI設計に対する方法論○ デザイン上の視点がメイン○ 細かく作ることでデザイン上の再利用性は上がる○ 状態管理や振る舞いの設計はスコープ外コンポーネントの状態/振る舞いを設計/実装するにはまた別の方法を用意する必要がある
Vue.js + デザインパターン15あらゆる処理をSFCに組み込むと辛い○ 実装が肥大化する○ 読み込まないと意図が分からない○ テストが大変SFCには具体的なコンポーネントの振る舞いを記述し、振る舞いの詳細は別に管理した方が良い
Vue.js + デザインパターンじゃあ、ロジックをSFCから分離しよう!16
Vue.js + デザインパターンじゃあ、ロジックをSFCから分離しよう!→ そのロジックはどうやって組み立てる?→ そのロジックにはどんな構造が適切?17
Vue.js + デザインパターンじゃあ、ロジックをSFCから分離しよう!→ そのロジックはどうやって組み立てる?→ そのロジックにはどんな構造が適切?→ デザインパターンを利用する18
Vue.js + デザインパターン19● デザインパターン=システム設計の定番な構造/組み合わせパターン● バックエンド/フロントエンド問わず使えるはず
Vue.js + デザインパターン20● デザインパターン=システム設計の定番な構造/組み合わせパターン● バックエンド/フロントエンド問わず使えるはず
Vue.js + デザインパターン21● デザインパターン=システム設計の定番な構造/組み合わせパターン● バックエンド/フロントエンド問わず使えるはず→ 「ありがちな問題」の解消にも利用できるはず!
実際に使ってみた22● Stateパターン○ コンポーネントの状態をオブジェクト化する● Strategyパターン○ ロジックを丸ごと交換可能にする● First Class Collectionパターン○ コレクションをラップしたオブジェクトを作る
Stateパターン23
Stateパターン● コンポーネントの内部状態が変化した時に、振る舞いも連動して変わるようにする● 状態変化をオブジェクトの切り替えで実現する24
25例) TODOリスト
26
27
タスク毎の状態によって、背景色や文字色を切り替え28
29
30タスク毎の状態によって、追加テキストを表示
31
32class="todo-task":style="taskState.style">{{ content }} {{ taskState.notification }}期限: {{ limit }}
33class="todo-task":style="taskState.style">{{ content }} {{ taskState.notification }}期限: {{ limit }}
34class="todo-task":style="taskState.style">{{ content }} {{ taskState.notification }}期限: {{ limit }}TODOタスクの表示調整内容をtaskStateオブジェクトから取得
35class="todo-task":style="taskState.style">{{ content }} {{ taskState.notification }}期限: {{ limit }}TODOタスクの通知テキストをtaskStateオブジェクトから取得
36get taskState (): TaskState {const today = new Date()const limit = new Date(this.limit)const diff = limit.getTime() - today.getTime()const oneDay = 1000 * 60 * 60 * 24if (diff > oneDay * 3) {return new NormalState()}if (diff >= 0) {return new CloseToLimitState()}return new LimitOverState()}
37get taskState (): TaskState {const today = new Date()const limit = new Date(this.limit)const diff = limit.getTime() - today.getTime()const oneDay = 1000 * 60 * 60 * 24if (diff > oneDay * 3) {return new NormalState()}if (diff >= 0) {return new CloseToLimitState()}return new LimitOverState()}
38get taskState (): TaskState {const today = new Date()const limit = new Date(this.limit)const diff = limit.getTime() - today.getTime()const oneDay = 1000 * 60 * 60 * 24if (diff > oneDay * 3) {return new NormalState()}if (diff >= 0) {return new CloseToLimitState()}return new LimitOverState()}条件に応じて、適切なStateオブジェクトを選択
39Stateパターンの効果● 状態のパターンが増えたら、それに応じてStateの種類とStateの選択方法を変更すれば良い = htmlは変更無し● 複数のフィールド/プロパティを管理する必要が無くなる○ 関連するものは全てStateオブジェクトに統合される○ 異なる状態には、また別のStateオブジェクトを用意
Strategyパターン40
Strategyパターン41● ロジックを動的に交換可能にする● ロジックの切り替えを、分岐ではなくオブジェクトの交換で実現する
42例) ボタンの挙動切り替え
43
44Loading中はクリックしても反応しない
45
46
47Loading後はクリックするとポップアップ
48
49Loading後はクリックすると再読み込み
50
export interface ButtonBehavior {readonly label: stringonClick (): 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
export interface ButtonBehavior {readonly label: stringonClick (): 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
export interface ButtonBehavior {readonly label: stringonClick (): 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で定義・クリック時の処理をメソッドとして定義
export interface ButtonBehavior {readonly label: stringonClick (): 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
export interface ButtonBehavior {readonly label: stringonClick (): 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
export interface ButtonBehavior {readonly label: stringonClick (): 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ボタンクリック時の振る舞いをバリエーション毎に定義
57export 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()}}}
58export 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()}}}状態に応じたボタンの振る舞いを選択
59Loading Button:selectors="selectors":selected="selected"@click="onClick"/>class="button"@click="buttonBehavior.onClick">{{ buttonBehavior.label }}
60Loading Button:selectors="selectors":selected="selected"@click="onClick"/>class="button"@click="buttonBehavior.onClick">{{ buttonBehavior.label }}
61Loading Button:selectors="selectors":selected="selected"@click="onClick"/>class="button"@click="buttonBehavior.onClick">{{ buttonBehavior.label }}
62Loading Button:selectors="selectors":selected="selected"@click="onClick"/>class="button"@click="buttonBehavior.onClick">{{ buttonBehavior.label }}クリック時の具体的な振る舞いはbuttonBehaviorに移譲
63Strategyパターンの効果● コンポーネントの振る舞いをオブジェクトにすることで動的に処理を切り替えることができる● 「どの状況でどのように振る舞うか」を選ぶことに 集中できる● 条件分岐で肥大化しにくい
First Class Collectionパターン64
● 各コレクションをそれぞれ独自のクラスにラップする● コレクションに対する操作を、そのクラスの振る舞いとして定義することができる● プログラム上の意図を明示したIFを用意できるFirst Class Collection パターン65
66例) 商品カート
67
68
69カート内には複数の商品が存在する
70
71・カートから削除・削除せず後で買うといった設定が可能
72
73カート内の・購入する商品数・購入時の合計金額を表示ただし、後で買う商品は除外
74
75export 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})}// 省略}
76export 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})}// 省略}
77export 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})}// 省略}・コレクションに対する操作に メソッドとして名前を付けられる・「どのように」ではなく 「〜をしたい」に集中できる
78export 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)}}
79export 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コンポーネントでの実装は・コレクションの初期化・イベントと操作の紐付け・用途に応じた絞り込みのみで良い
80First Class Collectionパターンの効果● コレクションに対する操作を抽象化できる○ 直接配列をVueコンポーネントで持つと、配列操作でコードが肥大化しがち、意図が埋もれがち○ 「何をしたいか」をメソッドやプロパティとして明示的に記述できる
81First Class Collectionパターンの効果● (オマケ) immutableな実装にしやすい○ 添字参照で配列を更新しても、vueでは検知されない■ 破壊的/暗黙的な実装が入りやすい○ first class collection 自体をimmutableにする■ コレクションの更新 = 再代入になるので確実○ 性能的に問題が有り、オブジェクトを再利用したい■ 破壊的であることを明示したメソッドを定義
まとめ82
実際に使ってみた83● Stateパターン○ コンポーネントの状態をオブジェクト化する● Strategyパターン○ ロジックを丸ごと交換可能にする● First Class Collectionパターン○ コレクションをラップしたオブジェクトを作る
Vue + デザインパターン84● デザインパターン = システム設計の定番パターンなので、フロントのコンポーネント設計/実装でも活用可能● 複雑なロジックはコンポーネント内部に押し込まず、専門のモジュールに任せる● 専門のモジュールを組み立てるにあたって、デザインパターンが活用できる
考え方85● 「パーツの分解」だけに集中しない● 「パーツの状態/振る舞い」にも意識を向ける● 複雑な状態/振る舞いが有れば、それ自体をオブジェクト化する● デザインパターン = レシピ集(not マニュアル)
サンプルアプリ86掲載のアプリ + コード全体はこちらhttps://github.com/tooppoo/sample-for-vue-with-design-patterns
参考資料87● ThoughtWorksアンソロジー ―アジャイルとオブジェクト指向によるソフトウェアイノベーション● 増補改訂版Java言語で学ぶデザインパターン入門● オブジェクト指向における再利用のためのデザインパターン● martinFowler.com○ https://www.martinfowler.com/eaaDev/Notification.html
参考資料88
ご清聴ありがとうございました89