On 'leaky abstractions'

Recently I was reviewing a pull request, and a rather simple piece of code got my attention as a good example of 'leaky abstractions'. So, let's review it. I removed all specific implementation details and made the sample code very generic while maintaining the 'leaking pattern'.


public record Data(int Id, long Value, bool Flag);

public IEnumerable<Data> CallingMethodWithLeakingAbstractions() {
    var input = ProduceInput();
    
    var result = LeakingProcessing(input);
    result.AddRange(input.Where(x => !x.Flag));
    
    // probably do some other things unrelated to modifying the result
    
    return result;
}

private List<Data> LeakingProcessing(IList<Data> source) {
    return source
        .Where(x => x.Flag)
        .Select(ProcessValue)
        .ToList();
}

private Data ProcessValue(Data input) { ... };

We have a Data record (class) and some processing for it. So, why do I think this sample demonstrates the 'leaky abstractions'?

The calling method does a few things:

  • generates or receives from external dependency the input data;
  • calls a processing method (LeakingProcessing) to process the input;
  • performs additional processing of the result;
  • returns the result.

Additional result processing is exactly what I call 'leaking abstractions' here. Since the calling method is the consumer of the input producing and processing methods it should not be 'aware' of the implementation details for 'producing' and 'processing'. Someone may propose that the input processing is a multi-stage process, and the calling method is doing one of those stages. However, we clearly defined responsibility 'boundaries' with method names - 'calling', 'produce', 'processing' - and that is what we expect them to be doing. If the processing is indeed complex and requires multiple stages, all of that complexity should remain inside the processing method.

In this simple example we could maintain the boundaries by writing this kind of code:


public IEnumerable<Data> CallingMethod() {
    var input = ProduceInput();
    var result = NonLeakingProcessing(input);
    // probably do some other things unrelated to modifying the result
    return result;
}

private List<Data> NonLeakingProcessing(IList<Data> source) {
    return source
        .Select(x => x.Flag ? ProcessValue(x) : x)
        .ToList();
}

Abstractions begin to leak when they cross the boundaries we defined either by convention or by architecture design, regardless where the boundaries lie - in methods, classes, projects, or services and processes.

Comments

Popular posts from this blog

Introduction, and welcome!

On AsyncLazy in .NET

On Serilog 'log enrichment' feature