いつもは「吹き出し」で質問と回答を記述していますが、今回はClaudeの出力する図を簡単にブログに記述するため、吹き出しは使いません。 「吹き出し」はClaudeが複雑なフォーマットを出力したとき、吹き出しに載せ替えるのがかなり面倒で、以前から少し困っていた部分です。 今回のようにHxタグで「問」と「回答」を区別した方が見やすいかも知れないとも、思っています。
まだ、いろいろ試行錯誤している最中なので、また別の書き方を試すかも知れません。 ご了承ください。 なお、この記事も自分用の備忘録としてブログに掲載します。特に主張したいことはありません。
質問1
クリーンアーキテクチャについて、分かりやすく解説してください。 解説にコンピュータ言語が必要なら C# を使用してくれると助かります。
Claude回答
クリーンアーキテクチャについて、実践的な例を交えながら説明させていただきます。
クリーンアーキテクチャの主な目的は、以下の4つです:
フレームワークに依存しない
テスト可能
UIに依存しない
データベースに依存しない
これを実現するために、アプリケーションを以下の4つの層に分けます:
Entities (エンティティ層):ビジネスロジックの中核となるオブジェクト
Use Cases (ユースケース層):アプリケーション固有のビジネスルール
Interface Adapters (インターフェースアダプター層):外部とのやり取りを変換する層
Frameworks & Drivers (フレームワーク層):データベースやWeb APIなどの外部要素
では、具体的な実装例を見ていきましょう。ここでは簡単なユーザー管理システムを例に説明します。
x
// Domain Layer (Entities)
public class User
{
public int Id { get; private set; }
public string Name { get; private set; }
public string Email { get; private set; }
public User(string name, string email)
{
Name = name;
Email = email;
}
}
// Application Layer (Use Cases)
public interface IUserRepository
{
Task<User> GetByIdAsync(int id);
Task<IEnumerable<User>> GetAllAsync();
Task<User> CreateAsync(User user);
}
public class CreateUserUseCase
{
private readonly IUserRepository _userRepository;
public CreateUserUseCase(IUserRepository userRepository)
{
_userRepository = userRepository;
}
public async Task<User> ExecuteAsync(string name, string email)
{
var user = new User(name, email);
return await _userRepository.CreateAsync(user);
}
}
// Interface Adapters Layer
public class UserController
{
private readonly CreateUserUseCase _createUserUseCase;
public UserController(CreateUserUseCase createUserUseCase)
{
_createUserUseCase = createUserUseCase;
}
public async Task<IActionResult> CreateUser(CreateUserRequest request)
{
var user = await _createUserUseCase.ExecuteAsync(request.Name, request.Email);
return new CreatedResult($"/users/{user.Id}", new UserResponse(user));
}
}
// Data Layer (Frameworks & Drivers)
public class SqlUserRepository : IUserRepository
{
private readonly DbContext _context;
public SqlUserRepository(DbContext context)
{
_context = context;
}
public async Task<User> CreateAsync(User user)
{
var userEntity = new UserEntity
{
Name = user.Name,
Email = user.Email
};
_context.Users.Add(userEntity);
await _context.SaveChangesAsync();
return new User(userEntity.Name, userEntity.Email);
}
// その他のメソッドの実装...
}
// DTOs
public class CreateUserRequest
{
public string Name { get; set; }
public string Email { get; set; }
}
public class UserResponse
{
public int Id { get; set; }
public string Name { get; set; }
public string Email { get; set; }
public UserResponse(User user)
{
Id = user.Id;
Name = user.Name;
Email = user.Email;
}
}
この実装例の特徴を説明します:
依存関係の方向:
内側の層(Entities, Use Cases)は外側の層を知りません
依存関係は常に内側に向かいます
依存性の注入:
インターフェース(IUserRepository)を使用して、具体的な実装から抽象化しています
これにより、データベースの実装を変更しても、ビジネスロジックに影響を与えません
境界の明確化:
各層は明確に分かれており、それぞれの責務が明確です
DTOを使用してデータの受け渡しを行い、層間の結合を緩めています
この設計の利点:
テストが容易(モックオブジェクトを使用可能)
機能の追加や変更が容易
フレームワークやデータベースの変更が容易
コードの見通しが良く、保守性が高い
実際の開発では、プロジェクトの規模や要件に応じて、この構造をカスタマイズすることができます。
ご質問やより詳しく知りたい部分がありましたら、お気軽にお申し付けください。
質問2
4つの「層」は、TCP/IPやビルの階層構造ではなく、円形の内側から皮で包まれるように、同心円状の多重円形構造で表されるのですか。 もし、そうならなぜ階層構造ではなく円形構造なのでしょぅか。
Claude回答
はい、その通りです。クリーンアーキテクチャは、通常「クリーンアーキテクチャの同心円」として図示されます。これには重要な理由があります。
円形構造が採用されている理由は以下の通りです:
依存関係の方向性の視覚化
依存関係が「内側に向かう」という重要な原則を直感的に表現できます
外側の層は内側の層に依存できますが、その逆は許されません
TCP/IPのような階層構造では、上下の層同士が相互に依存する可能性があります
保護レベルの表現
最も重要なビジネスルール(Entities)が中心にあり、外部の変更から「保護」されているイメージを表現できます
まるで玉ねぎの層のように、外側の層が内側の層を保護する構造を視覚的に示せます
柔軟性と安定性の関係
中心に近いほど安定していて変更が少なく、外側に行くほど柔軟で変更が多いという性質を表現できます
例えば、最も外側のUIやデータベースは頻繁に変更される可能性がありますが、中心のビジネスルールは比較的安定しています
独立性の強調
各層が完全な円として描かれることで、それぞれの層が独立して存在できることを示します
内側の層は外側の層の存在を知らなくても機能できます
プラグイン・アーキテクチャの実現
外側の層は「プラグイン」として扱うことができ、必要に応じて差し替え可能であることを示します
例えば、データベースをSQLServerからMongoDBに変更しても、内側の層には影響を与えません
これは階層構造との重要な違いです:
階層構造:上下の層が相互に依存する可能性がある
円形構造:依存関係が常に内側にのみ向かう
この円形構造により、ソフトウェアの:
保守性
テスト容易性
変更への耐性
スケーラビリティ
が向上します。各層が明確に分離され、依存関係が一方向に制限されることで、システム全体の堅牢性が高まるのです。
質問3
依存関係は、Entities (エンティティ層)のような「人間の業務」が中心で、Interface Adapters や Frameworks & Drivers のような、デバイス系の処理が変更可能な外部の存在と考えるのですか。 従来のソフトウェアだとデバイスの方に、業務処理が依存する考え方になっていたと思うのですが、これと逆の考え方をするということでしょうか。 これは設計思想の革新なのでしょうか。
Claude回答
はい、その理解が正確です。これは実際、ソフトウェア設計における重要なパラダイムシフトといえます。
従来のアプローチと比較してみましょう:
従来のアプローチ(デバイス中心):
データベースの構造が先にあり、それに業務ロジックを合わせる
フレームワークの制約に業務ロジックを適応させる
UIの要件がビジネスロジックの実装を左右する
クリーンアーキテクチャ(ビジネス中心):
ビジネスルール(業務の本質)が中心
技術的な実装の詳細は外側に位置する「実装の詳細」として扱う
データベースやUIは「取り替え可能なプラグイン」として扱う
この設計思想の革新的な点は:
本質と実装の分離
ビジネスロジック(本質)とテクニカルな実装(詳細)を明確に分離
これにより、ビジネスロジックの純粋性が保たれる
技術的負債の軽減
フレームワークやデータベースの変更が、核となるビジネスロジックに影響を与えない
新技術への移行がスムーズに
ドメイン駆動設計との親和性
xxxxxxxxxx
// ビジネスロジックが純粋な形で表現できる
public class Invoice
{
public void CalculateTotal()
{
// ビジネスルールのみを表現
// DBやUIの詳細は意識しない
}
}
テスタビリティの向上
xxxxxxxxxx
// ビジネスロジックのテストが容易に
public class OrderProcessingTests
{
[Fact]
public void WhenOrderExceedsLimit_ShouldRequireApproval()
{
var order = new Order(amount: 10000);
Assert.True(order.RequiresApproval());
// DBやUIの存在を気にせずテスト可能
}
}
これは確かに革新的な考え方の転換です:
従来: システムの技術的制約に業務を合わせる
クリーン: 業務の本質を中心に据え、技術はそれに奉仕する
この変化の背景には:
ソフトウェアの長期保守の重要性の認識
技術の急速な変化への対応の必要性
ビジネスロジックの価値の再認識
があります。
特に重要なのは、この設計により:
ビジネスロジックが技術的な実装の詳細から保護される
システムの寿命が延びる
新技術への適応が容易になる
という点です。これは単なる技術的な改善ではなく、ソフトウェア開発の本質的な価値の再定義といえるでしょう。
質問4
革新的すぎて、4つの層の設計の順序が想像できないのですが、 従来のデバイス系へビジネス系が依存する設計では、先にデバイス系のクラスライブラリの設計を行ってから、ビジネス系を設計していくやり方になったと思います。 クリーンアーキテクチャでは、ビジネス系クラスライブラリを先に設計してから、デバイス系を設計していくのでしょうか。 設計におけるトップダウン・ボトムアップといった「考える順番」が想像できないのです。 設計開発において、トップダウン・ボトムアップは相互に行き来する事は、分かっていますが、基本的方向性のようなものを知りたいです。
Claude回答
クリーンアーキテクチャにおける設計の進め方について、実践的な例で説明させていただきます。
基本的な方向性は「内側から外側へ」(ビジネス中心のトップダウン)ですが、実際の設計プロセスを見てみましょう。
例として、「注文管理システム」の設計プロセスを段階的に示します:
xxxxxxxxxx
// Step 1: まずビジネスの核となるEntitiesを定義
public class Order
{
public OrderId Id { get; }
public CustomerId CustomerId { get; }
public Money TotalAmount { get; private set; }
public OrderStatus Status { get; private set; }
// ビジネスルール:注文の状態遷移
public void Submit()
{
if (Status != OrderStatus.Draft)
throw new InvalidOperationException("注文は下書き状態でのみ提出可能です");
Status = OrderStatus.Submitted;
}
public void Approve()
{
if (Status != OrderStatus.Submitted)
throw new InvalidOperationException("提出済の注文のみ承認可能です");
if (TotalAmount > Money.FromYen(1000000))
throw new InvalidOperationException("100万円以上の注文は上級承認が必要です");
Status = OrderStatus.Approved;
}
}
// Step 2: Use Cases(アプリケーションのユースケース)を定義
public interface IOrderRepository
{
Task<Order> GetByIdAsync(OrderId id);
Task SaveAsync(Order order);
}
public class SubmitOrderUseCase
{
private readonly IOrderRepository _orderRepository;
public SubmitOrderUseCase(IOrderRepository orderRepository)
{
_orderRepository = orderRepository;
}
public async Task ExecuteAsync(OrderId orderId)
{
var order = await _orderRepository.GetByIdAsync(orderId);
order.Submit();
await _orderRepository.SaveAsync(order);
}
}
// Step 3: Interface Adapters(APIコントローラーやリポジトリの実装)
public class OrderController
{
private readonly SubmitOrderUseCase _submitOrderUseCase;
public async Task<IActionResult> SubmitOrder(string orderId)
{
await _submitOrderUseCase.ExecuteAsync(new OrderId(orderId));
return Ok();
}
}
// Step 4: 最後にFrameworks & Drivers(具体的なインフラ実装)
public class SqlOrderRepository : IOrderRepository
{
private readonly DbContext _context;
public async Task<Order> GetByIdAsync(OrderId id)
{
var orderEntity = await _context.Orders.FindAsync(id.Value);
return MapToDomain(orderEntity);
}
}
この設計プロセスの特徴は:
設計の出発点:ドメインモデリング
ビジネスの専門家との対話から始まる
データベースやUIを考える前に、業務の本質を理解し形式化
x
// まずはビジネスルールを純粋な形で表現
public class Order
{
public bool CanBeApproved()
{
return Status == OrderStatus.Submitted
&& TotalAmount <= Money.FromYen(1000000);
}
}
インターフェースの定義による境界の確立
xxxxxxxxxx
// 内側の層が必要とするインターフェースを定義
public interface IOrderRepository
{
// 内側から見て必要な操作のみを定義
Task<Order> GetByIdAsync(OrderId id);
}
外側の層は内側の要求に応える形で実装
xxxxxxxxxx
// インターフェースの具体的な実装は後から決定
public class SqlOrderRepository : IOrderRepository
{
// 内側の層が定義したインターフェースを実装
}
実際の設計プロセスでは:
初期フェーズ
ドメインエキスパートとの対話
ユースケースの洗い出し
中心となるビジネスルールの特定
反復的な設計プロセス
内側から外側へと設計を進める
必要に応じて内側の層を見直す
テストを書きながら設計を検証
技術的な実装の決定
ビジネスロジックが固まってから
外部インターフェースの具体的な実装を決定
この方式のメリット:
ビジネスロジックが技術的な制約に影響されない
要件の変更に強い
テストが書きやすい
技術の変更が容易
ただし、注意点として:
完全な一方向の設計は現実的ではない
ある程度の行き来は必要
プロトタイプを作りながら検証も重要
チームの習熟が必要
従来の設計手法とは異なるアプローチ
移行には学習期間が必要
プロジェクトの特性による適用度の調整
小規模プロジェクトでは簡略化も可能
ビジネスロジックの複雑さに応じて調整
このように、基本的な方向性は「内側から外側へ」ですが、実践では反復的かつ柔軟なアプローチが求められます。
質問5
従来手法であれば、まずデータ設計やアーキテクチャ設計などデバイスの「扱い安さ」やデバイスの性能を最大化する事を念頭に設計していました。 クリーンアーキテクチャの設計をするときに、4つの層を区別する「基準」や「概念」を教えてください。 概念的に何を「拠り所」にして考えれば良いのか想像できません。
Claude回答
クリーンアーキテクチャにおける4つの層を区別する基準について、具体例を交えて説明させていただきます。
各層を区別する主な「拠り所」は、「変更の理由」です。つまり、「なぜその部分が変更されるのか」という変更の動機に基づいて層を分けます。
xxxxxxxxxx
// 1. Entities層
// 変更理由:ビジネスそのものの規則が変わる場合
public class Product
{
public ProductId Id { get; }
public Money Price { get; private set; }
public int StockQuantity { get; private set; }
public bool CanPurchase(int quantity)
{
// このルールが変更されるのは、ビジネスの基本方針が変わる時のみ
return StockQuantity >= quantity;
}
public void DeductStock(int quantity)
{
if (!CanPurchase(quantity))
throw new DomainException("在庫不足です");
StockQuantity -= quantity;
}
}
// 2. Use Cases層
// 変更理由:業務フローや操作の手順が変わる場合
public class PurchaseProductUseCase
{
private readonly IProductRepository _productRepository;
private readonly IPaymentGateway _paymentGateway;
private readonly IOrderRepository _orderRepository;
public async Task<PurchaseResult> ExecuteAsync(PurchaseCommand command)
{
// この処理順序が変更されるのは、業務フローが変わる時
var product = await _productRepository.GetByIdAsync(command.ProductId);
if (!product.CanPurchase(command.Quantity))
throw new ApplicationException("購入できません");
// 支払い処理
await _paymentGateway.ProcessPaymentAsync(command.PaymentInfo);
// 在庫処理
product.DeductStock(command.Quantity);
await _productRepository.SaveAsync(product);
// 注文作成
var order = new Order(command.UserId, product, command.Quantity);
await _orderRepository.SaveAsync(order);
return new PurchaseResult(order.Id);
}
}
// 3. Interface Adapters層
// 変更理由:データの表現形式や外部とのやり取りの方法が変わる場合
public class ProductController
{
private readonly PurchaseProductUseCase _purchaseUseCase;
[HttpPost("products/{productId}/purchase")]
public async Task<IActionResult> Purchase(
string productId,
PurchaseRequestDto request)
{
// DTOからドメインの命令に変換する部分
// UIやAPIの形式が変わった時に、この部分が変更される
var command = new PurchaseCommand(
new ProductId(productId),
request.Quantity,
new UserId(request.UserId),
MapToPaymentInfo(request.Payment)
);
var result = await _purchaseUseCase.ExecuteAsync(command);
return Ok(new PurchaseResponseDto(result));
}
}
// 4. Frameworks & Drivers層
// 変更理由:技術的な実装の詳細が変わる場合
public class SqlProductRepository : IProductRepository
{
private readonly DbContext _context;
public async Task<Product> GetByIdAsync(ProductId id)
{
// データベースの技術や構造が変わった時に、
// この実装部分が変更される
var entity = await _context.Products
.FirstOrDefaultAsync(p => p.Id == id.Value);
return entity != null
? MapToDomain(entity)
: throw new ProductNotFoundException(id);
}
}
各層の区別の基準は:
Entities層(最内層)
「ビジネスそのものの規則」を表現
変更の理由:ビジネスの本質的なルールが変わる時のみ
xxxxxxxxxx
// 例:商品の在庫チェックルール
public bool CanPurchase(int quantity)
{
// このルールが変わるのは、ビジネスの方針変更時のみ
return StockQuantity >= quantity;
}
Use Cases層
「特定のユースケースにおける業務フロー」を表現
変更の理由:業務の進め方や手順が変わる時
xxxxxxxxxx
// 例:商品購入の手順
public async Task ExecutePurchaseAsync()
{
// この処理順序が変わるのは、業務フローの変更時
await ValidateStock();
await ProcessPayment();
await CreateOrder();
}
Interface Adapters層
「外部とのデータのやり取りの方法」を表現
変更の理由:データの表現形式やUIが変わる時
xxxxxxxxxx
// 例:APIリクエストの処理
public async Task<IActionResult> Purchase(PurchaseRequestDto request)
{
// UIやAPIの形式が変わった時に、この変換部分が変更される
var command = MapToCommand(request);
var result = await _useCase.ExecuteAsync(command);
return Ok(MapToResponse(result));
}
Frameworks & Drivers層(最外層)
「技術的な実装の詳細」を表現
変更の理由:使用する技術やフレームワークが変わる時
xxxxxxxxxx
// 例:データベースアクセス
public async Task SaveAsync(Product product)
{
// DBMSが変わった時に、この実装部分が変更される
var entity = MapToEntity(product);
_context.Products.Add(entity);
await _context.SaveChangesAsync();
}
層を分ける際の重要な考え方:
変更の波及効果を考える
内側の層の変更は外側に影響する
外側の層の変更は内側に影響しない
xxxxxxxxxx
// 良い例:外側の変更が内側に影響しない
public interface IProductRepository
{
// 内側から見た必要な操作のみを定義
Task<Product> GetByIdAsync(ProductId id);
}
依存関係の方向を意識する
内側の層は外側の層を知らない
外側の層は内側の層に依存する
xxxxxxxxxx
// 良い例:Use Cases層はEntities層のみを知っている
public class OrderUseCase
{
// Entities層の Product を使用
private readonly IProductRepository _repository;
}
境界での型変換を意識する
層をまたぐときはデータ形式を変換する
各層は自身の型を持つ
xxxxxxxxxx
// Interface Adapters層でのデータ変換
public ProductDto MapToDto(Product product)
{
return new ProductDto
{
Id = product.Id.Value,
Price = product.Price.Amount
};
}
この「変更の理由」という視点を持つことで:
各コードがどの層に属するべきかが明確になる
適切な抽象化のレベルを決定できる
変更の影響範囲を予測しやすくなる
これにより、ソフトウェアの保守性と拡張性が向上します。
質問6
Interface Adapters層の視点で見ると、DBMSもOS等のAPIも、外部のWeb-APIも皆「外部」という解釈になるのですか。 Frameworks & Drivers層の対象は、具体的なDBMSやネットワークライブラリやフレームワークを対象に設計するということですか。 Frameworks & Drivers層で具体的なOSやネットワークやミドルウェアやフレームワークに対応し、Interface Adapters層は、そういった具体的な「実体」の存在を抽象化して、意識しなくて良いようにする役割の層という認識で良いでしょうか。
Claude回答
はい、その理解は正確です! 具体例を用いて、Interface Adapters層とFrameworks & Drivers層の関係性をより詳しく説明させていただきます。
xxxxxxxxxx
// Interface Adapters層
// 外部システムとの通信を抽象化したインターフェース
public interface IExternalSystemGateway