65.9K
CodeProject 正在变化。 阅读更多。
Home

SimpleHelpers 来拯救世界!

starIconstarIconstarIcon
emptyStarIcon
starIcon
emptyStarIcon

3.30/5 (8投票s)

2017年8月13日

CPOL

5分钟阅读

viewsIcon

10638

Helper 方法应该是简单的,而这些 SimpleHelpers 也能让你的代码更简单!

引言

很久很久以前,在一个遥远的Thus data center 中,有一位程序员厌倦了一次又一次地编写相同的代码。他喝着咖啡,走到打印室去打印他 400 页的源代码,然后回到他的打孔卡牌组,突然灵光一闪:如果我只写一张打孔卡,然后在其他打孔卡中重用它呢?我就可以节省 40 张打孔卡(和大约 50 页的源代码)!

Helper 方法就此诞生。

好吧,这可能只是对当时发生的事情有点夸张的描述,但谁知道呢?在一个无限的宇宙中,星球大战在哪里都可能是一部纪录片。重点是,样板代码已经存在很久了,而且困扰了很久。如今,我们有 Resharper 和 IDE,它们会神奇地告诉你如何写出更好的代码。你不再需要自己琢磨如何写那段代码的最佳方式,你告诉 IDE 你想要什么,它就会提供重排代码的选项。

但是,当那个神奇的黑盒子无法识别你的样板代码模式,并且不提供帮助时,该怎么办?你会拿出你发霉的、可重用的打孔卡牌吗?不!你可以从 nuget.org 下载 GPS.SimpleHelpers,让你的生活变得轻松很多。

背景

GPS.SimpleHelpers 是一个类库集合,它可以帮助你快速处理一些非常棘手的用例,这些用例在 lambda 出现并永远改变了世界(朝着好的方向)之前,一直没有好的处理方式。

Simple Helpers 目前涵盖了三个问题领域:代码计时、异常处理和数据编组。

Using the Code

首先,你可以从 Github 获取完整的源代码。如果你不关心完整的源代码,只想开始使用 GPS.SimpleHelpers,你可以使用你喜欢的包管理器(我喜欢 paket)通过安装包 GPS.SimpleHelpers 从 Nuget.org 获取它。

秒表

首先,让我们谈谈你项目中一些最重复的样板代码。使用 Stopwatch 类进行指标收集。我们都写过这样的代码

var sw = new Stopwatch();

sw.Start();

// Do something expensive

System.Diagnostics.Debug.WriteLine(sw.Elapsed);
sw.Stop();

这多写了四行代码!如果能用一行代码完成所有这些工作,那该多好?

StopwatchHelpers.TimeAction(() => /* Do something expensive */,
  e => Debug.WriteLine);

现在我们已经封装了这个模式,不必一遍又一遍地编写相同的样板代码了。

让我们看看我们是如何做到的。

public static void TimeAction(Action action, Action<long> onFinish)
{
  var sw = new System.Diagnostics.Stopwatch();

  sw.Start();

  action();

  onFinish(sw.ElapsedMilliseconds);

  sw.Stop();
}

实际上是相同的代码,我们只是将两个 Actions 作为输入来执行。这项技术的一个优点是,你可以封装你的时间处理逻辑,比如这样。

public void LogTimer(long elapsed)
{
  if(elapsed >= 1000) _log.DEBUG($RED ALERT! {elapsed} ms passed!);

  if(elapsed >= 500) _log.INFO(${elapsed} ms passed.);
}

// ..
StopwatchHelpers.TimeAction(MyLongAction, LogTimer);
// ..

现在你可以将复杂的处理逻辑注入到你的计时中,而不会污染你的方法中不相关的逻辑。

TimeAction 有以下重载。它们的工作方式都相同

public static TReturn TimeAction<TData, TReturn>(
 TData value, Func<TData, TReturn> func, Action<long> onFinish)
public static void TimeAction<TData>(
     TData value, Action<TData> action, Action<long> onFinish)
public static void TimeAction(Action action, Action<long> onFinish)
public static long TimeAction(Action action)
public static long TimeAction<TData>(TData value, Action<TData> action)

Try-Catch-Finally

另一个常见的样板代码活动是 try-catch-finally 块。

var dbContext = new DbContext();
var exceptions = new ConcurrentQueue<Exception>();

