メインコンテンツへスキップ
中級22分で読める

SOLID原則とクリーンアーキテクチャ入門

ソフトウェア設計の基本であるSOLID原則と、それを活かしたクリーンアーキテクチャの概念と実践方法を詳しく解説します。

システム開発SOLID原則クリーンアーキテクチャ設計原則ソフトウェア設計TypeScript

🎯 この記事で学べること

  • 1
    SOLID原則の5つの原則を理解し、実際のコードに適用できます
  • 2
    各原則が解決する具体的な問題と適用方法を習得できます
  • 3
    クリーンアーキテクチャの基本概念と実装方法を学べます
  • 4
    TypeScriptでの実装パターンを身につけられます
  • 5
    実際のプロジェクトでの段階的適用方法を理解できます

読了時間: 約5

Instagramの13人が作った奇跡

2010年、フォトシェアアプリ「Instagram」。

開発チームは、わずか13人。しかも、サーバーサイドエンジニアは2人だけ。マイク・クリーガーとケビン・システロムが、このアプリを支えていた。

ローンチから2ヶ月で100万ユーザー。1年で1000万ユーザー。2年後、10億ドルでFacebookに買収された。

なぜ、こんなに小さなチームで、これほど巨大なサービスを支えられたのか?

秘密は、コードの構造にあった。

「我々のコードは、まるでレゴブロックのようだった」

後にUberのCTOとなったマイクは振り返る。「新しい機能を追加するとき、既存のコードを壊すことはなかった。すべてが独立したピースとして動作していた」

Instagramのコードベースには、一つの哲学が貫かれていた。SOLID原則

Robert C. Martin(通称Uncle Bob)が提唱した、5つの設計原則。これが、13人という小チームで世界を変えるプロダクトを作る原動力となった。

WhatsAppの32人vs Facebook の25,000人

2014年2月19日。

FacebookがWhatsAppを190億ドルで買収した日。この金額は、当時史上最高の企業買収だった。

興味深いのは、両社の組織規模だ。

Facebook:25,000人の従業員、数百万行のコード WhatsApp:32人の従業員、数万行のコード

1人当たりの生産性を計算すると、WhatsAppの方が圧倒的に高い。なぜこんなことが可能だったのか?

WhatsAppの共同創設者、ブライアン・アクトンは答えた。「我々のコードは、非常にシンプルだった。一つの機能は、一つのことだけをする。新しい機能を追加するときも、既存のコードに手を加える必要がなかった」

これが、単一責任原則の威力だ。

Twitter の失われた週末

2008年10月、Twitter本社。

エンジニアのジャックは、絶望していた。金曜日の夕方、「ダイレクトメッセージの文字数制限を140文字から280文字に変更してほしい」という簡単な要求を受けた。

月曜の朝までに対応すれば十分だと思った。

しかし、週末をすべて費やしても、作業は終わらなかった。

なぜか?

// Twitterの問題があったコード(簡略化)
class TwitterService {
  postTweet(userId, message) {
    // ツイート投稿
    if (message.length > 140) {
      throw new Error('Too long');
    }
    this.saveToDatabase(message);
    this.updateUserStats(userId);
    this.sendNotifications(message);
    this.updateTimeline(userId);
    this.indexForSearch(message);
  }
  
  sendDirectMessage(fromUserId, toUserId, message) {
    // DM送信
    if (message.length > 140) { // ここも変更が必要
      throw new Error('Too long');
    }
    this.saveDirectMessage(fromUserId, toUserId, message);
    this.sendPushNotification(toUserId);
    this.markAsUnread(toUserId);
  }
  
  // 他にも20個のメソッドが、同じ文字数制限ロジックを重複して持っていた
}

文字数制限のロジックが、20箇所に散在していた。一箇所変更すると、他の機能が壊れる。修正すると、また別の機能に影響する。

ジャックは、この週末の経験から重要なことを学んだ。一つのクラスが、複数の責任を持ってはいけない

