OO之美:好代码和坏代码

本节将介绍以下内容:

  • 编码的规范
  • 面向对象指导

引言

好的代码,是练出来的。坏的代码,是惯出来的。

那么,代码是写给计算机的吗?不是,代码其实是写给人的。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;
}

总体来说,让代码告诉它自己,是好代码的体现,而一堆没有意义的代码堆积是让人无法接受和容忍的坏代码。

遵守编码规范

编码规范,就是编码最佳实践,是前辈在编码这件事上的积累和总结,是智慧的延续和工业的实践。在软件产业日益蓬勃的今天,软件工业在于如何更有效率地进行生产这件事儿上,有了巨大的进步和积累,编码规范正是如此。例如可以随意列出很多的规范:

  • 命名规范。
  • 避免行数过多的方法。
  • 代码缩进。
  • 异常规范。
  • 设计规范。
  • 注释规范。
  • 文件的组织规范。
  • 配置规范。
  • 发布与部署规范。
  • 测试规范。
  • SQL规范。

在以上每个领域都有N条“法规”,以最佳实践的条款被总结出来,每个条款都渗透着很多前人的智慧。同时,编码规范的应用是有选择和场合的,不同的软件公司和产品,对编码规范都有一定的理解和取舍。但是,没有规范的编码,一定是有问题、潜伏着坏代码的幽灵。

遵守命名规则

命名已经被反复强调了,遵守编码规范首当其冲就是对于命名规范的遵守,对于命名规则,通常可选择的体系主要有:

  • Pascal Casing,混合使用大小写字母,每个单词的首字母必须是大写,例如FirstName。
  • Camel Casing,混合使用大小写字母,第一个单词的首字母是小写,其他单词的首字母是大写,例如firstName。
  • 匈牙利命名法,通过属性、类型和对象描述混合来表示,例如frmMainWindow,表示一个窗体实例的命名。

不过,对于不同的语言体系而言,一般有着不同的命名规范和体系,很多不同的语言对于命名规范的选择也有差别。以C#语言为例,最基本的命名规则包括:

以Pascal Casing风格定义命名空间、类及其成员、接口、方法、事件、枚举等。

· 以Camel Casing规范定义参数、私有成员。

· 避免使用匈牙利命名法。

· 以Attribute作为特性的后缀。

· 以Delegate作为委托的后缀。

· 以Exception作为异常的后缀。

当然,规范还有很多,而这种积累来自于平时对于代码的理解和运用。

多注释,少废话

代码,一定是给人看的,而代码本身的逻辑又决定于方法、类型和依赖的关系之中,所以,必要的注释,是必需且必要的。通过注释的进一步解释,来辅助性地告知代码的逻辑、算法或者流程,不仅是好习惯,更是好代码。另一方面,注释不是“无病呻吟”,没有必要表述那些显而易见的逻辑或者说明,同时注意区分单行注释和多行注释的应用。

在.NET平台下,XML格式的注释还肩负了另一项重要的使命,那就是根据注释生成代码文档。例如:

/// <summary>
/// 根据用户信息,构建标签信息
/// </summary>
/// <param name="memberId">用户Id,根据用户Id,获取<see cref="Member"/>的实例信息</param>
/// <param name="tag">标签信息</param>
/// <returns>标签信息对象</returns>
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语言特性的各个细节:

  • 基于类的继承、组合和多态。面向对象的基本特征就是扩展良好,而.NET的面向对象特性,在本书第1章 “OO大智慧”已经有了详细的讨论。
  • 面向接口和抽象类,接口和抽象类是语言层的抽象载体,而面向抽象编程的设计原则在实际编码中的应用之一就是面向接口和抽象类,详见8.4节“面向抽象编程:接口和抽象类”。
  • 基于委托和事件的回调。回调是一种扩展良好的实现机制,提供动态扩展性表现,使得框架能够以委托来调用用户代码:
private void btnLogin_Click(object sender, RoutedEventArgs e)
{
    MessageBox.Show("Hello, Windows Phone.");
}

就像给框架提供了一个“钩子”来动态地将用户代码扩展到框架的逻辑,在单击按钮的时候,执行用户代码的流程逻辑,并将这个流程注入到框架行为中。在.NET中,可以通过委托实现线程的安全回调,而事件正是这种模式的最佳实践,详情参考9.7节“一脉相承:委托、匿名方法和Lambda表达式”。

  • 应用扩展方法。扩展方法,本身就是为扩展而生的,详见13.2节“赏析C# 3.0”。
  • 以部分类延伸类的组织,在很多情况下,为了便于组织和物理上的方便,将一个类分布在多个独立的文件,是一种合适的处理方式;另一方面,对于越来越多的自动生成代码情况,部分类提供了“手动”扩展支持。例如,应用LINQ to SQL作为数据访问层框架时,通常可以通过Visual Studio自动生成实体类:
