【问题标题】:Is this a reasonable approach to building flexible implementations [closed]这是构建灵活实现的合理方法吗?
【发布时间】:2016-09-23 16:50:40
【问题描述】:

我是数以千计的开发人员使用的几个 C# 库的作者。我经常被要求自定义实现以启用边缘情况。我使用了以下方法,每种方法都有其优点。请允许我列出它们,如果没有其他原因,那么对可扩展性感兴趣的新手开发人员可能会开始看到可供他们使用的模式。

继承。我在开发人员可以继承的非密封类中使用abstractvirtual 方法,并根据他们的逻辑使用override

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

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

Caveat> 有时你写的类必须是sealed。事实上,sealed 是您编写库时类的一个很好的默认值。在这种情况下,您需要考虑其他类似...

构造函数注入。我在一个类中使用了optional 构造参数,使开发人员能够传入自定义逻辑。

public interface IService
{
    void Process();
}

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

Caveat> 有时,您正在编写的类需要维护一个内部状态,该状态要求它们是单例(维护一个 static 实例)。在这种情况下,您需要考虑其他类似...

属性注入。我使用了类似工厂的属性,开发人员可以用他们自己的逻辑覆盖类的默认实现。

public interface IService
{
    void Process();
}

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

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

Caveat> 属性注入很好,但它的行为很像构造函数注入,因为它需要interface 和在具体class 中的interface 实现。有时您只是想让开发人员覆盖一个小的实现(一个或两个方法),就像继承(上图)但不需要基础。

这是我要解决的问题。

我想要一种让开发人员感觉更轻量且不会引入大量新移动部件的方法。所以,我已经登陆了。我想提出这种方法。我从未使用过它,也无法捍卫它的优点或缺陷。出于这个原因,我在问这个问题。 这种模式是合理的、明智的、有问题的还是一个绝妙的想法?看起来不错。

这种模式可能已经有了名字。我不知道。这是要点:

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

这是一个完整的示例实现:

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;
}

短吗?方法是开发人员可以覆盖的属性。

从 API 表面来看,确实没有任何变化。开发人员仍然调用await class.SaveAsync() 并且它像宣传的那样工作。但是,开发人员现在可以选择使用class.SaveAsync = MyNewMethod,而不会中断使用前后事件包装方法的内部逻辑。

我马上看到的可接受的缺点:

  1. 我不能使用ref参数
  2. 我不能使用optional参数
  3. 我不能使用params参数
  4. 我不能使用方法覆盖

除此之外,我看不出这种方法有什么严重的问题。当时间适合需要refoptional 的方法时,我将不得不更改模式。但是为什么不用完全像这样的所有候选方法来编写我的库呢?当然,这对我来说是更多的代码。但我不介意。

感谢您抽出宝贵时间。

【问题讨论】:

  • 它似乎是Strategy 模式。
  • 这过于基于意见。
  • @DanielA.White 确实如此。这个问题属于programmers.stackexchange.com - 即使目标群体可能正在这里阅读它。
  • @jaco0646:是的,但是策略模式具有强制性接口。如果我正确理解了这个问题,那么接口就是 OP 试图避免的。
  • @JerryNixon:在我看来,这实际上是一个稍微复杂一点的高阶函数。是什么阻止您简单地公开 Action&lt;T&gt; 参数、Func&lt;T&gt; 参数,甚至是 Task&lt;T&gt; 参数来完成同样的事情?

标签: c# design-patterns dependency-injection uwp


【解决方案1】:

这是一个有效的选项,但它有一些缺点(正如您已经提到的)。

除了你提到的那些之外,还有一个事实是你的方法不能是通用的。 (例如,您不能有 Func&lt;T&gt;&lt;string, T&gt;)。

虽然我不建议使用属性,但当不同的客户端开始写入属性时可能会变得混乱。它会创建很难排除故障的共享状态。

我宁愿为这些方法使用构造函数注入。示例

public class SomeClass
{
    readonly Func<string> _createId;
    public SomeClass() : this(null) {}
    public SomeClass(Func<string> createId)
    {
        _createId = createId ?? () => Guid.NewGuid().ToString();
    }

    public void SomeMethod()
    {
        var id = _createId();
        // do something
    }
}

如果你有一个单例类,而不是设置属性注入,我会创建一个配置方法,它与上面示例中的构造函数相同。这样可以更轻松地查看这些功能的配置位置。示例:

