Search code examples
c#interfacesolid-principlesobject-composition

How to use async code when previous implementation was synchronous


Recently I was learning about composition in object oriented programming. In many articles about it, there's an example of FileStore class (or however you call it) that implements interface like:

interface IStore
{
   void Save(int id, string data);
   string Read(int id);
}

FileStore class implements this interface and everything is fine. It can save and read files. Now, tutorial articles often say that FileStore can be easily exchanged to, for example, SqlStore, because it can implement the same interface. This way client classes can store to SQL instead of filesystem, just by injecting SqlStore instead of FileStore. It sounds nice and everything, but when I actually thought about it, I don't really know how to implement it. Database operations are good candidates to be done asynchronously. In order to use this fact, I would need to return a Task instead of just string in Read(int id) method. Also Save(int id, string data) could return a Task. With this in mind, I cannot really use IStore for my SqlStore class.

One solution I see is to make IStore like this:

interface IStore
{
   Task Save(int id, string data);
   Task<string> Read(int id);
}

Then, my FileStore class would need to be changed a little bit, it would use Task.FromResult(...).

This solution seems inconvenient to me, because FileStore has to pretend some asynchronous characteristics.

What is the solution that you would propose? I'm curious, because in tutorials everything always sound easy and doable, but when it comes to actual programming, things get complicated.


Solution

  • I've seen different ways to solve this problem, but the chosen approach always depend on the situation you are.

    1. Changing the interface to be Task-based.

    If you don't have any implementations (or they can be easily changed) or you don't want to keep any synchronous version, the easiest is to just change the interface:

    interface IStore
    {
       Task SaveAsync(int id, string data);
       Task<string> ReadAsync(int id);
    }
    
    1. Extending the interface

    Another option is to add the asynchronous version of the methods to the interface. This might not be the best option if there are implementations that are synchronous or if there are too many implementations already.

    interface IStore
    {
        void Save(int id, string data);
        Task SaveAsync(int id, string data);
    
        string Read(int id);
        Task<string> ReadAsync(int id);
    }
    
    1. Creating an asynchronous version through inheritance

    This option makes the most sense when you can't change the original interface or when not all implementations need an asynchronous version. Of course, you'd need to decide whether you ask for an IStore or an IAsyncStore.

    interface IStore
    {
        void Save(int id, string data);
        string Read(int id);
    }
    
    interface IAsyncStore : IStore
    {
        Task SaveAsync(int id, string data);
        Task<string> ReadAsync(int id);
    }
    

    Note: for the case of I/O operations (which both FileStore and SqlStore do in your example), there should only be asynchronous methods.