単一責任原則:一つのことを、完璧に

単一責任原則(SRP)は、SOLID原則の「S」。

「クラスを変更する理由は、一つでなければならない」

Twitterの問題を、この原則で解決してみよう。

// 改善後:責任を分離した設計
class MessageValidator {
  constructor(maxLength) {
    this.maxLength = maxLength;
  }
  
  validate(message) {
    if (message.length > this.maxLength) {
      throw new Error(`Message too long (max: ${this.maxLength})`);
    }
    return true;
  }
  
  // 文字数制限が変わったとき、このクラスだけ修正すれば済む
  updateMaxLength(newMaxLength) {
    this.maxLength = newMaxLength;
  }
}

class TweetService {
  constructor(validator, database, notificationService) {
    this.validator = validator;
    this.database = database;
    this.notificationService = notificationService;
  }
  
  async postTweet(userId, message) {
    this.validator.validate(message); // バリデーションは専門クラスに委任
    
    const tweet = await this.database.saveTweet(userId, message);
    await this.notificationService.notifyFollowers(tweet);
    
    return tweet;
  }
}

class DirectMessageService {
  constructor(validator, database, notificationService) {
    this.validator = validator; // 同じバリデーターを再利用
    this.database = database;
    this.notificationService = notificationService;
  }
  
  async sendDirectMessage(fromUserId, toUserId, message) {
    this.validator.validate(message); // 同じロジック、別のクラス
    
    const dm = await this.database.saveDirectMessage(fromUserId, toUserId, message);
    await this.notificationService.notifyUser(toUserId, dm);
    
    return dm;
  }
}

この設計なら、文字数制限の変更はMessageValidatorだけを修正すれば済む。週末を失うこともない。

開放閉鎖原則:Airbnbの200種類の家

2015年、Airbnb。

物件の種類が爆発的に増えていた。アパート、一軒家、キャンプ場、城、樹上の家、イグルー。プロダクトマネージャーは、毎週新しいタイプの物件を追加したがった。

エンジニアのサラは、このコードを書いていた。

// 問題のあったコード
class PropertyPriceCalculator {
  calculatePrice(property) {
    if (property.type === 'apartment') {
      return property.basePrice * 1.0;
    } else if (property.type === 'house') {
      return property.basePrice * 1.2;
    } else if (property.type === 'castle') {
      return property.basePrice * 5.0;
    } else if (property.type === 'treehouse') {
      return property.basePrice * 2.0;
    }
    // 新しいタイプを追加するたびに、このメソッドを修正する
  }
}

新しい物件タイプが追加されるたびに、サラはcalculatePriceメソッドを修正しなければならなかった。月に4回、火曜日の会議で新しいタイプが決まり、金曜日までに実装が必要だった。

「毎週コアロジックを触るのは危険すぎる」

サラは、開放閉鎖原則を適用した。

// 改善後:拡張に開いて、修正に閉じた設計
class Property {
  constructor(type, basePrice) {
    this.type = type;
    this.basePrice = basePrice;
  }
  
  // 各物件タイプが独自の価格計算ロジックを持つ
  calculatePrice() {
    throw new Error('Subclass must implement calculatePrice');
  }
}

class Apartment extends Property {
  calculatePrice() {
    return this.basePrice * 1.0;
  }
}

class House extends Property {
  calculatePrice() {
    return this.basePrice * 1.2;
  }
}

class Castle extends Property {
  calculatePrice() {
    return this.basePrice * 5.0;
  }
}

class TreeHouse extends Property {
  calculatePrice() {
    return this.basePrice * 2.0;
  }
}

// 新しいタイプ:既存コードに一切手を加えずに追加可能
class Igloo extends Property {
  calculatePrice() {
    // イグルーは寒いので安くする
    return this.basePrice * 0.8;
  }
}

