メインコンテンツまでスキップ

フレームワーク自作ガイド - 設計から公開まで

このドキュメントの目的

フレームワークを自作する際に必要な知識・ノウハウ・注意事項を体系的に学びます。「使う側」から「作る側」への視点転換を行います。


目次

  1. 自作する前に考えること
  2. フレームワーク設計の原則
  3. 拡張ポイントの設計
  4. API設計とバージョニング
  5. エラーハンドリング戦略
  6. ドキュメントとサンプル
  7. よくある失敗パターン
  8. まとめ

自作する前に考えること

本当にフレームワークが必要か?

┌─────────────────────────────────────────────────────────────┐
│ フレームワーク作成の判断基準 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 作るべき場合: │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ ✓ 同じパターンのコードを3回以上書いている │ │
│ │ ✓ 既存フレームワークでは解決できない固有の課題がある │ │
│ │ ✓ チームで共通の基盤として長期利用する予定がある │ │
│ │ ✓ メンテナンスするリソースと覚悟がある │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 作るべきでない場合: │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ ✗ 既存フレームワークで十分対応可能 │ │
│ │ ✗ 「なんとなくカッコいいから」という理由 │ │
│ │ ✗ 1つのプロジェクトでしか使わない │ │
│ │ ✗ メンテナンスする時間・人員がない │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 代替案を検討: │
│ ・ライブラリで十分では? │
│ ・既存FWの拡張・プラグインで対応できないか? │
│ ・設定やアノテーションで解決できないか? │
│ │
└─────────────────────────────────────────────────────────────┘

フレームワークのコスト

┌─────────────────────────────────────────────────────────────┐
│ フレームワーク作成のコスト │
├─────────────────────────────────────────────────────────────┤
│ │
│ 初期コスト: │
│ ├── 設計・実装: 数週間〜数ヶ月 │
│ ├── ドキュメント作成: 実装と同等以上の労力 │
│ └── サンプル・チュートリアル作成 │
│ │
│ 継続コスト(これが重要): │
│ ├── バグ修正・セキュリティパッチ │
│ ├── 依存ライブラリのアップデート対応 │
│ ├── 新機能リクエストへの対応 │
│ ├── 破壊的変更時のマイグレーションサポート │
│ ├── ユーザーからの質問対応 │
│ └── ドキュメントの継続的更新 │
│ │
│ 「作るのは1回、メンテナンスは永遠」 │
│ │
└─────────────────────────────────────────────────────────────┘

フレームワーク設計の原則

1. 最小限の驚き原則(Principle of Least Astonishment)

┌─────────────────────────────────────────────────────────────┐
│ 最小限の驚き原則 │
├─────────────────────────────────────────────────────────────┤
│ │
│ ユーザーが「予想通り」に動くことが最優先 │
│ │
│ 悪い例: │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ // saveなのに削除される...!? │ │
│ │ user.save(); // 内部で古いデータを削除してから保存 │ │
│ │ │ │
│ │ // getなのに副作用がある...!? │ │
│ │ user.getName(); // 内部でDBにアクセスログを記録 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 良い例: │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ // 名前から動作が予測できる │ │
│ │ user.save(); // 保存する │ │
│ │ user.deleteAndSave(); // 削除してから保存 │ │
│ │ user.getName(); // 名前を取得(副作用なし) │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ チェックポイント: │
│ □ メソッド名から動作が予測できるか? │
│ □ 副作用は明示されているか? │
│ □ 戻り値の型は適切か? │
│ □ 例外は予測可能か? │
│ │
└─────────────────────────────────────────────────────────────┘

2. 設定より規約、ただし逃げ道を用意

┌─────────────────────────────────────────────────────────────┐
│ Convention over Configuration │
├─────────────────────────────────────────────────────────────┤
│ │
│ デフォルトは「よくあるケース」に最適化 │
│ ただし、必ずカスタマイズ可能にする │
│ │
│ 良い設計: │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ // デフォルトで動く(設定なし) │ │
│ │ @Entity │ │
│ │ class User { } // テーブル名は自動で "users" │ │
│ │ │ │
│ │ // カスタマイズも可能 │ │
│ │ @Entity(table = "app_users") │ │
│ │ class User { } // テーブル名を明示的に指定 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 悪い設計: │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ // 必ず設定が必要(面倒) │ │
│ │ @Entity(table = "users", schema = "public", ...) │ │
│ │ class User { } │ │
│ │ │ │
│ │ // または、カスタマイズ不可能(柔軟性がない) │ │
│ │ @Entity // テーブル名は必ずクラス名の複数形 │ │
│ │ class User { } // 変更する方法がない │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘

3. 薄いラッパーより厚い抽象化

┌─────────────────────────────────────────────────────────────┐
│ 適切な抽象化レベル │
├─────────────────────────────────────────────────────────────┤
│ │
│ 薄いラッパー(価値が低い): │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ // 単なるメソッド呼び出しの転送 │ │
│ │ class MyHttpClient { │ │
│ │ HttpClient client; │ │
│ │ Response get(String url) { │ │
│ │ return client.get(url); // 何も追加しない │ │
│ │ } │ │
│ │ } │ │
│ │ → 元のライブラリを直接使えばいい │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 価値のある抽象化: │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ // 共通の関心事を処理 │ │
│ │ class ApiClient { │ │
│ │ Response get(String endpoint) { │ │
│ │ return client.get(baseUrl + endpoint) │ │
│ │ .header("Authorization", getToken()) │ │
│ │ .timeout(config.getTimeout()) │ │
│ │ .retry(3) │ │
│ │ .onError(this::handleError) │ │
│ │ .execute(); │ │
│ │ } │ │
│ │ } │ │
│ │ → 認証、タイムアウト、リトライ、エラー処理を抽象化 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘

4. 依存関係を最小限に

┌─────────────────────────────────────────────────────────────┐
│ 依存関係の管理 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 依存が多いフレームワークの問題: │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ ・バージョン競合(Dependency Hell) │ │
│ │ ・セキュリティ脆弱性の伝播 │ │
│ │ ・ビルド時間の増加 │ │
│ │ ・ユーザーのアップグレードが困難 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ ベストプラクティス: │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 1. コア機能は依存なしで実装 │ │
│ │ │ │
│ │ 2. オプション機能は別モジュールに │ │
│ │ my-framework-core (依存なし) │ │
│ │ my-framework-jackson (Jackson連携) │ │
│ │ my-framework-spring (Spring連携) │ │
│ │ │ │
│ │ 3. 依存は provided/optional として宣言 │ │
│ │ ユーザーが自分でバージョンを選べる │ │
│ │ │ │
│ │ 4. 依存ライブラリのパッケージを shade しない │ │
│ │ (どうしても必要な場合を除く) │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘

拡張ポイントの設計

拡張ポイントとは

┌─────────────────────────────────────────────────────────────┐
│ 拡張ポイント │
├─────────────────────────────────────────────────────────────┤
│ │
│ フレームワークの動作をユーザーがカスタマイズできる場所 │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ フレームワークの処理 │ │
│ │ │ │
│ │ リクエスト受信 │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ ┌────────────────┐ │ │
│ │ │ 拡張ポイント1 │ ← ユーザーがカスタマイズ可能 │ │
│ │ │ (Filter) │ │ │
│ │ └────────────────┘ │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ ルーティング │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ ┌────────────────┐ │ │
│ │ │ 拡張ポイント2 │ ← ユーザーがカスタマイズ可能 │ │
│ │ │ (Interceptor) │ │ │
│ │ └────────────────┘ │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ ハンドラ実行 │ │
│ │ │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘

拡張ポイントの実装パターン

1. インターフェース/抽象クラス

// ========================================
// フレームワークが定義するインターフェース
// ========================================
public interface RequestFilter {
/**
* リクエストをフィルタリングする
* @return true: 処理継続, false: 処理中断
*/
boolean filter(Request request);
}

// デフォルト実装を提供(使わなくてもOK)
public class NoOpFilter implements RequestFilter {
@Override
public boolean filter(Request request) {
return true; // 常に通過
}
}

// ========================================
// ユーザーが実装
// ========================================
public class AuthenticationFilter implements RequestFilter {
@Override
public boolean filter(Request request) {
String token = request.getHeader("Authorization");
return tokenValidator.validate(token);
}
}

