本节将介绍以下内容:
好的代码,是练出来的。坏的代码,是惯出来的。
那么,代码是写给计算机的吗?不是,代码其实是写给人的。Martin Fowler说:任何一个傻瓜都可以写出计算机可以理解的代码。唯有写出人类容易理解的代码,才是优秀程序员。那么,本文要探讨的其实是写出给人看的好代码,不涉及具体的代码技巧,只关注泛化的代码实践,通过一系列条款来过滤应该关注的好代码和坏代码。
命名到底有多重要呢?
重要到这几乎是很多软件项目成功或者失败的“罪魁祸首”,究其原因,代码不光支撑了0和1在计算机系统中运行的业务逻辑,同时也是开发者进行交流与研究的标准语言。没有意义或者有歧义的命名,就像两个等待交流的人,面对了一堆火星文无从下口,让交流变成灾难,也就导致很多问题。
同时,好的命名是自说明的,让代码告诉开发者“我是谁,我做什么,我怎么做”。当然,除了静态式的必要的注释说明之外,动态式的代码也可以包含传递信息的作用,让代码告诉你它自己,因为代码是“活的代码”。
例如,以某个缓存容器为例,泛型参数明确了容器的Key和Value的关系,其中的方法也基本明确了作为缓存容器所具有的方法:Add、Set、Clear、Refresh和IsExist,而TryGetValue是Try-Parse模式的应用体现。其中的变量container表示了容器载体;expiration表示了过期时间;config表示了容器的配置信息。
public class AtCache<TKey, TValue>
{
public int Count{ }
public List<TValue> Items{ }
public int Expiration { }
public void Add(TKey key, TValue value){ }
public void Set(TKey key, TValue value, int expiry){}
public bool TryGetValue(TKey key, out TValue value){}
public void Clear(){ }
public bool IsExist(TKey key){ }
protected void Refresh(){ }
private ReaderWriterLockSlim rwLocker = new ReaderWriterLockSlim();
private Dictionary<TKey, CacheItem<TKey, TValue>> container = new Dictionary<TKey, CacheItem<TKey, TValue>>();
private int expiration;
private DateTime lastRefresh = DateTime.Now;
private IAtCacheConfiguration config;
private List<TValue> items;
}
总体来说,让代码告诉它自己,是好代码的体现,而一堆没有意义的代码堆积是让人无法接受和容忍的坏代码。
编码规范,就是编码最佳实践,是前辈在编码这件事上的积累和总结,是智慧的延续和工业的实践。在软件产业日益蓬勃的今天,软件工业在于如何更有效率地进行生产这件事儿上,有了巨大的进步和积累,编码规范正是如此。例如可以随意列出很多的规范:
在以上每个领域都有N条“法规”,以最佳实践的条款被总结出来,每个条款都渗透着很多前人的智慧。同时,编码规范的应用是有选择和场合的,不同的软件公司和产品,对编码规范都有一定的理解和取舍。但是,没有规范的编码,一定是有问题、潜伏着坏代码的幽灵。
命名已经被反复强调了,遵守编码规范首当其冲就是对于命名规范的遵守,对于命名规则,通常可选择的体系主要有:
不过,对于不同的语言体系而言,一般有着不同的命名规范和体系,很多不同的语言对于命名规范的选择也有差别。以C#语言为例,最基本的命名规则包括:
以Pascal Casing风格定义命名空间、类及其成员、接口、方法、事件、枚举等。
· 以Camel Casing规范定义参数、私有成员。
· 避免使用匈牙利命名法。
· 以Attribute作为特性的后缀。
· 以Delegate作为委托的后缀。
· 以Exception作为异常的后缀。
当然,规范还有很多,而这种积累来自于平时对于代码的理解和运用。
代码,一定是给人看的,而代码本身的逻辑又决定于方法、类型和依赖的关系之中,所以,必要的注释,是必需且必要的。通过注释的进一步解释,来辅助性地告知代码的逻辑、算法或者流程,不仅是好习惯,更是好代码。另一方面,注释不是“无病呻吟”,没有必要表述那些显而易见的逻辑或者说明,同时注意区分单行注释和多行注释的应用。
在.NET平台下,XML格式的注释还肩负了另一项重要的使命,那就是根据注释生成代码文档。例如:
///
/// 根据用户信息,构建标签信息
///
/// 用户Id,根据用户Id,获取 的实例信息
/// 标签信息
/// 标签信息对象
public Tag BuildTag(int memberId, string tag)
{
return new Tag();
}
在Visual Studio中,可以通过选择PropertiesàBuild来设置“XML documentation files”选项输出生成XML信息,例如上面的注释信息被生成为:
<?xml version="1.0"?>
<doc>
<assembly>
<name>Anytao.Inside.Ch03.GoodCode</name>
</assembly>
<members>
<member name="M:Anytao.Inside.Ch03.GoodCode.Tag.BuildTag(System.Int32,System.String)">
<summary>
根据用户信息,构建标签信息
</summary>
<param name="memberId">用户Id,根据用户Id,获取<see cref="T:Anytao.Inside.Ch03. GoodCode.Member"/>的实例信息</param>
<param name="tag">标签信息</param>
<returns>标签信息对象</returns>
</member>
</members>
</doc>
通过SandCastle工具就可以基于上述信息生成标准统一的文档信息,基于此方式就可以建立类似于MSDN文档的项目帮助文件,大大简化了这项“复杂”的工作。
命名空间,是逻辑上的组织单元,通过命名空间建立对代码的有机组织,是现代语言的一大“创举”,《Java夜未眠》作者蔡学镛说:一个语言是否适合大型开发,可以从它对模块、命名空间(或类似概念)支持的良窳看出端倪。从这个意义上说,命名空间并不是大型开发或者团队开发最重要的核心概念,但却是加分的必要因素。
关于.NET命名空间的详细内容,请参考7.3节“using的多重身份”。
设计模式是好的,而滥用模式是不好的。了解和熟悉设计模式,是需要实践和思考的过程,模式并不是一切问题的灵丹妙药,而且大多时候的滥用反而造成更多的问题。滥用模式体现在两个方面:
举一个简单的例子,策略模式是将算法从宿主类中剥离出来,将易于变化的部分封装为接口,例如:
public interface ITax
{
decimal Calculate(decimal value);
}
public class FoodTax : ITax
{
public decimal Calculate(decimal value)
{
return new decimal(1 + 0.15) * value;
}
}
public class RetailTax : ITax
{
public decimal Calculate(decimal value)
{
return new decimal(1 + 0.1) * value;
}
}
对于算法分离而言,通过ITax策略可以很好地进行不同行业(例如饮食FoodTax或者零售RetailTax)税率的计算,不同的行业提供不同的算法策略,然而对于变化的税率而言,这种实现的方式略显过度,越来越多的算法策略将造成代码的过度膨胀。所以完全可以对策略的方式进行改良,利用委托将税率算法分离看起来更加简洁而优雅:
public interface ITax
{
decimal Calculate(Func<decimal> rateProvider, decimal value);
}
public class Tax : ITax
{
public decimal Calculate(Func<decimal> rateProvider, decimal value)
{
var rate = rateProvider.Invoke();
return rate * value;
}
}
一下子清爽了很多,避免了“策略”带来过度膨胀,又很好地解决了税率算法的变化与分离,对于客户端的消费并没有太大的差别。
《倚天屠龙记》中有一个重要的片段,张三丰指点张无忌修炼太极,有一段“此时无招胜有招”的精彩论述,武术上真正的无敌不在乎一招一式的死记硬背,也不在于一刀一剑的激情挥洒。同样的道理,似乎更适合用于软件设计与模式,很多时候,架构与设计的极致不在于对模式的“应用”,而在于对模式的“活用”,在于灵魂附体,在于无招胜有招。
线程安全是重要的,在数据共享或同步的场合应将线程安全作为必须考虑的因素,不安全的代码将在多线程运行时造成严重的问题。例如,单例模式就是这样一个需要特别注意的例子:
public sealed class Singleton
{
Singleton() { }
public static Singleton Instance
{
get
{
if (instance == null)
{
instance = new Singleton();
}
return instance;
}
}
private static Singleton instance = null;
}
因此,你可以考虑通过“双锁”机制来保证线程的安全,不过在.NET平台还可以有更简单的实现方式:
public sealed class Singleton
{
static Singleton() { }
Singleton() { }
public static Singleton Instance
{
get
{
return instance;
}
}
static readonly Singleton instance = new Singleton();
}
利用静态构造函数只能被执行一次且在运行库加载类成员时的特点,保证了instance的线程安全,避免了不必要的锁检查开销。关于静态构造函数,详见8.8节“动静之间:静态和非静态”。
线程安全是个大课题,需要仔细咀嚼。
软件开发就像爬山,而有意思的事情在于,我们爬的并不是一座山,而是一座又一座的山,似乎永无尽头。所以爬山的过程其实是这样,爬上了一座,又从这一座下来,然后接着爬向下一座,并且继续如此反复,才能到最高的巅峰。
图3-15 软件开发的爬山模型(图片来源:百度百科,上图为富春山居图部分截图)
所以,可以把软件开发中这种不断重构和完善的过程,叫作爬山模型(图3-15)。爬山模型的重点在于只有通过不断地重构和演进,才能不断地完善和进步,并最终达到软件产品的高峰。
代码重构是个系统工程,有很多值得借鉴的方法,在《Refactoring: Improving the Design of Existing Code》一书中有详细的讨论:
纵观本书,也从很多方面对于重构提供了思考和实践。
扩展性是衡量一个软件产品的重要尺度之一。通过合适的设计为软件系统赋予一定程度的扩展,是架构师着手设计的重要考虑因素,如图3-16所示。
图3-16 架构的考量
扩展是个大课题,涉及软件系统的方方面面,依赖于粒度不同的架构格局。举例来说,数据库设计可以考虑在横向或纵向的扩展、在多层架构中实现可适配的数据层、为业务层实现注入逻辑设计、在UI层提供可配置的界面选择以及为物理架构提供横向扩展的部署设计。实现基于服务的系统,就意味着在服务层支持扩展良好的高层架构;而一个面向接口的设计,将是为扩展提供可能的选择之一;采用ASP.NET MVC构建的Web系统,将在很多方面被赋予扩展的标签,基于管线模型的设计将扩展深入到几乎所有的方面,例如ActionFilter、ViewEngine、Route、HtmlHelper、ModelBinder以及Controller,开发者可以轻易地替换所有原有支持元素,扩展出不同的“个性”功能;而MEF(Managed Extensibility Framework)则实现了更灵活的扩展设计,基于MEF可以发现并使用扩展,甚至在应用程序之间重用扩展。
在语言层面,考量扩展性的指标遍布于.NET语言特性的各个细节:
private void btnLogin_Click(object sender, RoutedEventArgs e)
{
MessageBox.Show("Hello, Windows Phone.");
}
就像给框架提供了一个“钩子”来动态地将用户代码扩展到框架的逻辑,在单击按钮的时候,执行用户代码的流程逻辑,并将这个流程注入到框架行为中。在.NET中,可以通过委托实现线程的安全回调,而事件正是这种模式的最佳实践,详情参考9.7节“一脉相承:委托、匿名方法和Lambda表达式”。
[global::System.Data.Linq.Mapping.TableAttribute(Name="dbo.Users")]
public partial class User : INotifyPropertyChanging, INotifyPropertyChanged
{
}
在这种情况下,就可以考虑通过部分类的方式,为实体类User增加新的成员、继承统一的基类:
public partial class User : EntityBase
{
public bool IsAdmin { get; set; }
}
<httpModules>
<add name="TimeLogModule" type="Anytao.Devkit.Core.Web.Modules.TimeLogModule, Anytao.Devkit.Core" />
</httpModules>
例如,上述配置可以将TimeLogModule注入到HTTP管道,从而将每个请求的处理时间输出到日志。
public class TimeLogModule : IHttpModule
{
public void Dispose()
{
}
public void Init(HttpApplication context)
{
context.BeginRequest += (sender, e) =>
{
var sw = new Stopwatch();
HttpContext.Current.Items["StopWatch"] = sw;
sw.Start();
};
context.EndRequest += (sender, e) =>
{
var sw = (Stopwatch)HttpContext.Current.Items["StopWatch"];
sw.Stop();
TimeSpan ts = sw.Elapsed;
string result = string.Format("{0}ms", ts.TotalMilliseconds);
Logger.Log(result);
};
}
}
扩展无处不在。软件设计师的职责,在于将这种无处不在深入到软件系统的各个环节,为各种可能提供基础与准备。
性能,永远是任何软件产品衡量的标准,就像一把标准的千分尺,可以精度准确地为产品打上分数,在.NET中性能的指标体现在语言的各个方面,在本书6.4节“性能优化的多方探讨”中,对于性能的问题有详细讨论。
质量的保证,一直是复杂的软件开发的软肋,为了保证软件产品的完美,测试是整个开发流程中最重要的部分。现代软件开发也衍生出很科学的测试方法、方式和制度,不管是黑盒的还是白盒的,只要逮住Bug,就是好测试。与传统测试方式比较,测试驱动开发(Test Driven Development,TDD)已经被证明是非常靠谱和科学的开发方式。TDD至少在两个方面为软件开发注入活力:
因此,测试驱动是值得提倡和普及的,将由人的信任测试,转变为由代码的信任测试,信任的是测试,而不是开发者自己。
开发者经常挂在嘴边的一句话是:给我足够的时间,我将实现得更好。然而,实际的情况往往是,开发的周期和开发的进度总是存在着冲突,进而带来进度和质量之间的妥协,而妥协的关键在于平衡。
作为开发者而言,需要评估设计和实现所花费的时间,然后根据评估的结果对进度做以平衡,很多时候,并没有一次就很完美的设计,只有当下适合的设计。平衡进度与质量的关键,是建立起行之有效的开发流程和进度计划,将资源、进度和质量有机地整合在可控制的框架管理中,并准备好三个要素之间的缓冲带,在适合的时候做好调整的准备。
破的窗,将导致更多的窗户被打破,是《程序员修炼之道》一书阐释的“破窗效应”。而亡羊补牢,未为晚也,养成好的代码习惯和意识,学会独立地思考和重构,远远重要于在破的窗补破的局。