// 価格計算エンジン:修正不要で新しいタイプに対応
class PropertyPriceCalculator {
  calculatePrice(property) {
    return property.calculatePrice(); // 各オブジェクトが自分で計算
  }
}

この設計により、新しい物件タイプを追加するとき、既存のコードに触れる必要がなくなった。新しいクラスを追加するだけで済む。

サラの週末は平和になった。

リスコフの置換原則:Uberの5種類の車

2012年、Uber。

最初はタクシーだけだった。しかし、事業拡大とともに、様々な車種が追加された。UberX、UberXL、UberBLACK、UberSUV、UberPOOL。

エンジニアのマルコスは、こんなコードを書いていた。

// 問題のあったコード
class Vehicle {
  startRide() {
    console.log('Ride started');
  }
}

class Taxi extends Vehicle {
  startRide() {
    console.log('Taxi ride started');
  }
}

class UberPOOL extends Vehicle {
  startRide() {
    if (this.hasOtherPassengers()) {
      throw new Error('Cannot start ride with other passengers boarding');
    }
    console.log('UberPOOL ride started');
  }
  
  hasOtherPassengers() {
    // 他の乗客がまだ乗車していない場合の複雑なロジック
    return Math.random() > 0.5;
  }
}

// このコードは期待通りに動作しない
function startAllRides(vehicles) {
  vehicles.forEach(vehicle => {
    vehicle.startRide(); // UberPOOLで例外が発生する可能性
  });
}

UberPOOLは、他の車種とは動作が異なる。同じstartRide()メソッドを呼んでも、例外が発生することがある。

これはリスコフの置換原則違反だった。「派生クラスは、基底クラスと置換可能でなければならない」

マルコスは設計を見直した。

// 改善後:LSP準拠の設計
class Vehicle {
  constructor(type) {
    this.type = type;
  }
  
  // すべての車種で共通の動作
  requestRide(passenger) {
    console.log(`${this.type} ride requested for ${passenger.name}`);
  }
  
  // 各車種が独自に実装
  canStartImmediately() {
    return true; // デフォルトはすぐ開始可能
  }
}

class Taxi extends Vehicle {
  constructor() {
    super('Taxi');
  }
  
  canStartImmediately() {
    return true; // タクシーはすぐ開始可能
  }
}

class UberPOOL extends Vehicle {
  constructor() {
    super('UberPOOL');
    this.waitingPassengers = [];
  }
  
  canStartImmediately() {
    return this.waitingPassengers.length === 0; // 他に待っている乗客がいない場合のみ
  }
  
  addWaitingPassenger(passenger) {
    this.waitingPassengers.push(passenger);
  }
}

// どの車種でも同じように処理できる
class RideService {
  startRide(vehicle, passenger) {
    if (vehicle.canStartImmediately()) {
      console.log(`Starting ${vehicle.type} ride for ${passenger.name}`);
      return true;
    } else {
      console.log(`${vehicle.type} ride delayed - waiting for other passengers`);
      return false;
    }
  }
}

// すべての車種で期待通りに動作
const vehicles = [new Taxi(), new UberPOOL()];
const rideService = new RideService();
const passenger = { name: 'Alice' };

vehicles.forEach(vehicle => {
  rideService.startRide(vehicle, passenger); // 例外なし、期待通りの動作
});

この設計なら、どの車種でも同じインターフェースで扱える。新しい車種を追加しても、既存のコードは動作し続ける。

インターフェース分離原則:Spotifyの音楽業界革命

2008年、スウェーデン・ストックホルム。

Spotify創業者のダニエル・エクは、音楽業界の複雑さに直面していた。

アーティスト、レコード会社、配信プラットフォーム、ユーザー。それぞれが異なるニーズを持っている。

初期の設計はこうだった。

// 問題のあったコード:すべてを一つのインターフェースで
class MusicPlatformUser {
  // ユーザー機能
  playMusic(track) { /* 音楽再生 */ }
  createPlaylist(name) { /* プレイリスト作成 */ }
  
