Claude Code で実現した「インフラ層を後回しにしたクリーンアーキテクチャ開発」の感動体験

Claude Code で実現した「インフラ層を後回しにしたクリーンアーキテクチャ開発」の感動体験

こんにちは!ワンダーソフトコーヒーです。

本日は、津留将史さんにご寄稿いただいた Claude Code を利用した感動体験をお届けします!

はじめに

こんにちは!ワンダーソフトさんで業務委託として働いている津留と申します。主にフロントエンド(Next.js)とバックエンド(Scala)の開発を担当しています。

ワンダーソフトではクリーンアーキテクチャを意識した実装やモダンなクラウドインフラを活用されていてかなり学ばせていただいています。もちろん生成AIの活用もかなり積極的に行っています

そんなワンダーソフトの開発現場ですが、最近、クリーンアーキテクチャでの開発において、Claude Code を活用したことで、今まで自分には難しいと感じていた開発アプローチが実現できました。

その体験が非常に感動的だったので、ここに記録として残しておきます。

従来の課題

クリーンアーキテクチャでは、以下のような層構造で開発を行います。

  • Entities (ドメイン層): ビジネスルールの中核
  • Use Cases (ユースケース層): アプリケーションのビジネスルール
  • Interface Adapters (アダプター層): 外部とのインターフェース
  • Frameworks & Drivers (インフラ層): データベース、Web、デバイスなど

上記のようにレイヤーを意識して開発を行うことで、責務の明確化やテスト駆動開発の実現など、様々なメリットがあります。

詳しくは「クリーンアーキテクチャ」と検索していただいて、その記事にお任せしたいのですが、この記事では、クリーンアーキテクチャのドメイン層とユースケース層の実装について、どのように実装するかを考えていきます。

クリーンアーキテクチャによる実装をすると、一般的にはインフラ層やアダプター層の実装は後回しにして、ドメイン層とユースケース層の実装を先に行うことができるという話もあります。インフラの用意を待たずにコード上でPoC的にビジネスロジックを実装できるというのはなかなか魅力的ですよね。しかし、実際には私はそうもいきませんでした。

というのも、私の中では以下のような課題があったからです。

  • インフラ層の実装(クエリなど)がないと、実装のイメージがわからない
  • ドメイン層やユースケース層だけでは、実際の動作が想像しづらい

このような課題があったため、インフラ層の実装がないとドメイン層やユースケース層の実装ができないという状況でした。

Claude Code との出会い

ところが Claude Code を活用することで、先の課題が解消しました。
ワンダーソフトでは Claude Code を使わせていただいているので開発の進め方も従来とは違っています。

ここでは、 Claude Code を活用しながらどのようにクリーンアーキテクチャを実装していくことができたかをご紹介します。

実際にどんなものを作ったかをお伝えすることはできませんが、ここではブログの投稿を管理する画面を作ったと仮定しておきます。

※ サンプルのコードは実際に動かしていませんので、あくまで参考程度にご覧ください。

1. 不足している考慮事項の補完

いつもなら真っ先にテーブルを作って、そのテーブルを操作するモデルやインフラ層の実装をしながら、実際に動くものをドメイン層やユースケース層を用意しながら実装していくところだったのですが、今回はテーブル定義こそ先に作ってみたものの、そのテーブルを操作するインフラ層の実装はスキップして、ドメイン層とユースケース層の実装を行いました。

実際にインフラ層の実装がなくても、Claude Code が以下のような点を補完してくれます。

  • データベースのクエリ構造の推測
  • 外部サービスとの連携パターンの提案
  • エラーハンドリングの考慮
  • Scalaの型システムを活用した設計の提案(型安全性によりクリーンアーキテクチャの実装がより確実に)

正直いうと私は、Scalaエンジニアとしてはまだまだ未熟で型をうまく活用した実装が自分1人ではできないこともあります(実際読めるけど書けないことは結構あります)。

そのような私のスキルレベルの場合、上記に書いているようなエラーハンドリングの考慮やScalaの型システムを活用したインターフェイスの設計は難しく、結局実際に実装をしながらインターフェイスを作っていくことになります。
しかし、Claude Codeを使って書く場合は、インターフェイスとなる trait だけ軽く用意しておけば意図した設計になるように補完してくれたり、提案してくれたりします。
例えば、ブログのデータを管理するリポジトリのインターフェイスの場合は以下のような trait やエラー だけを用意しておきます。

