全书分为两大部分:第一部分“用.NET构建框架”,包括前两章,对构建框架所需的一些基本模式进行讨论;第二部分“创建框架的层”,从第三章到第七章,详细讲解三层架构的设计模式以及商业产品的一些高级话题。
第一章 新的框架、新的模型、新的度量
n 构建应用程序越容易,就越容易产生不成熟的应用程序。
这是关于XML Web服务、分布式应用和.NET的简便性衍生出来的一个问题,由于很多技术在.NET里能够轻松实现,很多程序员都是在对系统框架理解不深刻时就开始开发应用程序,这样的应用程序就很有可能是不成熟的。
n 思想不需要很复杂,但要高效,那些简单而有效的、具有战略性的、具有实用性的设计决定将让你走得更远。
并不是采用越好的技术就越能达到效果,有时候用简单的技术往往就能够很好的满足业务的需要。类似的情况还有,虽然遵守范式是很好的选择,但在实际应用中,大型数据库设计往往出于优化等目的而打破标准的设计范式;在面向对象设计中,纯粹面向对象的设计有时候并非最佳解决方案,等等。因此,因地制宜,用最恰当的技术解决问题才是最佳方案。
BTW,在物流的理论里,记得有句不错的话:“物流不一定非得用飞机、火车,只要满足客户的需求,就是用自行车送货,也是物流。”看来,上述的思想在各行各业都是相通的。
n “过于超前(too much too soon)”的哲学在分布式计算的世界中随处可见。
从哲学的角度去理解系统架构和设计模式,或许有一番新天地,呵呵。
第二章 框架模式:异常处理、日志记录和跟踪
在系统设计中,往往需要提供统一的异常处理机制,在显示给用户友好的错误信息的同时,又能为开发和测试人员提供查错的详细日志,就像书中所说:在运行时提供足够的信息,以便系统能够被有效的监控――这是实现鲁棒(robust)框架的要素之一。
以往开发的系统中(不管是Delphi还是ASP.NET),基本上都是通过自己创建异常基类,以提供统一的异常处理支持,这一点和书中的思路是一致的,但自己以往的设计功能尚不完整(应付那样的系统已经足够了,呵呵),主要关注于日志记录和友好的错误信息显示,用的方案也比较“土”,关于书中提到的异常链接、调用堆栈、Soap异常等问题更是基本上没有深入研究。正像书中所说:错误或者异常处理是每个人都会碰到的事情,但遗憾的是,很少有人能够做的很好。这次,就好好的看一下正规的解决方案。
那么,异常处理框架应该注意的问题有哪些呢?总结下来,有以下几点:
1. 信息是解决任何问题的关键。事实上,出现的错误信息越准确、越充足、越易于理解,对于解决问题就越有利。因此,要尽可能详细、正确的记录异常信息。
2. 抛出异常的地方很多,何时需要记录日志?为了让事情简单一些,我建议在最外面的可视层记录日志,或者就是我所说的异常边界(exception boundary)。这其实同.NET如何看待一个应用程序以及你在哪里指定配置文件是一致的。这也是大家熟知的应用程序级别(application level)。对于GUI的开发,这是很简单的,日志记录在你显示错误之前进行。……只有在被认为是需要的情况下,才进行详细信息的跟踪。除了在异常中记录日志外,如果有特殊的调试要求,我们也可以直接使用Trace.WriteLine之类的方法来输出日志,config文件中<system.diagnostics>配置选项将同时对上述两种方式起作用。
处理SOAP异常和SOAP错误、远程跟踪、自定义SOAP异常处理等和“远程”相关的话题这次不打算深入研究,这里我们只说一说如何建立一个自己的异常基类。.NET已经提供了一个System.Exception作为所有异常的父类,并提供多个具体的异常类供我们使用,比如System.IO.IOException等。为了让用户实现自己的异常类,.NET提供了两个各具意义的类System.ApplicationException和System.SystemException,分别代表应用程序异常(通常不是特别严重,而且是可恢复的)和系统异常(通常是很严重的、不可恢复的),并要求(或者说建议)我们的任何异常类都从这两个类继承(而不要从System.Exception直接继承),其目的就是要明确地区分异常地分类含义。废话少说,我们从System.ApplicationException继承来建立自己的异常类,以集中增加我们所需的功能。一般情况下,在该基类中集中了越多的增值(value-added)特征,在你的框架中整合和权衡就越容易。
书中举例给出的异常基类仅用于说明书中的思想和观点,无法直接编译通过、在实际使用中可能也不是最佳方案。参考其原理后,自己对以前写过的异常类进行整理和完善,并添加了详细的注释和示例,该类已经在VS.NET2003/Windows2000Server环境下测试通过,有需要的朋友可以稍加修改后直接使用:
using System;
using System.Diagnostics;
namespace SunLibrary.Framework
{
/*
本类通过在应用程序名称相对应的配置文件中配置跟踪开关来控制日志的记录,方法如下:
<configuration>
<system.diagnostics>
<!-- 添加跟踪开关,value取值为TraceLevel枚举:Off=0 错误=1 警告=2 Info=3 Verbose=4 -->
<switches>
<add name="GlobalSwitch" value="3" />
</switches>
<!-- 设置自动刷新输出缓冲区,并且缩进4个空格 -->
<trace autoflush="true" indentsize="4">
<!-- 移除默认的跟踪侦听器,并添加自己需要的跟踪侦听器。
对侦听器的添加和移除也可以通过Trace.Listeners.Add/Remove编程方式实现。
下面添加了文件日志和事件日志,一般并不需要记录事件日志,以避免日志被填满 -->
<listeners>
<remove type="System.Diagnostics.DefaultTraceListener"/>
<add name="GlobalLogFileListener" type="System.Diagnostics.TextWriterTraceListener"
initializeData="C:\GlobalLogFileListener.log" />
<add name="GlobalEventLogListener" type="System.Diagnostics.EventLogTraceListener"
initializeData="GlobalEventLogListener" />
</listeners>
</trace>
</system.diagnostics>
</configuration>
*/
/// <summary>
/// 异常基类:为应用程序框架提供统一异常处理机制(主要是异常日志)
/// http://www.cnblogs.com/happyprogram
/// </summary>
public class BaseException : System.ApplicationException
{
/// <summary>
/// 获取全局跟踪开关的静态成员
/// </summary>
static TraceSwitch GlobalSwitch = new TraceSwitch("GlobalSwitch", "应用程序全局跟踪开关");
/// <summary>
/// 异常代码私有成员
/// </summary>
private int m_nCode;
/// <summary>
/// 异常代码
/// </summary>
public int Code
{
get { return m_nCode; }
set { m_nCode = value; }
}
/// <summary>
/// 构造函数
/// </summary>
/// <param name="oSource">引发异常的对象</param>
/// <param name="nCode">异常代码</param>
/// <param name="sMessage">异常描述信息</param>
/// <param name="oInnerException">导致当前异常的异常类(这样就可以跟踪异常堆栈)</param>
/// <param name="bLog">是否要记录日志</param>
public BaseException(object oSource, int nCode, string sMessage, System.Exception oInnerException,
bool bLog) : base("(Code="+nCode+") "+sMessage, oInnerException)
{
//保存引发异常的对象名称和异常代码
if (oSource != null)
base.Source = oSource.ToString();
Code = nCode;
//在指定记录日志并且配置项不是Off时,记录日志
if (bLog)
{
//记录日志
Trace.WriteLineIf(GlobalSwitch.Level!=TraceLevel.Off,Format());
}
}
/// <summary>
/// 构造函数
/// </summary>
public BaseException(){;}
/// <summary>
/// 构造函数
/// </summary>
/// <param name="sMessage">异常描述信息</param>
/// <param name="bLog">是否要记录日志</param>
public BaseException(string sMessage, bool bLog) :
this(sMessage, null, bLog) {;}
/// <summary>
/// 构造函数
/// </summary>
/// <param name="sMessage">异常描述信息</param>
/// <param name="oInnerException">导致当前异常的异常类(这样就可以跟踪异常堆栈)</param>
/// <param name="bLog">是否要记录日志</param>
public BaseException(string sMessage, System.Exception oInnerException, bool bLog) :
this(null, 0, sMessage, oInnerException, bLog) {;}
/// <summary>
/// 构造函数
/// </summary>
/// <param name="sMessage">异常描述信息</param>
/// <param name="bLog">是否要记录日志</param>
public BaseException(object oSource, int nCode, string sMessage, bool bLog) :
this(oSource, nCode, sMessage, null, bLog) {;}
/// <summary>
/// 自定义的格式化异常信息
/// 实现方式和Exception类提供的ToString()类似,但可读性可能更好一些
/// </summary>
/// <returns></returns>
private string Format()
{
//格式化时间和操作系统信息
string res = String.Format("##### {0:yyyy-MM-dd hh:mm:ss} - {1} #####\r\n",
DateTime.Now,Environment.OSVersion);
//遍历异常链,返回格式化信息
Exception oInnerException = this;
while (oInnerException != null)
{
res += FormatException(oInnerException);
oInnerException = oInnerException.InnerException;
}
res += "\r\n";
return res;
}
/// <summary>
/// 格式化单个异常信息
/// </summary>
/// <param name="e"></param>
/// <returns></returns>
private string FormatException(Exception e)
{
//基本信息
string res = "\r\n[" + e.GetType().ToString() + ": " + e.Message + "]\r\n";
//跟踪堆栈
StackTrace st = new StackTrace(e,true);
//遍历堆栈中的帧,生成每一帧的信息
for(int i=0;i<st.FrameCount;i++)
{
//得到当前帧、从当前帧得到当前方法
System.Diagnostics.StackFrame sf = st.GetFrame(i);
System.Reflection.MemberInfo mi = sf.GetMethod();
//格式化方法名称
res += " " + mi.DeclaringType.Namespace + "." + mi.DeclaringType.Name + ": "
+ mi.ToString();
//取得文件名、行号、列号(release版由于优化原因可能得不到以下信息)
if(sf.GetFileName() != string.Empty)
{
res += " in " + sf.GetFileName() + ":" + sf.GetFileLineNumber();
}
res += "\r\n";
}
return res;
}
}
}
三层嵌套异常示例代码(在ASP.NET的WebForm中通过Button1按钮执行)
private void ThrowException1()
{
throw new SunLibrary.Framework.BaseException(this,0,"Inner Exception 1 (no log)",null,false);
}
private void ThrowException2()
{
try
{
ThrowException1();
}
catch (Exception e)
{
throw new SunLibrary.Framework.BaseException(this,0,"Inner Exception 2 (no log)",e,false);
}
}
private void Button1_Click(object sender, System.EventArgs e)
{
try
{
ThrowException2();
}
catch (System.Exception innerException)
{
throw new SunLibrary.Framework.BaseException(
this,1,"Outer Exception (write log here)",innerException,true);
}
}
执行后记录的日志结果如下:
##### 2002-01-01 12:43:26 - Microsoft Windows NT 5.0.2195.0 #####
[SunLibrary.Framework.BaseException: (Code=1) Outer Exception (write log here)]
[SunLibrary.Framework.BaseException: (Code=0) Inner Exception 2 (no log)]
SunExampleWeb.WebForm1: Void ThrowException2() in d:\exampleweb\webform1.aspx.cs:62
SunExampleWeb.WebForm1: Void Button1_Click(System.Object, System.EventArgs)
in d:\exampleweb\webform1.aspx.cs:70
[SunLibrary.Framework.BaseException: (Code=0) Inner Exception 1 (no log)]
SunExampleWeb.WebForm1: Void ThrowException1() in d:\exampleweb\webform1.aspx.cs:51
SunExampleWeb.WebForm1: Void ThrowException2() in d:\exampleweb\webform1.aspx.cs:58
以上应该是建立.NET异常处理框架的一个初步的实践,可以基于上述基础进行更多的扩展。
本章的小结是这样的:
在.NET中的异常处理不仅仅是抛出对象以及捕获对象。对于一个健壮的系统,需要考虑许多设计元素;而提供一个良好的异常处理、日志记录和跟踪架构是其中的首要步骤。在这一章中,我们描述了一些最佳实践,用来决定何时抛出、捕获和最终记录错误。
我们讨论了Web服务应用程序对异常处理的差别。本章最有价值的部分就是你如何利用FCL,以便通过跟踪提供全局的错误日志控制。最后,对于构建远程跟踪工具,我提供了一个起点;这样,作为一个服务提供者,你就能够从Internet的任何地方接收产品系统的完整而详细的信息。因此,你的异常框架设计得越好,你花费在支持模式上的时间就越少,你就会有更多时间进行编码。这应该是很有诱惑力的
。