How do I wrap a service so it is simpler
https://softwareengineering.stackexchange.com/questions/366039
-
29-01-2021 - |
سؤال
We have a dependency to a third-party service which exposes a gigantic interface of which we only need like 3 methods. Additionally, the interface changes frequently...
I've decided to wrap the interface in a class in our project and only expose the methods that we need.
But I'm unsure how I should handle the return values...
The interface returns an object of type Storage
. We internally have a type StorageModel
which is our internal representation of a Storage
.
What would you return in the mapper: Storage
or StorageModel
?
We have a DataService StorageService
which get a dependency of the wrapper injected.
Currently I'm doing it basically like this:
public class StorageService
{
private readonly IExternalStorageWrapper externalStorageWrapper;
public StorageService(IExternalStorageWrapper externalStorageWrapper)
{
this.externalStorageWrapper = externalStorageWrapper;
}
public StorageModel GetStorage(int storageId)
{
return this.externalStorageWrapper.GetStorage(storageId).ConvertToStorageModel();
}
}
public class ExternalStorageWrapper : IExternalStorageWrapper
{
public Storage GetStorage(int storageId)
{
using(var ext = new ExternalStorage())
{
return ext.GetStorage(storageId);
}
}
}
What would you say:
- Is it good like above, that the wrapper returns the external
Storage
object and the internalStorageService
returns the internalStorageModel
? - Or would you return a
StorageModel
in the wrapper already?
المحلول
In my optinion, the wrapper should deal with all things related to the external library. This means that the public interface of the Wrapper must not name any external types.
Mapping external types to the respective types of your application is part of the wrapper's duties. If this is not a trivial operation, then you may use the various tools available to decompose the problem, e.g. injecting a translator object. However, the translator must still be part of your wrapper module, and no other part of your application may depend on it.
This way, the rest of your application is completely immune not only to changes in the library, but also to replacements of the library for another one.
نصائح أخرى
I've decided to wrap the interface in a class in our project and only expose the methods that we need.
That's ok. This is also known as Adapter.
You choose the Adapter pattern, so the aim here is transforming one interface (library model) into another (domain model). So, if something from the former reaches the domain model, the adapter is failing to its purpose.
According to the previous arguments, the adapter should return the StorageModel
.
Ultimately, your domain "speaks" a specific language, where Storage
is a stranger.
But I'm unsure how I should handle the return values...
The key here is to know for what reason you are wrapping/adapting the library.
Adapter, Decorator, Facade patterns might have similarities but they are fairly different. As different as the problems they solve.
That said, you might be also interested in:
Facade pattern (hiding complexity)
Decorator pattern (interface enhancement). When to use it
What's an anti-corruption layer and how it's used (hiding low quality and messy code)
You can't effectively wrap a library by duplicating it.
What you should wrap is you usage of the library and that means not exposing it objects, in this case Storage. Don't try to duplicate them either.
Use the library, but keep it contained. So in your case, assuming your using the StorageService to store things you should wrap it in repositories
MyPocoObjectRepo
MyPocoObject GetObject(string id);
where MyPocoObject is entirely your data and business logic. Not a duplication of Storage or a DataReader or anything
The answer is that it depends on whether or not you ever need to access Storage
directly from a class that isn't StorageModel
.
If you're going to wrap the library, it makes sense to also wrap the object returned by it to let you future proof changes made by the library in the future. However if you ever need to use Storage
directly, it means there may be need to return Storage
according to the situation. An argument could be said for obliging Storage
usage here to be StorageModel
as you likely want to remain consistent throughout your program.
I would strongly recommend you wrap both the interface and the returned object if you're not already doing so, though again, that only makes sense if you're only using StorageModel
throughout your program and not Storage
.