trait BlogPostRepository {
  def findAll(): Either[BlogPostRepositoryError, List[BlogPost]]
  def findById(id: BlogPostId): Either[BlogPostRepositoryError, Option[BlogPost]]
  def save(post: BlogPost): Either[BlogPostRepositoryError, BlogPost]
}

sealed abstract class BlogPostRepositoryError
object BlogPostRepositoryError {
  case object SaveFailed extends BlogPostRepositoryError
  case object NotFound extends BlogPostRepositoryError
  case object DatabaseError extends BlogPostRepositoryError
}

sealed abstract class BlogPostError
object BlogPostError {
  case object NotFound extends BlogPostError
  case object InvalidState extends BlogPostError
  case object ValidationError extends BlogPostError
}

上記のようなインターフェイスは Claude Code が大体察して補完してくれるので楽々実装できました。正直エラー内容なんて最初からどんなのがあるかぱっと思いつかないですからね。これが結構助かります。
このように安心してリポジトリのインターフェイスだけ用意できたら、あとはお楽しみのドメイン層とユースケースの実装に集中することができました。

2. 実装イメージの具体化

ドメインモデルはドメインの概念を表現するものです。

ブログを投稿する場合、ブログのタイトルや本文、公開日時などの情報を入力して投稿することになります。そして管理画面では投稿されたブログを一覧表示したり、詳細を表示したり、編集したり、削除したりすることになります。さらに公開・非公開などの設定もトグルやボタンなどでポチッとかんたんに行えたりしますよね。

これをドメインモデルに落とし込みます。

例えばこのような形でしょうか。

case class BlogPost(
  id: BlogPostId,
  title: String,
  content: String,
  status: BlogPostStatus,
  publishedAt: Option[LocalDateTime]
)

object BlogPost {
  // ブログのポストを作成(メモリ上にできるだけ)
  def create(title: String, content: String): BlogPost = {
    BlogPost(BlogPostId.generate(), title, content, BlogPostStatus.Draft, None)
  }

  // ブログのポストを公開
  def publish(self: BlogPost): BlogPost = {
    self.copy(
      status = BlogPostStatus.Published,
      publishedAt = Some(LocalDateTime.now())
    )
  }

  // ブログのポストを非公開
  def unpublish(self: BlogPost): BlogPost = {
    self.copy(
      status = BlogPostStatus.Draft,
      publishedAt = None
    )
  }
}

sealed abstract class BlogPostStatus
object BlogPostStatus {
  case object Draft extends BlogPostStatus
  case object Published extends BlogPostStatus
}

case class BlogPostId(value: String) extends AnyVal
object BlogPostId {
  def generate(): BlogPostId = BlogPostId(java.util.UUID.randomUUID().toString)
}

こんな感じでモデルに対してちゃんと機能を実装してあげます。

このように、ビジネスロジックから先に実装するのではなく、まずドメインモデルを作り、その振る舞いをしっかり定義していくことで、表現豊かなドメインモデルが出来上がります。

コードを見ただけで「何ができるのか」がすぐに分かるので、可読性も高くなります。

逆に、ドメインモデルがただのデータの入れ物だけだと、そのモデルを見ても何ができるのか分からず、結局ロジックを追いかける羽目になります。 これはDDDの文脈で言う「ドメインモデル貧血症(Anemic Domain Model)」ですね。

貧血症のドメインモデルでは、ビジネスロジックがユースケース層やサービス層に散らばってしまい、ドメインの知識が分散してしまいます。その結果、同じようなロジックが複数箇所に重複したり、ドメインの変更時に影響範囲が予測しづらくなったりします。

 一方、表現豊かなドメインモデルでは、ビジネスルールがドメインモデル自身に集約されるため、ドメインの知識が一箇所にまとまり、保守性が向上します。

さて、こうして表現豊かなドメインモデルができてきたら、次はいよいよビジネスロジック(ユースケース層)の実装です。 プラモデルで言うなら、ドメインモデルは細かいパーツですが、それを組み立てていくのがユースケースを実装する工程です。これはシステムのコアであり、実装していて一番楽しい部分ですよね(私だけ?)。

ドメインモデルが表現豊かなら、ユースケース層の実装もスムーズに進められます。 例えば、以下のようにブログの新規投稿や公開・非公開のユースケースを実装してみます。

