こんにちは。Webフロントエンドエンジニアの松井菜穂子です。
ZOZOテクノロジーズに入社して一年ほど経ちます。
あるサービスの立ち上げから運用まで、Webフロントエンドのチームリーダー・開発メンバーとして関わってきました。
当記事では、当社のWebフロントエンド開発現場にあった問題と、それぞれの課題に対して堅実に積み重ねた技術的な改善方法についてご紹介します。
- はじめに
- その1: Vueコンポーネントを綺麗に分割する
- その2: Vuexをシンプルにする
- その3: TypeScriptを活用する
- 型関連のTips
- Enumへの変換
- その4: 開発ルールを制定する
- その5: 改善を可視化する
- おわりに
はじめに
改善対象のプロダクトのWebフロントエンドは、以下のような技術で構成されているSPAです。
- フレームワーク:Vue.js
- 状態管理:Vuex
- 言語:TypeScript
これらは、当社の数ある開発プロダクトのスタックの中でも比較的モダンなものです。
インターネット上にたくさんの関連情報があり、OSSでもこれらの技術に対応した様々なライブラリが公開されています。
モダンな技術でも負債は生まれる
そんな人気の技術を使っていて、何が問題なのか疑問に思われるかもしれません。
当社では、ZOZOTOWNやWEARのような大規模で息の長いサービスを運用するチームの他に、新規の事業を担当する開発チームをたくさん抱えています。
そういったチームでのスクラッチ開発は、まさにベンチャー企業のようなスピード感溢れるスタイルで行われています。
後々の保守変更に耐えうる技術構成を取るため、開発メンバーの手に馴染んでいないモダンなスタックを選定するというチャレンジをしたのが今回のパターンでした。
手探りで実装を進めた結果、各所で設計方針のブレが発生し、いわゆるレガシーな技術構成で開発するよりもかえって可読性が低く保守しづらいコードになってしまいます。
これ以降、この状態のコードを「負債」と表現します。
負債を何故改善するのか
負債の定義や、その改善によるビジネスインパクトがかかるコストに見合うかどうかの見極めは難しいものです。
今回も実際にその見極めが正確にできたわけではありませんが、それでも時間を割いてコードを改善しようと決めたのは、前向きなチームを作ることが重要だと思ったからです。
試行錯誤しながらサービスを完成させていくようなフェーズは、開発メンバーがそれぞれ機械的にタスクをこなすだけでは成り立ちません。
ある程度機転を利かせながら時にはイレギュラーなことにも挑戦できる環境がもっとも大事だと思っています。
その環境を作るためには開発メンバーが前向きに仕事に取り組めるような雰囲気作りが必要です。 エンジニアが前向きに開発に励んでいるサービスはそうでないサービスに比べて長期的にビジネス成果も上がるものだと信じています。
しかし、負債がそれを阻害しているかもしれません。
当チームでは、下記のような理由でメンバーのモチベーションが下がってしまうことがありました。
要因
- コード間の依存度が高く、変更作業の影響が読めない。
- 実装をする上で様々な知見を必要とする領域が多数あり、作業ハードルが高い。
- 良いコードを書いているという実感がない。
チーム作りといえばマネジメント関連の施策を思い浮かべがちですが、上記のような要因を取り除くことでも前向きなチーム作りができます。
今回は、当チームで積み重ねた地道な開発施策をご紹介させていただこうと思います。
その1: Vueコンポーネントを綺麗に分割する
まずは、Vue.jsのプロダクトで大きなベースとなるVueコンポーネントの設計です。
Vueコンポーネントの多くには「要因1. コード間の依存度が高く、変更作業の影響が読めない」に当てはまる負債がありました。
例えば下記のような、リンクテキストを表現するLinkField
コンポーネントです。
当プロダクトではクラス構文を用いてコンポーネントを定義しており、Vuexとのバインディングにvuex-class
ライブラリを使用しています。
テンプレート
<template> <router-link :to="`${localePath}${to}`"><slot></slot></router-link> </template>
コンポーネントクラス
import { Component, Vue } from 'vue-property-decorator' import { Prop } from 'vue-property-decorator' import { State } from 'vuex-class' @Component class LinkField extends Vue { @Prop({ type: String, required: true }) to!: string // Propで遷移先パスを受け取る @State('localePath') localePath?: string // Vuex Stateの"localePath"をバインド }
Stateから取得しているlocalePath
という値は、以下のような仕様に基づいています。
- URLパスの第一・第二要素は国と言語を表す
- 例:
/us/en
→ アメリカ・英語、/de/de
→ ドイツ・ドイツ語
- 例:
- サービス内のページ遷移は基本的に国・言語固定で行うため、遷移前のURLパスの第一・第二要素を保持したい
- 例: 商品一覧から注文一覧への遷移
/us/en/items
→/us/en/orders
- 例: 商品一覧から注文一覧への遷移
- 遷移前の国・言語を取得して遷移先に指定する処理を全てのリンク箇所で記述するのは面倒なので、共通化したい
- Stateに国と言語のパスを保持しておき、
LinkField
コンポーネント内でパスを結合することで共通化
- Stateに国と言語のパスを保持しておき、
使用例
<link-field :to="/orders">注文一覧へ</link-field> <!-- to属性には第三要素以降のみを指定でOK -->
最初は便利でしたが、このように汎用的なコンポーネントがVuex State(=状態)と密に結合している設計だと、LinkField
コンポーネントが素直に使えない場面が出てきます。
例えば、国・言語を切り替えつつ他のページに遷移させるリンクを置きたいときです。
StateのlocalePath
はグローバルな状態を管理するプロパティであるため、LinkField
コンポーネントの都合で書き換えると他の要素に予期せぬ影響が出てしまいます。
localePath
の変更なくコンポーネントの外からURLパスの第一・第二要素を渡したい場合、コンポーネントに手を入れずに実現できません。
こういった、状態と密結合である故に汎用的に使えないコンポーネントがたくさんありました。
コンポーネントの利用を諦めてスパゲッティコードのような記述をしてしまったり、限定的な用途のPropを追加したためさらに使いづらくなったりして、負債になっていきました。
解決策
当プロダクトでは、当初からコンポーネント分割の基準として導入していたAtomic Designという設計手法に基づいた簡潔なルールを元に、リファクタリングを行いました。
「Atom」「Molecule」レベルの汎用コンポーネントは、Vuex Stateを参照しないというルールです。
Atomic Designでの分割は、コンポーネントの見た目の粒度を基準に行っていました。
それに沿ってコンポーネントに閉じ込める共通実装は見た目基準のものだけにし、状態は切り離そうという考えです。
ただし、この設計手法をとっている例はよく聞きますが、どんなコンポーネントにとっても最適かというとそうではないかもしれません。
LinkField
の例でもlocalePath
に依存しないケースの方が少なく、そのためにわざわざ共通処理を引き剥がすのが正しいか、この1つをとれば色んな意見が出ると思います。
しかし目的は前向きなチーム作りなので、他への影響の配慮でストレスを感じない実装環境作りを最優先とし、汎用コンポーネントからVuexを分離することで可能な限り薄く作るようにしました。
テンプレート
<template> <router-link :to="to"><slot></slot></router-link> </template>
コンポーネントクラス
import { Component, Vue, Prop } from 'vue-property-decorator' @Component export default class LinkField extends Vue { @Prop({ type: String, required: true }) to!: string // Propで遷移先パスを受け取る }
使用例テンプレート
<link-field :to="`${localePath}/orders`">注文一覧へ</link-field>
使用例コンポーネントクラス
import { Component, Vue } from 'vue-property-decorator' @Component export default class Page extends Vue { localePath: string = '' // Vuex Stateから取得しても、ページ内で宣言してもOK }
ここまで改修した後、LinkField
コンポーネントで共通化している見た目基準の実装が下線スタイルくらいであることに気づき、このコンポーネントは削除することになりました。
そして直接Vue Router内蔵のRouterLink
を使用することになりました。
各ページコンポーネントの記述量は少し増えることもありますが、汎用コンポーネントの使い方について深く考えず実装できるようになり、コードを気持ちよく書くことができるようになります。
ある程度汎用コンポーネントの機能が上記のような見た目基準の粒度に揃うと、ページの実装がコンポーネントの単純な組み合わせ作業のみで完結します。
多方面への影響を考慮せずにシンプルな見積もりができるようになり、急なレイアウト変更のようなタスクにも前向きに取り組めるようになりました。
コンポーネントカタログで汎用化を促す
汎用コンポーネントがVuexを参照しない見た目基準の粒度であることを保障するため、コンポーネントカタログを役立てました。
コンポーネントカタログとは、コンポーネントの仕様書をプログラムで提供できる仕組みのことを指します。
Vue.jsを用いたプロダクトでは、よくStorybookが使われるようです。
当プロダクトでも初期段階でStorybookがインストールされていましたが、運用のハードルが高かったため廃止し独自のカタログを作成しました。
詳細はこちらの記事をご覧ください。Nuxt.js向けの説明ですが、大まかな設計はVue.jsにおいても同様です。
プロダクトに即したカタログ実装の例を以下に示します。
コード
カタログ自体もVue.jsのSPAになっています。
Atomic Designを意識したコンポーネント分割のため、ページもAtomic Designのレベルで分割(atoms.vue
/ molecules.vue
/ organisms.vue
)しました。
テンプレート(atoms.vue
)
<template> <h1>Atoms</h1> <h3>CheckField</h3> <p>valueはBoolean, Arrayどちらでも対応します。</p> <props-spec :component="CheckField"></props-spec> <div>inline設定</div> <div> <check-field v-model="isInline">inline</check-field> </div> <div>Booleanで使う</div> <p>isCheck1 = {{ isCheck1 }}, isCheck2 = {{ isCheck2 }}</p> <div> <check-field v-model="isCheck1" :inline="isInline">isCheck1</check-field> <check-field v-model="isCheck2" :inline="isInline">isCheck2</check-field> </div> <div>Arrayで使う</div> <p>someArr: {{ someArr }}</p> <div> <check-field value="a" v-model="someArr" :inline="isInline">option a</check-field> <check-field value="b" v-model="someArr" :inline="isInline">option b</check-field> <check-field value="c" v-model="someArr" :inline="isInline" disabled>option c</check-field> </div> <hr /> <h3>PillButton</h3> <props-spec :component="PillButton"></props-spec> <div>theme=default(デフォルト)</div> <div> <pill-button>Button</pill-button> </div> <div>theme=black</div> <div> <pill-button theme="black">Button</pill-button> </div> <div>theme=blue</div> <div> <pill-button theme="blue">Button</pill-button> </div> <div>theme=red</div> <div> <pill-button theme="red">Button</pill-button> </div> <div>disabled</div> <p>themeが指定されていてもdisabledのスタイルで上書きされます。</p> <div> <pill-button disabled="disabled" theme="red">Button</pill-button> </div> <div>clickイベントを指定した場合</div> <p>buttonClicked: {{ buttonClicked }}</p> <div> <pill-button @click="clickButton">Button</pill-button> </div> <div>toを指定した場合</div> <p>clickイベントが指定されていても発火しません。</p> <div> <pill-button to="/" @click="clickButtonNotTriggered">Button</pill-button> </div> <hr /> <!-- 以下、他コンポーネントのカタログ実装が続く --> </template>
コンポーネントクラス(atoms.vue
)
import { Component, Vue } from 'vue-property-decorator' // プロダクトコードからコンポーネントをインポート import CheckField from '@/components/atoms/CheckField.vue' import PillButton from '@/components/atoms/PillButton.vue' @Component({ components: { CheckField, PillButton } }) export default class UiAtomsView extends Vue { CheckField = CheckField PillButton = PillButton isInline = true isCheck1 = true isCheck2 = true someArr = [] buttonClicked = false clickButton() { this.buttonClicked = true } clickButtonNotTriggered() { alert('This does not show up') } }
サンプル
PropsSpec
という自作コンポーネントがプロダクトコードのコンポーネントのProp定義をパースし、表レイアウトで表示する作りになっています。
当チームではatom, moleculeの実装を追加・編集する際にはかならずカタログに仕様を記載する運用にしました。
この仕組みは、コンポーネントの一覧の提供だけでなく、実装者・コードレビュアーに汎用的かつシンプルな設計を意識させる働きをしてくれました。
カタログからはVuexを切り離しているので、Stateへの参照も自然に抑制できました。
何より、保守効率を考慮した汎用設計という面倒な作業が、綺麗に整頓されたカタログの画面を見ながら行えるためフロントエンドエンジニアにとって楽しい仕事になります。
このような工夫も、前向きなチーム作りに貢献した取り組みでした。
その2: Vuexをシンプルにする
次に、フロントエンドの重要な状態データの管理を担うVuexです。
VuexのState・Actionが扱いづらくなっていたことが、前向きになる上で最も大きな障壁だったかもしれません。
ここで抱えていた負債は「2. 実装をする上で様々な知見を必要とする領域が多数あり、作業ハードルが高い」に当てはまります。
バックエンドAPIとの通信に関わるデータがVuexで扱われていたため、構造的な記述がされていないと、外部仕様に詳しくないメンバーにとっては手を出しにくい領域となってしまいます。
その結果、他チームとの連携が多いメンバーにVuex周りの実装タスクが属人化し、忙しいときでも上手く作業分担ができないことでさらにチームの雰囲気が悪くなっていきます。
この状況を打開するため、Vuexで何を扱っているかということの明確化と、仕様に精通していないと手が出せない部分の線引きをしました。
Stateのツリー構造を見直し
Vuexでは、Stateが保持する状態データの形式は決まっていません。
プロダクトの仕様に合わせて決定する必要がありますが、今回はその検討に時間を割かず思い思いに実装した結果、次のような保守性の低い設計となってしまいました。
改修前のツリー構造
- ページ1モジュール
- ページ1の状態データ
- ページ1の状態データ
- リソース1モジュール
- リソース1の状態データ
- リソース1の状態データ
- ページ2の状態データ
- ページ3モジュール
- ページ3の状態データ
- ページ3の状態データ
- リソース2の状態データ
- リソース2の状態データ
- 共通モジュール
- グローバル画面要素1の状態データ
ここで「リソース」とは固有画面に紐づかないようなデータのことを表しています(例:ログインユーザー)。
また、「グローバル画面要素」とは固有画面に紐づかない画面要素のことです(例:ヘッダー)。
ページ基準で切ったはずのモジュールの中にリソース基準のモデルのデータが入ったり、その逆だったりと、粒度の異なるデータが同列に混在している状態です。
上記は説明のため「ページ」や「リソース」といった汎用的な名称で区切っているため、混在している状況が明確ですが、実際の値はより複雑です。
それぞれのプロパティをよく見てデータバインド先の実装と照らし合わせないとモジュール・モデルが何基準なのか非常に分かりづらいです。
そこで、状態データはページ等の画面上の要素基準で切ることを基本ルールとして定め、リソースは共通モジュールに配置しました。
スコープを適切に伝えるため、単一画面のみに属する状態データはVuexに乗せず、ページコンポーネントのDataに閉じ込めました。
以下のように、Vuexには画面を跨る状態データと共通データのみが残ります。
改修後のツリー構造
- ページカテゴリ1モジュール
- ページカテゴリ1の状態データ
- ページカテゴリ1の状態データ
- ページカテゴリ2モジュール
- ページカテゴリ2の状態データ
- ページカテゴリ2の状態データ
- 共通モジュール
- グローバル画面要素1の状態データ
- リソース1モジュール
- リソース1の状態データ
- リソース1の状態データ
ここではただ完璧な設計を目指すのではなく、サービスやシステムの要件も意識して設計しました。
おそらくVuex Stateの使い方として、上記のような画面要素の基準でのモジュール分割はベストプラクティスではないかもしれません。
個人的にも、リソース基準でデータを管理する方がアプリケーションの保守性を高く保てるように感じていました。
しかし、状態データの主な取得元である既存APIがページに最適化されたインタフェース設計で実装されており、リソース基準でのモデルを作るのに苦労しそうでした。
そこで、APIの改修やモデル変換のための仕組み作りといったコストの大きい施策の実施を待たずに、今回は簡単な構成への変更に踏み切りました。
Stateの見通しが良くなり、画面跨ぎの状態データ設計を誰でも行えるようになりました。
Actionの責務を最小限に限定
Vuexで扱うデータのほとんどがAPI通信を介して取得されるので、非同期通信に関わる広範囲の領域の処理がActionに集約されていました。
Actionが状態管理ライブラリの一部としての役割を超え、以下のような機能を全て手続きとして詰め込んだ関数となり、読みにくいコードを生み出していました。
- API通信の抽象化
- API通信エラーハンドリング
- APIレスポンスモデルからアプリケーションモデルへの変換
この機能をそれぞれ適切なクラスに分割し、Actionからは必要な処理を呼び出すのみにしました。
API通信の抽象化はAdapterクラスで行う
当プロダクトにはAPI通信にまつわる共通仕様が複数あり、API連携機能を実装する上で全ての仕様を把握しておかなくても、肝心なロジックに集中して実装できるという状況が理想でした。
そのため、API通信ライブラリを直接使うのではなく、通信を抽象化したAdapterクラスの中に共通の振る舞いを隠蔽しました。
// Adapter class APIAdapter { private async baseRequestAPI() { // 通信の共通処理(エラーハンドリングも含む) // 実際にAPIへのリクエストを行う // 下記4メソッドからこのprivateメソッドを呼ぶ } async requestGetAPI<Res, Req = void>(endpoint: string, req: Req): Promise<Res> { // GETメソッドの処理 // reqをURLパラメータにシリアライズする処理など } async requestPostAPI<Req, Res>(endpoint: string,req: Req) { // POSTメソッドの処理 } async requestPutAPI(endpoint: string) { // PUTメソッドの処理 } async requestDeleteAPI(endpoint: string) { // DELETEメソッドの処理 } } // 利用例 const apiAdapter = new APIAdapter() const orders = await apiAdapter.requestGetAPI<OrdersGetResponse>('/orders')
API通信と同様、他の外部連携ロジック用にもそれぞれのAdapterクラスを切りました。
CookieやLocalStorage等へのアクセスや、WebViewでの動作時のネイティブアプリとの連携等です。
小難しいシステム仕様に依存する処理が隠蔽でき、Actionの見通しが良くなります。
APIレスポンスモデルからアプリケーションモデルへの変換はRepositoryクラスで行う
コンポーネントやVuexで扱う状態データに値を格納する際、APIレスポンスのモデルをフロントエンドで扱いやすくするためのアプリケーションモデルに変換する必要があります。
変換処理はコード量が多くなりがちなのでRepositoryパターンに見立ててクラス内にまとめました。
class OrderRepository { async getAll(): Promise<Array<Order>> { // データ取得・モデルの変換処理 } async get(id: string): Promise<Order> { // データ取得・モデルの変換処理 } }
Stateに関連しない処理はVuex外へ移動
当然ですが、ActionからはVuexでの状態管理に関連のない処理は排除しました。
Stateの整理をしたことによりVuexで扱わなくなったデータの取得処理は、Actionではなくコンポーネントのメソッドに移動しました。
値を加工するだけの純粋関数等、AdapterやRepositoryに置き場所のない処理はhelperファイルに退避させました。
上記のような改善を行ったことで、Vuex上の実装が「なんとなく難しそうなコード」に見えていたのが、内部でどんな処理を行っているか把握しやすくなりました。
その3: TypeScriptを活用する
TypeScriptの観点でも負債がありました。
静的型付け言語に不慣れなメンバーも多く、宣言するのに最適な型がわからず、ひとまず動くコードを書くためにany
型が多く使われていました。
any
型が多いということは、それだけでエンジニアのやる気を削ぐ原因になります。
多くのフロントエンドエンジニアにとって学習コストの高い型付き言語をわざわざ導入したメリットをしっかりと活かすため、「要因2. 実装をする上で様々な知見を必要とする領域が多数あり、作業ハードルが高い」を解決するアプローチとしてあらかじめサービス仕様に則ったモデルの型定義を用意しました。
アプリケーションモデルとAPIレスポンスモデルの定義は、チーム内外のメンバーとの会話が必須な作業です。
当チームではアプリケーションモデルを、限りなくユビキタス言語に近い言葉を使って表現する汎用的なモデルとして位置付けていました。
設計方針を統一するため、詳細機能の実装前にモデルのプロパティレベルの命名について十分な時間をとって話し合い、型に落とし込んで実装してしまいました。
APIレスポンスモデルについては、リーダーがバックエンドチームとすり合わせたインタフェース仕様をまとめて型定義しました。
今回は手作業で行いましたが、OpenAPIの自動生成の仕組みを使えばさらに効率的かと思います。
参考:OpenAPI3を使ってみよう!Go言語でクライアントとスタブの自動生成まで!
参考記事はGo言語の紹介ですが、OpenAPIのAPIクライアント自動生成はTypeScriptにも対応しています。
事前にモデルをまとめて定義しておけば、その後のRepositoryやコンポーネントの実装のハードルが格段に下がります。
Repositoryの実装例
class OrderRepository { async get(id: string): Promise<Order> { const rawOrder = apiAdapter.requestGetAPI<OrderGetResponse>(`/orders/${id}`) // OrderGetResponseは既に定義済み return { id: rawOrder.id, shipments: rawOrder.details.map(d => toShipment(d)) } // Order型になっていなければコンパイルエラーが出る } }
OrderRepository.get()
の実装者は以下を把握していなくても作業ができます。
GET /orders/{id}
のインタフェース定義- アプリケーションモデルの定義。
OrderGetResponse > details
をshipments
と呼ぶこと等
定義済みの型を組み合わせて実装ができるので、any
が自然と減っていきます。
また「モデルの定義」という作業のみを事前に切り出しているため、メンバーがどのようなアプローチを経てモデルの定義をしているかが他メンバーからも把握しやすくなりました。
さらにモデリングやそのための情報収集に興味を持つメンバーが増え、作業属人化の解消にも繋がりました。
型関連のTips
上述のモデル関連以外にも、型を上手く使えていない場面があったので、helperで工夫して改善しました。
コンポーネントのData初期化
コンポーネントのDataが、ビューモデルのプロパティとしてどんなデータを持つかを型で表現できていないと、読みにくく手を入れたくないコンポーネントになってしまいます。
Vue.jsの仕様上、クラスの初期化時に値を代入できないData(APIから値を受け取る場合等)に対して一旦nullを代入する必要があります。
宣言部分をdata: T | null = null
のように記述しなければならず、本当に意味上nullableなのかどうかをよく考えて読み解く必要がありました。
これを解決するLateInit
デコレータを作成しました。
詳細:クラススタイルVueコンポーネントの変数初期化を改善する
またクラスの初期化時に値があるものでも、Dataの初期化時にコンポーネントのVueインスタンスにアクセスできないため一旦nullで初期化するような例がありました。
import { Component, Vue } from 'vue-property-decorator' @Component export default class Hoge extends Vue { // fuga: Fuga = this.getFuga() ←created等でアクセスするthisと異なるためNG fuga: Fuga | null = null // 一旦nullを代入 created() { this.fuga = this.getFuga() } }
これを解決するため、Data
デコレータを作成しました。
export const Data = <T>(initializer: (vm: Vue) => T) => createDecorator((options, key) => { options.mixins = [ ...options.mixins || [], { data(this: Vue) { return { [key]: initializer(this) } } } ] })
import { Component, Vue } from 'vue-property-decorator' @Component export default class Hoge extends Vue { @Data(vm => vm.getFuga()) fuga!: Fuga }
Enumへの変換
型を使って仕様を伝える上で、TypeScriptのEnumは非常に有用です。
あるプロパティがどんな値を持ちうるかを列挙できるので、アプリケーションモデルやコンポーネントのPropの型に積極的に使いました。
コンポーネントPropの例
import { Component, Vue, Prop } from 'vue-property-decorator' @Component class PillButton extends Vue { @Prop({ type: String, default: PillButton.Theme.Default }) theme!: PillButton.Theme } namespace PillButton { export enum Theme { // クラスと同名のnamespace内でenumをexportすれば、import先の他コンポーネントからもPillButton.Themeとして参照できる Default = 'default', Black = 'black', Blue = 'blue', Red = 'red' } } export default PillButton
アプリケーションモデルの例
interface Gender { code: Gender.Type label: string } namespace Gender { export enum Type { Male = 'MALE', Female = 'FEMALE' } } export default Gender
APIレスポンスに含まれるコード値をアプリケーションモデルに詰める際、元の値はstring型のため、Enumに変換する必要がでてきます。
Gender.Type['MALE']
のようにして変換できれば良いのですが、これはGender.Type.Male
に上手く変換されてくれないため、変換関数を自作しました。
export function mapToEnum<T>(enumObject: T, value: any): T[keyof T] | undefined { if (typeof enumObject === 'object') { for (const key in enumObject) { if (enumObject.hasOwnProperty(key) && enumObject[key] === value) { return enumObject[key] } } } else if (enumObject instanceof Array) { return enumObject.find(value) } }
mapToEnum(Gender.Type, 'MALE')
はGender.Type.Male
として使えます。
このように、複雑な型定義が必要な部分は色々な事情に詳しいメンバーが事前に実装しておくことで、他メンバーが前向きにTypeScriptを利用できるようになりました。
その4: 開発ルールを制定する
ここから先は、「要因3. 良いコードを書いているという実感がない」を解消するための施策です。
コード改善の大半には、チーム内での決め事が付いて回ります。
作った仕組みが意図しない方法で利用されると改善の恩恵を受けられない場合があるため、作成時の前提を守って実装してもらうことが非常に重要です。
メンバーがコードを読んで前提を察してくれることを期待せず、決め事を開発ルールとしてテキスト化しておくと、やるべきことがクリアになってより前向きになれます。
当チームではGitHubリポジトリで開発を行なっているので、Pull Requestテンプレートにチェックリストとしてルールを記載しました。
レビュイーが当てはまる項目にチェックを付け、チェックされているルールが本当に遵守されているかをレビュアーが確認するフローとしました。
時として、どうしてもルールの例外を認めなければならない実装があります。
そういったケースが予想される場合には、「基本的にXXXする」というような強制力を弱めたルールを制定しておきました。
ルールが守れない事情をPull Requestのコメント等で説明することをレビュイーに促せるため、例外を認めるかどうかの点においてチーム内で合意形成できるようになります。
ルールの重要度が高くない場合や時間が取れない場合には、Pull Requestテンプレートへの記述でなくSlack等のコミュニケーションツールでの共有のみでも良いことにしました。
開発ルールを定める面倒臭さが、改善のための仕組み作りの足枷とならないようにするためです。
負債を生まないための開発ルールに対してチーム内で共通認識が生まれたことにより、良いコードを書くための道筋が見えるようになりました。
その5: 改善を可視化する
多くの開発チームでは、リファクタリングのみを行う期間が取れることの方が少なく、機能開発の傍らの作業となることが多いかと思います。
当チームでもビジネス案件をこなしながら改善を行なっていたので、常に「この改善に時間をかけて良いのだろうか」という不安が付き纏いました。
改善への取り組み中の段階ではその問いに対する答えは出ないので、改善タスクの区切りがついたところで振り返りをし、効果が出たことを可視化するよう努めました。
ただし冒頭で述べた通り、改善の効果は定量的に評価できないケースが多いです。
感覚的で曖昧な基準の評価になってしまいますが、前向きなチーム作りを目的とした施策なのでそれで良いと割り切るようにしました。
当社ではチームで隔週KPTを行なっているので、メンバーが自然にKeepとして「この改善をしたらXXXが書きやすくなった」等の所感を共有してくれました。
勿論、Problemとしてあまり効果が出なかったと感じた改善も共有されました。
可視化という観点では、タスク管理の厳密さ、透明性も前向きなチーム作りに貢献します。
当チームではJIRAのガントチャート機能を使用してタスクを整理しました。
ビジネスマイルストーンに合わせた案件リリーススケジュールの合間にリファクタリング作業が収まるようにチケットを切り、チーム内外の全員が参照できる状態にしました。
スケジュールの組み立ての際には、振り返りでの結果を元にして、やるべきリファクタリングを取捨選択します。
改善の効果測定の基準が曖昧な状態でも、上記のような可視化をすることで改善作業に自信が持てるようになってきます。
チケットの見積もりに収まる期間であれば時間を目いっぱい使うことができます。
またビジネス観点のスケジュールに与えるインパクトを常に確認しながら作業ができるため、伸び伸びと改善に打ち込むことができるようになりました。
おわりに
コードの負債を改善するための取り組みを紹介しました。
エンジニアにとって毎日向き合う対象であるコードが綺麗になっていくことは、チームの雰囲気の向上に大きな効果をもたらします。
しかし、どれをとっても誰かが単独で取り組んでやり遂げられるものではなく、マクロ・ミクロ両方からの改善アプローチが必要です。
説明した施策を振り返ってみても、リーダーが漠然と感じた課題をメンバーに伝え、メンバーから「こんな方法で解決できるのでは」という提案があって初めて前進したパターンがほとんどでした。
また、リファクタリングとビジネス案件に割くコストをバランスよく割り振るためには、ビジネスサイドの協力が欠かせません。
紹介したVue.js+Vuex+TypeScriptの例に限らず、またWebフロントエンドの領域以外の方にとっても、改善の取り組みの参考になれば幸いです。
チームの雰囲気が暗い・後ろ向きになっていると感じたら、リーダーの方はメンバーをランチや飲みの場に誘うだけでなく、コードの負債について話し合いの場を設けてみてはいかがでしょうか。
メンバーの方は「負債があるせいでXXXできない」という場に遭遇したら、改善の時間が欲しいとリーダーに掛け合ってみてはいかがでしょうか。
今回ご紹介した施策には、以下の開発メンバーと共に取り組みました。
- 高橋智仁(@anaheim0894)
- 権守健嗣(@AmatsukiKu)
- 茨木暢仁(@niba1122)
- 安藤正人
- 松浦麻奈未(@mtmn07384)
ZOZOテクノロジーズには、こんな改善を日々実施しているチームや、改善施策を見守ってくれる環境があります。
一緒に前向きなチーム作りを盛り上げてくれるエンジニアを大募集中です。
ご興味のある方は、こちらからぜひご応募ください。