From 57110a79e8ab6e286a942ef4e019290dec8ab637 Mon Sep 17 00:00:00 2001 From: "Jerrett D. Davis" Date: Tue, 7 Oct 2025 00:14:30 -0500 Subject: [PATCH] feat(facade): implemented fluent facade pattern. --- README.md | 48 +- docs/index.md | 11 +- docs/patterns/structural/facade/facade.md | 416 ++++++++++++++++++ docs/patterns/toc.yml | 2 + .../Structural/Facade/Facade.cs | 239 ++++++++++ .../Structural/Facade/TypedFacade.cs | 295 +++++++++++++ .../FacadeDemo/FacadeDemo.cs | 318 +++++++++++++ .../FacadeDemo/FacadeDemoTests.cs | 194 ++++++++ .../Structural/Facade/FacadeTests.cs | 317 +++++++++++++ .../Structural/Facade/TypedFacadeTests.cs | 256 +++++++++++ 10 files changed, 2085 insertions(+), 11 deletions(-) create mode 100644 docs/patterns/structural/facade/facade.md create mode 100644 src/PatternKit.Core/Structural/Facade/Facade.cs create mode 100644 src/PatternKit.Core/Structural/Facade/TypedFacade.cs create mode 100644 src/PatternKit.Examples/FacadeDemo/FacadeDemo.cs create mode 100644 test/PatternKit.Examples.Tests/FacadeDemo/FacadeDemoTests.cs create mode 100644 test/PatternKit.Tests/Structural/Facade/FacadeTests.cs create mode 100644 test/PatternKit.Tests/Structural/Facade/TypedFacadeTests.cs diff --git a/README.md b/README.md index 877f74f..8f424f3 100644 --- a/README.md +++ b/README.md @@ -348,12 +348,50 @@ var validated = Decorator.Create(static x => 100 / x) var output = validated.Execute(5); // (100 / 5) + 5 = 25 ``` +### Facade (unified subsystem interface) +```csharp +using PatternKit.Structural.Facade; + +// Simplify complex e-commerce operations +public record OrderRequest(string ProductId, int Quantity, string CustomerEmail, decimal Price); +public record OrderResult(bool Success, string? OrderId = null, string? ErrorMessage = null); + +var orderFacade = Facade.Create() + .Operation("place-order", (in OrderRequest req) => { + // Coordinate inventory, payment, shipping, notifications + var reservationId = inventoryService.Reserve(req.ProductId, req.Quantity); + var txId = paymentService.Charge(req.Price * req.Quantity); + var shipmentId = shippingService.Schedule(req.CustomerEmail); + notificationService.SendConfirmation(req.CustomerEmail); + + return new OrderResult(true, OrderId: Guid.NewGuid().ToString()); + }) + .Operation("cancel-order", (in OrderRequest req) => { + inventoryService.Release(req.ProductId); + paymentService.Refund(req.ProductId); + return new OrderResult(true); + }) + .Default((in OrderRequest _) => new OrderResult(false, ErrorMessage: "Unknown operation")) + .Build(); + +// Simple client code - complex subsystem coordination hidden +var result = orderFacade.Execute("place-order", orderRequest); + +// Case-insensitive operations +var apiFacade = Facade.Create() + .OperationIgnoreCase("Status", (in string _) => "System OK") + .OperationIgnoreCase("Version", (in string _) => "v2.0") + .Build(); + +var status = apiFacade.Execute("STATUS", ""); // Works with any casing +``` + --- ## šŸ“š Patterns Table -| Category | Patterns āœ“ = implemented | -| -------------- |----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| **Creational** | [Factory](docs/patterns/creational/factory/factory.md) āœ“ • [Composer](docs/patterns/creational/builder/composer.md) āœ“ • [ChainBuilder](docs/patterns/creational/builder/chainbuilder.md) āœ“ • [BranchBuilder](docs/patterns/creational/builder/chainbuilder.md) āœ“ • [MutableBuilder](docs/patterns/creational/builder/mutablebuilder.md) āœ“ • [Prototype](docs/patterns/creational/prototype/prototype.md) āœ“ • [Singleton](docs/patterns/creational/singleton/singleton.md) āœ“ | -| **Structural** | [Adapter](docs/patterns/structural/adapter/fluent-adapter.md) āœ“ • [Bridge](docs/patterns/structural/bridge/bridge.md) āœ“ • [Composite](docs/patterns/structural/composite/composite.md) āœ“ • [Decorator](docs/patterns/structural/decorator/decorator.md) āœ“ • Facade (planned) • Flyweight (planned) • Proxy (planned) | -| **Behavioral** | [Strategy](docs/patterns/behavioral/strategy/strategy.md) āœ“ • [TryStrategy](docs/patterns/behavioral/strategy/trystrategy.md) āœ“ • [ActionStrategy](docs/patterns/behavioral/strategy/actionstrategy.md) āœ“ • [ActionChain](docs/patterns/behavioral/chain/actionchain.md) āœ“ • [ResultChain](docs/patterns/behavioral/chain/resultchain.md) āœ“ • [ReplayableSequence](docs/patterns/behavioral/iterator/replayablesequence.md) āœ“ • [WindowSequence](docs/patterns/behavioral/iterator/windowsequence.md) āœ“ • Command (planned) • Mediator (planned) • Memento (planned) • Observer (planned) • State (planned) • Template Method (planned) • Visitor (planned) | +| Category | Patterns āœ“ = implemented | +| -------------- |--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| **Creational** | [Factory](docs/patterns/creational/factory/factory.md) āœ“ • [Composer](docs/patterns/creational/builder/composer.md) āœ“ • [ChainBuilder](docs/patterns/creational/builder/chainbuilder.md) āœ“ • [BranchBuilder](docs/patterns/creational/builder/chainbuilder.md) āœ“ • [MutableBuilder](docs/patterns/creational/builder/mutablebuilder.md) āœ“ • [Prototype](docs/patterns/creational/prototype/prototype.md) āœ“ • [Singleton](docs/patterns/creational/singleton/singleton.md) āœ“ | +| **Structural** | [Adapter](docs/patterns/structural/adapter/fluent-adapter.md) āœ“ • [Bridge](docs/patterns/structural/bridge/bridge.md) āœ“ • [Composite](docs/patterns/structural/composite/composite.md) āœ“ • [Decorator](docs/patterns/structural/decorator/decorator.md) āœ“ • [Facade](docs/patterns/structural/facade/facade.md) āœ“ • Flyweight (planned) • Proxy (planned) | +| **Behavioral** | [Strategy](docs/patterns/behavioral/strategy/strategy.md) āœ“ • [TryStrategy](docs/patterns/behavioral/strategy/trystrategy.md) āœ“ • [ActionStrategy](docs/patterns/behavioral/strategy/actionstrategy.md) āœ“ • [ActionChain](docs/patterns/behavioral/chain/actionchain.md) āœ“ • [ResultChain](docs/patterns/behavioral/chain/resultchain.md) āœ“ • [ReplayableSequence](docs/patterns/behavioral/iterator/replayablesequence.md) āœ“ • [WindowSequence](docs/patterns/behavioral/iterator/windowsequence.md) āœ“ • [Command](docs/patterns/behavioral/command/command.md) āœ“ • [Mediator](docs/patterns/behavioral/mediator/mediator.md) āœ“ • Memento (planned) • Observer (planned) • State (planned) • Template Method (planned) • Visitor (planned) | diff --git a/docs/index.md b/docs/index.md index 79b2b0a..3ceb82c 100644 --- a/docs/index.md +++ b/docs/index.md @@ -62,11 +62,11 @@ if (parser.Execute("123", out var value)) PatternKit will grow to cover **Creational**, **Structural**, and **Behavioral** patterns with fluent, discoverable APIs: -| Category | Patterns āœ“ = implemented | -| -------------- |---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| **Creational** | [Factory](patterns/creational/factory/factory.md) āœ“ • [Composer](patterns/creational/builder/composer.md) āœ“ • [ChainBuilder](patterns/creational/builder/chainbuilder.md) āœ“ • [BranchBuilder](patterns/creational/builder/chainbuilder.md) āœ“ • [MutableBuilder](patterns/creational/builder/mutablebuilder.md) āœ“ • [Prototype](patterns/creational/prototype/prototype.md) āœ“ • [Singleton](patterns/creational/singleton/singleton.md) āœ“ | -| **Structural** | [Adapter](patterns/structural/adapter/fluent-adapter.md) āœ“ • [Bridge](patterns/structural/bridge/bridge.md) āœ“ • [Composite](patterns/structural/composite/composite.md) āœ“ • Decorator (planned) • Facade (planned) • Flyweight (planned) • Proxy (planned) | -| **Behavioral** | [Strategy](patterns/behavioral/strategy/strategy.md) āœ“ • [TryStrategy](patterns/behavioral/strategy/trystrategy.md) āœ“ • [ActionStrategy](patterns/behavioral/strategy/actionstrategy.md) āœ“ • [ActionChain](patterns/behavioral/chain/actionchain.md) āœ“ • [ResultChain](patterns/behavioral/chain/resultchain.md) āœ“ • [Command](patterns/behavioral/command/command.md) āœ“ • Iterator (planned) • [Mediator](behavioral/mediator/mediator.md) āœ“ • Memento (planned) • Observer (planned) • State (planned) • Template Method (planned) • Visitor (planned) | +| Category | Patterns āœ“ = implemented | +| -------------- |----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| **Creational** | [Factory](patterns/creational/factory/factory.md) āœ“ • [Composer](patterns/creational/builder/composer.md) āœ“ • [ChainBuilder](patterns/creational/builder/chainbuilder.md) āœ“ • [BranchBuilder](patterns/creational/builder/chainbuilder.md) āœ“ • [MutableBuilder](patterns/creational/builder/mutablebuilder.md) āœ“ • [Prototype](patterns/creational/prototype/prototype.md) āœ“ • [Singleton](patterns/creational/singleton/singleton.md) āœ“ | +| **Structural** | [Adapter](patterns/structural/adapter/fluent-adapter.md) āœ“ • [Bridge](patterns/structural/bridge/bridge.md) āœ“ • [Composite](patterns/structural/composite/composite.md) āœ“ • [Decorator](patterns/structural/decorator/index.md) āœ“ • [Facade](patterns/structural/facade/facade.md) āœ“ • Flyweight (planned) • Proxy (planned) | +| **Behavioral** | [Strategy](patterns/behavioral/strategy/strategy.md) āœ“ • [TryStrategy](patterns/behavioral/strategy/trystrategy.md) āœ“ • [ActionStrategy](patterns/behavioral/strategy/actionstrategy.md) āœ“ • [ActionChain](patterns/behavioral/chain/actionchain.md) āœ“ • [ResultChain](patterns/behavioral/chain/resultchain.md) āœ“ • [Command](patterns/behavioral/command/command.md) āœ“ • [ReplayableSequence](patterns/behavioral/iterator/replayablesequence.md) āœ“ • [WindowSequence](patterns/behavioral/iterator/windowsequence.md) āœ“ • [Mediator](behavioral/mediator/mediator.md) āœ“ • Memento (planned) • Observer (planned) • State (planned) • Template Method (planned) • Visitor (planned) | Each pattern will ship with: @@ -98,4 +98,3 @@ public class StrategyTests : TinyBddXunitBase } } ``` - diff --git a/docs/patterns/structural/facade/facade.md b/docs/patterns/structural/facade/facade.md new file mode 100644 index 0000000..ef7783b --- /dev/null +++ b/docs/patterns/structural/facade/facade.md @@ -0,0 +1,416 @@ +# Facade Pattern + +> **Fluent unified interface to complex subsystem operations** + +## Overview + +The **Facade** pattern provides a simplified, unified interface to a complex subsystem or set of interfaces. PatternKit's implementation offers a fluent, allocation-light way to coordinate multiple subsystem calls behind named operations: + +- **Named operations** that map to complex subsystem interactions +- **Default fallback** for unknown operations +- **Case-insensitive** operation matching support +- **Thread-safe** and reusable after building + +Facades decouple clients from subsystem complexity, making your APIs cleaner and easier to use while hiding coordination logic behind simple operation names. + +## Mental Model + +Think of a facade as a **hotel concierge**: +- Clients make **simple requests** ("book dinner reservation") +- The facade **coordinates** multiple subsystems (restaurant service, transportation, confirmation) +- Clients don't need to know **implementation details** of each subsystem + +``` +ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā” +│ Client │ +│ facade.Execute("process", order) │ +ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜ + │ +ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā–¼ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā” +│ Facade │ +│ Operation: "process" │ +│ ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā” │ +│ │ 1. inventoryService.Reserve() │ │ +│ │ 2. paymentService.Charge() │ │ +│ │ 3. shippingService.Schedule() │ │ +│ │ 4. notificationService.Send() │ │ +│ ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜ │ +ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜ +``` + +## Quick Start + +### Basic Facade + +```csharp +using PatternKit.Structural.Facade; + +// Simple calculator facade +var calc = Facade<(int a, int b), int>.Create() + .Operation("add", (in (int a, int b) input) => input.a + input.b) + .Operation("multiply", (in (int a, int b) input) => input.a * input.b) + .Operation("subtract", (in (int a, int b) input) => input.a - input.b) + .Build(); + +var sum = calc.Execute("add", (5, 3)); // 8 +var product = calc.Execute("multiply", (5, 3)); // 15 +``` + +### Coordinating Multiple Subsystems + +```csharp +// E-commerce order facade +public class OrderFacade +{ + private readonly InventoryService _inventory; + private readonly PaymentService _payment; + private readonly ShippingService _shipping; + private readonly NotificationService _notification; + + public Facade BuildFacade() + { + return Facade.Create() + .Operation("process", ProcessOrder) + .Operation("cancel", CancelOrder) + .Operation("refund", RefundOrder) + .Default((in OrderRequest req) => + new OrderResult { Status = "Unknown operation" }) + .Build(); + } + + private OrderResult ProcessOrder(in OrderRequest req) + { + // Coordinate multiple subsystems + var inventoryReserved = _inventory.Reserve(req.Items); + var paymentTx = _payment.Charge(req.PaymentMethod, req.Total); + var shipmentId = _shipping.Schedule(req.Address, req.Items); + _notification.SendConfirmation(req.CustomerId, shipmentId); + + return new OrderResult + { + Status = "Processed", + TransactionId = paymentTx, + ShipmentId = shipmentId + }; + } + + private OrderResult CancelOrder(in OrderRequest req) + { + _inventory.Release(req.OrderId); + _payment.Void(req.OrderId); + _shipping.Cancel(req.OrderId); + _notification.SendCancellation(req.CustomerId); + + return new OrderResult { Status = "Cancelled" }; + } + + private OrderResult RefundOrder(in OrderRequest req) + { + _shipping.InitiateReturn(req.OrderId); + _payment.Refund(req.OrderId); + _inventory.Restock(req.Items); + _notification.SendRefundConfirmation(req.CustomerId); + + return new OrderResult { Status = "Refunded" }; + } +} + +// Usage +var facade = new OrderFacade().BuildFacade(); +var result = facade.Execute("process", orderRequest); +``` + +### Default Fallback + +```csharp +// Facade with default operation for unknown commands +var api = Facade.Create() + .Operation("status", (in string _) => "System operational") + .Operation("version", (in string _) => "v2.1.0") + .Default((in string cmd) => $"Unknown command: {cmd}") + .Build(); + +var status = api.Execute("status", ""); // "System operational" +var unknown = api.Execute("help", ""); // "Unknown command: help" +``` + +### Case-Insensitive Operations + +```csharp +// Case-insensitive operation matching +var greetingFacade = Facade.Create() + .OperationIgnoreCase("Hello", (in string name) => $"Hello, {name}!") + .OperationIgnoreCase("Goodbye", (in string name) => $"Goodbye, {name}!") + .Build(); + +var greet1 = greetingFacade.Execute("hello", "Alice"); // "Hello, Alice!" +var greet2 = greetingFacade.Execute("HELLO", "Bob"); // "Hello, Bob!" +var greet3 = greetingFacade.Execute("HeLLo", "Carol"); // "Hello, Carol!" +``` + +## API Reference + +### Creating a Facade + +```csharp +public static Builder Create() +``` + +Creates a new builder for constructing a facade. + +### Registering Operations + +```csharp +public Builder Operation(string name, Operation handler) +``` + +Registers a named operation. Operation names are **case-sensitive**. + +**Parameters:** +- `name`: Unique operation name +- `handler`: Delegate `TOut Operation(in TIn input)` that coordinates subsystems + +**Throws:** +- `ArgumentException` if an operation with the same name already exists + +```csharp +public Builder OperationIgnoreCase(string name, Operation handler) +``` + +Registers a named operation with **case-insensitive** matching. + +āš ļø **Note**: Cannot mix case-sensitive and case-insensitive operations in the same facade. + +### Default Operation + +```csharp +public Builder Default(Operation handler) +``` + +Configures a default operation to invoke when the requested operation is not found. + +**Example:** +```csharp +var facade = Facade.Create() + .Operation("known", (in int x) => $"Result: {x}") + .Default((in int x) => "Operation not found") + .Build(); +``` + +### Building the Facade + +```csharp +public Facade Build() +``` + +Builds an immutable, thread-safe facade. + +**Throws:** +- `InvalidOperationException` if no operations and no default are configured + +### Executing Operations + +```csharp +public TOut Execute(string operationName, in TIn input) +``` + +Executes the named operation with the given input. + +**Throws:** +- `InvalidOperationException` if operation not found and no default configured + +**Example:** +```csharp +var result = facade.Execute("process", orderData); +``` + +```csharp +public bool TryExecute(string operationName, in TIn input, out TOut output) +``` + +Attempts to execute the named operation. Returns `false` if not found and no default exists. + +**Returns:** `true` if operation executed; `false` otherwise + +**Example:** +```csharp +if (facade.TryExecute("process", orderData, out var result)) +{ + Console.WriteLine($"Success: {result}"); +} +else +{ + Console.WriteLine("Operation not found"); +} +``` + +### Checking Operations + +```csharp +public bool HasOperation(string operationName) +``` + +Checks whether an operation with the given name is registered. + +**Example:** +```csharp +if (facade.HasOperation("process")) +{ + var result = facade.Execute("process", data); +} +``` + +## Use Cases + +### 1. Simplifying Complex APIs + +```csharp +// Before: Client must understand multiple services +var user = userService.GetUser(userId); +var permissions = permissionService.GetPermissions(user); +var roles = roleService.GetRoles(user); +var settings = settingsService.GetSettings(user); + +// After: Simple facade operation +var userContext = userFacade.Execute("loadContext", userId); +``` + +### 2. Legacy System Integration + +```csharp +// Hide legacy system complexity behind modern facade +var legacyFacade = Facade.Create() + .Operation("migrate", (in LegacyRequest req) => + { + // Complex legacy calls hidden here + var data = legacySystem.GetData(req.Id); + var transformed = transformer.Convert(data); + var validated = validator.Validate(transformed); + return new ModernResponse(validated); + }) + .Build(); +``` + +### 3. Microservices Coordination + +```csharp +// Coordinate multiple microservice calls +var checkoutFacade = Facade.Create() + .Operation("checkout", (in CheckoutRequest req) => + { + var cartItems = cartService.GetItems(req.CartId); + var prices = pricingService.Calculate(cartItems); + var taxInfo = taxService.CalculateTax(prices, req.ShippingAddress); + var payment = paymentService.Process(req.PaymentInfo, prices.Total + taxInfo.TaxAmount); + var order = orderService.Create(cartItems, payment); + + return new CheckoutResult + { + OrderId = order.Id, + Total = prices.Total + taxInfo.TaxAmount + }; + }) + .Build(); +``` + +### 4. Command Pattern Alternative + +```csharp +// Use facade as a lightweight command dispatcher +var commandFacade = Facade.Create() + .Operation("create-user", CreateUserHandler) + .Operation("delete-user", DeleteUserHandler) + .Operation("update-user", UpdateUserHandler) + .Default((in Command cmd) => new CommandResult { Success = false, Error = "Unknown command" }) + .Build(); + +var result = commandFacade.Execute("create-user", command); +``` + +## Performance Characteristics + +- **Allocation-light**: Uses arrays internally, minimal allocations after build +- **O(1) operation lookup**: Dictionary-based operation resolution +- **Thread-safe**: Immutable after building, safe for concurrent use +- **Zero boxing**: Uses `in` parameters for readonly references + +## Best Practices + +### āœ… Do + +- Use facades to **simplify complex subsystems** +- Group **related operations** in a single facade +- Make operations **stateless** (capture services in closures if needed) +- Use **descriptive operation names** +- Provide **default operations** for better error handling + +### āŒ Don't + +- Don't use facades for **simple 1-to-1 mappings** (use direct calls) +- Don't **mix unrelated operations** in one facade +- Don't make operations **stateful** (facades should be reusable) +- Don't use as a **god object** (keep facades focused) + +## Comparison with Other Patterns + +| Pattern | Purpose | When to Use | +|---------|---------|-------------| +| **Facade** | Simplify complex subsystem | Multiple services need coordination | +| **Adapter** | Convert one interface to another | Interface incompatibility | +| **Decorator** | Add behavior to objects | Enhance existing behavior | +| **Proxy** | Control access to objects | Lazy loading, access control | + +## Advanced Examples + +### Async Coordination (using Task) + +```csharp +// Facade coordinating async operations +var asyncFacade = Facade>.Create() + .Operation("fetch", async (in string url) => + { + var http = new HttpClient(); + var cache = new CacheService(); + + // Check cache first + if (cache.TryGet(url, out var cached)) + return cached; + + // Fetch and cache + var content = await http.GetStringAsync(url); + await cache.SetAsync(url, content); + return content; + }) + .Build(); + +var content = await facade.Execute("fetch", "https://api.example.com/data"); +``` + +### Error Handling + +```csharp +var robustFacade = Facade.Create() + .Operation("process", (in Request req) => + { + try + { + var step1 = service1.Process(req); + var step2 = service2.Process(step1); + var step3 = service3.Process(step2); + return new Result { Success = true, Data = step3 }; + } + catch (Exception ex) + { + logger.LogError(ex, "Process failed"); + return new Result { Success = false, Error = ex.Message }; + } + }) + .Build(); +``` + +## See Also + +- [Adapter Pattern](../adapter/adapter.md) - For interface conversion +- [Decorator Pattern](../decorator/decorator.md) - For behavior enhancement +- [Strategy Pattern](../../behavioral/strategy/strategy.md) - For algorithm selection + diff --git a/docs/patterns/toc.yml b/docs/patterns/toc.yml index 401267f..c18d0a5 100644 --- a/docs/patterns/toc.yml +++ b/docs/patterns/toc.yml @@ -67,3 +67,5 @@ href: structural/composite/composite.md - name: Decorator href: structural/decorator/decorator.md + - name: Facade + href: structural/facade/facade.md diff --git a/src/PatternKit.Core/Structural/Facade/Facade.cs b/src/PatternKit.Core/Structural/Facade/Facade.cs new file mode 100644 index 0000000..4b9fe77 --- /dev/null +++ b/src/PatternKit.Core/Structural/Facade/Facade.cs @@ -0,0 +1,239 @@ +namespace PatternKit.Structural.Facade; + +/// +/// Fluent, allocation-light facade that provides a simplified interface to complex subsystem operations. +/// Maps named operations to coordinated subsystem interactions. Build once, then call or +/// with an operation name. +/// +/// Input type for operations. +/// Output type produced by operations. +/// +/// +/// Mental model: A facade hides the complexity of multiple subsystems behind a simple, +/// unified interface. Instead of clients needing to understand and coordinate multiple subsystem calls, +/// they invoke a single named operation on the facade. +/// +/// +/// Use cases: +/// +/// Simplify complex library or framework APIs. +/// Coordinate multiple service calls into single operations. +/// Provide a cleaner API for legacy or third-party code. +/// Decouple clients from subsystem implementation details. +/// +/// +/// +/// Immutability: After , the facade is immutable and safe for concurrent reuse. +/// +/// +/// +/// +/// var orderFacade = Facade<OrderRequest, OrderResult>.Create() +/// .Operation("process", req => { +/// var inventory = inventoryService.Reserve(req.Items); +/// var payment = paymentService.Charge(req.Payment); +/// var shipment = shippingService.Schedule(req.Address); +/// return new OrderResult(inventory, payment, shipment); +/// }) +/// .Operation("cancel", req => { +/// inventoryService.Release(req.OrderId); +/// paymentService.Refund(req.OrderId); +/// return new OrderResult { Status = "Cancelled" }; +/// }) +/// .Default(req => new OrderResult { Status = "Unknown" }) +/// .Build(); +/// +/// var result = orderFacade.Execute("process", request); +/// +/// +public sealed class Facade +{ + /// + /// Delegate representing a facade operation that coordinates subsystem interactions. + /// + /// The input value (readonly via in). + /// The result after coordinating subsystem calls. + public delegate TOut Operation(in TIn input); + + private readonly Dictionary _operationIndices; + private readonly Operation[] _operations; + private readonly Operation? _default; + + private Facade(Dictionary operationIndices, Operation[] operations, Operation? defaultOp) + { + _operationIndices = operationIndices; + _operations = operations; + _default = defaultOp; + } + + /// + /// Executes the named operation with the given . + /// + /// The name of the operation to execute. + /// The input value (readonly via in). + /// The result of the operation. + /// Thrown when the operation is not found and no default is configured. + /// + /// If the operation name is not found, the default operation is invoked (if configured). + /// Otherwise, an exception is thrown. + /// + public TOut Execute(string operationName, in TIn input) + { + if (_operationIndices.TryGetValue(operationName, out var index)) + return _operations[index](in input); + + return _default is not null + ? _default(in input) + : throw new InvalidOperationException($"Operation '{operationName}' not found and no default configured."); + } + + /// + /// Attempts to execute the named operation. Returns false if the operation is not found + /// and no default is configured. + /// + /// The name of the operation to execute. + /// The input value (readonly via in). + /// The result of the operation, or default on failure. + /// true if the operation was found or a default exists; otherwise false. + public bool TryExecute(string operationName, in TIn input, out TOut output) + { + if (_operationIndices.TryGetValue(operationName, out var index)) + { + output = _operations[index](in input); + return true; + } + + if (_default is not null) + { + output = _default(in input); + return true; + } + + output = default!; + return false; + } + + /// + /// Checks whether an operation with the given name is registered. + /// + /// The operation name to check. + /// true if the operation exists; otherwise false. + public bool HasOperation(string operationName) + => _operationIndices.ContainsKey(operationName); + + /// + /// Creates a new for constructing a facade. + /// + /// A new instance. + /// + /// + /// var facade = Facade<int, string>.Create() + /// .Operation("double", x => (x * 2).ToString()) + /// .Operation("square", x => (x * x).ToString()) + /// .Build(); + /// + /// var result = facade.Execute("double", 5); // "10" + /// + /// + public static Builder Create() => new(); + + /// + /// Fluent builder for . + /// + public sealed class Builder + { + private readonly Dictionary _operationIndices; + private readonly List _operations = new(8); + private Operation? _default; + private StringComparer _comparer = StringComparer.Ordinal; + + internal Builder() + { + _operationIndices = new Dictionary(StringComparer.Ordinal); + } + + /// + /// Registers a named operation that coordinates subsystem interactions. + /// + /// The unique name for this operation. + /// The operation delegate. + /// This builder for chaining. + /// Thrown when an operation with the same name is already registered. + /// + /// Operation names are case-sensitive. If you need case-insensitive names, use . + /// + public Builder Operation(string name, Operation handler) + { + if (_operationIndices.ContainsKey(name)) + throw new ArgumentException($"Operation '{name}' is already registered.", nameof(name)); + + _operationIndices[name] = _operations.Count; + _operations.Add(handler); + return this; + } + + /// + /// Registers a named operation with case-insensitive matching. + /// + /// The unique name for this operation (case-insensitive). + /// The operation delegate. + /// This builder for chaining. + /// Thrown when an operation with the same name (ignoring case) is already registered. + public Builder OperationIgnoreCase(string name, Operation handler) + { + // Switch to case-insensitive comparer if this is the first case-insensitive operation + if (_comparer == StringComparer.Ordinal && _operations.Count == 0) + { + _comparer = StringComparer.OrdinalIgnoreCase; + var newDict = new Dictionary(StringComparer.OrdinalIgnoreCase); + foreach (var kvp in _operationIndices) + newDict[kvp.Key] = kvp.Value; + _operationIndices.Clear(); + foreach (var kvp in newDict) + _operationIndices[kvp.Key] = kvp.Value; + } + else if (_comparer == StringComparer.Ordinal) + { + throw new InvalidOperationException( + "Cannot mix case-sensitive and case-insensitive operations. Use OperationIgnoreCase for all operations or none."); + } + + if (_operationIndices.ContainsKey(name)) + throw new ArgumentException($"Operation '{name}' is already registered.", nameof(name)); + + _operationIndices[name] = _operations.Count; + _operations.Add(handler); + return this; + } + + /// + /// Configures a default operation to invoke when the requested operation is not found. + /// + /// The default operation delegate. + /// This builder for chaining. + /// + /// Only one default operation can be configured. Calling this multiple times will replace the previous default. + /// + public Builder Default(Operation handler) + { + _default = handler; + return this; + } + + /// + /// Builds an immutable with the registered operations. + /// + /// An immutable facade instance. + /// Thrown when no operations are registered. + public Facade Build() + { + if (_operations.Count == 0 && _default is null) + throw new InvalidOperationException("At least one operation or a default must be configured."); + + return new Facade( + new Dictionary(_operationIndices, _comparer), + _operations.ToArray(), + _default); + } + } +} diff --git a/src/PatternKit.Core/Structural/Facade/TypedFacade.cs b/src/PatternKit.Core/Structural/Facade/TypedFacade.cs new file mode 100644 index 0000000..b4ab1dc --- /dev/null +++ b/src/PatternKit.Core/Structural/Facade/TypedFacade.cs @@ -0,0 +1,295 @@ +using System.Linq.Expressions; +using System.Reflection; +using System.Runtime.ExceptionServices; + +namespace PatternKit.Structural.Facade; + +/// +/// Typed, compile-time safe facade that uses an interface contract to eliminate magic strings. +/// Provides a simpler alternative to string-based facades with full IntelliSense and refactoring support. +/// +/// The interface defining the facade's operations. +/// +/// +/// Usage Pattern: Define an interface for your facade contract, then use the fluent builder +/// to map each method to its implementation handler. +/// +/// +/// Advantages over string-based facade: +/// +/// Compile-time safety - typos caught at compile time. +/// IntelliSense support - IDE shows available operations. +/// Refactoring friendly - rename methods safely. +/// Type-safe parameters - method signatures enforce correct types. +/// +/// +/// +/// +/// +/// // 1. Define your interface +/// public interface ICalculator +/// { +/// int Add(int a, int b); +/// int Multiply(int a, int b); +/// } +/// +/// // 2. Build using fluent API +/// var calc = TypedFacade<ICalculator>.Create() +/// .Map(x => x.Add, (int a, int b) => a + b) +/// .Map(x => x.Multiply, (int a, int b) => a * b) +/// .Build(); +/// +/// var result = calc.Add(5, 3); // Type-safe: 8 +/// +/// +public static class TypedFacade + where TFacadeInterface : class +{ + /// + /// Creates a new builder for constructing a typed facade. + /// + /// A new instance. + /// Thrown if is not an interface. + public static Builder Create() + { + if (!typeof(TFacadeInterface).IsInterface) + throw new InvalidOperationException($"{typeof(TFacadeInterface).Name} must be an interface."); + + return new Builder(); + } + + /// + /// Fluent builder for . + /// + public sealed class Builder + { + private readonly Dictionary _handlers = new(); + + internal Builder() { } + + /// + /// Maps a method with no parameters to its implementation handler. + /// + public Builder Map( + Expression>> methodSelector, + Func handler) + { + var method = ExtractMethodInfo(methodSelector); + ValidateMethodSignature(method, typeof(TResult)); + + if (_handlers.ContainsKey(method)) + throw new ArgumentException($"Method '{method.Name}' is already mapped.", nameof(methodSelector)); + + _handlers[method] = handler; + return this; + } + + /// + /// Maps a method with one parameter to its implementation handler. + /// + public Builder Map( + Expression>> methodSelector, + Func handler) + { + var method = ExtractMethodInfo(methodSelector); + ValidateMethodSignature(method, typeof(TResult), typeof(T1)); + + if (_handlers.ContainsKey(method)) + throw new ArgumentException($"Method '{method.Name}' is already mapped.", nameof(methodSelector)); + + _handlers[method] = handler; + return this; + } + + /// + /// Maps a method with two parameters to its implementation handler. + /// + public Builder Map( + Expression>> methodSelector, + Func handler) + { + var method = ExtractMethodInfo(methodSelector); + ValidateMethodSignature(method, typeof(TResult), typeof(T1), typeof(T2)); + + if (_handlers.ContainsKey(method)) + throw new ArgumentException($"Method '{method.Name}' is already mapped.", nameof(methodSelector)); + + _handlers[method] = handler; + return this; + } + + /// + /// Maps a method with three parameters to its implementation handler. + /// + public Builder Map( + Expression>> methodSelector, + Func handler) + { + var method = ExtractMethodInfo(methodSelector); + ValidateMethodSignature(method, typeof(TResult), typeof(T1), typeof(T2), typeof(T3)); + + if (_handlers.ContainsKey(method)) + throw new ArgumentException($"Method '{method.Name}' is already mapped.", nameof(methodSelector)); + + _handlers[method] = handler; + return this; + } + + /// + /// Maps a method with four parameters to its implementation handler. + /// + public Builder Map( + Expression>> methodSelector, + Func handler) + { + var method = ExtractMethodInfo(methodSelector); + ValidateMethodSignature(method, typeof(TResult), typeof(T1), typeof(T2), typeof(T3), typeof(T4)); + + if (_handlers.ContainsKey(method)) + throw new ArgumentException($"Method '{method.Name}' is already mapped.", nameof(methodSelector)); + + _handlers[method] = handler; + return this; + } + + /// + /// Builds the typed facade instance. + /// + /// An instance implementing . + /// Thrown when not all interface methods are mapped. + public TFacadeInterface Build() + { + // Validate all methods are implemented + var interfaceMethods = typeof(TFacadeInterface) + .GetMethods(BindingFlags.Public | BindingFlags.Instance) + .Where(m => !m.IsSpecialName); // Exclude property getters/setters + + var unmappedMethods = interfaceMethods + .Where(m => !_handlers.ContainsKey(m)) + .ToList(); + + if (unmappedMethods.Any()) + { + var methodNames = string.Join(", ", unmappedMethods.Select(m => m.Name)); + throw new InvalidOperationException( + $"Not all methods are mapped. Missing: {methodNames}"); + } + + // Create dynamic proxy instance using RealProxy fallback + return TypedFacadeProxyFactory.CreateProxy(_handlers); + } + + private static MethodInfo ExtractMethodInfo(Expression expression) + { + if (expression.Body is not UnaryExpression unary) + throw new ArgumentException("Expression must be a method selector.", nameof(expression)); + + if (unary.Operand is not MethodCallExpression methodCall) + throw new ArgumentException("Expression must select a method.", nameof(expression)); + + if (methodCall.Object is not ConstantExpression constant) + throw new ArgumentException("Invalid method selector expression.", nameof(expression)); + + if (constant.Value is not MethodInfo method) + throw new ArgumentException("Expression does not reference a valid method.", nameof(expression)); + + return method; + } + + private static void ValidateMethodSignature(MethodInfo method, Type returnType, params Type[] parameterTypes) + { + if (method.ReturnType != returnType) + throw new ArgumentException( + $"Method '{method.Name}' return type mismatch. Expected: {returnType.Name}, Actual: {method.ReturnType.Name}"); + + var methodParams = method.GetParameters(); + if (methodParams.Length != parameterTypes.Length) + throw new ArgumentException( + $"Method '{method.Name}' parameter count mismatch. Expected: {parameterTypes.Length}, Actual: {methodParams.Length}"); + + for (int i = 0; i < parameterTypes.Length; i++) + { + if (methodParams[i].ParameterType != parameterTypes[i]) + throw new ArgumentException( + $"Method '{method.Name}' parameter {i} type mismatch. Expected: {parameterTypes[i].Name}, Actual: {methodParams[i].ParameterType.Name}"); + } + } + } +} + +/// +/// Factory for creating typed facade proxy instances using System.Reflection.DispatchProxy where available, +/// or falling back to a reflection-based approach for older frameworks. +/// +internal static class TypedFacadeProxyFactory where TInterface : class +{ + public static TInterface CreateProxy(Dictionary handlers) + { +#if NETSTANDARD2_0 + // For .NET Standard 2.0, use a simple object wrapper with reflection + return new ReflectionProxy(handlers).GetTransparentProxy(); +#else + // For modern .NET, use DispatchProxy + var proxy = System.Reflection.DispatchProxy.Create>(); + ((TypedFacadeDispatchProxy)(object)proxy).Initialize(handlers); + return proxy; +#endif + } + +#if NETSTANDARD2_0 + private sealed class ReflectionProxy + { + private readonly Dictionary _handlersByName; + + public ReflectionProxy(Dictionary handlers) + { + _handlersByName = handlers.ToDictionary(kvp => kvp.Key.Name, kvp => kvp.Value); + } + + public TInterface GetTransparentProxy() + { + // For .NET Standard 2.0, we need to use Castle.DynamicProxy or require manual implementation + // Since we want zero dependencies, we'll throw an instructive error + throw new PlatformNotSupportedException( + $"TypedFacade<{typeof(TInterface).Name}> requires .NET Standard 2.1 or higher for automatic proxy generation. " + + "For .NET Standard 2.0, please use TypedFacadeBase with manual implementation."); + } + } +#endif +} + +#if !NETSTANDARD2_0 +/// +/// DispatchProxy implementation for typed facades. +/// +/// The interface type to proxy. +public class TypedFacadeDispatchProxy : System.Reflection.DispatchProxy + where TInterface : class +{ + private Dictionary _handlers = new(); + + internal void Initialize(Dictionary handlers) + { + _handlers = handlers; + } + + protected override object? Invoke(MethodInfo? targetMethod, object?[]? args) + { + if (targetMethod == null) + throw new InvalidOperationException("Target method is null."); + + if (!_handlers.TryGetValue(targetMethod, out var handler)) + throw new InvalidOperationException($"No handler registered for method '{targetMethod.Name}'"); + + try + { + return handler.DynamicInvoke(args); + } + catch (TargetInvocationException ex) when (ex.InnerException != null) + { + // Unwrap TargetInvocationException to preserve the original exception + throw ex.InnerException; + } + } +} +#endif diff --git a/src/PatternKit.Examples/FacadeDemo/FacadeDemo.cs b/src/PatternKit.Examples/FacadeDemo/FacadeDemo.cs new file mode 100644 index 0000000..bda3128 --- /dev/null +++ b/src/PatternKit.Examples/FacadeDemo/FacadeDemo.cs @@ -0,0 +1,318 @@ +using PatternKit.Structural.Facade; + +namespace PatternKit.Examples.FacadeDemo; + +/// +/// Demonstrates the Facade pattern by providing a simplified interface to a complex e-commerce order processing subsystem. +/// +public static class FacadeDemo +{ + // Simulated subsystem services + public sealed class InventoryService + { + private readonly Dictionary _stock = new() + { + ["WIDGET-001"] = 100, + ["GADGET-002"] = 50, + ["DEVICE-003"] = 25 + }; + + public bool Reserve(string productId, int quantity, out string reservationId) + { + if (!_stock.TryGetValue(productId, out var available) || available < quantity) + { + reservationId = string.Empty; + return false; + } + + _stock[productId] -= quantity; + reservationId = $"RES-{Guid.NewGuid():N}"; + return true; + } + + public void Release(string reservationId) => Console.WriteLine($"Released reservation: {reservationId}"); + + public void Restock(string productId, int quantity) + { + if (_stock.ContainsKey(productId)) + _stock[productId] += quantity; + } + } + + public sealed class PaymentService + { + public static bool Charge(string paymentMethod, decimal amount, out string transactionId) + { + if (amount <= 0) + { + transactionId = string.Empty; + return false; + } + + transactionId = $"TX-{Guid.NewGuid():N}"; + Console.WriteLine($"Charged ${amount:F2} to {paymentMethod}"); + return true; + } + + public static void Refund(string transactionId) => Console.WriteLine($"Refunded transaction: {transactionId}"); + + public static void Void(string transactionId) => Console.WriteLine($"Voided transaction: {transactionId}"); + } + + public sealed class ShippingService + { + public static string Schedule(string address, string productId, int quantity) + { + var shipmentId = $"SHIP-{Guid.NewGuid():N}"; + Console.WriteLine($"Scheduled shipment {shipmentId} to {address}"); + return shipmentId; + } + + public static void Cancel(string shipmentId) => Console.WriteLine($"Cancelled shipment: {shipmentId}"); + + public static string InitiateReturn(string shipmentId) + { + var returnId = $"RET-{shipmentId[5..]}"; + Console.WriteLine($"Initiated return: {returnId}"); + return returnId; + } + } + + public sealed class NotificationService + { + public static void SendOrderConfirmation(string email, string orderId) + => Console.WriteLine($"Sent confirmation to {email} for order {orderId}"); + + public static void SendCancellation(string email, string orderId) + => Console.WriteLine($"Sent cancellation notice to {email} for order {orderId}"); + + public static void SendRefundNotice(string email, decimal amount) + => Console.WriteLine($"Sent refund notice to {email} for ${amount:F2}"); + } + + // Request/Result DTOs + public sealed record OrderRequest( + string ProductId, + int Quantity, + string CustomerEmail, + string ShippingAddress, + string PaymentMethod, + decimal Price); + + public sealed record OrderResult( + bool Success, + string? OrderId = null, + string? ErrorMessage = null, + string? TransactionId = null, + string? ShipmentId = null); + + /// + /// Facade that simplifies complex order processing workflow + /// + public sealed class OrderProcessingFacade( + InventoryService inventory, + PaymentService payment, + ShippingService shipping, + NotificationService notification + ) + { + private readonly Dictionary _orders = new(); + + public Facade BuildFacade() + { + return Facade.Create() + .Operation("place-order", PlaceOrder) + .Operation("cancel-order", CancelOrder) + .Operation("process-return", ProcessReturn) + .Default((in OrderRequest _) => + new OrderResult(false, ErrorMessage: "Unknown operation")) + .Build(); + } + + private OrderResult PlaceOrder(in OrderRequest request) + { + Console.WriteLine($"\n=== Placing Order ==="); + Console.WriteLine($"Product: {request.ProductId}, Qty: {request.Quantity}"); + + // Step 1: Reserve inventory + if (!inventory.Reserve(request.ProductId, request.Quantity, out var reservationId)) + { + return new OrderResult(false, ErrorMessage: "Insufficient inventory"); + } + + // Step 2: Process payment + var total = request.Price * request.Quantity; + if (!PaymentService.Charge(request.PaymentMethod, total, out var transactionId)) + { + inventory.Release(reservationId); + return new OrderResult(false, ErrorMessage: "Payment failed"); + } + + // Step 3: Schedule shipping + var shipmentId = ShippingService.Schedule(request.ShippingAddress, request.ProductId, request.Quantity); + + // Step 4: Generate order ID and store + var orderId = $"ORD-{Guid.NewGuid():N}"; + _orders[orderId] = (reservationId, transactionId, shipmentId); + + // Step 5: Send confirmation + NotificationService.SendOrderConfirmation(request.CustomerEmail, orderId); + + Console.WriteLine($"Order {orderId} placed successfully!\n"); + + return new OrderResult( + Success: true, + OrderId: orderId, + TransactionId: transactionId, + ShipmentId: shipmentId); + } + + private OrderResult CancelOrder(in OrderRequest request) + { + Console.WriteLine($"\n=== Cancelling Order ==="); + + if (string.IsNullOrEmpty(request.ProductId) || !_orders.TryGetValue(request.ProductId, out var orderData)) + { + return new OrderResult(false, ErrorMessage: "Order not found"); + } + + // Step 1: Cancel shipment + ShippingService.Cancel(orderData.shipmentId); + + // Step 2: Void payment + PaymentService.Void(orderData.transactionId); + + // Step 3: Release inventory + inventory.Release(orderData.reservationId); + + // Step 4: Send notification + NotificationService.SendCancellation(request.CustomerEmail, request.ProductId); + + // Step 5: Remove order + _orders.Remove(request.ProductId); + + Console.WriteLine($"Order {request.ProductId} cancelled successfully!\n"); + + return new OrderResult(Success: true, OrderId: request.ProductId); + } + + private OrderResult ProcessReturn(in OrderRequest request) + { + Console.WriteLine($"\n=== Processing Return ==="); + + if (string.IsNullOrEmpty(request.ProductId) || !_orders.TryGetValue(request.ProductId, out var orderData)) + { + return new OrderResult(false, ErrorMessage: "Order not found"); + } + + // Step 1: Initiate return shipment + var returnId = ShippingService.InitiateReturn(orderData.shipmentId); + + // Step 2: Process refund + var refundAmount = request.Price * request.Quantity; + PaymentService.Refund(orderData.transactionId); + + // Step 3: Restock inventory + inventory.Restock(request.ProductId, request.Quantity); + + // Step 4: Send refund notice + NotificationService.SendRefundNotice(request.CustomerEmail, refundAmount); + + // Step 5: Remove order + _orders.Remove(request.ProductId); + + Console.WriteLine($"Return processed successfully!\n"); + + return new OrderResult(Success: true, OrderId: request.ProductId); + } + } + + /// + /// Demonstrates the facade pattern simplifying complex order operations + /// + public static void Run() + { + Console.WriteLine("=== Facade Pattern Demo: E-Commerce Order Processing ===\n"); + + // Create subsystem services + var inventory = new InventoryService(); + var payment = new PaymentService(); + var shipping = new ShippingService(); + var notification = new NotificationService(); + + // Create facade + var orderProcessor = new OrderProcessingFacade(inventory, payment, shipping, notification); + var facade = orderProcessor.BuildFacade(); + + // Example 1: Place an order (complex operation simplified) + var orderRequest = new OrderRequest( + ProductId: "WIDGET-001", + Quantity: 5, + CustomerEmail: "customer@example.com", + ShippingAddress: "123 Main St, Springfield", + PaymentMethod: "VISA-****1234", + Price: 29.99m); + + var result = facade.Execute("place-order", orderRequest); + + if (result.Success) + { + Console.WriteLine($"āœ“ Order placed: {result.OrderId}"); + Console.WriteLine($" Transaction: {result.TransactionId}"); + Console.WriteLine($" Shipment: {result.ShipmentId}"); + + // Example 2: Cancel the order + var cancelRequest = orderRequest with { ProductId = result.OrderId }; + var cancelResult = facade.Execute("cancel-order", cancelRequest); + + if (cancelResult.Success) + { + Console.WriteLine($"āœ“ Order cancelled: {cancelResult.OrderId}"); + } + } + + // Example 3: Place another order and process return + var order2 = new OrderRequest( + ProductId: "GADGET-002", + Quantity: 2, + CustomerEmail: "another@example.com", + ShippingAddress: "456 Oak Ave, Shelbyville", + PaymentMethod: "MC-****5678", + Price: 49.99m); + + var result2 = facade.Execute("place-order", order2); + + if (result2.Success) + { + Console.WriteLine($"āœ“ Second order placed: {result2.OrderId}"); + + // Process return + var returnRequest = order2 with { ProductId = result2.OrderId }; + var returnResult = facade.Execute("process-return", returnRequest); + + if (returnResult.Success) + { + Console.WriteLine($"āœ“ Return processed: {returnResult.OrderId}"); + } + } + + // Example 4: Try unknown operation with default fallback + var unknownResult = facade.Execute("unknown-operation", orderRequest); + Console.WriteLine($"\nāœ— Unknown operation handled: {unknownResult.ErrorMessage}"); + + // Example 5: Using TryExecute + Console.WriteLine("\n=== Using TryExecute ==="); + if (facade.TryExecute("place-order", orderRequest, out var tryResult)) + { + Console.WriteLine($"āœ“ TryExecute succeeded: {tryResult.Success}"); + } + + Console.WriteLine("\n=== Benefits Demonstrated ==="); + Console.WriteLine("1. Complex multi-step workflow hidden behind simple 'place-order' operation"); + Console.WriteLine("2. Subsystem coordination (inventory, payment, shipping, notification) abstracted"); + Console.WriteLine("3. Error handling and rollback logic encapsulated"); + Console.WriteLine("4. Client code is clean and doesn't need to know subsystem details"); + Console.WriteLine("5. Reusable facade can be used across the application"); + } +} + diff --git a/test/PatternKit.Examples.Tests/FacadeDemo/FacadeDemoTests.cs b/test/PatternKit.Examples.Tests/FacadeDemo/FacadeDemoTests.cs new file mode 100644 index 0000000..c39f069 --- /dev/null +++ b/test/PatternKit.Examples.Tests/FacadeDemo/FacadeDemoTests.cs @@ -0,0 +1,194 @@ +using PatternKit.Structural.Facade; +using TinyBDD; +using TinyBDD.Xunit; +using Xunit.Abstractions; +using static PatternKit.Examples.FacadeDemo.FacadeDemo; + +namespace PatternKit.Examples.Tests.FacadeDemo; + +[Feature("Examples - Facade Pattern: E-Commerce Order Processing")] +public sealed class FacadeDemoTests(ITestOutputHelper output) : TinyBddXunitBase(output) +{ + private static (OrderProcessingFacade processor, Facade facade) CreateFacade() + { + var inventory = new InventoryService(); + var payment = new PaymentService(); + var shipping = new ShippingService(); + var notification = new NotificationService(); + var processor = new OrderProcessingFacade(inventory, payment, shipping, notification); + var facade = processor.BuildFacade(); + return (processor, facade); + } + + [Scenario("Place order successfully coordinates all subsystems")] + [Fact] + public Task PlaceOrder_Success() + => Given("an order processing facade", CreateFacade) + .When("placing a valid order", ctx => + { + var request = new OrderRequest( + ProductId: "WIDGET-001", + Quantity: 5, + CustomerEmail: "test@example.com", + ShippingAddress: "123 Main St", + PaymentMethod: "VISA-1234", + Price: 29.99m); + return ctx.facade.Execute("place-order", request); + }) + .Then("order succeeds", r => r.Success) + .And("order ID is generated", r => !string.IsNullOrEmpty(r.OrderId)) + .And("transaction ID is returned", r => !string.IsNullOrEmpty(r.TransactionId)) + .And("shipment ID is returned", r => !string.IsNullOrEmpty(r.ShipmentId)) + .AssertPassed(); + + [Scenario("Place order with insufficient inventory fails gracefully")] + [Fact] + public Task PlaceOrder_InsufficientInventory() + => Given("an order processing facade", CreateFacade) + .When("placing order with excessive quantity", ctx => + { + var request = new OrderRequest( + ProductId: "DEVICE-003", + Quantity: 1000, + CustomerEmail: "test@example.com", + ShippingAddress: "123 Main St", + PaymentMethod: "VISA-1234", + Price: 99.99m); + return ctx.facade.Execute("place-order", request); + }) + .Then("order fails", r => !r.Success) + .And("error message indicates inventory issue", r => r.ErrorMessage != null && r.ErrorMessage.Contains("inventory")) + .AssertPassed(); + + [Scenario("Cancel order reverses all subsystem operations")] + [Fact] + public Task CancelOrder_Success() + => Given("a facade with a placed order", () => + { + var ctx = CreateFacade(); + var request = new OrderRequest( + ProductId: "WIDGET-001", + Quantity: 2, + CustomerEmail: "test@example.com", + ShippingAddress: "123 Main St", + PaymentMethod: "VISA-1234", + Price: 19.99m); + var result = ctx.facade.Execute("place-order", request); + return (ctx.facade, result.OrderId, request); + }) + .When("cancelling the order", ctx => + { + var cancelRequest = ctx.request with { ProductId = ctx.OrderId }; + return ctx.facade.Execute("cancel-order", cancelRequest); + }) + .Then("cancellation succeeds", r => r.Success) + .And("order ID matches", r => r.OrderId != null) + .AssertPassed(); + + [Scenario("Process return handles product return flow")] + [Fact] + public Task ProcessReturn_Success() + => Given("a facade with a placed order", () => + { + var ctx = CreateFacade(); + var request = new OrderRequest( + ProductId: "GADGET-002", + Quantity: 3, + CustomerEmail: "test@example.com", + ShippingAddress: "456 Oak Ave", + PaymentMethod: "MC-5678", + Price: 39.99m); + var result = ctx.facade.Execute("place-order", request); + return (ctx.facade, result.OrderId, request); + }) + .When("processing a return", ctx => + { + var returnRequest = ctx.request with { ProductId = ctx.OrderId }; + return ctx.facade.Execute("process-return", returnRequest); + }) + .Then("return succeeds", r => r.Success) + .And("order ID matches", r => r.OrderId != null) + .AssertPassed(); + + [Scenario("Unknown operation uses default fallback")] + [Fact] + public Task UnknownOperation_UsesDefault() + => Given("an order processing facade", CreateFacade) + .When("executing unknown operation", ctx => + { + var request = new OrderRequest( + ProductId: "WIDGET-001", + Quantity: 1, + CustomerEmail: "test@example.com", + ShippingAddress: "123 Main St", + PaymentMethod: "VISA-1234", + Price: 9.99m); + return ctx.facade.Execute("invalid-operation", request); + }) + .Then("operation fails", r => !r.Success) + .And("error indicates unknown operation", r => r.ErrorMessage != null && r.ErrorMessage.Contains("Unknown")) + .AssertPassed(); + + [Scenario("TryExecute provides safe operation execution")] + [Fact] + public Task TryExecute_SafeExecution() + => Given("an order processing facade", CreateFacade) + .When("using TryExecute", ctx => + { + var request = new OrderRequest( + ProductId: "WIDGET-001", + Quantity: 1, + CustomerEmail: "test@example.com", + ShippingAddress: "123 Main St", + PaymentMethod: "VISA-1234", + Price: 14.99m); + var success = ctx.facade.TryExecute("place-order", request, out var result); + return (success, result); + }) + .Then("execution succeeds", r => r.success) + .And("result is valid", r => r.result.Success) + .AssertPassed(); + + [Scenario("Multiple orders can be processed sequentially")] + [Fact] + public Task MultipleOrders_Sequential() + => Given("an order processing facade", CreateFacade) + .When("placing multiple orders", ctx => + { + var order1 = new OrderRequest("WIDGET-001", 2, "user1@test.com", "Addr1", "VISA", 29.99m); + var order2 = new OrderRequest("GADGET-002", 3, "user2@test.com", "Addr2", "MC", 39.99m); + + var result1 = ctx.facade.Execute("place-order", order1); + var result2 = ctx.facade.Execute("place-order", order2); + + return (result1, result2); + }) + .Then("both orders succeed", r => r.result1.Success && r.result2.Success) + .And("both have unique order IDs", r => r.result1.OrderId != r.result2.OrderId) + .AssertPassed(); + + [Scenario("Facade simplifies complex workflow into single operation call")] + [Fact] + public Task Facade_SimplifiesComplexWorkflow() + => Given("an order processing facade", CreateFacade) + .When("client executes single 'place-order' operation", ctx => + { + // Client only needs to call one operation + var request = new OrderRequest( + ProductId: "WIDGET-001", + Quantity: 1, + CustomerEmail: "simple@example.com", + ShippingAddress: "Easy St", + PaymentMethod: "VISA", + Price: 49.99m); + + // Behind the scenes: inventory reservation, payment processing, + // shipping scheduling, notification sending all coordinated + return ctx.facade.Execute("place-order", request); + }) + .Then("complex subsystem coordination is hidden", r => r.Success) + .And("client receives simple result", r => r.OrderId != null) + .And("all subsystems coordinated transparently", r => + r.TransactionId != null && r.ShipmentId != null) + .AssertPassed(); +} \ No newline at end of file diff --git a/test/PatternKit.Tests/Structural/Facade/FacadeTests.cs b/test/PatternKit.Tests/Structural/Facade/FacadeTests.cs new file mode 100644 index 0000000..36ba1f0 --- /dev/null +++ b/test/PatternKit.Tests/Structural/Facade/FacadeTests.cs @@ -0,0 +1,317 @@ +using PatternKit.Structural.Facade; +using TinyBDD; +using TinyBDD.Xunit; +using Xunit.Abstractions; + +namespace PatternKit.Tests.Structural.Facade; + +[Feature("Structural - Facade (simplified subsystem interface)")] +public sealed class FacadeTests(ITestOutputHelper output) : TinyBddXunitBase(output) +{ + // Simulated subsystems + private sealed class InventoryService + { + public List Log { get; } = []; + + public void Reserve(string item) => Log.Add($"reserve:{item}"); + + public void Release(string item) => Log.Add($"release:{item}"); + } + + private sealed class PaymentService + { + public List Log { get; } = []; + + public string Charge(decimal amount) + { + Log.Add($"charge:{amount}"); + return $"tx-{amount}"; + } + + public void Refund(string txId) => Log.Add($"refund:{txId}"); + } + + private sealed class ShippingService + { + public List Log { get; } = []; + + public string Schedule(string address) + { + Log.Add($"ship:{address}"); + return $"ship-{address}"; + } + } + + private sealed record OrderRequest(string Item, decimal Amount, string Address); + + private sealed record OrderResult(string Status, string? TxId = null, string? ShipId = null); + + [Scenario("Single operation executes subsystem coordination")] + [Fact] + public Task Single_Operation_Executes() + => Given("a facade with one operation", () => + { + var inventory = new InventoryService(); + var payment = new PaymentService(); + var shipping = new ShippingService(); + + var facade = Facade.Create() + .Operation("process", (in req) => + { + inventory.Reserve(req.Item); + var txId = payment.Charge(req.Amount); + var shipId = shipping.Schedule(req.Address); + return new OrderResult("Processed", txId, shipId); + }) + .Build(); + + return (facade, inventory, payment, shipping); + }) + .When("execute 'process' operation", ctx => + { + var result = ctx.facade.Execute("process", new OrderRequest("Widget", 99.99m, "123 Main")); + return (result, ctx.inventory, ctx.payment, ctx.shipping); + }) + .Then("result shows processed", r => r.result.Status == "Processed") + .And("inventory called", r => r.inventory.Log.Contains("reserve:Widget")) + .And("payment called", r => r.payment.Log.Contains("charge:99.99")) + .And("shipping called", r => r.shipping.Log.Contains("ship:123 Main")) + .And("transaction ID returned", r => r.result.TxId == "tx-99.99") + .And("shipment ID returned", r => r.result.ShipId == "ship-123 Main") + .AssertPassed(); + + [Scenario("Multiple operations with different subsystem coordination")] + [Fact] + public Task Multiple_Operations_Different_Flows() + => Given("a facade with process and cancel operations", () => + { + var inventory = new InventoryService(); + var payment = new PaymentService(); + + var facade = Facade.Create() + .Operation("process", (in req) => + { + inventory.Reserve(req.Item); + var txId = payment.Charge(req.Amount); + return new OrderResult("Processed", txId); + }) + .Operation("cancel", (in req) => + { + inventory.Release(req.Item); + payment.Refund($"tx-{req.Amount}"); + return new OrderResult("Cancelled"); + }) + .Build(); + + return (facade, inventory, payment); + }) + .When("execute both operations", ctx => + { + var req = new OrderRequest("Widget", 50m, "456 Oak"); + ctx.facade.Execute("process", req); + ctx.facade.Execute("cancel", req); + return (ctx.inventory, ctx.payment); + }) + .Then("inventory shows reserve then release", r => + r.inventory.Log is ["reserve:Widget", "release:Widget"]) + .And("payment shows charge then refund", r => + r.payment.Log is ["charge:50", "refund:tx-50"]) + .AssertPassed(); + + [Scenario("Unknown operation with default returns default result")] + [Fact] + public Task Unknown_Operation_Uses_Default() + => Given("a facade with default operation", () => + Facade.Create() + .Operation("greet", (in name) => $"Hello, {name}") + .Default((in _) => "Unknown operation") + .Build()) + .When("execute unknown operation", f => f.Execute("unknown", "World")) + .Then("returns default result", r => r == "Unknown operation") + .AssertPassed(); + + [Scenario("Unknown operation without default throws")] + [Fact] + public Task Unknown_Operation_No_Default_Throws() + => Given("a facade without default", () => + Facade.Create() + .Operation("double", (in x) => x * 2) + .Build()) + .When("execute unknown operation", f => Record.Exception(() => f.Execute("unknown", 5))) + .Then("throws InvalidOperationException", ex => ex is InvalidOperationException) + .And("message mentions operation not found", ex => ex!.Message.Contains("unknown") && ex.Message.Contains("not found")) + .AssertPassed(); + + [Scenario("TryExecute returns true for existing operation")] + [Fact] + public Task TryExecute_Existing_Returns_True() + => Given("a facade with operations", () => + Facade.Create() + .Operation("triple", (in x) => x * 3) + .Build()) + .When("TryExecute existing operation", f => + { + var success = f.TryExecute("triple", 7, out var result); + return (success, result); + }) + .Then("returns true", r => r.success) + .And("output is correct", r => r.result == 21) + .AssertPassed(); + + [Scenario("TryExecute returns false for unknown operation without default")] + [Fact] + public Task TryExecute_Unknown_No_Default_Returns_False() + => Given("a facade without default", () => + Facade.Create() + .Operation("add", (in x) => x + 1) + .Build()) + .When("TryExecute unknown operation", f => + { + var success = f.TryExecute("unknown", 5, out var result); + return (success, result); + }) + .Then("returns false", r => !r.success) + .And("output is default", r => r.result == 0) + .AssertPassed(); + + [Scenario("TryExecute returns true for unknown operation with default")] + [Fact] + public Task TryExecute_Unknown_With_Default_Returns_True() + => Given("a facade with default", () => + Facade.Create() + .Operation("double", (in x) => x * 2) + .Default((in _) => -1) + .Build()) + .When("TryExecute unknown operation", f => + { + var success = f.TryExecute("unknown", 5, out var result); + return (success, result); + }) + .Then("returns true", r => r.success) + .And("output is default result", r => r.result == -1) + .AssertPassed(); + + [Scenario("HasOperation returns true for registered operations")] + [Fact] + public Task HasOperation_Registered_Returns_True() + => Given("a facade with operations", () => + Facade.Create() + .Operation("add", (in x) => x + 1) + .Operation("multiply", (in x) => x * 2) + .Build()) + .When("check for registered operation", f => (f.HasOperation("add"), f.HasOperation("multiply"), f.HasOperation("unknown"))) + .Then("add exists", r => r.Item1) + .And("multiply exists", r => r.Item2) + .And("unknown does not exist", r => !r.Item3) + .AssertPassed(); + + [Scenario("Duplicate operation names throw on build")] + [Fact] + public Task Duplicate_Operation_Throws() + => Given("a builder with duplicate operation", () => + Record.Exception(() => + Facade.Create() + .Operation("test", (in x) => x) + .Operation("test", (in x) => x + 1) + .Build())) + .When("exception thrown", ex => ex) + .Then("throws ArgumentException", ex => ex is ArgumentException) + .And("message mentions operation name", ex => ex!.Message.Contains("test")) + .AssertPassed(); + + [Scenario("OperationIgnoreCase handles case-insensitive matching")] + [Fact] + public Task OperationIgnoreCase_Matches() + => Given("a facade with case-insensitive operation", () => + Facade.Create() + .OperationIgnoreCase("Greet", (in name) => $"Hi, {name}") + .Build()) + .When("execute with different casing", f => (f.Execute("greet", "Alice"), f.Execute("GREET", "Bob"), f.Execute("GrEeT", "Carol"))) + .Then("all match", r => r is { Item1: "Hi, Alice", Item2: "Hi, Bob", Item3: "Hi, Carol" }) + .AssertPassed(); + + [Scenario("Build without operations or default throws")] + [Fact] + public Task Build_Empty_Throws() + => Given("an empty builder", () => Record.Exception(() => Facade.Create().Build())) + .When("exception thrown", ex => ex) + .Then("throws InvalidOperationException", ex => ex is InvalidOperationException) + .And("message mentions configuration required", ex => ex!.Message.Contains("operation") || ex.Message.Contains("default")) + .AssertPassed(); + + [Scenario("Build with only default succeeds")] + [Fact] + public Task Build_Default_Only_Succeeds() + => Given("a builder with only default", () => + Facade.Create() + .Default((in x) => $"default:{x}") + .Build()) + .When("execute any operation", f => f.Execute("anything", 42)) + .Then("returns default result", r => r == "default:42") + .AssertPassed(); + + [Scenario("Facade is reusable and thread-safe")] + [Fact] + public Task Facade_Reusable() + => Given("a reusable facade", () => + { + var callCount = new List(); + var facade = Facade.Create() + .Operation("increment", (in x) => + { + callCount.Add(x); + return x + 1; + }) + .Build(); + return (facade, callCount); + }) + .When("execute multiple times", ctx => + { + ctx.facade.Execute("increment", 1); + ctx.facade.Execute("increment", 2); + ctx.facade.Execute("increment", 3); + return ctx.callCount.Count; + }) + .Then("executed 3 times", count => count == 3) + .AssertPassed(); + + [Scenario("Complex subsystem coordination example")] + [Fact] + public Task Complex_Coordination() + => Given("a multi-subsystem facade", () => + { + var log = new List(); + var facade = Facade.Create() + .Operation("order", (in product) => + { + log.Add("1:validate"); + log.Add("2:reserve-inventory"); + log.Add("3:charge-payment"); + log.Add("4:schedule-shipping"); + log.Add("5:send-confirmation"); + return $"Order placed: {product}"; + }) + .Operation("return", (in orderId) => + { + log.Add("1:validate-return"); + log.Add("2:schedule-pickup"); + log.Add("3:refund-payment"); + log.Add("4:restock-inventory"); + return $"Return processed: {orderId}"; + }) + .Build(); + return (facade, log); + }) + .When("execute order then return", ctx => + { + var orderResult = ctx.facade.Execute("order", "Laptop"); + var returnResult = ctx.facade.Execute("return", "ORD-123"); + return (orderResult, returnResult, ctx.log); + }) + .Then("order completed", r => r.orderResult == "Order placed: Laptop") + .And("return completed", r => r.returnResult == "Return processed: ORD-123") + .And("all 9 subsystem calls made", r => r.log.Count == 9) + .And("order flow correct", r => r.log[0] == "1:validate" && r.log[4] == "5:send-confirmation") + .And("return flow correct", r => r.log[5] == "1:validate-return" && r.log[8] == "4:restock-inventory") + .AssertPassed(); +} \ No newline at end of file diff --git a/test/PatternKit.Tests/Structural/Facade/TypedFacadeTests.cs b/test/PatternKit.Tests/Structural/Facade/TypedFacadeTests.cs new file mode 100644 index 0000000..20fc2e9 --- /dev/null +++ b/test/PatternKit.Tests/Structural/Facade/TypedFacadeTests.cs @@ -0,0 +1,256 @@ +using PatternKit.Structural.Facade; +using TinyBDD; +using TinyBDD.Xunit; +using Xunit.Abstractions; + +namespace PatternKit.Tests.Structural.Facade; + +[Feature("Structural - TypedFacade (compile-time safe interface-based facade)")] +public sealed class TypedFacadeTests(ITestOutputHelper output) : TinyBddXunitBase(output) +{ + // Test interfaces + public interface ICalculator + { + int Add(int a, int b); + int Subtract(int a, int b); + int Multiply(int a, int b); + } + + public interface IOrderService + { + OrderResult PlaceOrder(OrderRequest request); + OrderResult CancelOrder(string orderId); + bool CheckInventory(string productId, int quantity); + } + + public interface ISimpleService + { + string GetStatus(); + } + + public interface IComplexService + { + string Operation1(string arg); + int Operation2(int a, int b); + bool Operation3(string s, int n, bool flag); + decimal Operation4(decimal a, decimal b, decimal c, decimal d); + } + + public sealed record OrderRequest(string ProductId, int Quantity, decimal Price); + public sealed record OrderResult(bool Success, string? OrderId = null); + + [Scenario("TypedFacade creates compile-time safe interface implementation")] + [Fact] + public Task TypedFacade_Creates_Safe_Implementation() + => Given("a typed facade for ICalculator", () => + TypedFacade.Create() + .Map(x => x.Add, (int a, int b) => a + b) + .Map(x => x.Subtract, (int a, int b) => a - b) + .Map(x => x.Multiply, (int a, int b) => a * b) + .Build()) + .When("using the facade", calc => (calc.Add(5, 3), calc.Subtract(10, 4), calc.Multiply(6, 7))) + .Then("add works", r => r.Item1 == 8) + .And("subtract works", r => r.Item2 == 6) + .And("multiply works", r => r.Item3 == 42) + .AssertPassed(); + + [Scenario("TypedFacade with complex domain operations")] + [Fact] + public Task TypedFacade_Complex_Domain_Operations() + => Given("an order service facade", () => + { + var orders = new Dictionary(); + var inventory = new Dictionary { ["WIDGET"] = 100, ["GADGET"] = 50 }; + + return TypedFacade.Create() + .Map(x => x.PlaceOrder, (OrderRequest req) => + { + if (!inventory.TryGetValue(req.ProductId, out var stock) || stock < req.Quantity) + return new OrderResult(false); + + inventory[req.ProductId] -= req.Quantity; + var orderId = $"ORD-{Guid.NewGuid():N}"; + orders[orderId] = req; + return new OrderResult(true, orderId); + }) + .Map(x => x.CancelOrder, (string orderId) => + { + if (!orders.Remove(orderId, out var order)) + return new OrderResult(false); + + inventory[order.ProductId] += order.Quantity; + return new OrderResult(true, orderId); + }) + .Map(x => x.CheckInventory, (string productId, int quantity) => + inventory.TryGetValue(productId, out var stock) && stock >= quantity) + .Build(); + }) + .When("placing and managing orders", service => + { + var hasStock = service.CheckInventory("WIDGET", 10); + var order1 = service.PlaceOrder(new OrderRequest("WIDGET", 10, 99.99m)); + var order2 = service.PlaceOrder(new OrderRequest("WIDGET", 200, 99.99m)); // Too many + var cancelled = service.CancelOrder(order1.OrderId!); + return (hasStock, order1, order2, cancelled); + }) + .Then("inventory check succeeds", r => r.hasStock) + .And("first order succeeds", r => r.order1.Success && r.order1.OrderId != null) + .And("oversized order fails", r => !r.order2.Success) + .And("cancellation succeeds", r => r.cancelled.Success) + .AssertPassed(); + + [Scenario("TypedFacade with no parameters")] + [Fact] + public Task TypedFacade_No_Parameters() + => Given("a simple service facade", () => + TypedFacade.Create() + .Map(x => x.GetStatus, () => "Operational") + .Build()) + .When("calling parameterless method", service => service.GetStatus()) + .Then("returns expected value", status => status == "Operational") + .AssertPassed(); + + [Scenario("TypedFacade with varying parameter counts")] + [Fact] + public Task TypedFacade_Varying_Parameters() + => Given("a complex service facade", () => + TypedFacade.Create() + .Map(x => x.Operation1, (string arg) => $"Result: {arg}") + .Map(x => x.Operation2, (int a, int b) => a + b) + .Map(x => x.Operation3, (string s, int n, bool flag) => + flag && s.Length > n) + .Map(x => x.Operation4, (decimal a, decimal b, decimal c, decimal d) => + a + b + c + d) + .Build()) + .When("calling methods with different signatures", service => ( + service.Operation1("test"), + service.Operation2(5, 10), + service.Operation3("hello", 3, true), + service.Operation4(1.5m, 2.5m, 3.5m, 4.5m))) + .Then("1-param method works", r => r.Item1 == "Result: test") + .And("2-param method works", r => r.Item2 == 15) + .And("3-param method works", r => r.Item3 == true) + .And("4-param method works", r => r.Item4 == 12.0m) + .AssertPassed(); + + [Scenario("TypedFacade throws if not all methods are mapped")] + [Fact] + public Task TypedFacade_Throws_If_Incomplete() + => Given("a builder missing method implementations", () => + Record.Exception(() => + TypedFacade.Create() + .Map(x => x.Add, (int a, int b) => a + b) + // Missing Subtract and Multiply + .Build())) + .When("building", ex => ex) + .Then("throws InvalidOperationException", ex => ex is InvalidOperationException) + .And("mentions missing methods", ex => + ex!.Message.Contains("Subtract") && ex.Message.Contains("Multiply")) + .AssertPassed(); + + [Scenario("TypedFacade throws if method mapped twice")] + [Fact] + public Task TypedFacade_Throws_If_Duplicate_Mapping() + => Given("a builder with duplicate mapping", () => + Record.Exception(() => + TypedFacade.Create() + .Map(x => x.Add, (int a, int b) => a + b) + .Map(x => x.Add, (int a, int b) => a * b) // Duplicate! + .Map(x => x.Subtract, (int a, int b) => a - b) + .Map(x => x.Multiply, (int a, int b) => a * b) + .Build())) + .When("exception thrown", ex => ex) + .Then("throws ArgumentException", ex => ex is ArgumentException) + .And("mentions method name", ex => ex!.Message.Contains("Add")) + .AssertPassed(); + + [Scenario("TypedFacade throws if not an interface")] + [Fact] + public Task TypedFacade_Requires_Interface() + => Given("attempting to create facade from non-interface", () => + Record.Exception(TypedFacade.Create)) + .When("exception thrown", ex => ex) + .Then("throws InvalidOperationException", ex => ex is InvalidOperationException) + .And("mentions interface requirement", ex => ex!.Message.Contains("interface")) + .AssertPassed(); + + [Scenario("TypedFacade preserves exceptions from handlers")] + [Fact] + public Task TypedFacade_Preserves_Exceptions() + => Given("a facade that throws", () => + TypedFacade.Create() + .Map(x => x.GetStatus, () => throw new InvalidOperationException("Test error")) + .Build()) + .When("calling throwing method", service => Record.Exception(service.GetStatus)) + .Then("exception is preserved", ex => ex is InvalidOperationException) + .And("message is preserved", ex => ex!.Message == "Test error") + .AssertPassed(); + + [Scenario("TypedFacade is reusable")] + [Fact] + public Task TypedFacade_Is_Reusable() + => Given("a calculator facade", () => + { + var callCount = new[] { 0 }; // Use array to capture by reference + var calc = TypedFacade.Create() + .Map(x => x.Add, (int a, int b) => { callCount[0]++; return a + b; }) + .Map(x => x.Subtract, (int a, int b) => a - b) + .Map(x => x.Multiply, (int a, int b) => a * b) + .Build(); + return (calc, callCount); + }) + .When("calling multiple times", ctx => + { + ctx.calc.Add(1, 2); + ctx.calc.Add(3, 4); + ctx.calc.Add(5, 6); + return ctx.callCount[0]; + }) + .Then("all calls executed", count => count == 3) + .AssertPassed(); + + [Scenario("TypedFacade with closure captures")] + [Fact] + public Task TypedFacade_Captures_Closures() + => Given("a facade with captured state", () => + { + var log = new List(); + var service = TypedFacade.Create() + .Map(x => x.GetStatus, () => + { + log.Add("called"); + return "OK"; + }) + .Build(); + return (service, log); + }) + .When("calling method", ctx => + { + ctx.service.GetStatus(); + ctx.service.GetStatus(); + return ctx.log.Count; + }) + .Then("closure captured state updated", count => count == 2) + .AssertPassed(); + + [Scenario("TypedFacade provides IntelliSense and compile-time safety")] + [Fact] + public Task TypedFacade_Compile_Time_Safety() + => Given("a typed calculator facade", () => + TypedFacade.Create() + .Map(x => x.Add, (int a, int b) => a + b) + .Map(x => x.Subtract, (int a, int b) => a - b) + .Map(x => x.Multiply, (int a, int b) => a * b) + .Build()) + .When("IDE provides IntelliSense", calc => + { + // This demonstrates compile-time safety: + // - IDE shows available methods (Add, Subtract, Multiply) + // - Method signatures are enforced (two ints, returns int) + // - Typos are caught at compile time + var result = calc.Add(10, 20); // IntelliSense works here! + return result; + }) + .Then("method call is type-safe", result => result == 30) + .AssertPassed(); +}