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.

1

Share

Download to read offline

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

Download to read offline

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

Related Books

Free with a 30 day trial from Scribd

See all

Related Audiobooks

Free with a 30 day trial from Scribd

See all

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
  • AlexeyGolub

    Dec. 13, 2019

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

Views

Total views

149

On Slideshare

0

From embeds

0

Number of embeds

0

Actions

Downloads

1

Shares

0

Comments

0

Likes

1

×