現役リーダーが考える強靭なコーディング手法とは
コーディングは1行1行が負債である
前提として「コーディングは1行1行が負債である」と言う認識を持つことがとても重要です。「壊れやすいもの」と「壊れにくいもの」それらの違いは接合部の数で決まります。つまり、パーツ数、コード数を極力少なくすることが強靭なコーディングをすると言うこと。それを押さえた上で、更に上層の話をしていきます。
Point
✅ コーディングは1行1行が負債である。
フレームワークに頼り過ぎない
OSSで活発に活動されているフレームワークはとても有用であり、合理的な最新のトレンド手法を取り入れた「学ぶ価値のあるもの」であることは間違いありません。しかし同時に、こういったフレームワークは頻繁にアップデートされ、毎年のように記述変更を伴うアップグレード対応をしていかなければいけません。
社内エンジニアをしていると数十個のリポジトリをメンテナンスすることは普通であり、それら全てを常に最新の状態に保つことは至難の技です。必ず機能追加案件の優先度が上がり、アップデート作業の優先度が落とされるのは目に見えています。
そんな状況下では、何周もアップデートされていないプロジェクトだらけになり、エンジニアのモチベーションは下がり、新人の離脱率は増えていくことでしょう。
したがって、プロジェクトを運営する上で「フレームワークは効率的だが、完全に頼ってしまうのはアンチパターンである」と私は結論付けました。
また、フレームワークがHTTPルーターであれば入力がHTTP形式で扱いにくく、テンプレートエンジンも独自関数に頼った実装になるため、「フレームワーク内はプレゼンテーション層である」とも定義しておきます。
Point
✅ フレームワークは効率的だが、完全に頼ってしまうのはアンチパターン。
✅ フレームワーク内はプレゼンテーション層である。
✅ フレームワーク機能の使用は最小限にとどめる。
基礎フレームは全て独自SDKで実装する
私は問題の一つの解として「基礎フレームは最小限のパッケージのみ使用した独自SDKで実装すること」に行き着きました。ここでは外部依存を極力使用せず、主に環境本来の機能のみで実装することが肝になってきます。
とはいえ、外せないパッケージは存在するため、あまり気にし過ぎず、枯れて(使い果たされて)インターフェース変更がほぼ起きないパッケージを吟味したならそれは使用していきます。
そこに変更が起きたなら、それは事故として「仕方がなかった」と諦めましょう。
Point
✅ 基礎フレームは最小限のパッケージのみ使用した独自SDKで実装する。
SDKとはどういったものか?
うまく想像できない方のために、具体的にSDKとはどう言ったものか具体的なインターフェースを示しておきます。*例はnode.js
Auth(認証 + ユーザー操作モジュール)
import {Auth} from 'my-sdk'
// ログイン
const auth = await Auth.signIn(username, password);
// パスワード変更
await auth.changePassword(oldPassword, newPassword);
// ログアウト
await auth.signOut();
Post(投稿操作モジュール)
*認証はcookieやlocalStorageから自動で行われる。
import {Post} from 'my-sdk'
// 投稿一覧
const posts = await Post.index(query);
// 投稿の新規作成
const newPost = await Post.create(postData)
// 投稿の編集
const updatedPost = await Post.update(postId, updatePart)
// 投稿の削除
const deletedPost = await delete(postId)
まあこういった機能関数セットです。
SDK(Software Development Kit)とは、iOSなら〇〇Kit、AWS SDKならS3ClientやDynamoClient、AWS Amplifyの場合はAuthやAPIがSDKそのものです。
これらを使用した経験のある方は理解できると思いますが、これらのインターフェースがフレームワークのアップデートに影響を受けることは皆無であり、実装について完全に無関心でいられるため、とても有難い存在です。
例に挙げたiOSやAWSのSDKは「全ての開発者の要求を満たすよう考えて作成されたSDK」であるため膨大なインターフェースが生えていますが、実際のサービスではその半分も使用しません。また、実際のサービスで行う1つのアクションに対して、こういったSDKでは2〜4ステップの実行が必要な場合もあります。
こういった一般向けSDKを、実際のサービスにフィットしたSDKにまとめ上げることが出来ることも自作SDKならではの恩恵ですね。
特に私はAmplifyフレームワーク(gen1)が好きなので、GitHubのリポジトリを研究してAmplify風の使用感を目指したインターフェースを作成しています。
独自SDKのメリットとデメリット
こういった「サービスにフィットしたSDK」を外部依存に頼らない形で完成させることは、爆アド(とても高いアドバンテージがあること)であると私は考えています。
メリット
外部依存に左右されにくい
テストが容易
コントローラーに毎回同じ処理を書く必要がなくなり、実装の一貫性を保証できる
SDKの内部実装について無関心でいられる
SDKのインターフェースさえ固まっていれば、内部実装については柔軟に調整できる
フレームワークの乗り換えに柔軟に対応できる
メンテナンスコストが大きく削減される
日常の作業にもSDKが使える
デメリット
新規参入する際の学習コストを払う必要がある
今回SDKの組み方については触れませんが、テストがしやすく、モックしやすい手法で組んで頂ければと思います。
独自SDKを作成する上で気をつけること
SDKの作成で特に気をつける事を列挙しておきます。
ここで登場する「インターフェース」とは Auth.signIn(username, password) など「機能の呼び出し形式」のことであり、決して TypeScript の interface とは混同しないで下さい。
インターフェースの引数や内部挙動の変更をしない
インターフェースの廃止には注意を払う
インターフェースさえ固めていれば問題ありません。(仕様変更しない)
インターフェースが固まっていることが即ち「強靭なシステム」に繋がります。
ただし、インターフェースの追加に関しては必ず発生するため、深く考える必要はありません。
ちょっと愚痴 : TypeScript の ”interface”
あんなものは interface ではありません(強硬派)
型に対するめちゃくちゃ細かい interface として一応意味は通っていますが、Cのヘッダファイルが本来のインターフェースと言えるものです。
私がいつもやっていることですが、もし「インターフェース・デザイナー」なる職種が存在したなら、TypeScript の interface なんてものは全く仕事に使えないものであり、ノイズになるため「その名を改めよ」と常々思っています…
Point
✅ インターフェースは考えられる未来までしっかり考えてデザインする。
✅ インターフェースの変更にはあらゆる影響を考えて慎重になる。
✅ MS製とMeta製は信用するな。
ラッパーパッケージも作成してしまう
ラッパーパッケージとは、既存パッケージの中で使用するインターフェースのみ公開した自作パッケージです。近年JavaScript界隈では、長年スタンダードに使用されてきた時間操作パッケージの moment.js が非推奨となり、代替パッケージとして day.js がスタンダートに置き換わりました。この移行作業に苦労した方は少なくないはずです。
こういった事態でも、ラッパーパッケージを使用しておけば移行がとてもスムースに出来た経験から、私は外部パッケージの使用にはできるだけラッパーパッケージを使用する方針で指示しています。
具体例
datetime(自作パッケージ)の実装例
// import moment from 'moment-timezone' <-- 廃止
import dayjs from 'dayjs' // <-- 差し替え
export function dateTime(date) {
// return moment(date); <-- 廃止
return dayjs(date); // <-- 差し替え
}
export default dateTime;
実際には独自の初期化やクラス化をしたり、使用するインターフェースを実装しますが、基本的にラッパーはこのような簡素なものでも十分に効果を発揮します。
「差し替え時に実装側で編集するファイルが発生しない」これを実現することが強靭なコーディングをする第一歩です。ファイル編集して良いことなんて一つもありません。
Point
✅ 外部パッケージは(できるだけ)そのまま使用せずラッパーパッケージを作成して使用する。
✅ 「差し替え時に実装側で編集するファイルが発生しない」これを実現することが強靭なコーディングをする第一歩。
フレームワーク + 独自SDK を採用すると何が起こるのか?
まとめとして、これまで述べてきたことを集約した成果物について具体例を交えて解説していきます。ある程度想像できていると思いますが、実際の成果物を見るとその偉大さがより伝わることと思います。
先ほど「フレームワークはプレゼンテーション層である」と述べました。
これはつまり、フレームワーク内ではフレームワークの責務のみ実行し、実装についてはSDKのインターフェースに引数を入力し、出力を返すだけになると言うことです。具体的にAPIの実装を例にすると、APIフレームワークは「HTTPリクエストを受け取り、ミドルウェア処理をし、目的のコントローラーまでルーティングすること」"だけ"が責務となるため、コントローラー実装は以下のように変化します。
フレームワークに依存した実装
import {Post} from './models/index.js'
export class PostController {
...CRUD処理
myMethod(reqest) {
const {param1, param2} = reqest.body;
...auth情報の取り出し
...
...この辺りでまた reqest から何か取り出したりする
...
...const subData = getSubDataForMyMethod(data);
...
...めっちゃ長い処理
...
return result;
}
// mayMethodが大きくなり過ぎるため
// ここにプライベートなメソッドを追加したりする
getSubDataForMyMethod(data) {
...
}
}
↓
SDKを利用した実装
import MySDK from 'my-sdk'
export class PostController {
...CRUD処理
myMethod(req, res) {
const {param1, param2} = req.body; // <-- 引数を取り出して
return MySDK.myMethod(param1, param2); // <-- 出力を返すだけ!
}
}
APIのコントローラー処理は全て「引数を取り出してSDKに入力し、出力を返すだけ」になり、とても簡素なものになります。
ユニットテスト時にいちいちURLやヘッダー情報、URL形式のクエリパラメーターを入力する必要もありません。SDKなのでリポジトリパターンよりも優れた構造になっているため機能の使い回しがとても簡単なことも特筆すべき点です。更に、このフレームワークから別のフレームワークに乗り換える際も最小限のコストで移行できます。
そもそもAPIに大きなフレームワークを使用する意味も薄くなり「expressやlumenで十分」と思えてきませんか?
アプリ側のテンプレートエンジンも基本的には「サービスデータをデザイン通りにレンダリングすること」"だけ"が責務なので、レンダリング処理以外は全てSDKへ入力して出力を得るだけになり、とてもコンパクトになります。これなら未来に訪れるフレームワークの乗り換えにも対応出来そうですね!
いかがでしたでしょうか?
独自SDKの実装に興味が湧いたなら❤️押して頂けると励みになります!!
追記
* 弊社では今のところアプリ側とサーバー側では別々のSDKを作成しています。
* 現在全面的にAWSを使用しており、単純な関数パッケージだとリソースアクセス不具合が発生しやすいため、バックエンド側はAWS SDKのようなサーバー・クライアント型のマイクロサービスSDKを多数使用する構成を採用しています。
* 現在採用の観点からNode.jsで統一していますが、本来であればGoのような言語自体の更新リスクの低い言語が望ましいと考えています。(ただしGoの場合はエンジニアの要求レベルがかなり上がります)
この記事が気に入ったらサポートをしてみませんか?