2. コールバック/リスナー

// ========================================
// イベントベースの拡張
// ========================================
public interface LifecycleListener {
default void onStartup(Context context) { }
default void onShutdown(Context context) { }
default void onError(Context context, Throwable error) { }
}

// フレームワーク側
public class Framework {
private List<LifecycleListener> listeners = new ArrayList<>();

public void addListener(LifecycleListener listener) {
listeners.add(listener);
}

void startup() {
// ... 起動処理 ...
listeners.forEach(l -> l.onStartup(context));
}
}

// ユーザー側
framework.addListener(new LifecycleListener() {
@Override
public void onStartup(Context context) {
logger.info("アプリケーション起動");
warmupCache();
}
});

3. プラグインシステム

// ========================================
// プラグインインターフェース
// ========================================
public interface Plugin {
String getName();
void install(FrameworkBuilder builder);
}

// ========================================
// プラグイン実装例
// ========================================
public class MetricsPlugin implements Plugin {
@Override
public String getName() {
return "metrics";
}

@Override
public void install(FrameworkBuilder builder) {
builder.addFilter(new MetricsFilter());
builder.addEndpoint("/metrics", new MetricsEndpoint());
}
}

// ========================================
// 使用方法
// ========================================
Framework framework = Framework.builder()
.install(new MetricsPlugin())
.install(new SecurityPlugin())
.build();

4. Service Provider Interface (SPI)

// ========================================
// META-INF/services による自動検出
// ========================================

// インターフェース定義
public interface Serializer {
String getContentType();
byte[] serialize(Object obj);
Object deserialize(byte[] data, Class<?> type);
}

// META-INF/services/com.example.Serializer に記載
// com.example.JsonSerializer
// com.example.XmlSerializer

// フレームワークが自動でロード
ServiceLoader<Serializer> loader = ServiceLoader.load(Serializer.class);
for (Serializer serializer : loader) {
register(serializer.getContentType(), serializer);
}

拡張ポイント設計の注意点

┌─────────────────────────────────────────────────────────────┐
│ 拡張ポイント設計の注意点 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 1. 拡張ポイントは慎重に選ぶ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ ・多すぎる → 複雑で理解困難 │ │
│ │ ・少なすぎる → 柔軟性がない │ │
│ │ ・「ここをカスタマイズしたい」という声を収集 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 2. 拡張ポイントの実行順序を明確に │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ ・複数のフィルターがある場合の実行順序は? │ │
│ │ ・優先度の指定方法は? │ │
│ │ ・ドキュメントで明示する │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 3. デフォルト動作を提供 │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ ・拡張しなくても動くようにする │ │
│ │ ・default メソッドや NoOp 実装を用意 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 4. 拡張ポイントの契約を明確に │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ ・いつ呼ばれるか │ │
│ │ ・何を渡されるか(null の可能性は?) │ │
│ │ ・何を返すべきか │ │
│ │ ・例外を投げていいか │ │
│ │ ・スレッドセーフである必要があるか │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘

API設計とバージョニング

公開APIの設計

┌─────────────────────────────────────────────────────────────┐
│ 公開APIの設計 │
├─────────────────────────────────────────────────────────────┤
│ │
│ API = ユーザーとの「契約」 │
│ 一度公開したら簡単には変更できない │
│ │
│ 公開するもの(Public API): │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ ・ユーザーが直接使うクラス・メソッド │ │
│ │ ・拡張ポイントのインターフェース │ │
│ │ ・設定クラス │ │
│ │ ・例外クラス │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 公開しないもの(Internal): │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ ・実装の詳細 │ │
│ │ ・ヘルパークラス │ │
│ │ ・内部データ構造 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 区別の方法: │
│ ・パッケージ分離: api/ と internal/ │
│ ・アノテーション: @PublicApi, @Internal │
│ ・可視性: public vs package-private │
│ ・Javaモジュールシステム (module-info.java) │
│ │
└─────────────────────────────────────────────────────────────┘

セマンティックバージョニング

