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. Speaker: Alexey Golub @Tyrrrz
Ledger
LedgerRepository
LedgerService
LedgerController
System boundary (entry point)
Service layer
Data access layer
Domain model
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. Causational indirectness
• Hard to trace
• Hard to reason about
• Implicit dependencies & DI made us lazy
Speaker: Alexey Golub @Tyrrrz
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. 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. 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. Obscured complexity
• Module complexity is reduced
• Total complexity is increased
• Assumptions between communicating modules
Speaker: Alexey Golub @Tyrrrz
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. 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. Abstraction is a great tool
and a terrible goal
Speaker: Alexey Golub @Tyrrrz
28. Is OOP the wrong tool for
the job?
Speaker: Alexey Golub @Tyrrrz
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. 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. 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. 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. 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. 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. 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. 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. 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
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. [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
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
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. 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