後方互換性を保つと開発が楽になる事に気づいた
こんにちはハトです。最近の気づきを記事にしました。ここで言う互換性とは後方互換性のことです。皆さんからの意見お待ちしています。
追記: 2021-06-19
こちらのgoogle cloudのDevOpsに関するドキュメントが非常によくまとめられているので、こちらを読んだほうがいいですね^^
互換性のメリット
互換性を考えるときに、2つの部分に対して適応できる。
- 境界(インターフェイス)における互換性
- 境界内部における互換性
境界(インターフェイス)における互換性
webアプリケーションを考えたとき代表的な境界は次の2つ。
- DB-webアプリケーション間(データ)
- webアプリケーション-フロントエンドSPA間(API)
いわゆるスキーマがかけるところ。
境界(インターフェース)の互換性があれば次のメリットがある。
- データの互換性があれば、DBを変更したあとのバックエンドの修正が非同期でもよい。マイグレーションが非同期に実行できる。
- 今後モバイル版対応などで、複数バージョンを同時に運用していてもデータの互換性があれば1つのDBで対応できる。
- APIの互換性があればバックエンドを変更しても、フロントエンドの修正が非同期でもよい。
境界内部における互換性
webアプリケーションを考えたとき代表的な境界内部は次の2つ。
- webアプリケーション内のメソッドやサービスなど
- フロントエンドSPA内でのコンポーネント
境界内部の互換性があれば次のメリットがある。
- バックエンドにおけるメソッドやサービス、フロントエンドにおけるコンポーネントの互換性があれば、他の開発者がそのメソッドやコンポーネントが壊れる(なくなる)という心配なしにチーム開発できる。
- 互換性を保たない開発と比べて今までのテストが壊れにくい。
互換性のデメリット
互換性を保つということは破壊的なことができないということなので次のデメリットが存在する。
- ドメインモデルなどを利用していると、既存の言葉が間違っていた際にrenameしたくなる。しかしrenameは破壊的であり互換性を壊してしまうのでできない。
- 良くない設計を維持してしまう。理想と現実を埋めるためのコードがごちゃごちゃする。
- イテレーションサイクルで学習した結果いまの形が間違っているとわかっても素早く反映できない。
互換性を保つメリットよりも保たないメリットのほうが上回れば破壊的変更をすべきだと思う。
互換性を保った変更のしかた
データ(DB)の場合
基本的にfast-forward的な感じで追加であれば互換性を壊さない。
- カラムの追加
- enum型カラムに対する値の追加
逆に以下は互換性を壊す
- カラムのrename
- カラムの削除
- カラムの属性変更
- enum型カラムの値の変更
例
ユーザーテーブルが次のようであった場合
status... ACTIVE, PENDING, TEMPORARY
id | name | status |
---|---|---|
1 | エレンイェーガー | ACTIVE |
2 | ミカサアッカーマン | PENDING |
3 | アルミンアルレルト | TEMPORARY |
次のような変更は互換性がない
カラムの属性を変更(idを文字列に変更)
id | full_name | status |
---|---|---|
'1' | エレンイェーガー | ACTIVE |
'2' | ミカサアッカーマン | PENDING |
'3' | アルミンアルレルト | TEMPORARY |
カラムのrename(nameをfull_nameに変更)
id | full_name | status |
---|---|---|
1 | エレンイェーガー | ACTIVE |
2 | ミカサアッカーマン | PENDING |
3 | アルミンアルレルト | TEMPORARY |
アプリケーション側でいままでnameとして取得していた部分が壊れる
カラムの削除(statusの削除)
id | name |
---|---|
1 | エレンイェーガー |
2 | ミカサアッカーマン |
3 | アルミンアルレルト |
アプリケーション側でstatusに依存しているコードがあればこわれる。
カラムの分解(nameをfirst_nameとlast_nameに変更)
id | first_name | last_name | status |
---|---|---|---|
1 | エレン | イェーガー | ACTIVE |
2 | ミカサ | アッカーマン | PENDING |
3 | アルミン | アルレルト | TEMPORARY |
アプリケーション側でいままでnameとして取得していた部分が壊れる
enum型の属性の破壊的変更(statusのACTIVEをUSINGに変更)
id | name | status |
---|---|---|
1 | エレンイェーガー | USING |
2 | ミカサアッカーマン | PENDING |
3 | アルミンアルレルト | TEMPORARY |
アプリケーション側でいままでstatus ACTIVEに依存する部分が壊れる
次のような変更は互換性がある
カラムの追加(ageを追加)
id | name | status | age |
---|---|---|---|
1 | エレンイェーガー | ACTIVE | 19歳 |
2 | ミカサアッカーマン | PENDING | 19歳 |
3 | アルミンアルレルト | TEMPORARY | 19歳 |
enum型の属性の値の追加(statusにUNPAIDを追加し既存のstatusはいじらない)
id | name | status |
---|---|---|
1 | エレンイェーガー | ACTIVE |
2 | ミカサアッカーマン | PENDING |
3 | アルミンアルレルト | TEMPORARY |
4 | ライナーブラウン | UNPAID |
アプリケーション側がACTIVE, PENDING, TEMPORARYのみを認識して処理していた場合、statusがUNPAIDのライナーはそれらの処理には引っかからない。しかしアプリケーションは壊れない。
APIの場合
データの互換性と同様に基本的にfast-forward的な追加であれば互換性を壊さない。
- bodyに対する要素の追加
- enum型要素の値の追加
- エンドポイントの追加
逆に以下は互換性を壊す
- エンドポイント(URL)のrenameや1つのAPIの複数分割
- bodyの要素のrenameや属性変更
- bodyの要素の削除
- enum型要素の値の変更
- ヘッダーの要素の値の変更
例
データ型とほとんど同じなので例を1つ出してあとは省略。
APIが次のようであった場合
GET /users
responose body
[
{
"id": 1,
"name": "エレンイェーガー",
"status": "ACTIVE"
},{
"id": 2,
"name": "ミカサアッカーマン",
"status": "PENDING"
}
]
次のような変更は互換性がない
エンドポイントのrename(usersをuser-indexに変更)
GET /user-index
responose body
[
{
"id": "1",
"fullName": "エレンイェーガー",
"status": "ACTIVE"
},{
"id": 2,
"name": "ミカサアッカーマン",
"status": "PENDING"
}
]
次のような変更は互換性がある
エンドポイントの追加(userIdを追加)
GET /users/:userId
responose body
{
"id": "1",
"fullName": "エレンイェーガー",
"status": "ACTIVE"
}
コンポーネントの場合
機能トグル開発をすれば互換性を維持しつつ開発できて便利。
以下はReactの例です。例として、親のページコンポーネントがリストコンポーネントを扱っているときを考えます。
import React from 'react'
// 子。使われるコンポーネント
type AwesomeListProps = {
items: string[];
}
const AwesomeList: React.FC<AwesomeListProps> = ({ items }) => {
return (<div>
{items.map(item => (
<div>Awesome {item} !!!</div>
))}
</div>)
}
// 親
const SimplePage = () => {
const items: string[] = ['red', 'blue', 'breen'];
return (<div>
<h2>リスト</h2>
<AwesomeList
items={items}
/>
</div>)
}
ここで、何かしらの理由でAwesomeList
を作り変えたいなと思ったとします。何かしらの理由とは例えばパフォーマンスなどです。単純にpropsを1つ追加するだけで済むならそれでいいのですが、大幅にAwesomeListを書き換えたいときに機能トグルを使うと便利です。
import React from 'react'
// 子。使われるコンポーネント
type AwesomeListProps = {
items: string[];
}
const AwesomeList: React.FC<AwesomeListProps> = ({ items }) => {
return (<div>
{items.map(item => (
<div>Awesome {item} !!!</div>
))}
</div>)
}
const GoodPerformanceList: React.FC<AwesomeListProps> = ({ items }) => {
return (<div>
{items.map(item => (
<div>good {item} !!!</div>
))}
</div>)
}
// 親
const SimplePage = () => {
const items: string[] = ['red', 'blue', 'breen'];
return (<div>
<h2>リスト</h2>
{process.env.USE_GOOD_PERFORMANCE_LIST === 'true' ? (
<GoodPerformanceList
items={items}
/>
) : (
<AwesomeList
items={items}
/>
)}
</div>)
}
環境変数USE_GOOD_PERFORMANCE_LISTがtrueのときだけ有効になります。このように書くことで、どちらのコンポーネントも同じpropsを渡すように意識できますし、このままpushしても他の人の開発中のコードには影響を与えません。特に影響がないことがわかればAwesomeListをコードから消すことができます。
Discussion