C
C#3w ago
giadif

Inheritance or composition

This is going to be a long one, totally get it if nobody makes it to the end 😅 I'm writing an open-source library for writing durable workflows, similar in principle to Azure Durable Functions, where you can write persistent workflows using async/await. However, unlike Durable Functions, my library introduces a dedicated awaitable type called DTask. Because DTask requires infrastructural support, you can't just await one inside a regular async method. To run them, providers must implement its infrastructure components (for storage access, serialization, etc.). One of these components is called DAsyncHost and, as of today, is an abstract class: it exposes and implements some methods to interact with these durable tasks (StartAsync, ResumeAsync), but also defines several abstract callbacks that implementations must override. For example, OnDelayAsync determines how to handle DTask.Delay, OnSucceedAsync defines what to do when a durable task completes. Link to the class: https://github.com/gianvitodifilippo/DTasks/blob/main/src/DTasks/Infrastructure/DAsyncHost.cs I went with an abstract class so I could provide virtual methods with default implementations and add new features in the future without breaking existing implementations (thanks to virtuals). However, I'm not a big fan of abstract classes and I usually prefer composition over inheritance, and I’m starting to wonder if DAsyncHost should have been a concrete class that could instead be injected into those classes that today are its child classes. I'm struggling now even more because I want to add a new overload of the StartAsync method that allows callers to pass a context object that the host can access during the whole execution. To do so, I thought of adding a type parameter to the type (DAsyncHost<TContext>) and a new StartAsync(TContext context, ...) method, but this started to feel like a smell to me. What are your thoughts? Have you had some experience with similar designs?
4 Replies
Ꜳåąɐȁặⱥᴀᴬ
i may be wrong but adding a context seems like a big change, isn't it going to touch a lot of types?
giadif
giadifOP3w ago
A few. Today I have only AspNetCoreDAsyncHost, but I plan on implementing AzureFunctionsDAsyncHost and external libraries may want to implement their own provider. I was wondering "what if they don't want/need the context" or "what if they want callers to pass extra arguments to their public methods" Since the StartAsync method returns ValueTask, you can't run multiple DTask objects in parallel using the same host, so I was also thinking of keeping it as it is and have implementations specify their own methods to pass extra values to the execution. Something like:
using (var contextScope = host.SetContext(myContext))
{
await host.StartAsync(...);
}
using (var contextScope = host.SetContext(myContext))
{
await host.StartAsync(...);
}
Anton
Anton3w ago
Your issue is that the implementation convenience is tied to the usage. Nothing stops you from having an abstract class for convenience of the users of your API. Internally, use an interface. Now these are two separate things and there's no more problem.
giadif
giadifOP2w ago
I think you pointed me in the right direction, I'll see how to apply this in my case. Thanks a lot!

Did you find this page helpful?