try
{
  dbContext.Table.ForEach(rec =>
  {
  try
    {
      SomeActionsWithDbContext(dbContext);
    }
    catch (Exception ex)
    {
      ex.LogSomewhere(your message);
      enqueue(exceptions, ex);
    }
  }
}
finally
{
  someCleanupLogic(exceptions);
  dbContext.Dispose();
}

ProcessExceptions(exceptions);

这是很多代码!让我们用 SafeCallHelpers 再试一次。

var dbContext = new DbContext();
var exceptions = new ConcurrentQueue();

SafeCallHelpers.TryCall(() =>
  { enqueue(exceptions, SafeCallHelpers.TryCall(() =>
    { someActionsWithDbContext(dbContext); });
  }
  ,() =>
  { 
    someCleanupLogic(exceptions);
    dbContext.Dispose();
  });

ProcessExceptions(exceptions);

现在,你可以移除那些丑陋的样板代码模板。使用这种模式有几个优点

  • 1. 强制你实现一致的 Exception 处理。
  • 2. 强制你实现一致的 finally 块。非常容易忘记使用 finally 块来释放你的 IDisposable 对象。

这个助手可能不是你的风格。没关系,与 StopwatchHelpers 不同,它是我个人偏好,并且不喜欢深度嵌套的 try-catch-finally 块,我觉得那些块很难阅读,尤其是随着嵌套越来越深。

数据编组

并发处理会让你开始理解为异步编码的价值(是的,我想我刚编了一个词)。为此,我们有像 ConcurrentDictionary 这样的结构,它使用 AddOrUpdateGetOrCreate 方法。GetOrCreate 非常有趣,因为它强制从 ConcurrentDictionary 返回一个值,所以你永远不需要为 null 返回值编写代码。

从语义上讲,这是非常有力的。当结果保证是请求对象的实例时,移除 null 引用检查可以使代码更简洁。微软通过 null 传播运算符提供了很大的帮助,但你仍然必须考虑传播产生的 null

以这段代码为例。

var dictionary = new ConcurrentDictionary<string, Record>();
using(var dbContext = new DbContext())
{
  Parallel.ForEach(dbContext.Table.Where(rec => rec.Field == someKey), rec =>
  {
    dictionary.AddOrUpdate(someKey, rec);
  }
}
var value = dictionary.GetOrAdd(someSpecificKey,
  () => { return new Record { RequiredField = Unset }; });

微软在 .Net 4.0 中的并行处理增强使得编写线程安全的代码变得轻而易举。但是如果我们想用顺序代码做同样的事情呢?你仍然可以使用 ConcurrentDictionary,但与标准的 Dictionary 对象相比,它的开销很大。

var dictionary = new Dictionary<string, Record>();
using(var dbContext = new DbContext())
{
  foreach(var rec in dbContext.Table.Where(rec => rec.Field == someKey))
  {
    dictionary.Add(someKey, rec);
  }
}
var value = dictionary[someKey];
if(value == null) value = new Record { RequiredField = Unset };

这是合理的,对吧?除了它非常非常容易忘记 null 检查,然后你就可能遇到臭名昭著的 null 引用异常。让我们用 Marshaller 来看看。

//  The Loop
var value = SafeMarshalling.GetOrBuild(
  () => { return dictionary[someKey]; },
  () => { return new Record { RequiredField = Unset }; });

太棒了!没有 null 引用检查!你总会得到一个值。

整合所有内容

好的,让我们把它们串联起来,证明它们都能正常工作。😊

var exceptions = new List<Exception>();
var dictionary = new Dictionary<string, string>();

AddException(exceptions, (SafeCallHelpers.TryCall(() =>
{
  StopwatchHelper(() =>
  {
    dictionary.Add(someKey, SafeMarshalling.GetOrBuild(
      () => SomeExpensiveGetter,
      () => SomeExpensiveBuilder));
  }, elapsed => LogElapsedTime);
}, CleanupMethod));

在这九行代码中,我们做了以下工作

  • 使用昂贵的 getter 或昂贵的 builder 来创建一个 object
  • 将该对象添加到 Dictionary
  • 捕获任何 Exceptions
  • 释放任何 IDisposable 对象。

让我们看看用传统方式编写的相同代码。

var exceptions = new List<Exception>();
var dictionary = new Dictionary<string, string>();
try
{
  var sw = new Stopwatch();
  sw.Start();
  var someValue = SomeExpensiveGetter();
  if(someValue == null) someValue = SomeExpensiveBuilder();
  dictionary.Add(someKey, someValue);
  LogElapsedTime(sw.Elapsed);
  sw.Stop();
}
catch(Exception ex)
{
  AddException(exceptions, ex);
}
finally
{
  CleanupMethod();
}

代码量翻倍了!而且我个人觉得它并没有更简洁。

结论

很容易认为 helper 方法只是真正的程序员不需要的拐杖。我不知道我是否是真正的程序员,但我从 13 岁就开始写代码了,并且在应用可重复的代码模式来解决复杂问题方面拥有相当不错的职业生涯。在你自己尝试之前,不要轻易否定 helper 方法的威力!

有趣的事实

我早在 1984 年就在 Commodore VIC 20 上的 CBM BASIC 2.0 中写下了我的第一个 helper 方法(子程序)。

修订

版本 1.0.0

  • 初始版本
© . All rights reserved.