0

I'm writing an SDK for the European Space Agency's DISCOs API.

This has around 10 endpoints with different sets of query parameters, each of which returns a different payload type with different link parameters which will also need to be resolved. I explicitly don't want my user to have to construct query strings themselves, these will all be explicitly typed parameters on my client(s)' methods.

So, by the time I've handled all of this, my client class could actually be fairly hefty. As a consequence of this, I'm considering creating a client for each endpoint to handle the validation of query parameters and ensure everything is strongly typed. These can then use a basic, generic "God Mode" client internally to do the actual querying.

The god mode client would look something like this:

public class DiscosClient : IDiscosClient { private readonly HttpClient _client; public DiscosClient(HttpClient client) { _client = client; } public async Task<DiscosResponse<T>?> Get<T>(string endpoint, string query) where T: DiscosModelBase { return await _client.GetFromJsonAsync<DiscosResponse<T>>($"{endpoint}?{query}"); } } 

However, this then means I have a whole bunch of clients which could be a bit of a mess in and of itself.

For example, I'd need DiscosObject, Propellant, Orbit clients with interfaces like these (I've not added filter parameters to my interfaces yet):

public interface IDiscosObjectClient { public Task<List<DiscosObject>> GetObjects(); public Task<DiscosObject> GetObject(); public Task<List<ObjectClass>> GetObjectClasses(); public Task<ObjectClass> GetObjectClass(); } 
public interface IDiscosPropellantClient { public Task<List<Propellant>> GetPropellants(); public Task<Propellant> GetPropellant(); } 
public interface IDiscosOrbitClient { public Task<List<OrbitDetails>> GetInitialOrbits(); public Task<OrbitDetails> GetInitialOrbit(); public Task<List<OrbitDetails>> GetDestinationOrbits(); public Task<OrbitDetails> GetDestinationOrbit(); } 

What's the best way to approach this? Should I just have one super-client which handles all of these endpoints or should I deconstruct that into many smaller clients? Am I missing an obvious trick?

4 Answers 4

1

Don't decide

Provide a root class that serves as a container or factory for all the other objects that will do the real work. You can expose one property per business area, with business-area-specific interfaces. Behind the interfaces, you can use whatever class structure you want; one superclass, many microclasses, or somewhere in between.

The root class also serves as a convenient place to put state, i.e. session-specific settings or resources that need disposal.

public interface IDiscoClient { IDiscoObjectClient Objects { get; } IDiscoOribitClient Oribits { get; } IDiscoPropellantClient Propellants { get; } } public sealed class DiscoClient : IDiscoClient, IDisposable { //Supports IDiscoObjectClient private readonly Foo _foo = new Foo(); //Supports all other interfaces private readonly Bar _bar = new Bar(); public IDiscoObjectClient Objects => _foo; public IDiscoOrbitClient Orbits => _bar; public IDiscoPropellantClient Propellants => _bar; public void Dispose() { _foo.Dispose(); _bar.Dispose(); } } //Client code using (var disco = new DiscoClient( new DiscoSettings { UseSsl = false } )) { var objects = await disco.Objects.GetList(); var orbits = await disco.Orbits.GetList(); var propellants = await disco.Propellants.GetList(); //etc.. } 
7

The best way to approach such kind of SDK design is by writing tests which use the classes or libs, and try out what kind of API will be easier to handle from a users point of view.

By "tests", I don't mean necessarily just a bunch of unit tests. Though unit tests may be a good point to start, you should write a test application, a proof-of-concept, something large enough to see how the SDK works in reality, but small enough you can still refactor the application, or throw it over board in case a certain design does not satisfy your expectations.

Software design in a vacuum does never work well - one needs context, and if there isn't enough context yet, create it by yourself.

1

If you have many grouped methods you could organise them by 'resource' in the client

ie

DiscoClient { IPropellant Propellants; IObject Objects; IOrbit Orbits; } 

This helps the user find the method they want, whilst also retaining a 1 to 1 relationship between client and server and allowing sharing of an underlying httpclient which accesses the same ip address for all methods.

Additionally, separating the methods out gives you a limited scope when refactoring and allows you to shorten your method names; I can do discoClient.Orbits.GetInitial() instead of client.GetInitialDiscoOrbit() removing the guesswork of what order you put the words into the method name and making intellisense options shorter

One thing to note is that you client class in these instances although "big" in terms of number of methods, is actually just a list of the Types, Paths and Parameters for each method and common HttpClient and deserialisation code.

As long as the methods are organised well for the user it doesn't add much extra 'weight' to put all the methods in a single library.

0

Single Library

In general, interface (in C#/Java sense) of a component is defined by a consumer, not by provider. Most good libraries only use interfaces in input/injected arguments, providing actual value via concrete methods. Focus on actual capabilities and API surface/power in library design instead of interfaces it exposes.

If consumer of an API has access to all of its methods, grouping of its methods is irrelevant. Consumers are free to write wrappers/adapters/facades combining or splitting your interfaces and only they have knowledge required for their grouping to be the most effective.

I recommend to use your implementation details as an additional limitation/guide. For example, resource management may be significantly easier when only one object is supplied to consumers.

Group of libraries

If only one interface (from the question) is provided per module/assembly/library, then the question is worth contemplating. From your description is does not look like API could be meaningfully split in completely independent pieces, which makes granular solution unacceptable.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.