vendredi 23 septembre 2016

Is this a reasonable approach to building flexible implementations

I am the author of several C# libraries used by thousands of developers. I am constantly asked for custom implementations to enable edge cases. I have used the following approaches and each has their merits. Allow me to list them, if for no other reason so novice developers interested in extensibility might begin to see the patterns available to them.

Inheritance. I use abstract and virtual methods in an non-sealed class that developers can inherit from and override with their logic.

public class DefaultLibrary
{
    public virtual void MyMethod()
    {
        // default logic
    }
}

public class CustomLibrary : DefaultLibrary
{
    public override void MyMethod()
    {
        // custom logic
    }
}

Caveat> Sometimes the classes you write must be sealed. In fact, sealed is a nice default for your classes when you are writing libraries. In this case, you need to consider something else like...

Constructor injection. I have used optional construction parameters in a class enabling developers to pass in custom logic.

public interface IService
{
    void Process();
}

public class DefaultLibrary
{
    IService _service;
    public DefaultLibrary(IService service)
    {
        _service = service;
    }
    public virtual void MyMethod()
    {
        _service.Process();
    }
}

Caveat> Sometimes the classes you are writing need to maintain an internal state that requires them to be singletons (maintaining a single static instance). In this case, you need to consider something else like...

Property injection. I have used factory-like properties which developers can overwrite the default implementation of the class with their own logic.

public interface IService
{
    void Process();
}

public class DefaultLibrary
{
    public IService Service { get; set; }

    public virtual void MyMethod()
    {
        Service.Process();
    }
}

Caveat> Property injection is nice but behaves a lot like constructor injection in that it requires an interface and an implementation of that interface in a concrete class. Sometimes you simply want to allow developers to override a small implementation (a single method or two), much like inheritance (above) but without requiring a base.

This is the problem I am trying to solve.

I want an approach that feels more light-weight to the developer and doesn't introduce a bunch of new moving parts. So, I have landed on this. I would like to propose this approach. I have never used it and cannot defend its merits or pitfalls. For that reason, I am asking this question. Is this pattern reasonable, sensible, problematic, or a wonderful idea? It seems nice.

This pattern likely already has a name. I do not know it. Here's the gist:

public class CustomLibrary
{
    private void CallMyMethod()
    {
        MyMethod?.Invoke();
    }
    public Action MyMethod { get; set; }
}

Here's a full, sample implementation:

private async void CallSaveAsync(string value)
{
    if (RaiseBeforeSave(value))
    {
        await SaveAsync?.Invoke();
        RaiseAfterSave(value);
    }
}

private Func<Task> _saveAsync;
public Func<Task> SaveAsync
{
    get { return _saveAsync ?? DefaultSaveAsync; }
    set { _saveAsync = value; }
}

private async Task DefaultSaveAsync()
{
    await Task.CompletedTask;
}

The short of it? Methods are properties developers can over write.

From an API surface level, there really is no change. The developer still calls await class.SaveAsync() and it works as advertised. However, a developer now has the option to use class.SaveAsync = MyNewMethod without interrupting the internal logic that wraps the method with before and after events.

Acceptable downsides I see right away:

  1. I cannot use ref parameters
  2. I cannot use optional parameters
  3. I cannot use params parameters
  4. I cannot use method overrides

Aside from that I cannot see a dramatic problem with this approach. When the time is right for methods that require ref or optional I will have to change patterns. But why not write my libraries with all the candidate methods exactly like this? It's more code for me, sure. But I don't mind.

Thank you for taking the time.

Aucun commentaire:

Enregistrer un commentaire