Successfully reported this slideshow.
We use your LinkedIn profile and activity data to personalize ads and to show you more relevant ads. You can change your ad preferences anytime.

Alexey Golub - Dependency absolution (application as a pipeline) | Svitla Smart Talks

75 views

Published on

Application-as-a-pipeline, an alternative architectural approach to dependency injection based on principles from functional programming.

Published in: Technology
  • Be the first to comment

Alexey Golub - Dependency absolution (application as a pipeline) | Svitla Smart Talks

  1. 1. Speaker: Alexey Golub @Tyrrrz Dependency absolution Application as a pipeline
  2. 2. Who am I? Speaker: Alexey Golub @Tyrrrz Open source developer • https://github.com/Tyrrrz • 6 active projects • 4000 total stars • 400k total downloads Senior software developer • Svitla Systems, Inc • C#, ASP.NET Core, Docker, AWS Interests • C#, F#, JavaScript • Functional programming • Language design, parsing, DSLs • Product design, photography
  3. 3. Best code is no code at all Bad design decisions lead to writing more code in the long run Speaker: Alexey Golub @Tyrrrz
  4. 4. Domain modeling Speaker: Alexey Golub @Tyrrrz
  5. 5. Object-oriented approach Speaker: Alexey Golub @Tyrrrz public class Ledger { public Guid Id { get; } public decimal Balance { get; private set; } public Ledger(Guid id, decimal initialBalance) { /* ... */ } public void Debit(decimal amount) { /* ... */ } public void Credit(decimal amount) { /* ... */ } public static Ledger Create(decimal initialBalance = 0.0M) { /* ... */ } public static Ledger Retrieve(Guid id) { /* ... */ } }
  6. 6. public void Debit(decimal amount) { if (Balance < amount) throw new InsufficientFundsException(); Balance -= amount; Database.Save(this); } Speaker: Alexey Golub @Tyrrrz Business logic Dependency
  7. 7. Speaker: Alexey Golub @Tyrrrz Object Dependency
  8. 8. Speaker: Alexey Golub @Tyrrrz Object Dependency Object Abstraction Dependency Mock
  9. 9. public void Debit(decimal amount) { if (Balance < amount) throw new InsufficientFundsException(); Balance -= amount; ServiceLocator.GetService<IDatabase>().Save(this); } Speaker: Alexey Golub @Tyrrrz
  10. 10. public void Debit(decimal amount) { if (Balance < amount) throw new InsufficientFundsException(); Balance -= amount; ServiceLocator.GetService<IDatabase>().Save(this); } private readonly IDatabase _database; /* ... */ public void Debit(decimal amount) { if (Balance < amount) throw new InsufficientFundsException(); Balance -= amount; _database.Save(this); } Speaker: Alexey Golub @Tyrrrz Abstract dependency
  11. 11. Benefits of dependency injection • Inversion of control • Lifetime management • Decoupled implementations • Reduced module complexity • Isolated testing Speaker: Alexey Golub @Tyrrrz
  12. 12. Layered architecture Speaker: Alexey Golub @Tyrrrz
  13. 13. Speaker: Alexey Golub @Tyrrrz Domain models Data access Services System boundary
  14. 14. Layered/onion architecture public class Ledger { public Guid Id { get; set; } public decimal Amount { get; set; } } public interface ILedgerRepository { Task AddAsync(Ledger ledger); Task UpdateAsync(Ledger ledger); Task<Ledger> RetrieveAsync(Guid id); } public interface ILedgerService { Task<Ledger> CreateAsync(decimal initialBalance = 0.0M); Task<Ledger> RetrieveAsync(Guid id); Task DebitAsync(Guid id, decimal amount); Task CreditAsync(Guid id, decimal amount); } Speaker: Alexey Golub @Tyrrrz
  15. 15. Speaker: Alexey Golub @Tyrrrz Ledger LedgerRepository LedgerService LedgerController System boundary (entry point) Service layer Data access layer Domain model
  16. 16. Speaker: Alexey Golub @Tyrrrz Ledger LedgerRepository LedgerService LedgerController TransactionFee Counterparty TransactionController TransactionService FeeService Statement StatementService CounterpartyService FeeRepository TransactionRepositoryCounterpartyRepository StatementRepository ScheduledJob NotificationService
  17. 17. Layers of issues Speaker: Alexey Golub @Tyrrrz
  18. 18. Hard to combine with OOD • Existing paradigms became anti-patterns • Encapsulation is gone • Inheritance is disfavored • Static classes & methods are “untestable” Speaker: Alexey Golub @Tyrrrz
  19. 19. Causational indirectness • Hard to trace • Hard to reason about • Implicit dependencies & DI made us lazy Speaker: Alexey Golub @Tyrrrz
  20. 20. Leaky async • Async stems from IO side-effects and leaks into business logic • Unnecessary state machines impact performance • Code becomes redundantly verbose Speaker: Alexey Golub @Tyrrrz
  21. 21. public class LedgerService : ILedgerService { private readonly ILedgerRepository _repository; /* ... */ public async Task DebitAsync(Guid ledgerId, decimal amount) { var ledger = await _repository.GetByIdAsync(ledgerId); if (ledger is null) throw new EntityNotFoundException(); if (ledger.Balance < amount) throw new InsufficientFundsException(); ledger.Balance -= amount; await _repository.SaveChangesAsync(); } } Speaker: Alexey Golub @Tyrrrz Leaky async
  22. 22. public interface ILedgerService { Task<Ledger> CreateAsync(decimal initialBalance = 0.0M); Task<Ledger> RetrieveAsync(Guid id); Task DebitAsync(Guid id, decimal amount); Task CreditAsync(Guid id, decimal amount); } Speaker: Alexey Golub @Tyrrrz ILedgerService reveals the fact that LedgerService depends on ILedgerRepository
  23. 23. Obscured complexity • Module complexity is reduced • Total complexity is increased • Assumptions between communicating modules Speaker: Alexey Golub @Tyrrrz
  24. 24. Mock-based testing • Implementation-aware • Very brittle • Massive time sink Speaker: Alexey Golub @Tyrrrz
  25. 25. // Arrange var transactionRepositoryMock = new Mock<ITransactionRepository>(); transactionRepositoryMock.Setup(x => x.GetAll()) .Returns(testData.AsQueryable()); var counterpartyServiceMock = new Mock<ICounterpartyService>(); counterpartyServiceMock.Setup(x => x.GetCounterpartyAsync(It.IsAny<Transaction>())) .ReturnsAsync(testCounterparty); var transactionService = new TransactionService( transactionRepositoryMock.Object, counterpartyServiceMock.Object ); var transaction = new Transaction { // ... } // Act await transactionService.ExecuteTransactionAsync(transaction); // Assert // ... Speaker: Alexey Golub @Tyrrrz Implementation-aware
  26. 26. Autotelic abstractions • Every object requires an explicit abstraction • Abstractions are needed for the sole purpose of mocking • Abstractions don’t try to encapsulate behavior • Abstractions are owned by implementations instead of consumers Speaker: Alexey Golub @Tyrrrz
  27. 27. Abstraction is a great tool and a terrible goal Speaker: Alexey Golub @Tyrrrz
  28. 28. Is OOP the wrong tool for the job? Speaker: Alexey Golub @Tyrrrz
  29. 29. Functional architecture Speaker: Alexey Golub @Tyrrrz
  30. 30. ImpurePure Speaker: Alexey Golub @Tyrrrz
  31. 31. Side-effects Data transformation Speaker: Alexey Golub @Tyrrrz Validating, parsing, mapping, ordering, filtering, projecting… Reading request, writing to DB, enqueuing messages…
  32. 32. ImpurePure Speaker: Alexey Golub @Tyrrrz
  33. 33. ImpurePure Speaker: Alexey Golub @Tyrrrz
  34. 34. Pure functions Speaker: Alexey Golub @Tyrrrz
  35. 35. Speaker: Alexey Golub @Tyrrrz F(x)Data in Data out Isolation SQL
  36. 36. Stateless Speaker: Alexey Golub @Tyrrrz
  37. 37. Speaker: Alexey Golub @Tyrrrz var fees = _invoiceService.CalculateFees(); Can I make any assumptions about _invoiceService?
  38. 38. Speaker: Alexey Golub @Tyrrrz var fees = InvoiceLogic.CalculateFees(ledger, invoiceDate); Dependencies are clear which makes the intent clear
  39. 39. Data-driven Speaker: Alexey Golub @Tyrrrz
  40. 40. private readonly CommonDbContext _dbContext; /* ... */ public IEnumerable<Transaction> GetCompletedTransactions(Guid ledgerId) { return _dbContext.Transactions .Where(t => t.LedgerId == ledgerId) .Where(t => t.IsCompleted); } Speaker: Alexey Golub @Tyrrrz Data container
  41. 41. Speaker: Alexey Golub @Tyrrrz public static IEnumerable<Transaction> GetCompletedTransactions( IEnumerable<Transaction> allTransactions, Guid ledgerId) { return allTransactions .Where(t => t.LedgerId == ledgerId) .Where(t => t.IsCompleted); }
  42. 42. Deterministic Speaker: Alexey Golub @Tyrrrz
  43. 43. public static DateTimeOffset GetInvoiceDate(int day) { var instant = DateTimeOffset.Now; var potentialInvoiceDate = instant.Day >= day ? instant.AddDays(day - instant.Day) : instant.AddMonths(-1).AddDays(day - instant.Day); if (potentialInvoiceDate.DayOfWeek == DayOfWeek.Saturday) potentialInvoiceDate = potentialInvoiceDate.AddDays(-1); else if (potentialInvoiceDate.DayOfWeek == DayOfWeek.Sunday) potentialInvoiceDate = potentialInvoiceDate.AddDays(-2); return potentialInvoiceDate; } Non-deterministic Speaker: Alexey Golub @Tyrrrz
  44. 44. public static DateTimeOffset GetInvoiceDate(DateTimeOffset instant, int day) { var potentialInvoiceDate = instant.Day >= day ? instant.AddDays(day - instant.Day) : instant.AddMonths(-1).AddDays(day - instant.Day); if (potentialInvoiceDate.DayOfWeek == DayOfWeek.Saturday) potentialInvoiceDate = potentialInvoiceDate.AddDays(-1); else if (potentialInvoiceDate.DayOfWeek == DayOfWeek.Sunday) potentialInvoiceDate = potentialInvoiceDate.AddDays(-2); return potentialInvoiceDate; } Speaker: Alexey Golub @Tyrrrz
  45. 45. Inherently testable Speaker: Alexey Golub @Tyrrrz
  46. 46. var result = InvoiceLogic.GetInvoiceDate(instant, day); Speaker: Alexey Golub @Tyrrrz
  47. 47. private static IEnumerable<TestCaseData> GetTestCases() { yield return new TestCaseData( new DateTimeOffset(2019, 12, 05, 00, 00, 00, TimeSpan.Zero), 20, new DateTimeOffset(2019, 11, 20, 00, 00, 00, TimeSpan.Zero) ); yield return new TestCaseData( new DateTimeOffset(2019, 12, 25, 00, 00, 00, TimeSpan.Zero), 22, new DateTimeOffset(2019, 12, 20, 00, 00, 00, TimeSpan.Zero) ); } [Test] [TestCaseSource(nameof(GetTestCases))] public void GetInvoiceDate_Test(DateTimeOffset instant, int day, DateTimeOffset expectedResult) { // Act var actualResult = InvoiceLogic.GetInvoiceDate(instant, day); // Assert Assert.That(actualResult, Is.EqualTo(expectedResult)); } Speaker: Alexey Golub @Tyrrrz
  48. 48. Pure-impure segregation Speaker: Alexey Golub @Tyrrrz
  49. 49. public static class Program { public static void Main(string[] args) { var max = int.Parse(Console.ReadLine()); for (var i = 1; i <= max; i++) { if (i % 2 == 0) Console.WriteLine(i); } } } Logic Side-effects Speaker: Alexey Golub @Tyrrrz
  50. 50. public static class Program { public static IEnumerable<int> EnumerateEvenNumbers(int max) => Enumerable.Range(1, max).Where(i => i % 2 == 0); public static void Main(string[] args) { var max = int.Parse(Console.ReadLine()); foreach (var number in EnumerateEvenNumbers(max)) Console.WriteLine(number); } } Logic Side-effects Speaker: Alexey Golub @Tyrrrz
  51. 51. public static class Program { public static TOut Pipe<TIn, TOut>(this TIn in, Func<TIn, TOut> transform) => transform(in); public static IEnumerable<int> EnumerateEvenNumbers(int max) => Enumerable.Range(1, max).Where(i => i % 2 == 0); public static void Main(string[] args) { Console.ReadLine() .Pipe(int.Parse) .Pipe(EnumerateEvenNumbers) .ToList() .ForEach(Console.WriteLine); } } Speaker: Alexey Golub @Tyrrrz
  52. 52. private readonly ICounterpartyRepository _counterpartyRepository; private readonly ILogger _logger; /* ... */ public async Task<Counterparty> GetCounterpartyAsync(Transaction transaction) { var counterparties = await _counterpartyRepository.GetAll().ToArrayAsync(); foreach (var counterparty in counterparties) { if (!counterparty.SupportedTransactionTypes.Contains(transaction.Type)) continue; if (!counterparty.SupportedCurrencies.Contains(transaction.Currency)) continue; _logger.LogInformation($"Transaction {transaction.Id} routed to '{counterparty}'"); return counterparty; } throw new CounterpartyNotFoundException("No counterparty found to execute this transaction."); } Speaker: Alexey Golub @Tyrrrz
  53. 53. public static async Task<Counterparty> GetCounterpartyAsync( ICounterpartyRepository counterpartyRepository, ILogger logger, Transaction transaction) { var counterparties = await counterpartyRepository.GetAll().ToArrayAsync(); foreach (var counterparty in counterparties) { if (!counterparty.SupportedTransactionTypes.Contains(transaction.Type)) continue; if (!counterparty.SupportedCurrencies.Contains(transaction.Currency)) continue; logger.LogInformation($"Transaction {transaction.Id} routed to '{counterparty}'"); return counterparty; } throw new CounterpartyNotFoundException("No counterparty found to execute this transaction."); } Speaker: Alexey Golub @Tyrrrz
  54. 54. public delegate Task<IReadOnlyList<Counterparty>> AsyncCounterpartyResolver(); public delegate void LogHandler(string message); public static async Task<Counterparty> GetCounterpartyAsync( AsyncCounterpartyResolver getCounterpartiesAsync, LogHandler log, Transaction transaction) { var counterparties = await getCounterpartiesAsync(); foreach (var counterparty in counterparties) { if (!counterparty.SupportedTransactionTypes.Contains(transaction.Type)) continue; if (!counterparty.SupportedCurrencies.Contains(transaction.Currency)) continue; log($"Transaction {transaction.Id} routed to '{counterparty}'"); return counterparty; } throw new CounterpartyNotFoundException("No counterparty found to execute this transaction."); } Speaker: Alexey Golub @Tyrrrz
  55. 55. public static Counterparty GetCounterparty( IReadOnlyList<Counterparty> counterparties, LogHandler log, Transaction transaction) { foreach (var counterparty in counterparties) { if (!counterparty.SupportedTransactionTypes.Contains(transaction.Type)) continue; if (!counterparty.SupportedCurrencies.Contains(transaction.Currency)) continue; log($"Transaction {transaction.Id} routed to '{counterparty}'"); return counterparty; } throw new CounterpartyNotFoundException("No counterparty found to execute this transaction."); } Speaker: Alexey Golub @Tyrrrz
  56. 56. public static Counterparty GetCounterparty( IReadOnlyList<Counterparty> counterparties, Transaction transaction) { foreach (var counterparty in counterparties) { if (!counterparty.SupportedTransactionTypes.Contains(transaction.Type)) continue; if (!counterparty.SupportedCurrencies.Contains(transaction.Currency)) continue; return counterparty; } throw new CounterpartyNotFoundException("No counterparty found to execute this transaction."); } Speaker: Alexey Golub @Tyrrrz
  57. 57. public static Counterparty? TryGetCounterparty( IReadOnlyList<Counterparty> counterparties, Transaction transaction) { foreach (var counterparty in counterparties) { if (!counterparty.SupportedTransactionTypes.Contains(transaction.Type)) continue; if (!counterparty.SupportedCurrencies.Contains(transaction.Currency)) continue; return counterparty; } return null; } Speaker: Alexey Golub @Tyrrrz
  58. 58. public static IEnumerable<Counterparty> GetAvailableCounterparties( IEnumerable<Counterparty> counterparties, Transaction transaction) { return counterparties .Where(c => c.SupportedTransactionTypes.Contains(transaction.Type)) .Where(c => c.SupportedCurrencies.Contains(transaction.Currency)) } Speaker: Alexey Golub @Tyrrrz
  59. 59. private readonly ICounterpartyRepository _counterpartyRepository; private readonly ILogger _logger; /* ... */ public async Task<Counterparty> GetCounterpartyAsync(Transaction transaction) { var counterparties = await _counterpartyRepository.GetAll().ToArrayAsync(); var counterparty = CounterpartyLogic .GetAvailableCounterparties(counterparties, transaction) .FirstOrDefault(); _logger.LogInformation($"Transaction {transaction.Id} routed to '{counterparty}'"); return counterparty ?? throw new CounterpartyNotFoundException("No counterparty found to execute this transaction."); } Speaker: Alexey Golub @Tyrrrz
  60. 60. [HttpGet] public async Task<IActionResult> GetCounterparty(Transaction transaction) { var counterparties = await _dbContext.Counterparties.ToArrayAsync(); var counterparty = CounterpartyLogic .GetAvailableCounterparties(counterparties, transactions) .FirstOrDefault(); _logger.LogInformation($"Transaction {transaction.Id} routed to '{counterparty}'"); if (counterparty is null) return NotFound("No counterparty found to execute this transaction."); return Ok(counterparty); } Speaker: Alexey Golub @Tyrrrz
  61. 61. Speaker: Alexey Golub @Tyrrrz Controller
  62. 62. Speaker: Alexey Golub @Tyrrrz DbContext Controller Pull data Filter counterparties Select counterparty Calculate fee Deduct balance Push data … …
  63. 63. Pure-impure segregation principle • Impure functions can call pure functions • Pure functions cannot call impure functions • Impure functions should be pushed outwards • Work towards maximum purity Speaker: Alexey Golub @Tyrrrz
  64. 64. Speaker: Alexey Golub @Tyrrrz Pure layer Impure layer System boundary
  65. 65. Testing Speaker: Alexey Golub @Tyrrrz
  66. 66. Speaker: Alexey Golub @Tyrrrz Unit tests Functional tests Integration tests
  67. 67. Speaker: Alexey Golub @Tyrrrz Functional tests Unit tests Integration tests
  68. 68. Speaker: Alexey Golub @Tyrrrz Functional tests Logic tests Integration tests
  69. 69. Recipe for reliable tests 1. Cover functional requirements (Functional tests) 2. Cover business logic (Logic tests) 3. Cover system-wide integration (Integration tests) Speaker: Alexey Golub @Tyrrrz
  70. 70. Functional tests shouldn’t be difficult • TestServer (ASP.NET Core) • WebApplicationFactory (ASP.NET Core) • Browser (NancyFx) • TestConsole (System.CommandLine) • VirtualConsole (CliFx) • Docker (*anything*) Speaker: Alexey Golub @Tyrrrz
  71. 71. Summary • Avoid introducing dependencies • Avoid meaningless abstractions • Avoid tests that rely on mocks • Avoid cargo cult programming • Prefer pure-impure segregation • Prefer pure functions for business logic • Prefer pipelines to hierarchies • Prefer functional tests Speaker: Alexey Golub @Tyrrrz
  72. 72. Consider checking out • Functional architecture by Mark Seemann • Async injection by Mark Seemann • Test-induced damage by David Heinemeier Hansson • TDD is dead, long live testing by David Heinemeier Hansson • Functional principles for OOD by Jessica Kerr • Railway-oriented programming by Scott Wlaschin Speaker: Alexey Golub @Tyrrrz
  73. 73. Thank you! Speaker: Alexey Golub @Tyrrrz

×