┌─────────────────────────────────────────────────────────────┐
│ セマンティックバージョニング │
├─────────────────────────────────────────────────────────────┤
│ │
│ MAJOR.MINOR.PATCH │
│ │ │ │ │
│ │ │ └── バグ修正 │
│ │ └──────── 後方互換の機能追加 │
│ └────────────── 破壊的変更 │
│ │
│ 例: 1.2.3 → 1.2.4 │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ バグ修正のみ │ │
│ │ ユーザーのコードは変更不要 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 例: 1.2.3 → 1.3.0 │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 新機能追加(後方互換あり) │ │
│ │ ユーザーのコードは変更不要 │ │
│ │ 新機能を使いたければ使える │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 例: 1.2.3 → 2.0.0 │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 破壊的変更あり │ │
│ │ ユーザーのコード修正が必要な可能性 │ │
│ │ マイグレーションガイドを提供すること │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘

非推奨(Deprecation)の扱い

// ========================================
// 正しい非推奨の手順
// ========================================

// 1. まず @Deprecated を付けて代替を示す
/**
* @deprecated 2.0で削除予定。代わりに {@link #newMethod()} を使用してください。
*/
@Deprecated(since = "1.5", forRemoval = true)
public void oldMethod() {
// 内部で新メソッドを呼び出す(互換性維持)
newMethod();
}

public void newMethod() {
// 新しい実装
}

// 2. 少なくとも1メジャーバージョンは維持
// 1.5 で deprecated → 2.0 で削除

// 3. マイグレーションガイドを用意
// MIGRATION.md に変更点と対応方法を記載

APIの安定性レベル

┌─────────────────────────────────────────────────────────────┐
│ API安定性レベル │
├─────────────────────────────────────────────────────────────┤
│ │
│ @Stable │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ ・本番利用可能 │ │
│ │ ・破壊的変更はメジャーバージョンのみ │ │
│ │ ・十分にテスト済み │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ @Beta │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ ・本番利用は自己責任 │ │
│ │ ・マイナーバージョンで変更の可能性あり │ │
│ │ ・フィードバック歓迎 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ @Experimental │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ ・実験的機能 │ │
│ │ ・いつでも変更・削除の可能性あり │ │
│ │ ・本番利用非推奨 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ @Internal │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ ・内部実装 │ │
│ │ ・ユーザーは使用禁止 │ │
│ │ ・警告なく変更される │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘

エラーハンドリング戦略

エラーの種類と対応

┌─────────────────────────────────────────────────────────────┐
│ エラーハンドリング │
├─────────────────────────────────────────────────────────────┤
│ │
│ 1. プログラミングエラー(バグ) │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 例: null参照、範囲外アクセス │ │
│ │ 対応: 非検査例外(RuntimeException)を投げる │ │
│ │ 理由: 呼び出し側で回復不可能 │ │
│ │ │ │
│ │ throw new IllegalArgumentException( │ │
│ │ "name は null であってはなりません"); │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 2. 回復可能なエラー │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 例: ファイルが見つからない、ネットワークエラー │ │
│ │ 対応: 検査例外 or 戻り値(Optional、Result型) │ │
│ │ 理由: 呼び出し側で適切に処理できる │ │
│ │ │ │
│ │ // 方法1: 検査例外 │ │
│ │ User findUser(Long id) throws UserNotFoundException; │ │
│ │ │ │
│ │ // 方法2: Optional │ │
│ │ Optional<User> findUser(Long id); │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 3. フレームワーク内部エラー │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 例: 設定ミス、初期化失敗 │ │
│ │ 対応: 専用の例外クラスを用意 │ │
│ │ 理由: ユーザーが原因を特定しやすい │ │
│ │ │ │
│ │ throw new FrameworkConfigurationException( │ │
│ │ "データソースが設定されていません", │ │
│ │ "application.yml に datasource を追加してください"│ │
│ │ ); │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘

良いエラーメッセージ

