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

让您的代码不言而喻

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.73/5 (116投票s)

2014年9月1日

CPOL

10分钟阅读

viewsIcon

164975

downloadIcon

513

如何结合多种技术来提高代码的可读性。

目录

引言

当然,有很多最佳实践、模式和建议,比如“给变量起个好名字”、“保持方法简短”、“不要重复自己”等等。这些实践或多或少被称为“干净代码”。但即使有所有这些建议,你仍然无法摆脱一件事:编程语言带来的噪音。如果你使用像 C# 这样的通用语言,那么你必须处理它有限的词汇量。因此,如果不做一些努力,你将不得不阅读噪音才能理解代码的作用。更重要的是,你将不得不重新设计代码的部分内容才能理解其意图。看看一个小例子,花一分钟尝试理解这段代码的意图

var now = DateTime.Now;
if (now.Month == 12 && (now.Day >= 1 && now.Day <= 23)) {
	if (MessageBox.Show("Give a discount?", Application.ProductName, MessageBoxButtons.YesNo, MessageBoxIcon.Question) == DialogResult.Yes) {
		invoice.Amount = Math.Round(invoice.Amount - (invoice.Amount*15/100), 2, MidpointRounding.AwayFromZero);
        MessageBox.Show("The discount was given.", Application.ProductName, MessageBoxButtons.OK, MessageBoxIcon.Information);
	}
}

明白了?所以,这是一段命名恰当的代码(discount、invoice、amount)——我猜,没有它会花费你更长的时间。

但谁能读懂呢?是你、我以及其他有一定编程背景的人。

通常你会这样描述这段代码的意图
如果今天是 12 月 1 日到 23 日之间,并且用户同意给予(你可以称之为“圣诞节”)折扣,那么发票金额将减少 15%,用户将被告知此过程。

嗯,这听起来很简单,但与代码相比……代码看起来一点都不简单。那些带有按钮和图标的 `MessageBox`,`DialogResult` 是怎么回事?使用了 `Math`,但你必须弄清楚 `Math.Round(invoice.Amount - (invoice.Amount*15/100), 2, MidpointRounding.AwayFromZero)` 究竟做了什么,并通过理解 `now.Month == 12 && (now.Day >= 1 && now.Day <= 23)` 来了解哪个日期至关重要。所以代码并没有真正体现出我们上面描述的意图。

现在,为了了解这篇文章是关于什么的,看看下面的代码片段,如果你有兴趣了解它是如何实现的,请继续阅读

if (DateTime.Now.IsBetween(1.December(), 23.December())) {
	if (User.Confirms("Give a discount?")) {
		invoice.Amount = invoice.Amount - 15.Percent();
		Inform.User.About("The discount was given.");
	}
}

现在谁能读懂它?我想你的奶奶也能!;)

一些建议

本文将向您展示如何实现上述可读代码。对一些人来说,这看起来像是过度工程化。但请记住,这只是一个说明可以实现什么的例子。本文将向您介绍那些导致我给出第二个示例的技术。和往常一样,这不是“全有或全无”。明智地选择您的工具,并将其简化为实际解决方案。但有了这些技术,我相信您会时不时地以不同的方式思考您的方法,并且使用其中一种技术可能会为您带来更清晰的解决方案。此外,所有这些都是通过标准的 C# 语法完成的——没有魔法,没有技巧。

可下载的项目包含本文中显示的所有代码。但它并非完全具备任何功能,也未经测试,并且它不会为您提供任何可以直接链接到您的项目并准备好使用的内容——很抱歉……

所有示例都用 C# 编写,但我认为许多展示的技术都可以移植到其他语言。

我们开始吧

首先,我想重申给类、方法和变量起有意义的名字的重要性。使用像“invoice”或“amount”这样的名字是一个好的开始。这不花你任何钱,而且是让你的代码不言自明最简单的方法。我提过它的重要性吗?给你的类型起个有意义的名字,给你的类型起个有意义的名字……

引入新类型

嗯,从领域驱动设计的角度来看,为所有领域特定成员创建类型是一种模式——无论它们是否缺失。它的意图之一是(不出所料)让你的代码反映领域,以便更好地理解。所以我想你已经想到了我的代码片段也使用了一个领域类型:‘invoice’,而且我相信你也使用这种方法——对于典型的场景。但我们可以更频繁地使用这种方法。显然,我又引入了两种类型:一个 `User` 和一个名为 `Inform` 的类。但在底层还有一些其他的类型,它们让代码看起来如此流畅。但你没有注意到它们,对吧?有一个 `Day`,一个 `Amount`,还有一个 `Percentage`——这还不是新类型的完整列表……

