いつもは「吹き出し」で質問と回答を記述していますが、今回は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
{
Task<PaymentResult> ProcessPaymentAsync(PaymentInfo payment);
}
public interface IStorageGateway
{
Task<Stream> GetFileAsync(string key);
Task SaveFileAsync(string key, Stream content);
}
public interface IMessageQueueGateway
{
Task PublishAsync<T>(string topic, T message);
}
// Interface Adapters層のRepository
public interface IOrderRepository
{
Task<Order> GetByIdAsync(OrderId id);
Task SaveAsync(Order order);
}
// Frameworks & Drivers層
// 具体的な実装
public class SqlServerOrderRepository : IOrderRepository
{
private readonly SqlConnection _connection;
public async Task<Order> GetByIdAsync(OrderId id)
{
// SQL Server固有のコード
using var command = new SqlCommand(
"SELECT * FROM Orders WHERE Id = @Id",
_connection
);
command.Parameters.AddWithValue("@Id", id.Value);
// ...
}
}
public class PostgresOrderRepository : IOrderRepository
{
private readonly NpgsqlConnection _connection;
// PostgreSQL固有の実装
}
public class StripePaymentGateway : IExternalSystemGateway
{
private readonly StripeClient _client;
public async Task<PaymentResult> ProcessPaymentAsync(PaymentInfo payment)
{
// Stripe APIに特化した実装
var options = new PaymentIntentCreateOptions
{
Amount = payment.Amount.Value,
Currency = payment.Currency.Code,
};
var service = new PaymentIntentService(_client);
var intent = await service.CreateAsync(options);
// ...
}
}
public class AzureBlobStorageGateway : IStorageGateway
{
private readonly BlobServiceClient _client;
public async Task<Stream> GetFileAsync(string key)
{
// Azure Blob Storage固有の実装
var container = _client.GetContainerClient("files");
var blob = container.GetBlobClient(key);
return await blob.OpenReadAsync();
}
}
public class AWSStorageGateway : IStorageGateway
{
private readonly AmazonS3Client _client;
// AWS S3固有の実装
}
public class KafkaMessageQueue : IMessageQueueGateway
{
private readonly IProducer<string, byte[]> _producer;
public async Task PublishAsync<T>(string topic, T message)
{
// Apache Kafka固有の実装
var serialized = JsonSerializer.SerializeToUtf8Bytes(message);
await _producer.ProduceAsync(topic, new Message<string, byte[]>
{
Key = Guid.NewGuid().ToString(),
Value = serialized
});
}
}
// Use Cases層(参考:これらのゲートウェイを利用する側)
public class ProcessOrderUseCase
{
private readonly IOrderRepository _orderRepo;
private readonly IExternalSystemGateway _paymentGateway;
private readonly IStorageGateway _storageGateway;
private readonly IMessageQueueGateway _messageQueue;
public async Task ExecuteAsync(OrderId orderId)
{
// 具体的な実装を意識せずに利用可能
var order = await _orderRepo.GetByIdAsync(orderId);
var paymentResult = await _paymentGateway.ProcessPaymentAsync(order.PaymentInfo);
await _messageQueue.PublishAsync("order-processed", new { OrderId = orderId });
}
}
この構造の重要なポイントは:
Interface Adapters層の役割
すべての外部システムを抽象化したインターフェースを定義
具体的な技術の選択を隠蔽
ビジネスロジックに必要な操作のみを定義
Frameworks & Drivers層の役割
具体的な技術やフレームワークに依存する実装を提供
実際のAPI呼び出しやデータベース操作を行う
外部システム固有のエラーハンドリングを行う
境界での変換
xxxxxxxxxx
// Interface Adapters層
public interface IExternalSystemGateway
{
// ビジネスドメインの型を使用
Task<PaymentResult> ProcessPaymentAsync(PaymentInfo payment);
}
// Frameworks & Drivers層
public class StripePaymentGateway : IExternalSystemGateway
{
public async Task<PaymentResult> ProcessPaymentAsync(PaymentInfo payment)
{
// ドメインの型 → Stripe固有の型に変換
var stripePayment = new StripePayment
{
Amount = payment.Amount.Value,
Currency = payment.Currency.Code
};
// Stripe APIを呼び出し
var result = await _stripeClient.CreatePaymentAsync(stripePayment);
// Stripe固有の型 → ドメインの型に変換して返却
return new PaymentResult(result.Id, result.Status);
}
}
外部システムの種類による分類
Interface Adapters層で抽象化される主な外部システム:
データベース
Web API(自社・他社問わず)
メッセージキュー
ファイルストレージ
キャッシュシステム
メール送信サービス
認証サービス
等…
Frameworks & Drivers層での具体的な実装例:
データベース: SQL Server, PostgreSQL, MongoDB
ストレージ: AWS S3, Azure Blob Storage, GCS
メッセージキュー: Kafka, RabbitMQ, Azure Service Bus
Web API: REST, gRPC, GraphQL
認証: OAuth, OIDC, Active Directory
利点
外部システムの変更が容易
テストが容易(モック化が簡単)
ビジネスロジックの純粋性を保持
新しい技術への対応が容易
実装の考え方
xxxxxxxxxx
// 良い例:技術の詳細を完全に隠蔽
public interface IStorageGateway
{
Task<Stream> GetFileAsync(string key);
}
// 悪い例:特定の技術の詳細が漏れている
public interface IStorageGateway
{
Task<Stream> GetBlobAsync(string container, string blob); // Azure固有の概念
}
このように、Interface Adapters層は「何をするか」を定義し、Frameworks & Drivers層は「どのように実現するか」を実装する、という明確な役割分担があります。
質問7
Entities層は「ビジネスそのものの規則」を表現し、 Use Cases層は「特定のユースケースにおける業務フロー」を表現するということですが、 両者を区別する基準がイマイチよく分かりません。 例えば、在庫管理システムの場合、倉庫・物品・物品数・棚・輸送担当者・トラックなどのオブジェクトと、入庫・出庫・在庫問い合わせ・輸送などの操作があります。 これらは、Entities層とUse Cases層に設計するとき、それぞれどのように分類すれば良いのですか。
Claude回答
なるほど、Entities層とUse Cases層の区別について、在庫管理システムを例に具体的に説明させていただきます。
基本的な区別の基準は:
Entities層:「それ自体の規則」(不変の業務規則)
Use Cases層:「それらを組み合わせた操作の流れ」(具体的なビジネスケース)
xxxxxxxxxx
// Entities層:基本的なビジネスルールを持つオブジェクト
public class Item
{
public ItemId Id { get; }
public string Name { get; }
public Weight Weight { get; }
public Dimensions Dimensions { get; }
public Temperature RequiredTemperature { get; }
// アイテム自体の基本的なルール
public bool RequiresRefrigeration =>
RequiredTemperature.Celsius < 10;
}
public class Stock
{
public ItemId ItemId { get; }
public Quantity CurrentQuantity { get; private set; }
public Quantity MinimumQuantity { get; }
public Quantity MaximumQuantity { get; }
// 在庫自体の基本的なルール
public bool IsLow => CurrentQuantity < MinimumQuantity;
public bool CanAdd(Quantity quantity) =>
CurrentQuantity + quantity <= MaximumQuantity;
// 在庫操作の基本的なルール
public void Add(Quantity quantity)
{
if (!CanAdd(quantity))
throw new DomainException("最大在庫数を超えています");
CurrentQuantity += quantity;
}
public void Remove(Quantity quantity)
{
if (quantity > CurrentQuantity)
throw new DomainException("在庫不足です");
CurrentQuantity -= quantity;
}
}
public class Warehouse
{
public WarehouseId Id { get; }
public Temperature CurrentTemperature { get; }
public IReadOnlyList<StorageLocation> Locations { get; }
// 倉庫自体の基本的なルール
public bool CanStore(Item item)
{
return !item.RequiresRefrigeration ||
CurrentTemperature <= item.RequiredTemperature;
}
public StorageLocation FindSuitableLocation(Item item, Quantity quantity)
{
return Locations.FirstOrDefault(l =>
l.CanAccommodate(item.Dimensions, quantity));
}
}
public class StorageLocation
{
public LocationId Id { get; }
public Dimensions AvailableSpace { get; private set; }
// 保管場所自体の基本的なルール
public bool CanAccommodate(Dimensions itemDimensions, Quantity quantity)
{
var requiredSpace = itemDimensions * quantity.Value;
return AvailableSpace >= requiredSpace;
}
}
public class TransportVehicle
{
public VehicleId Id { get; }
public VehicleType Type { get; }
public Weight MaximumLoad { get; }
public bool HasRefrigeration { get; }
// 車両自体の基本的なルール
public bool CanTransport(Item item, Quantity quantity)
{
if (item.RequiresRefrigeration && !HasRefrigeration)
return false;
var totalWeight = item.Weight * quantity.Value;
return totalWeight <= MaximumLoad;
}
}
// Use Cases層:具体的な業務フローを表現
public class ReceiveItemsUseCase
{
private readonly IWarehouseRepository _warehouseRepo;
private readonly IStockRepository _stockRepo;
private readonly IItemRepository _itemRepo;
public async Task ExecuteAsync(ReceiveItemsCommand command)
{
// 業務フロー:入庫処理
var warehouse = await _warehouseRepo.GetByIdAsync(command.WarehouseId);
var item = await _itemRepo.GetByIdAsync(command.ItemId);
var stock = await _stockRepo.GetByItemIdAsync(command.ItemId);
// 1. 倉庫が商品を保管できるか確認
if (!warehouse.CanStore(item))
throw new ApplicationException("この倉庫では保管できません");
// 2. 適切な保管場所を検索
var location = warehouse.FindSuitableLocation(item, command.Quantity);
if (location == null)
throw new ApplicationException("適切な保管場所がありません");
// 3. 在庫を更新
stock.Add(command.Quantity);
// 4. 保管場所を記録
await _stockRepo.SaveAsync(stock);
await _warehouseRepo.SaveAsync(warehouse);
}
}
public class TransferItemsUseCase
{
private readonly IWarehouseRepository _warehouseRepo;
private readonly IStockRepository _stockRepo;
private readonly IVehicleRepository _vehicleRepo;
private readonly ITransportationService _transportService;
public async Task ExecuteAsync(TransferItemsCommand command)
{
// 業務フロー:倉庫間移動
var sourceWarehouse = await _warehouseRepo.GetByIdAsync(command.SourceWarehouseId);
var targetWarehouse = await _warehouseRepo.GetByIdAsync(command.TargetWarehouseId);
var item = await _itemRepo.GetByIdAsync(command.ItemId);
var stock = await _stockRepo.GetByItemIdAsync(command.ItemId);
var vehicle = await _vehicleRepo.GetByIdAsync(command.VehicleId);
// 1. 在庫確認
if (!stock.CanRemove(command.Quantity))
throw new ApplicationException("在庫が不足しています");
// 2. 輸送可能か確認
if (!vehicle.CanTransport(item, command.Quantity))
throw new ApplicationException("この車両では輸送できません");
// 3. 移動先の保管場所確認
if (!targetWarehouse.CanStore(item))
throw new ApplicationException("移動先倉庫では保管できません");
var targetLocation = targetWarehouse.FindSuitableLocation(item, command.Quantity);
if (targetLocation == null)
throw new ApplicationException("移動先に適切な保管場所がありません");
// 4. 輸送指示を作成
var transport = new TransportOrder(
vehicle,
sourceWarehouse,
targetWarehouse,
item,
command.Quantity
);
// 5. 輸送を開始
await _transportService.StartTransportAsync(transport);
}
}
この例から、区別のポイントを整理すると:
Entities層に含めるもの:
「もの」自体が持つべき性質や規則
xxxxxxxxxx
// 例:アイテムが冷蔵必要かどうか
public bool RequiresRefrigeration => RequiredTemperature.Celsius < 10;
// 例:在庫が追加可能かどうか
public bool CanAdd(Quantity quantity) =>
CurrentQuantity + quantity <= MaximumQuantity;
Use Cases層に含めるもの:
複数のEntitiesを組み合わせた操作の流れ
特定のビジネスケースにおける手順
xxxxxxxxxx
// 例:入庫処理の一連の流れ
public async Task ExecuteReceiveAsync()
{
// 1. 倉庫の確認
// 2. 保管場所の検索
// 3. 在庫の更新
// 4. 記録の更新
}
重要な区別のポイント:
a. 変更の理由が異なる
Entities層:ビジネスの基本ルールが変わる時
Use Cases層:業務の進め方が変わる時
b. スコープが異なる
Entities層:単一のオブジェクトの範囲
Use Cases層:複数のオブジェクトを跨ぐ処理
c. 依存関係
Entities層:他のオブジェクトをほとんど知らない
Use Cases層:必要なEntitiesを組み合わせて利用
実装上の違い:
Entities層:
xxxxxxxxxx
public class Stock
{
// 単一の責任:在庫数の管理
public void Add(Quantity quantity)
{
if (!CanAdd(quantity))
throw new DomainException("最大在庫数超過");
CurrentQuantity += quantity;
}
}
Use Cases層:
xxxxxxxxxx
public class TransferItemsUseCase
{
// 複数のEntitiesを調整:在庫移動の全体フロー
public async Task ExecuteAsync()
{
var stock = await GetStock();
var vehicle = await GetVehicle();
var warehouse = await GetWarehouse();
// 複数のチェックと操作を組み合わせる
ValidateTransfer(stock, vehicle, warehouse);
await ProcessTransfer(stock, vehicle, warehouse);
}
}
判断に迷う場合のガイドライン:
その規則は単一のオブジェクトで完結するか? → Entities層
複数のオブジェクトの協調が必要か? → Use Cases層
その操作は常に同じ方法で行われるか? → Entities層
状況によって手順が変わる可能性があるか? → Use Cases層
このように、「個々のオブジェクトの本質的な規則」と「それらを組み合わせた具体的な業務フロー」という視点で区別することで、より明確な設計が可能になります。
質問8
クリーンアーキテクチャの概要は、理解できた気がします。 説明を聞く限り、クリーンアーキテクチャを採用するのに相応しいソフトウェアは、業務システムになると思います。 逆に、人間の業務が特に定まっていない、汎用ソフトウェアにはクリーンアーキテクチャによる設計は相応しくないように思えますが、この認識は妥当でしょうか。
Claude回答
はい、その認識は的確です。クリーンアーキテクチャの適性について、具体例を交えて分析してみましょう。
クリーンアーキテクチャに適したシステム:
業務システム
xxxxxxxxxx
// 明確なビジネスルールを持つ例:販売管理システム
public class Order
{
public bool CanApprove()
{
// 明確な業務ルール
return Amount <= ApprovalLimit &&
Status == OrderStatus.Submitted &&
!RequiresSpecialApproval;
}
}
金融システム
xxxxxxxxxx
// 厳格なルールと手順を持つ例:決済システム
public class Transaction
{
// 明確な業務ルール
public bool IsValid()
{
return Amount > 0 &&
Balance >= Amount &&
!IsAccountFrozen;
}
}
在庫管理システム
xxxxxxxxxx
// 明確な業務フローを持つ例
public class InventoryUseCase
{
public async Task ProcessShipment()
{
// 定型的な業務フロー
await ValidateInventory();
await UpdateStock();
await CreateShippingOrder();
await NotifyWarehouse();
}
}
クリーンアーキテクチャが相対的に適さないシステム:
汎用ツール
xxxxxxxxxx
// 例:テキストエディタ
public class TextEditor
{
// ビジネスルールというより、
// 技術的な機能の実装が中心
public void InsertText(string text, int position)
{
// UIやファイルシステムと密接に結合
buffer.Insert(position, text);
updateDisplay();
saveUndoState();
}
}
システムユーティリティ
xxxxxxxxxx
// 例:ファイル圧縮ツール
public class Compressor
{
// 技術的な処理が主体
public void CompressFile(string input, string output)
{
using var stream = File.OpenRead(input);
using var zip = CreateZipStream(output);
// 技術的な処理が中心
}
}
ゲーム
xxxxxxxxxx
// 例:アクションゲーム
public class GameEngine
{
// ビジネスルールより、
// パフォーマンスや描画が重要
public void Update()
{
ProcessInput();
UpdatePhysics();
RenderFrame();
}
}
適性を判断する基準:
ビジネスルールの存在
適している:明確な業務規則がある
xxxxxxxxxx
// 例:保険料計算システム
public decimal CalculatePremium()
{
// 明確なビジネスルール
if (Age < 25) return BaseRate * 1.5m;
if (HasClaims) return BaseRate * 1.3m;
return BaseRate;
}
適さない:技術的な処理が主体
xxxxxxxxxx
// 例:画像処理ソフト
public void ApplyFilter(Image image)
{
// 技術的な処理が中心
foreach (var pixel in image.Pixels)
{
pixel.ApplyConvolution(kernel);
}
}
変更の性質
適している:ビジネスルールの変更が主
xxxxxxxxxx
// 例:与信管理システム
public bool IsEligible()
{
// ビジネスルールは頻繁に変更
return CreditScore >= MinimumScore &&
MonthlyIncome >= RequiredIncome &&
DebtRatio <= MaxDebtRatio;
}
適さない:技術要件の変更が主
xxxxxxxxxx
// 例:ネットワークモニタリングツール
public void MonitorNetwork()
{
// 技術的な要件が変更の中心
foreach (var packet in CapturePackets())
{
AnalyzeProtocol(packet);
UpdateStatistics();
}
}
アーキテクチャの複雑さと利点のバランス
適している場合:
xxxxxxxxxx
// 例:請求書発行システム
public class InvoiceGenerator
{
// 複雑なビジネスルールと外部システム連携
public async Task GenerateInvoice()
{
var order = await _orderRepository.GetOrder();
var pricing = await _pricingService.Calculate();
var tax = await _taxService.Calculate();
// 複雑なルールの分離が有効
if (await _approvalService.RequiresApproval())
{
await _workflowService.InitiateApproval();
}
}
}
適さない場合:
xxxxxxxxxx
// 例:単純なファイルビューア
public class FileViewer
{
// 単純な処理には過剰な可能性
public void DisplayFile(string path)
{
var content = File.ReadAllText(path);
_display.Show(content);
}
}
結論として:
クリーンアーキテクチャは、ビジネスロジックが中心で、長期的な保守が必要なシステムに適しています
一方、技術的な処理が主体で、ビジネスルールがないまたは単純なシステムでは、過剰な設計になる可能性があります
適用を検討する際は、システムの性質と保守の必要性のバランスを考慮することが重要です
このように、クリーンアーキテクチャの採用は、システムの性質や要件に応じて慎重に判断する必要があります。
質問9
汎用ソフトウェアを開発するときは、このクリーンアーキテクチャのFrameworks & Drivers層に組み込みやすいように作ると良いと思います。 そのような考え方は、存在するのでしょうか。 それともクリーンアーキテクチャ設計と汎用ソフトウェア設計の設計手法は、完全に分離しているのでしょうか。
Claude回答
はい、その考え方は非常に重要で価値のある視点です! 実際、多くの有名な汎用ソフトウェアライブラリやフレームワークは、クリーンアーキテクチャのような階層化されたシステムに統合しやすいように設計されています。
このアプローチを具体的に見てみましょう:
xxxxxxxxxx
// 1. 抽象化された汎用インターフェース
// これにより、クリーンアーキテクチャのInterface Adapters層で容易に採用可能
public interface IStorageProvider
{
Task<Stream> GetAsync(string key);
Task SaveAsync(string key, Stream data);
Task DeleteAsync(string key);
}
// 2. 具体的な実装
// Frameworks & Drivers層で直接使用可能
public class FileSystemStorage : IStorageProvider
{
private readonly string _basePath;
private readonly ILogger _logger;
public FileSystemStorage(string basePath, ILogger logger = null)
{
_basePath = basePath;
_logger = logger ?? NullLogger.Instance;
}
public async Task<Stream> GetAsync(string key)
{
var path = Path.Combine(_basePath, key);
_logger.LogDebug("Reading file: {path}", path);
return File.OpenRead(path);
}
// 他のメソッドの実装...
}
// 3. 拡張性を考慮した設計
public class StorageProviderBuilder
{
private ILogger _logger = NullLogger.Instance;
private IEncryption _encryption;
private ICompression _compression;
public StorageProviderBuilder WithLogger(ILogger logger)
{
_logger = logger;
return this;
}
public StorageProviderBuilder WithEncryption(IEncryption encryption)
{
_encryption = encryption;
return this;
}
public StorageProviderBuilder WithCompression(ICompression compression)
{
_compression = compression;
return this;
}
public IStorageProvider Build(string providerType, string configuration)
{
// 基本実装の上に、要求された機能を装飾子パターンで追加
IStorageProvider baseProvider = providerType switch
{
"file" => new FileSystemStorage(configuration, _logger),
"memory" => new InMemoryStorage(_logger),
_ => throw new ArgumentException("Unknown provider type")
};
if (_encryption != null)
baseProvider = new EncryptedStorageDecorator(baseProvider, _encryption);
if (_compression != null)
baseProvider = new CompressedStorageDecorator(baseProvider, _compression);
return baseProvider;
}
}
// 4. クリーンアーキテクチャでの使用例
// Interface Adapters層
public interface IDocumentRepository
{
Task<Document> GetByIdAsync(DocumentId id);
Task SaveAsync(Document document);
}
// Frameworks & Drivers層
public class StorageBasedDocumentRepository : IDocumentRepository
{
private readonly IStorageProvider _storage;
private readonly ISerializer _serializer;
public StorageBasedDocumentRepository(
IStorageProvider storage,
ISerializer serializer)
{
_storage = storage;
_serializer = serializer;
}
public async Task<Document> GetByIdAsync(DocumentId id)
{
using var stream = await _storage.GetAsync(id.ToString());
return await _serializer.DeserializeAsync<Document>(stream);
}
}
// 5. 使用例
public class Example
{
public async Task DemonstrateUsage()
{
// 汎用ライブラリの構成
var storage = new StorageProviderBuilder()
.WithLogger(new ConsoleLogger())
.WithEncryption(new AesEncryption())
.WithCompression(new GzipCompression())
.Build("file", "C:/data");
// クリーンアーキテクチャでの利用
var documentRepo = new StorageBasedDocumentRepository(
storage,
new JsonSerializer());
// Use Casesでの利用
var useCase = new DocumentProcessingUseCase(documentRepo);
await useCase.ExecuteAsync();
}
}
このアプローチには以下のような特徴があります:
設計原則の共有
SOLID原則の適用
依存性の逆転
インターフェースによる抽象化
統合しやすい設計パターンの採用
xxxxxxxxxx
// 装飾子パターンによる機能拡張
public class EncryptedStorageDecorator : IStorageProvider
{
private readonly IStorageProvider _inner;
private readonly IEncryption _encryption;
public async Task<Stream> GetAsync(string key)
{
var encrypted = await _inner.GetAsync(key);
return _encryption.Decrypt(encrypted);
}
}
柔軟な構成オプション
xxxxxxxxxx
// ビルダーパターンによる柔軟な構成
var storage = new StorageProviderBuilder()
.WithLogger(logger)
.WithCompression(compression)
.Build();
明確な境界の定義
xxxxxxxxxx
// 汎用インターフェース
public interface IStorageProvider
{
Task<Stream> GetAsync(string key);
}
// 業務特化インターフェース
public interface IDocumentRepository
{
Task<Document> GetByIdAsync(DocumentId id);
}
このように、両者の設計は実際には密接に関連しており、以下のような利点があります:
再利用性の向上
クリーンアーキテクチャへの統合が容易
様々なコンテキストでの使用が可能
保守性の向上
明確な責任の分離
テストの容易さ
拡張性の確保
新機能の追加が容易
既存機能の修正が局所化
具体的な設計のガイドライン:
インターフェースの設計
汎用的で明確な契約
最小限の依存性
実装の分離
具体的な実装の詳細を隠蔽
設定可能な振る舞い
拡張ポイントの提供
プラグイン機構
カスタマイズ可能なコンポーネント
このように、汎用ソフトウェアとクリーンアーキテクチャは、相互に補完し合う関係にあると考えられます。適切に設計された汎用ソフトウェアは、クリーンアーキテクチャの実装をより容易にし、より堅牢なシステムの構築を可能にします。
質問10
クリーンアーキテクチャに合わせる為に、汎用ソフトウェア設計に必要な制約やルールや方法論などが、ありましたら教えてください。 無ければ、「無い」ということで良いです。
Claude回答
クリーンアーキテクチャとの統合を念頭に置いた汎用ソフトウェア設計には、いくつかの重要な設計原則や推奨事項があります:
抽象化レベルの分離
xxxxxxxxxx
// 良い例:異なる抽象化レベルを提供
public interface IStorageProvider // 高レベル抽象化
{
Task<Stream> GetAsync(string key);
Task SaveAsync(string key, Stream data);
}
public class FileSystemProvider : IStorageProvider // 低レベル実装
{
private readonly FileSystemOptions _options;
private readonly IFileSystem _fileSystem; // さらに低レベルの抽象化
// 実装の詳細...
}
// 悪い例:抽象化レベルが混在
public interface IStorageProvider
{
Task<Stream> GetAsync(string key);
Task<FileInfo> GetFileInfoAsync(string key); // ファイルシステム固有の概念が漏れている
Task SaveAsync(string key, Stream data, FileMode mode); // 低レベルの詳細が漏れている
}
依存性の方向制御
xxxxxxxxxx
// 良い例:プラグイン可能な設計
public interface IPlugin
{
string Name { get; }
void Initialize(IServiceProvider services);
}
public class PluginHost
{
public void LoadPlugin(IPlugin plugin)
{
// プラグインはホストに依存するが、
// ホストはプラグインの具体的な実装に依存しない
}
}
設定の分離
xxxxxxxxxx
// 良い例:設定を分離
public class StorageOptions
{
public string BasePath { get; set; }
public int BufferSize { get; set; }
public bool EnableCache { get; set; }
}
public class StorageProvider
{
public StorageProvider(StorageOptions options)
{
// オプションを使用して初期化
}
}
拡張ポイントの提供
xxxxxxxxxx
public interface IStorageDecorator
{
Task<Stream> ProcessRead(Stream originalStream);
Task<Stream> ProcessWrite(Stream originalStream);
}
public class StorageProvider
{
private readonly List<IStorageDecorator> _decorators = new();
public void AddDecorator(IStorageDecorator decorator)
{
_decorators.Add(decorator);
}
}
副作用の制御
xxxxxxxxxx
// 良い例:副作用を分離
public interface IStorageMetrics
{
void RecordOperation(string operation, long duration);
}
public class MetricsDecorator : IStorageProvider
{
private readonly IStorageProvider _inner;
private readonly IStorageMetrics _metrics;
public async Task<Stream> GetAsync(string key)
{
var sw = Stopwatch.StartNew();
try
{
return await _inner.GetAsync(key);
}
finally
{
_metrics.RecordOperation("get", sw.ElapsedMilliseconds);
}
}
}
ライフサイクルの管理
xxxxxxxxxx
public interface IInitializable
{
Task InitializeAsync();
}
public interface IDisposableAsync
{
Task DisposeAsync();
}
public class StorageProvider : IInitializable, IDisposableAsync
{
// ライフサイクルの明示的な管理
}
これらの原則は、クリーンアーキテクチャとの親和性を高めますが、厳密な「制約」や「ルール」というよりは、「推奨事項」として捉えるべきです。
プロジェクトの性質や要件に応じて、これらの原則の適用度合いを調整することが重要です。また、これらの原則を過度に適用すると、不必要な複雑さを招く可能性があることにも注意が必要です。
したがって、「クリーンアーキテクチャのために必ず従うべき厳格なルール」は特にありませんが、上記のような設計原則を意識することで、より統合しやすい汎用ソフトウェアを設計することが可能になります。