┌─────────────────────────────────────────────────────────────┐
│ 良いエラーメッセージ │
├─────────────────────────────────────────────────────────────┤
│ │
│ 悪い例: │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ "Error occurred" │ │
│ │ "Invalid input" │ │
│ │ "null" │ │
│ │ "エラー" │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 良い例: │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ エラーメッセージに含めるべき情報: │ │
│ │ 1. 何が起きたか │ │
│ │ 2. なぜ起きたか(原因) │ │
│ │ 3. どうすれば解決できるか(アクション) │ │
│ │ │ │
│ │ 例: │ │
│ │ "データベース接続に失敗しました。 │ │
│ │ 原因: ホスト 'db.example.com:5432' に接続できません。│ │
│ │ 対処: ネットワーク設定を確認するか、 │ │
│ │ application.yml の database.host を確認して │ │
│ │ ください。" │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 例外クラス設計: │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ public class ConfigurationException extends ... { │ │
│ │ private final String property; │ │
│ │ private final String suggestion; │ │
│ │ │ │
│ │ public String getHelpMessage() { │ │
│ │ return String.format( │ │
│ │ "設定 '%s' が不正です。%s", │ │
│ │ property, suggestion │ │
│ │ ); │ │
│ │ } │ │
│ │ } │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘

ドキュメントとサンプル

ドキュメントの重要性

┌─────────────────────────────────────────────────────────────┐
│ ドキュメントの重要性 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 「ドキュメントがないフレームワークは存在しないのと同じ」 │
│ │
│ 必要なドキュメント: │
│ │
│ 1. Getting Started(最重要) │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ ・5分で動くサンプル │ │
│ │ ・コピペで動くコード │ │
│ │ ・最小限の設定 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 2. コンセプトガイド │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ ・なぜこのフレームワークを使うのか │ │
│ │ ・基本的な考え方・アーキテクチャ │ │
│ │ ・他のフレームワークとの違い │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 3. APIリファレンス │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ ・全ての公開クラス・メソッドの説明 │ │
│ │ ・パラメータと戻り値の説明 │ │
│ │ ・使用例 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 4. How-to ガイド │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ ・よくあるユースケースの実装方法 │ │
│ │ ・ステップバイステップの手順 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 5. マイグレーションガイド │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ ・バージョンアップ時の変更点 │ │
│ │ ・破壊的変更への対応方法 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘

サンプルプロジェクト

┌─────────────────────────────────────────────────────────────┐
│ サンプルプロジェクト │
├─────────────────────────────────────────────────────────────┤
│ │
│ サンプルの種類: │
│ │
│ 1. Minimal(最小構成) │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ ・動作確認用の最小コード │ │
│ │ ・依存は最小限 │ │
│ │ ・「とりあえず動かしたい」人向け │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 2. Full-featured(フル機能) │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ ・全機能を使った実践的なサンプル │ │
│ │ ・本番に近い構成 │ │
│ │ ・「本格的に使いたい」人向け │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 3. Integration(連携サンプル) │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ ・Spring Boot との連携 │ │
│ │ ・他のライブラリとの組み合わせ │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ サンプルの品質: │
│ ・定期的にビルド・テストする(CIで回す) │
│ ・本体のバージョンアップに追従する │
│ ・コメントで説明を入れる │
│ │
└─────────────────────────────────────────────────────────────┘

よくある失敗パターン

アンチパターン集