public static class SomeClass
{
    static Func<string> _createId = () => Guid.NewGuid().ToString();
    public static void Configure(Func<string> createId)
    {
        if(createId == null) throw new ArgumentNullException(nameof(createId));
        _createId = createId;
    }

    public static void SomeMethod()
    {
        var id = _createId();
        // do something
    }
}

另一种选择是在方法参数中请求 Func。

public class SomeClass
{
    public void SomeMethod() => 
        SomeMethod(() => Guid.NewGuid().ToString())

    public void SomeMethod(Func<string> createId)
    {
        var id = createId();
        // do something
    }
}

这会让它变得更麻烦,因为客户端每次调用方法时都必须提供一个 Func,但也更灵活和“简单”,因为现在他可以在每次调用时看到会发生什么。 这允许开发人员自己选择他想如何配置他的代码组织。他可以在每次调用时传入不同的Func(因为它们总是不同的),或者他可以选择创建一个局部变量并一直传递它(因为它总是相同的)。

上述方法也适用于单例。

【讨论】:

  • 我认为Configure() 是一个不错的选择。
【解决方案2】:

我认为您的解决方案是一个不错的解决方案,但设计过度 - 还有一些问题。

调用基本实现 - 可扩展性与允许自定义实现

鉴于您的解决方案,执行初始/基本逻辑会很痛苦。鉴于您的情况:

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()
{
    // Complete and return a fancy default task, like - actually saving stuff
}

提供自定义实现

假设我想使用自定义实现

// ExecuteCustomSave being a method with matching signature
FancyClass.SaveAsync = ExecuteCustomSave;
await FancyClass.SaveAsync();

在这种情况下,将执行 自定义实现 - 很好。 因此,您提供的解决方案是一个不错的方法。


提供可扩展性选项

现在假设我想利用一些可扩展性

FancyClass.SaveAsync = ExecuteCustomSave;
await FancyClass.SaveAsync(); // FancyClass.DefaultSaveAsync is not being called

要调用DefaultSaveAsync,必须这样写:

await FancyClass.SaveAsync();
FancyClass.SaveAsync = ExecuteCustomSave;
await FancyClass.SaveAsync();

甚至这个,取决于所需的执行顺序:

FancyClass.SaveAsync = ExecuteCustomSave;
await FancyClass.SaveAsync();
FancyClass.SaveAsync = null;
await FancyClass.SaveAsync();

维护自定义逻辑的可用性

正如您在问题中所写,您还希望能够为 static 类提供可扩展性 - 如果我误解了这一点,请纠正我。

让我们看一下这个场景(即使它是构造的):

// Let's assume the class is static this time
public static class FancyClass
{ [...] }

public class MyAwesomeExtension : IDisposable
{
    public MyAwesomeExtension()
    {
        // Override SaveAsync with custom logic
        FancyClass.SaveAsync = Save;
    }

    public async Task Save()
    {
        // Do something, might throw if in disposed state
    }

    // Implement IDisposable
}

public class SomeOtherClass
{
    public async Task SaveAllChanges()
    {
        await FancyClass.SaveAsync().ConfigureAwait(false);
    }
}

FancyClass 将调用提供的Func&lt;Task&gt;,而不知道此方法的提供者处于什么状态。 恕我直言,这是一件非常危险的事情——假设方法提供者仍然会完成它应该做的所有工作。


结论

这种模式是合理的、明智的、有问题的还是一个绝妙的想法?

就可用性而言,这种模式肯定有其缺点。
如上所述,您将限制任何图书馆使用者使用您的图书馆的方式。如果为消费者创造了提供override 的可能性,即使是staticsealed 类,您的解决方案也很好。 但是,您已经提到的限制以及其他人提到的限制都夸大了此解决方案的好处。可以提供自定义实现的可能方式是有限的。

在经济效益方面,这种模式并不是很讨喜。
创建、修改和维护库所需的额外工作将对工作负载产生中等到巨大的影响。以专业的方式,我会应用 KISS 原则 - 作为库提供者,提供可扩展性不是您的任务。在消费方面,有足够多的模式来处理这个问题。

【讨论】:

    猜你喜欢
    • 2019-03-10
    • 1970-01-01
    • 2010-10-15
    • 2013-12-02
    • 2021-04-30
    • 1970-01-01
    • 1970-01-01
    • 2012-11-11
    • 2019-09-01
    相关资源
    最近更新 更多