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
Because
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
What are your thoughts? Have you had some experience with similar designs?
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.csI 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?
