Friday Thoughts on abstraction and not wanting to know
Tags: abstraction, architecture
Published: 2024-02-02 15:57:16

Twice this week I found myself in a discussion about how to abstract a [multicast](https://en.wikipedia.org/wiki/Multicast)-like behavior. Both times the discussion revolved around "what part of the code should have what knowledge"?

The first situation was about retrieving information. To abstract the retrieval we introduced an interface - let's say `IRetrievalService`. For reasons beyond the scope of this text some of this information can still be in a legacy system. When we built this application we decided to ignore the legacy system as it had not been updated in years. But analysis showed we would lose revenue, and so the only solution was to also query the legacy service. One colleague introduced the switch logic in the consumer of the service, in our case a controller:

```typescript
try {
    const information = await this.retrievalService.retrieve()
        .catch(async (error) => {
            if (error instanceof NotFoundError) {
                return this.legacyRetrievalService.retrieve();
            }
            throw error;
        });
} catch (error) {
    // handle error scenario
}
```

Another colleague suggested solving this "within" the interface; by introducing a fallback implementation that calls both the current and - if needed - the legacy system:

```typescript
export class FallbackRetrievalService implements IRetrievalService
{
    constructor(
        private readonly retrievalService: IRetrievalService,
        private readonly fallbackRetrievalService: IRetrievalService,
    ) {
    }
    
    public async retrieve(): Promise<unknown> {
        return this.retrievalService.retrieve()
            .catch(async (error) => {
                if (error instanceof NotFoundError) {
                    return this.fallbackRetrievalService.retrieve();
                } 
                throw error;
            });
    }
}
```

Both approaches do _exactly_ the same. The difference is that for the first every part of the codebase that calls the `IRetrievalService` needs to have knowledge about when and how to fall back to the legacy service. The latter approach _abstracts_ that knowledge away with the result that _none_ of the parts of the codebase that call the `IRetrievalService` needs to know about the legacy service. In fact only two parts of the code need to have some knowledge about this behavior; the actual `FallbackRetrievalService` and the code that handles dependency injection/initialization. 

I think that for every abstraction that you write you need to ask yourself the question: Who needs to know what? I have seen too many occurrences where an interface required a table name as parameter - should the code calling the interface have the knowledge that the data is stored in a database? -, and too many occurrences where an interface could throw a database exception or an HTTP exception - again; should the code calling the interface have the knowledge that that is coming from a database or API? No. At least, not in my opinion.

By consequently asking yourself the question "where should this knowledge be?" you can make your code much more flexible and concise. If a year from now we decide to remove the legacy service, all that is to be done is change the dependency injection/initialization and remove a few files. None of the calling code needs to be changed. It also helps you with testing; you want to be sure that the fallback scenario is working as expected. With a `FallbackRetrievalService` you only need to test that class, and you don't need to test all possible variations at every location where it is called. As a bonus it is much easier to ensure the fallback (or multicast) is applied everywhere.