┌─────────────────────────────────────────────────────────────┐
│ よくある失敗パターン │
├─────────────────────────────────────────────────────────────┤
│ │
│ 1. 過度な抽象化(Overengineering) │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 症状: │ │
│ │ ・Factory の Factory の Factory... │ │
│ │ ・簡単なことをするのに10クラス必要 │ │
│ │ ・設定ファイルが複雑すぎる │ │
│ │ │ │
│ │ 対策: │ │
│ │ ・YAGNI(You Aren't Gonna Need It) │ │
│ │ ・実際のユースケースから設計する │ │
│ │ ・「3回使うまで抽象化しない」ルール │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 2. 設定地獄(Configuration Hell) │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 症状: │ │
│ │ ・何十行もの設定ファイルが必要 │ │
│ │ ・デフォルト値がない │ │
│ │ ・設定の意味が分からない │ │
│ │ │ │
│ │ 対策: │ │
│ │ ・賢いデフォルト値を提供 │ │
│ │ ・設定なしで動くようにする │ │
│ │ ・設定項目にドキュメントを付ける │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 3. 魔法すぎる(Too Much Magic) │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 症状: │ │
│ │ ・「なぜ動くのか」が分からない │ │
│ │ ・デバッグが困難 │ │
│ │ ・暗黙の動作が多すぎる │ │
│ │ │ │
│ │ 対策: │ │
│ │ ・明示的な設定オプションを用意 │ │
│ │ ・デバッグログを充実させる │ │
│ │ ・「内部で何が起きているか」を説明するドキュメント │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 4. 後方互換性の軽視 │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 症状: │ │
│ │ ・マイナーバージョンでAPIが変わる │ │
│ │ ・アップグレードのたびにコード修正が必要 │ │
│ │ ・マイグレーションガイドがない │ │
│ │ │ │
│ │ 対策: │ │
│ │ ・セマンティックバージョニングを守る │ │
│ │ ・deprecation period を設ける │ │
│ │ ・破壊的変更は慎重に、マイグレーションガイド必須 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 5. ドキュメント不足 │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 症状: │ │
│ │ ・「ソースを読め」状態 │ │
│ │ ・サンプルが古い/動かない │ │
│ │ ・エラーメッセージが不親切 │ │
│ │ │ │
│ │ 対策: │ │
│ │ ・コード書く時間と同等以上をドキュメントに │ │
│ │ ・サンプルはCIでテスト │ │
│ │ ・ユーザー視点でエラーメッセージを書く │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 6. NIH症候群(Not Invented Here) │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 症状: │ │
│ │ ・既存の良いライブラリを使わず自作 │ │
│ │ ・車輪の再発明 │ │
│ │ ・「うちは特別」という思い込み │ │
│ │ │ │
│ │ 対策: │ │
│ │ ・まず既存ライブラリを調査 │ │
│ │ ・本当に自作が必要か3回考える │ │
│ │ ・コア以外は既存ライブラリに任せる │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘

まとめ

フレームワーク作成チェックリスト

┌─────────────────────────────────────────────────────────────┐
│ フレームワーク作成チェックリスト │
├─────────────────────────────────────────────────────────────┤
│ │
│ 企画段階: │
│ □ 本当にフレームワークが必要か検討した │
│ □ 既存フレームワークで代替できないか確認した │
│ □ メンテナンスのリソースを確保した │
│ □ 対象ユーザーを明確にした │
│ │
│ 設計段階: │
│ □ 公開APIと内部実装を分離した │
│ □ 拡張ポイントを設計した │
│ □ デフォルト値を設定した(設定なしで動く) │
│ □ エラーメッセージを丁寧に書いた │
│ │
│ 実装段階: │
│ □ 依存ライブラリを最小限にした │
│ □ テストを十分に書いた │
│ □ Javadoc/ドキュメントを書いた │
│ │
│ 公開段階: │
│ □ Getting Started を用意した │
│ □ サンプルプロジェクトを用意した │
│ □ APIリファレンスを生成した │
│ □ バージョニングポリシーを決めた │
│ │
│ 運用段階: │
│ □ Issue対応のフローを決めた │
│ □ リリースプロセスを自動化した │
│ □ 破壊的変更のポリシーを決めた │
│ │
└─────────────────────────────────────────────────────────────┘

覚えておくべき原則

原則説明
最小限の驚きユーザーの予想通りに動く
設定より規約デフォルトで動く、カスタマイズも可能
薄いラッパーより厚い抽象化価値のある抽象化を提供
依存は最小限コアは依存なし、オプション機能は別モジュール
拡張ポイントは慎重に多すぎず少なすぎず
後方互換性を守るセマンティックバージョニング
ドキュメントは必須コードと同等以上の労力

参考資料

学ぶべきフレームワーク

優れた設計の参考になるフレームワーク:

フレームワーク学べるポイント
Spring FrameworkDIコンテナ、拡張性、後方互換性
Ruby on RailsCoC、生産性重視の設計
Express.jsミニマリズム、ミドルウェア設計
Reactコンポーネント設計、宣言的API
JUnitテストフレームワークの設計

推奨書籍

  • "Framework Design Guidelines" (Microsoft)
  • "Building Evolutionary Architectures"
  • "Clean Architecture" (Robert C. Martin)