  // アーティスト機能
  uploadTrack(trackData) { throw new Error('Not an artist'); }
  viewRoyalties() { throw new Error('Not an artist'); }
  
  // レーベル機能
  manageArtists() { throw new Error('Not a label'); }
  distributeTracks() { throw new Error('Not a label'); }
  
  // アドミン機能
  moderateContent() { throw new Error('Not an admin'); }
  viewAnalytics() { throw new Error('Not an admin'); }
}

一般ユーザーは、アーティスト向けの機能を使えない。アーティストは、レーベル向けの機能を使えない。しかし、すべてのメソッドを実装しなければならない。

ダニエルはインターフェース分離原則を適用した。「クライアントは、使用しないメソッドに依存すべきではない」

// 改善後:役割別に分離されたインターフェース
class MusicListener {
  playMusic(track) {
    console.log(`Playing: ${track.title}`);
  }
  
  createPlaylist(name) {
    console.log(`Created playlist: ${name}`);
  }
  
  followArtist(artist) {
    console.log(`Following: ${artist.name}`);
  }
}

class MusicCreator extends MusicListener {
  uploadTrack(trackData) {
    console.log(`Uploaded: ${trackData.title}`);
  }
  
  viewRoyalties() {
    console.log('Viewing royalty report');
  }
  
  updateProfile(profileData) {
    console.log('Profile updated');
  }
}

class RecordLabel {
  manageArtists() {
    console.log('Managing artist roster');
  }
  
  distributeTracks(tracks) {
    console.log(`Distributing ${tracks.length} tracks`);
  }
  
  viewLabelAnalytics() {
    console.log('Viewing label analytics');
  }
}

class PlatformAdmin {
  moderateContent(content) {
    console.log(`Moderating: ${content.type}`);
  }
  
  viewPlatformAnalytics() {
    console.log('Viewing platform-wide analytics');
  }
  
  manageLicenses() {
    console.log('Managing music licenses');
  }
}

// 各ユーザータイプが必要な機能だけを持つ
const listener = new MusicListener(); // リスナー向け機能のみ
const artist = new MusicCreator();     // 作成者向け機能も使用可能
const label = new RecordLabel();       // レーベル向け機能
const admin = new PlatformAdmin();     // 管理者向け機能

この分離により、各ユーザータイプは自分に関係ない機能に依存しなくて済む。リスナーは音楽を聞くことだけに集中できる。アーティストは楽曲制作とファンとの交流に集中できる。

結果は歴史が証明している。Spotifyは音楽業界を変革し、4億人のユーザーを獲得した。

依存性逆転原則:Netflix の技術革命

2007年、Netflix。

まだDVD郵送事業が主力だった時代。CTOのAdrian Cockcroftは、ストリーミングサービスへの転換を見据えていた。

しかし、既存のシステムには問題があった。

// 問題のあったコード:具象クラスに直接依存
class RecommendationEngine {
  constructor() {
    this.database = new MySQLDatabase();     // MySQL に強く依存
    this.emailService = new SMTPEmailService(); // SMTP に強く依存
  }
  
  generateRecommendations(userId) {
    const user = this.database.getUser(userId);
    const recommendations = this.calculateRecommendations(user);
    this.emailService.sendRecommendations(user.email, recommendations);
    return recommendations;
  }
}

この設計では、データベースをMySQLからCassandraに変更したい場合、RecommendationEngineクラス自体を修正しなければならない。メール送信をSMTPからAmazon SESに変更する場合も同じだ。

Adrianは依存性逆転原則を適用した。「高レベルモジュールは、低レベルモジュールに依存してはならない。どちらも抽象に依存すべきだ」

// 改善後:抽象に依存する設計
class RecommendationEngine {
  constructor(database, notificationService) {
    this.database = database;               // 抽象(インターフェース)に依存
    this.notificationService = notificationService; // 抽象に依存
  }
  
