1

I'm currently working with the following abstraction.

 public interface IFileRepository
    {
        void Save(string identifier, byte[] content);
        Task SaveAsync(string identifier, byte[] content);
        string GetUri(string identifier);
    }

Application services would use this interface to save files on a location based on a certain identifier and also retreive an Uri based on that identifier.

This is already forcing some clients to know more than they need to. For example a client that retrieves and prepares a list of products to show it in a HTML page doesn't have to know about how to save a product image, but only how to access that image via a given Uri.

In the same way some implementations could support the async/await programming model which enforces the methods to Task signature. Not only this is an implementation detail, but also some implementations will start throwing NotImplementedException since they might not support the async/await model. While the first violation of ISP might not hurt so much, the second one I think will almost double some interfaces just because one implementation supports the async/await programming model.

How do you efficiently create these types of abastractions so you would still stay in line with the SOLID principles?

My first thought is

public interface IFileRepository
{
    void Save(string identifier, byte[] content);
    string GetUri(string identifier);
}

public interface IAsyncFileRepository : IFileRepository
{
    Task SaveAsync(string identifier, byte[] content);
}

But I'm afraid I'm getting into the same hole.

  • Your two interface solution isn't the same hole; if an implementation doesn't support IAsyncFileRepository, you're merely prevented from calling its (non-existent) async implementation. – Robert Harvey Feb 12 '17 at 16:22
  • Out of curiosity, what does the GetUri method have to do with the File Repository concern? – Robert Harvey Feb 12 '17 at 16:26
  • I have two implementations for this interface. One knows how to build an Uri for file saved on the local system, the other knows how to build an Uri for the files saved on Amazon S3. – Adrian Iftode Feb 12 '17 at 19:40
  • Have you thought about having 2 services one sync and one async both implementing the same interface? – k3b Feb 13 '17 at 13:44
  • @k3b the problem async requires to return a Task<Result> object and not a Result, so I can't have one interface and async-sync implementations. – Adrian Iftode Feb 13 '17 at 14:11
2

If you don't like combining API's into one interface, you can always use multiple interfaces, or delegate signatures and skip interfaces entirely.

Ignoring SOLID principles (mainly because of the cargo cult problem), why should the implementer of your interfaces implement both the synchronous and asynchronous versions of each API? Either the method provides useful asynchronous behavior, or it does not. So you should only use one or the other.

See Should I expose asynchronous wrappers for synchronous methods?

  • I'm aware of the issue of async wrappers, this is way the implementer should not create wrappers, but provide the proper implementations. You are right about the fact the client decides what abstraction needs so he can choose one of the separated ones. I think I should always think in terms of clients needing abstractions, instead of creating abstractions that don't have much sense by themselves only. What do you think? – Adrian Iftode Feb 16 '17 at 7:13
  • 1
    The clients needs should usually drive interface design. Requiring the client to test whether asynchronous behavior is available and fall back when it isn't exposes an unnecessary implementation detail (unless the client behaves significantly differently in the async case). Always providing an asynchronous interface (even it it is just a stub that delegates to the synchronous version) is the right approach here. Anything else appears to me to walk the line of violating LSP. – Jules Feb 16 '17 at 9:39
  • 1
    To clarify my suggestion, the logic of the linked blog doesn't really apply, because rather than returning a task that performs a synchronous operation in anther that, I would suggest performing it in the caller thread and returning an already completed task, thus not wasting resources on an operation that didn't need a thread to be allocated. – Jules Feb 16 '17 at 9:43
  • 1
    @Jules The API should provide some guidance as to which operations are asynchronous in nature, and which are not. There is no point in providing an asynchronous version of an operation which has no value in being asynchronous. That is, not all operations should be available in asynchronous form, and those that are provided in that form, indicate there is a benefit in using them asynchronously. – Frank Hileman Feb 17 '17 at 17:02
  • @AdrianIftode Only create abstractions that are truly needed, and needed by more than one client. A convenience method is only "profitable" in a reusable library if that method is used by more than one client. – Frank Hileman Feb 17 '17 at 17:03
0

I think dry and kiss are more important than solid anyway. The interface as it stands repeats itself because the same method is essentially in there twice and by doing so it also makes the implementation more complex and raises these questions.

I would only define two synchronous methods. Then there are only two very simple to understand uses. The caller can pick up the burden of wrapping either call asynchronously if it needs to be called that way.

If an interface defines a Get and Save method it is telling me that the service will call either at some point and the implementer must implement both. If that isn't the case I don't think they should be in the same interface.

  • I would actually skip the synchronous signatures and expose only an async one. If you play with UWP a bit, you'll notice most of the newer MS API's only expose async signatures. – RubberDuck Feb 17 '17 at 1:17

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.