好的,现在我能读懂你的想法:“WTF——这太过度工程化了。”

我来解释一下:你对 .net 框架提供的类型感到满意,并且你使用现成的、开箱即用的东西——即使它并不真正适合你的解决方案!

  • 例如,我们对特定的日期感兴趣——不包括年份。但只有一个 `DateTime`,如果我们必须为我们的方法使用它,它就会产生噪音。
  • 我们总是对四舍五入到两位小数的数量感兴趣。但只有一个 `Decimal`,如果我们必须为我们的方法使用它,它就会产生噪音。
  • 我们对一些百分比计算感兴趣。但只有一个 `Decimal`,如果我们必须为我们的方法使用它,它就会产生噪音。

我想你明白了。看看这些类型中的一个——它本身远谈不上过度工程化

public struct Day {
	public int Month { get; private set; }
	public int DayOfMonth { get; private set; }

	public Day(int month, int dayOfMonth)
		: this() {
		Month = month;
		DayOfMonth = dayOfMonth;
	}
}

这些类型称为值类型,将它们引入你的代码,通过使用扩展方法运算符重载来创建流畅的 API。
(关于值类型,你可以看看我的另一篇文章);)

请记住,不一定总需要在代码中直接使用这些类型。它们可以用作与其他类型或结果的连接器,如后面所示。只需确保这些类型反映了您的意图——仅此而已!

扩展方法

扩展方法已经存在很长时间了。所以我认为你已经听说过它们了。简而言之——你可以用自己的方法扩展任何成员并添加功能。它是创建领域特定语言的绝佳方式,这些语言更易于阅读。所以,你也可能听说过流畅接口

在我的代码中,你会发现 `IntExtension`,它通过调用 `1.December()` 等来返回一个 `Day`。

public static Day December(this int dayOfMonth) {
	return new Day(12, dayOfMonth);
}

在上面的代码中,这些 `Day` 被委托给 `DateTimeExtension`,它通过返回 `true` 来判断给定的 `DateTime` 是否在给定的日期之间(嗯,你看代码不言自明……)

public static bool IsBetween(this DateTime value, Day from, Day to) {
	// Compare a dummy leap year instead.
	var comparableValue = new DateTime(2000, value.Month, value.Day);
	var comparableFrom = new DateTime(2000, from.Month, from.DayOfMonth);
	var comparableTo = new DateTime(2000, to.Month, to.DayOfMonth);
	return comparableFrom <= comparableValue && comparableTo >= comparableValue;
}

总而言之,这让你能够这样写:

if (DateTime.Now.IsBetween(1.December(), 23.December())) {...}

不错,不是吗?

顺便说一句,我更喜欢将操作放在相关类型的实例附近。所以,而不是使用 .net 框架的 `String.IsNullOrEmpty(customerName)` 方法,我习惯于编写自己的 `String` 扩展,将其用作 `customerName.IsNullOrEmpty()`。通过扩展方法,您可以与您的实例保持同步——不再使用通用的 `String` 类型。我认为这更具可读性。

如果你的方法返回一个 `bool`,用“Is...”、“Has...”、“Can...”等来命名。在 `Collection` 上引入一个新的扩展方法将允许你读到 `if(listOfArticles.HasEntries()) {...}` 而不是 `if(listOfArticles.Count > 0) {...}` ——你的意图再次是明确的。

但让我们回到我主要的那个代码示例……

另一个 `IntExtension` 叫做 `Percent`,它允许你写 `15.Percent()`。

public static Percentage Percent(this int value) {
	return new Percentage(value);
}

这返回了一个新类型 `Percentage` 的实例,看起来像这样:

public struct Percentage {
	public decimal Value { get; private set; }

	public Percentage(int value) : this() {
		Value = value;
	}

	public Percentage(decimal value) : this() {
		Value = value;
	}
}

有了这个新类型,我们就为下一个技巧做好了准备……

运算符重载

你有没有想过为什么我们可以写 `invoice.Amount = invoice.Amount - 15.Percent()` 并且一切都正常工作?我们的公式去哪儿了?