  generateRecommendations(userId) {
    const user = this.database.getUser(userId);
    const recommendations = this.calculateRecommendations(user);
    this.notificationService.send(user, recommendations);
    return recommendations;
  }
}

// 様々な実装を切り替え可能
class MySQLDatabase {
  getUser(userId) {
    console.log(`Getting user from MySQL: ${userId}`);
    return { id: userId, preferences: ['action', 'comedy'] };
  }
}

class CassandraDatabase {
  getUser(userId) {
    console.log(`Getting user from Cassandra: ${userId}`);
    return { id: userId, preferences: ['drama', 'sci-fi'] };
  }
}

class EmailNotificationService {
  send(user, recommendations) {
    console.log(`Sending email to ${user.id}: ${recommendations.length} recommendations`);
  }
}

class PushNotificationService {
  send(user, recommendations) {
    console.log(`Sending push notification to ${user.id}: ${recommendations.length} recommendations`);
  }
}

// 実行時に実装を選択可能
const mysqlDb = new MySQLDatabase();
const cassandraDb = new CassandraDatabase();
const emailService = new EmailNotificationService();
const pushService = new PushNotificationService();

// 様々な組み合わせが可能
const engine1 = new RecommendationEngine(mysqlDb, emailService);
const engine2 = new RecommendationEngine(cassandraDb, pushService);

この設計により、Netflixは技術スタックを柔軟に変更できるようになった。データベースを段階的にMySQLからCassandraに移行。通知システムをメールからプッシュ通知に変更。すべて、RecommendationEngineのコードを変更することなく実現した。

結果?Netflix は世界最大のストリーミングプラットフォームになった。

クリーンアーキテクチャ:Instagram の秘密兵器

話をInstagramに戻そう。

なぜ13人という小さなチームで、これほど巨大なスケールに対応できたのか?

秘密は、クリーンアーキテクチャにあった。

Uncle Bobが提唱したこのアーキテクチャは、システムを同心円状の層に分ける。外側から内側への依存のみを許可し、逆方向の依存は禁止する。

Instagramのコードは、この構造に従っていた。

// Instagram の写真投稿機能(簡略化)

// Entities(一番内側):ビジネスルールの核心
class Photo {
  constructor(imageData, caption, userId) {
    this.id = this.generateId();
    this.imageData = imageData;
    this.caption = caption;
    this.userId = userId;
    this.timestamp = new Date();
    this.filters = [];
    
    this.validateImageSize(imageData);
  }
  
  applyFilter(filterName) {
    if (!this.isSupportedFilter(filterName)) {
      throw new Error('Unsupported filter');
    }
    this.filters.push(filterName);
  }
  
  validateImageSize(imageData) {
    if (imageData.size > 10 * 1024 * 1024) { // 10MB制限
      throw new Error('Image too large');
    }
  }
  
  isSupportedFilter(filterName) {
    const supportedFilters = ['valencia', 'x-pro-ii', 'lo-fi', 'earlybird'];
    return supportedFilters.includes(filterName);
  }
}

// Use Cases(アプリケーションロジック)
class PostPhotoUseCase {
  constructor(photoRepository, notificationService, feedService) {
    this.photoRepository = photoRepository;
    this.notificationService = notificationService;
    this.feedService = feedService;
  }
  
  async execute(imageData, caption, userId, filterName) {
    // 1. ビジネスオブジェクトを作成
    const photo = new Photo(imageData, caption, userId);
    
    // 2. フィルターを適用
    if (filterName) {
      photo.applyFilter(filterName);
    }
    
    // 3. 保存
    await this.photoRepository.save(photo);
    
    // 4. フォロワーに通知
    await this.notificationService.notifyFollowers(userId, photo);
    
    // 5. フィードに追加
    await this.feedService.addToFeeds(photo);
    
    return photo;
  }
}

// Interface Adapters(外部とのやりとり)
class PhotoController {
  constructor(postPhotoUseCase) {
    this.postPhotoUseCase = postPhotoUseCase;
  }
  
