0

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.

Loreno
  • 668
  • 8
  • 26

2 Answers2

2

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.

Camilo Terevinto
  • 31,141
  • 6
  • 88
  • 120
0

When I create asynchronous methods myself, I always do this only for operations that can be done in a new thread, and are safe doing so.

So if I knew that I could make the Save and/or Read methods of the FileStore class run in a separate thread without problem I would definitely change the return values to Tasks.

If the interface must be intact (if it was not created by your project, or some other code is depending on its synchronous methods), async versions of the methods could be created besides the original ones something like this:

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

(It is recommended by the Microsoft coding guidelines to use the suffix Async in the end of any async methods.)

Another way could perhaps be to create a IStoreAsync interface with the async versions, depending on your needs.

Inside the async versions of the methods, they can now create a new thread that calls the synchronous methods, and wait for the thread to complete like this:

async Task<string> ReadAsync(int id) {
  string value = await Task.Run(() => Read(id));
  return value;
}

Note that there are probably a lot safer and better ways to create new threads, but Task.Run works as an example and for test purpose.

einord
  • 2,278
  • 2
  • 20
  • 26
  • Thanks for your input @einord. I like the most your idea of putting Async methods in the interface. That's something that seems to me to be the most "logical" and convenient approach. – Loreno Aug 19 '18 at 17:33
  • 2
    "I always do this only for operations that can be done in a new thread" then you do it for the wrong reasons. Asynchronous methods should be created for asynchronous operations. Any method can be run in a new thread using the `Thread` class, but that won't make it asynchronous. – Camilo Terevinto Aug 19 '18 at 17:53
  • @CamiloTerevinto I see. Could you please give me an example on how to make a method to run asynchronously without creating a new thread? – einord Aug 20 '18 at 09:36
  • @einord Easy: `var client = new HttpClient(); await client.GetAsync("someUrl");`. Asynchronous? Yes. Creates a new thread? No – Camilo Terevinto Aug 20 '18 at 11:09
  • @CamiloTerevinto Sorry, maybe I wasn't clear enough, I asked for an example to make a method run asynchronously, not call an asynchronous method. How does the `GetAsync` method execute its inner code without ultimately creating a new thread? – einord Aug 20 '18 at 13:12
  • Because [there is no thread](https://blog.stephencleary.com/2013/11/there-is-no-thread.html) – Camilo Terevinto Aug 20 '18 at 13:16
  • @CamiloTerevinto thanks! Great article! So as I understand it in the example, the Device driver interrupts the current thread instead. Do you know any code example for how this could be manually done? Or is this normally only occurring on low level methods? – einord Aug 20 '18 at 13:51