记住:`15.Percent()` 返回 `Percentage` 的实例。引入这个新类型后,我们现在可以告诉这个类型如何处理所有常见的运算符,比如减号(-)。这很棒(在我看来,显然)!这就是运算符重载,看起来是这样的:

public static decimal operator -(decimal value, Percentage percentage) {
	return value - (value*percentage.Value/100);
}

所以这是我们的公式——再也不会在你的代码其余部分混淆你,并且代码的意图(`invoice.Amount - 15.Percent()`)是绝对清晰的。

隐式转换

我不会保留我之前提到的第三个新类型——`Amount`。对于发票的金额,我首先创建了以下代码片段:

public struct Amount {
	public decimal Value { get; private set; }

	public Amount(decimal value) : this() {
		Value = value.Rounded(decimals: 2);
	}
}

再次,你可以看到新类型的优势:四舍五入现在在构造函数中完成,所以你再也不用考虑它了。它隐藏在你的常规代码中——消除了噪音和复杂性。

如果你一直认真阅读到这里,你可能会注意到我们 `Percentage` 的运算符重载只是返回一个 `Decimal`。但这个 `Decimal` 值被赋给了发票的 `Amount` 属性——它的类型是 `Amout`——而不是 `Decimal`。这是怎么做到的?

很简单——还有一些其他的运算符重载——称为隐式转换(或者再次我的另一篇文章——今天的最后一次推广);)

public static implicit operator Amount(decimal value)
{
	return new Amount(value);
}

public static implicit operator decimal(Amount value)
{
	return value.Value;
}

它们负责从 `Decimal` 到 `Amount` 以及反向的转换。所以编译器知道如何正确处理你的赋值,并且它无缝地集成到你的代码中。

静态助手

我认为你已经完成了这篇文章中最棘手的部分了。

现在,为了摆脱我们起点中嘈杂的代码,只剩下那些嘈杂的 `MessageBox` 了。这可以通过一些新的静态类轻松完成,这些类允许你调用:

User.Confirms("Give a discount?")
Inform.User.About("The discount was given.")

这是代码

public static class User {
	public static bool Confirms(string text) {
		return MessageBox.Show(text, Application.ProductName, MessageBoxButtons.YesNo, MessageBoxIcon.Question) == DialogResult.Yes;
	}
}

public static class Inform {
	public static class User {
		public static void About(string text) {
			MessageBox.Show(text, Application.ProductName, MessageBoxButtons.OK, MessageBoxIcon.Information);
		}
	}
}

简单但有效地减少了噪音。

你可能会担心与其他常见类(如 'User')发生冲突。为此,你可以添加一个名为 'The' 的类,这样你就可以写 `The.User.Confirms("Give a discount?")`。经验法则是,如果方法没有返回值,你可以将类命名为一个动词(如 `Inform`)。动词很少会与命名冲突。

就是这样!

结论

正如你所见,有很多方法可以从你的代码中去除噪音。我的例子可能在某种程度上有些极端,但我的意图是展示通过一些努力可以实现什么——仅仅使用 C# 开箱即用的工具。我最喜欢的方法是在其他类型不满足你的需求时引入新类型。这种方法是从领域驱动设计(DDD)借鉴而来的。DDD 指出为所有领域特定术语引入类型。通过这样做,我问自己为什么不将这种方法用于通用类型,例如 `Day`、`Percentage` 或 `Amount`?通过引入它们,我认识到它通过隐藏计算、使用运算符重载和隐式转换,可以实现更具可读性的代码。作为副作用,你的代码将保留在单个专用位置(新类型)——测试一次,减少错误。

使用扩展方法可以构建一个漂亮的流畅语法——如果合适就引入它们。

使用静态助手类只是在此之上的自由发挥——不太必要——但并非不具可读性。如果你的代码有很多噪音,可以考虑它们。

再次,通过了解你的工具来明智地选择。感谢您的阅读。

接下来呢?

  • 将你自己的类型封装在一个单独的 DLL 中,并构建你自己的“非特定领域”语言,该语言可以在你所有的项目中重复使用。
  • 如果你发现这篇文章很有用,请在投票后继续与我讨论。

链接

历史

  • 2014 年 10 月 27 日:感谢大家投票选出最佳“其他一切”文章
  • 2014 年 9 月 9 日:添加了链接部分
  • 2014 年 9 月 4 日:更改了引言和结论以澄清意图
  • 2014 年 9 月 2 日:修正了一些拼写错误

 

© . All rights reserved.