// ブログのポストを作成
class NewBlogPostUseCase(repository: BlogPostRepository) {
  def execute(title: String, content: String): Either[BlogPostError, BlogPost] = {
    val post = BlogPost.create(title, content) // ドメインモデルを作成
    repository.save(post).left.map {
      case BlogPostRepositoryError.SaveFailed => BlogPostError.ValidationError
      case BlogPostRepositoryError.DatabaseError => BlogPostError.ValidationError
      case _ => BlogPostError.ValidationError
    }
  }
}

// ブログのポストを公開
class PublishBlogPostUseCase(repository: BlogPostRepository) {
  def execute(id: BlogPostId): Either[BlogPostError, BlogPost] = {
    for {
      maybePost <- repository.findById(id).left.map {
        case BlogPostRepositoryError.DatabaseError => BlogPostError.ValidationError
        case _ => BlogPostError.ValidationError
      }
      post <- maybePost.toRight(BlogPostError.NotFound)
      publishedPost = BlogPost.publish(post)
      savedPost <- repository.save(publishedPost).left.map {
        case BlogPostRepositoryError.SaveFailed => BlogPostError.ValidationError
        case BlogPostRepositoryError.DatabaseError => BlogPostError.ValidationError
        case _ => BlogPostError.ValidationError
      }
    } yield savedPost
  }
}

こうやってみるとなかなか可読性が高いコードが仕上がってきました。
サンプルなのに良い出来栄えですねこれ(このコードも生成AIで書いています)。

Claude Code を使うとこういった実装は結構あっさりとできるようになります。

しかし、先に書いた通り今まではインフラ層の実装がないとイメージがわかなくて実現できなかったんですよね。特にエラー周りは実際にロジックを書きながら気づくことが多いので、その辺を先回りして Claude Code によって先にエラーなどの型も出力してもらったのでかなりスムーズに進めることができました。

3. テストの実装について

先ほどは書いていませんでしたが、ドメインモデルを実装しながら同時にテストも実装していくことができました。
ドメインモデルはメモリ上だけの話なので、かんたんにテストコードを書くことができます。
さらにユースケース層もリポジトリのインターフェイスだけに依存しているので、DIでモックを注入すればユースケースの中のロジックもテストしやすくなります。

ここまでテストを実装しながら、ドメイン層やユースケース層を実装してきたのですが、やっぱりインフラ層が絡まないテストは楽ちんですね。ここがかなり快適な開発体験で感動したところでもあります。

実際の成果

ということで、ざっとサンプルのような疑似コードを用意して話してみたのですがいかがでしたでしょうか。
今回の開発体験では、以下のような成果を得ることができました。

  • データベースの制約を気にせず、純粋なビジネスロジックに集中
  • ドメインの概念をより深く理解し、ドメインモデルをより豊かに実装できた
  • テスト駆動開発が行いやすい設計ができた

感動したポイント

インターフェイスやレイヤ化を意識して Claude Code で実装をしていくと、人間に優しいだけではなく AI にも優しい設計になっていくように感じました。

インターフェイスができると、そのインターフェイスを読み取って実装してくれるので、自然言語でふんわりとした指示をするより明確になります。だんだんコードが出来上がってくると、私は Claude Code に一度指示するだけであとはよしなに実装してくれます。

私はたまに「ok」と書いたりコップに水を汲みに行ったりしながらClaudeと話しているだけです。もちろんちゃんとコードも書いていますが、ほとんどのコードはClaude Code によって書いています。

生成AIが生まれる以前は、自分のスキルや脳内メモリの不足のせいで理想はわかっていてもなかなかその域には達することができなかったのですが、生成AIを活用することで、理想の開発の流れに一歩近づくことができました。

まとめ

Claude Code との出会いにより、今まで不可能だと思っていた開発アプローチが実現できました。これは単なるツールの活用ではなく、開発プロセス全体の変革につながる可能性を秘めていると感じています。

生成AIの活用により、より多くの開発者が、より効率的で創造的な開発を行えるようになることを期待しています。

今回の体験を通じて、Claude Code は単なるコード生成ツールではなく、開発プロセス全体を変革する可能性を秘めていると実感しました。

もしあなたがコーヒーを飲みながら開発するタイプなら、Claude Code と一緒にワンダーソフトのコーヒーを淹れることでより一層優雅な開発時間を過ごせるかもしれません。きっと、今までとは違う開発体験が待っているはずです。


この記事は、Claude Code を活用した開発体験の記録として書かれています。

ブログに戻る