  async handlePhotoUpload(request, response) {
    try {
      const { image, caption, filter } = request.body;
      const userId = request.user.id;
      
      const photo = await this.postPhotoUseCase.execute(
        image, caption, userId, filter
      );
      
      response.json({
        success: true,
        photo: {
          id: photo.id,
          caption: photo.caption,
          timestamp: photo.timestamp
        }
      });
    } catch (error) {
      response.status(400).json({
        success: false,
        error: error.message
      });
    }
  }
}

この構造の利点は何か?

1. テストが容易 各層を独立してテストできる。エンティティはビジネスロジックのみをテスト。ユースケースはアプリケーションロジックのみをテスト。

2. 技術変更が容易 データベースをMySQLからPostgreSQLに変更?Interface Adapters層だけを変更すれば済む。APIフォーマットを変更?Controller層だけの変更で対応可能。

3. ビジネスロジックが保護される フレームワークやライブラリの変更が、コアなビジネスロジックに影響しない。React.jsからVue.jsに変更しても、写真の投稿ロジックは変わらない。

これが、13人でFacebookに対抗できた理由だった。

Shopifyの成長を支えた段階的適用

Shopifyの創業者、トビアス・リュトケは、興味深い戦略を取った。

「最初から完璧なアーキテクチャを目指すな。必要になったときに、必要な分だけ適用しろ」

2006年のShopify:Ruby on Railsのシンプルな構成。SOLID原則もクリーンアーキテクチャも適用していない。

2008年:ユーザー数が1万を超えたとき、単一責任原則を適用開始。

2010年:ユーザー数が10万を超えたとき、開放閉鎖原則を適用。新しい支払い方法を追加しやすくした。

2012年:グローバル展開開始。インターフェース分離原則を適用。地域別の機能を分離した。

2015年:上場準備。依存性逆転原則を適用。レガシーシステムから新システムへの段階的移行を可能にした。

2018年:IPO後。完全なクリーンアーキテクチャを導入。

この段階的アプローチにより、Shopifyは継続的な成長を実現した。2024年現在、175万以上の店舗が利用する世界最大のeコマースプラットフォームとなっている。

実践への道筋

では、あなたのプロジェクトにSOLID原則とクリーンアーキテクチャを適用するには、どうすればいいか?

Phase 1: 単一責任原則から始める(1-2週間)

  • 一つのクラスが複数の理由で変更される箇所を特定
  • バリデーション、データアクセス、ビジネスロジックを分離
  • すぐに効果を実感できる

Phase 2: インターフェースの導入(2-4週間)

  • 具象クラスに依存している箇所を特定
  • インターフェースまたは抽象クラスを導入
  • テストが格段に容易になる

Phase 3: 開放閉鎖原則の適用(4-8週間)

  • 頻繁に変更される箇所を特定
  • Strategy パターンや Template Method パターンを適用
  • 新機能追加時の影響範囲が激減する

Phase 4: 依存性逆転の完全適用(2-3ヶ月)

  • 依存性注入コンテナの導入
  • 設定ファイルによる実装の切り替えを実現
  • 本番環境とテスト環境の完全分離が可能に

Phase 5: クリーンアーキテクチャの導入(3-6ヶ月)

  • レイヤーの明確な分離
  • ユースケース駆動の開発プロセス
  • 長期的な保守性の確保

重要なのは、一度にすべてを適用しようとしないこと。Instagram、Netflix、Shopify、すべての会社が段階的に導入している。

あなたのプロジェクトも、小さな一歩から始めよう。まずは、一つのクラスの責任を分離することから。

その小さな変更が、やがて大きな変革をもたらす。13人で世界を変えたInstagramのように。

SOLID原則は「完璧」を目指すものではありません。「改善」を目指すものです。完璧な設計はありませんが、より良い設計は常に存在します。小さな改善を積み重ねることで、大きな成果を得られます。

おすすめコース

関連記事