我是数千名开发人员使用的几个C#库的作者。我经常被要求进行自定义实现以启用边缘情况。我使用了以下方法,每个方法都有其优点。允许我列出它们,如果没有其他原因,那么对可扩展性感兴趣的新手开发人员可能会开始看到他们可用的模式。
继承即可。我在非密封类中使用abstract
和virtual
方法,开发人员可以使用它们的逻辑继承和override
。
public class DefaultLibrary
{
public virtual void MyMethod()
{
// default logic
}
}
public class CustomLibrary : DefaultLibrary
{
public override void MyMethod()
{
// custom logic
}
}
买者>有时您编写的课程必须是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();
}
}
买者>有时您正在编写的类需要维护一个内部状态,要求它们是单例(维护单个static
实例)。在这种情况下,您需要考虑其他类似的东西......
物业注入。我使用了类似工厂的属性,开发人员可以使用自己的逻辑覆盖类的默认实现。
public interface IService
{
void Process();
}
public class DefaultLibrary
{
public IService Service { get; set; }
public virtual void MyMethod()
{
Service.Process();
}
}
买者>注入属性很好但行为与构造函数注入非常相似,因为它需要interface
并在具体interface
中实现class
。有时你只是想让开发人员覆盖一个小实现(一个或两个方法),就像继承(上面)但不需要基础。
我想要一种让开发人员感觉更轻盈的方法,并且不会引入一堆新的移动部件。所以,我已经着手了。我想提出这种方法。我从未使用它,也无法捍卫它的优点或陷阱。因此,我问这个问题。 这种模式是合理的,明智的,有问题的还是一个好主意?看起来不错。
这种模式可能已有名称。我不知道。这是要点:
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
而不会中断使用事件前后事件封装方法的内部逻辑。
我立即看到了可接受的缺点:
ref
参数optional
参数params
参数除此之外,我无法看到这种方法存在严重问题。如果时间适合需要ref
或optional
的方法,我将不得不更改模式。但是为什么不用这样的所有候选方法编写我的库呢?对我来说,这是更多的代码,当然。但我不介意。
感谢您抽出宝贵时间。
答案 0 :(得分:1)
这是一个有效的选择,但它有一些缺点(正如你已经提到的)。
除了你提到的那些之外,还有一个事实是你的方法不能是通用的。 (例如,你不能拥有Func<T><string, T>
。
我不建议使用属性,当不同的客户开始写入属性时,这可能会变得混乱。它会创建共享状态,这很难排除故障。
我宁愿使用构造函数注入那些方法。实施例
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
}
}
如果你有一个单例类,而不是设置属性注入,我会创建一个配置方法,它与上例中的构造函数相同。这样可以更容易地查看这些Func的配置位置。例如:
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
(因为它们总是不同),或者他可以选择创建一个局部变量并一直传递它(因为它始终是相同的)。
上述内容也适用于单身人士。
答案 1 :(得分:1)
我认为你的解决方案很好,但过度设计 - 还有一些问题。
鉴于您的解决方案,执行初始/基本逻辑会很痛苦。根据您的情况:
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<Task>
,而不知道此方法的提供者所处的状态。
恕我直言,这是一件非常危险的事情 - 假设方法提供者仍然会做它应该做的所有工作。
这种模式是合理的,合理的,有问题的还是一个好主意?
在可用性方面,这种模式肯定有其缺点。
如上所述,您可以限制任何库消费者使用库的方式。如果仅为消费者创建override
的可能性,即使对于static
和sealed
类,您的解决方案也没问题。
但是,您已经提到的限制以及其他人提到的限制都超出了此解决方案的优势。可以提供自定义实现的可能方式是有限的。
在经济效率方面,这种模式不是很好。
创建,修改和维护库所需的额外工作将对工作负载产生中等至巨大的影响。以专业的方式,我将应用 KISS 原则 - 作为库提供者,提供可扩展性不是您的任务。在 消费方 上有足够多的模式来处理这个问题。