[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; }

}
  • 通过反射注入。反射特性是.NET平台非常有吸引力的语法游戏,通过反射可以实现动态注入设计,在3.2节“依赖的哲学”中有详细的讨论。
  • 基于DLR实现动态扩展。在.NET 4.0中,巨大的变革即是动态编程,为静态语言插上动态的翅膀,让动态扩展无处不在,详见14.3节“动态变革:dynamic”。
  • 让扩展可配置。在ASP.NET整体架构中,将Web请求的处理设计为管道模型,模型中的重要元素包括HttpApplication、HttpModule和HttpHandler等,而对于这些管道中的过滤器(HttpModule)和处理器(HttpHandler)则通过配置实现可插拔的扩展性设计:
<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);
            };
    }
}
  • 基于ConfigurationManager的配置扩展。一般来说,配置是为扩展而准备的,而扩展可通过配置注入。在.NET框架中提供非常优秀的配置支持,开发者完全可以通过这套完美的配置框架实现自定义的配置扩展。
  • 硬编码总是不好的。任何时候都尽可能将变化的部分从代码中分离,以配置或者其他方式加载,为扩展提供机会。

扩展无处不在。软件设计师的职责,在于将这种无处不在深入到软件系统的各个环节,为各种可能提供基础与准备。

性能是一把尺子

性能,永远是任何软件产品衡量的标准,就像一把标准的千分尺,可以精度准确地为产品打上分数,在.NET中性能的指标体现在语言的各个方面,在本书6.4节“性能优化的多方探讨”中,对于性能的问题有详细讨论。

信赖的是测试,不是自己

质量的保证,一直是复杂的软件开发的软肋,为了保证软件产品的完美,测试是整个开发流程中最重要的部分。现代软件开发也衍生出很科学的测试方法、方式和制度,不管是黑盒的还是白盒的,只要逮住Bug,就是好测试。与传统测试方式比较,测试驱动开发(Test Driven Development,TDD)已经被证明是非常靠谱和科学的开发方式。TDD至少在两个方面为软件开发注入活力:

  • 保证质量。足够的覆盖率将能保证软件质量和稳定性,系统的修改和变化将第一时间反馈在测试代码上,结果驱动的方式将能最大限度地评估变化对原有系统造成的影响,从而保证业务代码的正确性。
  • 驱动设计。另一方面,为了能够让业务代码具有可测试性,你应重新审视业务代码的设计,就像 Bob 大叔的名言:编写单元测试更像一种设计行为,文档行为而不是验证行为。编写单元测试缩短了反馈周期读数,最小读数基于功能验证。

因此,测试驱动是值得提倡和普及的,将由人的信任测试,转变为由代码的信任测试,信任的是测试,而不是开发者自己。

是进度还是质量,平衡是关键

开发者经常挂在嘴边的一句话是:给我足够的时间,我将实现得更好。然而,实际的情况往往是,开发的周期和开发的进度总是存在着冲突,进而带来进度和质量之间的妥协,而妥协的关键在于平衡。

作为开发者而言,需要评估设计和实现所花费的时间,然后根据评估的结果对进度做以平衡,很多时候,并没有一次就很完美的设计,只有当下适合的设计。平衡进度与质量的关键,是建立起行之有效的开发流程和进度计划,将资源、进度和质量有机地整合在可控制的框架管理中,并准备好三个要素之间的缓冲带,在适合的时候做好调整的准备。

结论

破的窗,将导致更多的窗户被打破,是《程序员修炼之道》一书阐释的“破窗效应”。而亡羊补牢,未为晚也,养成好的代码习惯和意识,学会独立地思考和重构,远远重要于在破的窗补破的局。

参考文献

  • Erich Gamma, Richard Helm, Ralph Johnson, John Vlisside,设计模式:可复用面向对象软件的基础
  • Martin Flower, Closure, http://www.martinfowler.com/bliki/Closure.html
  • 刘艺,Delphi设计模式,机械工业出版社
  • Juditb Bishop, C# 3.0 Design Patterns, O’Reilly
  • Brad Abrams, Internal Coding Guidelines,
  • http://blogs.msdn.com/b/brada/archive/2005/01/26/361363.aspx
©️2020 CSDN 皮肤主题: 岁月 设计师:pinMode 返回首页