《解剖PetShop》系列之 四:ASP.NET缓存 五:业务逻辑层设计

《解剖PetShop》系列之四 PetShop之ASP.NET缓存

四 PetShop之ASP.NET缓存
如果对微型计算机硬件系统有足够的了解,那么我们对于Cache这个名词一定是耳熟能详的。在CPU以及主板的芯片中,都引入了这种名为高速缓冲存 储器(Cache)的技术。因为Cache的存取速度比内存快,因而引入Cache能够有效的解决CPU与内存之间的速度不匹配问题。硬件系统可以利用 Cache存储CPU访问概率高的那些数据,当CPU需要访问这些数据时,可以直接从Cache中读取,而不必访问存取速度相对较慢的内存,从而提高了 CPU的工作效率。软件设计借鉴了硬件设计中引入缓存的机制以改善整个系统的性能,尤其是对于一个数据库驱动的Web应用程序而言,缓存的利用是不可或缺 的,毕竟,数据库查询可能是整个Web站点中调用最频繁但同时又是执行最缓慢的操作之一,我们不能被它老迈的双腿拖缓我们前进的征程。缓存机制正是解决这 一缺陷的加速器。
4.1  ASP.NET缓存概述
作为.Net框架下开发Web应用程序的主打产品,ASP.NET充分考虑了缓存机制。通过某种方法,将系统需要的数据对象、Web页面存储在内存 中,使得Web站点在需要获取这些数据时,不需要经过繁琐的数据库连接、查询和复杂的逻辑运算,就可以“触手可及”,如“探囊取物”般容易而快速,从而提 高整个Web系统的性能。
ASP.NET提供了两种基本的缓存机制来提供缓存功能。一种是应用程序缓存,它允许开发者将程序生成的数据或报表业务对象放入缓存中。另外一种缓存机制是页输出缓存,利用它,可以直接获取存放在缓存中的页面,而不需要经过繁杂的对该页面的再次处理。
应用程序缓存其实现原理说来平淡无奇,仅仅是通过ASP.NET管理内存中的缓存空间。放入缓存中的应用程序数据对象,以键/值对的方式存储,这便于用户在访问缓存中的数据项时,可以根据key值判断该项是否存在缓存中。
放入在缓存中的数据对象其生命周期是受到限制的,即使在整个应用程序的生命周期里,也不能保证该数据对象一直有效。ASP.NET可以对应用程序缓 存进行管理,例如当数据项无效、过期或内存不足时移除它们。此外,调用者还可以通过CacheItemRemovedCallback委托,定义回调方法 使得数据项被移除时能够通知用户。
在.Net Framework中,应用程序缓存通过System.Web.Caching.Cache类实现。它是一个密封类,不能被继承。对于每一个应用程序域, 都要创建一个Cache类的实例,其生命周期与应用程序域的生命周期保持一致。我们可以利用Add或Insert方法,将数据项添加到应用程序缓存中,如 下所示:

Cache[”First”] = “First Item”;
Cache.Insert(”Second”, “Second Item”);


我们还可以为应用程序缓存添加依赖项,使得依赖项发生更改时,该数据项能够从缓存中移除:

string[] dependencies = {”Second”};
Cache.Insert(”Third”, “Third Item”,
new System.Web.Caching.CacheDependency(null, dependencies));


与之对应的是缓存中数据项的移除。前面提到ASP.NET可以自动管理缓存中项的移除,但我们也可以通过代码编写的方式显式的移除相关的数据项:

Cache.Remove(”First”);


相对于应用程序缓存而言,页输出缓存的应用更为广泛。它可以通过内存将处理后的ASP.NET页面存储起来,当客户端再一次访问该页面时,可以省去 页面处理的过程,从而提高页面访问的性能,以及Web服务器的吞吐量。例如,在一个电子商务网站里,用户需要经常查询商品信息,这个过程会涉及到数据库访 问以及搜索条件的匹配,在数据量较大的情况下,如此的搜索过程是较为耗时的。此时,利用页输出缓存就可以将第一次搜索得到的查询结果页存储在缓存中。当用 户第二次查询时,就可以省去数据查询的过程,减少页面的响应时间。
页输出缓存分为整页缓存和部分页缓存。我们可以通过@OutputCache指令完成对Web页面的输出缓存。它主要包含两个参数: Duration和VaryByParam。Duration参数用于设置页面或控件进行缓存的时间,其单位为秒。如下的设置表示缓存在60秒内有效:

< %@ OutputCache Duration=“60“ VaryByParam=“none“ %>


只要没有超过Duration设置的期限值,当用户访问相同的页面或控件时,就可以直接在缓存中获取。
使用VaryByParam参数可以根据设置的参数值建立不同的缓存。例如在一个输出天气预报结果的页面中,如果需要为一个ID为txtCity的TextBox控件建立缓存,其值将显示某城市的气温,那么我们可以进行如下的设置:

< %@ OutputCache Duration=”60” VaryByParam=”txtCity” %>


如此一来,ASP.NET会对txtCity控件的值进行判断,只有输入的值与缓存值相同,才从缓存中取出相应的值。这就有效地避免了因为值的不同而导致输出错误的数据。
利用缓存的机制对性能的提升非常明显。通过ACT(Application Center Test)的测试,可以发现设置缓存后执行的性能比未设置缓存时的性能足足提高三倍多。
引入缓存看来是提高性能的“完美”解决方案,然而“金无足赤,人无完人”,缓存机制也有缺点,那就是数据过期的问题。一旦应用程序数据或者页面结果 值发生的改变,那么在缓存有效期范围内,你所获得的结果将是过期的、不准确的数据。我们可以想一想股票系统利用缓存所带来的灾难,当你利用错误过期的数据 去分析股市的风云变幻时,你会发现获得的结果真可以说是“失之毫厘,谬以千里”,看似大好的局面就会像美丽的泡沫一样,用针一戳,转眼就消失得无影无踪。
那么我们是否应该为了追求高性能,而不顾所谓“数据过期”所带来的隐患呢?显然,在类似于股票系统这种数据更新频繁的特定场景下,数据过期的糟糕表 现甚至比低效的性能更让人难以接受。故而,我们需要在性能与数据正确性间作出权衡。所幸的是,.Net Framework 2.0引入了一种新的缓存机制,它为我们的“鱼与熊掌兼得”带来了技术上的可行性。
.Net 2.0引入的自定义缓存依赖项,特别是基于MS-SQL Server的SqlCacheDependency特性,使得我们可以避免“数据过期”的问题,它能够根据数据库中相应数据的变化,通知缓存,并移除那 些过期的数据。事实上,在PetShop 4.0中,就充分地利用了SqlCacheDependency特性。
4.2 SqlCacheDependency特性
SqlCacheDependency特性实际上是通过System.Web.Caching.SqlCacheDependency类来体现的。 通过该类,可以在所有支持的SQL Server版本(7.0,2000,2005)上监视特定的SQL Server数据库表,并创建依赖于该表以及表中数据行的缓存项。当数据表或表中特定行的数据发生更改时,具有依赖项的数据项就会失效,并自动从 Cache中删除该项,从而保证了缓存中不再保留过期的数据。
由于版本的原因,SQL Server 2005完全支持SqlCacheDependency特性,但对于SQL Server 7.0和SQL Server 2000而言,就没有如此幸运了。毕竟这些产品出现在.Net Framework 2.0之前,因此它并没有实现自动监视数据表数据变化,通知ASP.NET的功能。解决的办法就是利用轮询机制,通过ASP.NET进程内的一个线程以指 定的时间间隔轮询SQL Server数据库,以跟踪数据的变化情况。
要使得7.0或者2000版本的SQL Server支持SqlCacheDependency特性,需要对数据库服务器执行相关的配置步骤。有两种方法配置SQL Server:使用aspnet_regsql命令行工具,或者使用SqlCacheDependencyAdmin类。
4.2.1  利用aspnet_regsql工具
aspnet_regsql工具位于Windows/Microsoft.NET/Framework/[版本]文件夹中。如果直接双击该工具的执行文件,会弹出一个向导对话框,提示我们完成相应的操作:


图4-1 aspnet_regsql工具

如图4-1所示中的提示信息,说明该向导主要用于配置SQL Server数据库,如membership,profiles等信息,如果要配置SqlCacheDependency,则需要以命令行的方式执行。以 PetShop 4.0为例,数据库名为MSPetShop4,则命令为:
aspnet_regsql -S localhost -E -d MSPetShop4 -ed
以下是该工具的命令参数说明:
-?  显示该工具的帮助功能;
-S  后接的参数为数据库服务器的名称或者IP地址;
-U  后接的参数为数据库的登陆用户名;
-P  后接的参数为数据库的登陆密码;
-E  当使用windows集成验证时,使用该功能;
-d  后接参数为对哪一个数据库采用SqlCacheDependency功能;
-t  后接参数为对哪一个表采用SqlCacheDependency功能;
-ed  允许对数据库使用SqlCacheDependency功能;
-dd  禁止对数据库采用SqlCacheDependency功能;
-et  允许对数据表采用SqlCacheDependency功能;
-dt  禁止对数据表采用SqlCacheDependency功能;
-lt  列出当前数据库中有哪些表已经采用sqlcachedependency功能。
以上面的命令为例,说明将对名为MSPetShop4的数据库采用SqlCacheDependency功能,且SQL Server采用了windows集成验证方式。我们还可以对相关的数据表执行aspnet_regsql命令,如:
aspnet_regsql -S localhost -E -d MSPetShop4 -t Item -et
aspnet_regsql -S localhost -E -d MSPetShop4 -t Product -et
aspnet_regsql -S localhost -E -d MSPetShop4 -t Category -et
当执行上述的四条命令后,aspnet_regsql工具会在MSPetShop4数据库中建立一个名为 AspNet_SqlCacheTablesForChangeNotification的新数据库表。该数据表包含三个字段。字段tableName记 录要追踪的数据表的名称,例如在PetShop 4.0中,要记录的数据表就包括Category、Item和Product。notificationCreated字段记录开始追踪的时间。 changeId作为一个类型为int的字段,用于记录数据表数据发生变化的次数。如图4-2所示:


图4-2 AspNet_SqlCacheTablesForChangeNotification数据表

除此之外,执行该命令还会为MSPetShop4数据库添加一组存储过程,为ASP.NET提供查询追踪的数据表的情况,同时还将为使用了 SqlCacheDependency的表添加触发器,分别对应Insert、Update、Delete等与数据更改相关的操作。例如Product数 据表的触发器:

CREATE TRIGGER dbo.[Product_AspNet_SqlCacheNotification_Trigger] ON [Product]
    FOR INSERT, UPDATE, DELETE AS BEGIN
    SET NOCOUNT ON
    EXEC dbo.AspNet_SqlCacheUpdateChangeIdStoredProcedure N’Product’
END


其中,AspNet_SqlCacheUpdateChangeIdStoredProcedure即是工具添加的一组存储过程中的一个。当对 Product数据表执行Insert、Update或Delete等操作时,就会激活触发器,然后执行 AspNet_SqlCacheUpdateChangeIdStoredProcedure存储过程。其执行的过程就是修改 AspNet_SqlCacheTablesForChangeNotification数据表的changeId字段值:

CREATE PROCEDURE dbo.AspNet_SqlCacheUpdateChangeIdStoredProcedure
            @tableName NVARCHAR(450)
        AS
        BEGIN
            UPDATE dbo.AspNet_SqlCacheTablesForChangeNotification WITH (ROWLOCK) SET changeId = changeId + 1
            WHERE tableName = @tableName
        END 
GO


4.2.2  利用SqlCacheDependencyAdmin类
我们也可以利用编程的方式来来管理数据库对SqlCacheDependency特性的使用。该类包含了五个重要的方法:

DisableNotifications
为特定数据库禁用 SqlCacheDependency对象更改通知
DisableTableForNotifications
为数据库中的特定表禁用SqlCacheDependency对象更改通知
EnableNotifications
为特定数据库启用SqlCacheDependency对象更改通知
EnableTableForNotifications
为数据库中的特定表启用SqlCacheDependency对象更改通知
GetTablesEnabledForNotifications
返回启用了SqlCacheDependency对象更改通知的所有表的列表

表4-1 SqlCacheDependencyAdmin类的主要方法

假设我们定义了如下的数据库连接字符串:
const string connectionStr = “Server=localhost;Database=MSPetShop4″;
那么为数据库MSPetShop4启用SqlCacheDependency对象更改通知的实现为:

protected void Page_Load(object sender, EventArgs e)
{
    if (!IsPostBack)
  {
      SqlCacheDependencyAdmin.EnableNotifications(connectionStr);
  }
}


为数据表Product启用SqlCacheDependency对象更改通知的实现则为:
SqlCacheDependencyAdmin.EnableTableForNotifications(connectionStr, “Product”);
如果要调用表4-1中所示的相关方法,需要注意的是访问SQL Server数据库的帐户必须具有创建表和存储过程的权限。如果要调用EnableTableForNotifications方法,还需要具有在该表上创建SQL Server触发器的权限。
虽然说编程方式赋予了程序员更大的灵活性,但aspnet_regsql工具却提供了更简单的方法实现对SqlCacheDependency的配 置与管理。PetShop 4.0采用的正是aspnet_regsql工具的办法,它编写了一个文件名为InstallDatabases.cmd的批处理文件,其中包含了对 aspnet_regsql工具的执行,并通过安装程序去调用该文件,实现对SQL Server的配置。
4.3 在PetShop 4.0中ASP.NET缓存的实现
PetShop作为一个B2C的宠物网上商店,需要充分考虑访客的用户体验,如果因为数据量大而导致Web服务器的响应不及时,页面和查询数据迟迟 得不到结果,会因此而破坏客户访问网站的心情,在耗尽耐心的等待后,可能会失去这一部分客户。无疑,这是非常糟糕的结果。因而在对其进行体系架构设计时, 整个系统的性能就显得殊为重要。然而,我们不能因噎废食,因为专注于性能而忽略数据的正确性。在PetShop 3.0版本以及之前的版本,因为ASP.NET缓存的局限性,这一问题并没有得到很好的解决。PetShop 4.0则引入了SqlCacheDependency特性,使得系统对缓存的处理较之以前大为改观。
4.3.1  CacheDependency接口
PetShop 4.0引入了SqlCacheDependency特性,对Category、Product和Item数据表对应的缓存实施了SQL Cache Invalidation技术。当对应的数据表数据发生更改后,该技术能够将相关项从缓存中移除。实现这一技术的核心是 SqlCacheDependency类,它继承了CacheDependency类。然而为了保证整个架构的可扩展性,我们也允许设计者建立自定义的 CacheDependency类,用以扩展缓存依赖。这就有必要为CacheDependency建立抽象接口,并在web.config文件中进行配 置。
在PetShop 4.0的命名空间PetShop.ICacheDependency中,定义了名为IPetShopCacheDependency接口,它仅包含了一个接口方法:

public interface IPetShopCacheDependency
{     
    AggregateCacheDependency GetDependency();
}


AggregateCacheDependency是.Net Framework 2.0新增的一个类,它负责监视依赖项对象的集合。当这个集合中的任意一个依赖项对象发生改变时,该依赖项对象对应的缓存对象都将被自动移除。
AggregateCacheDependency类起到了组合CacheDependency对象的作用,它可以将多个CacheDependency 对象甚至于不同类型的CacheDependency对象与缓存项建立关联。由于PetShop需要为Category、Product和Item数据表 建立依赖项,因而IPetShopCacheDependency的接口方法GetDependency()其目的就是返回建立了这些依赖项的 AggregateCacheDependency对象。
4.3.2  CacheDependency实现
CacheDependency的实现正是为Category、Product和Item数据表建立了对应的SqlCacheDependency类型的依赖项,如代码所示:

public abstract class TableDependency : IPetShopCacheDependency
{
    // This is the separator that’s used in web.config
    protected char[] configurationSeparator = new char[] { ‘,’ };
    protected AggregateCacheDependency dependency = new AggregateCacheDependency();
    protected TableDependency(string configKey)
    {
        string dbName = ConfigurationManager.AppSettings[”CacheDatabaseName”];
        string tableConfig = ConfigurationManager.AppSettings[configKey];
        string[] tables = tableConfig.Split(configurationSeparator);
        foreach (string tableName in tables)
            dependency.Add(new SqlCacheDependency(dbName, tableName));
    }
    public AggregateCacheDependency GetDependency()
  {
        return dependency;
    }
}


需要建立依赖项的数据库与数据表都配置在web.config文件中,其设置如下:
根据各个数据表间的依赖关系,因而不同的数据表需要建立的依赖项也是不相同的,从配置文件中的value值可以看出。然而不管建立依赖项的多寡,其 创建的行为逻辑都是相似的,因而在设计时,抽象了一个共同的类TableDependency,并通过建立带参数的构造函数,完成对依赖项的建立。由于接 口方法GetDependency()的实现中,返回的对象dependency是在受保护的构造函数创建的,因此这里的实现方式也可以看作是 Template Method模式的灵活运用。例如TableDependency的子类Product,就是利用父类的构造函数建立了Product、Category 数据表的SqlCacheDependency依赖:

public class Product : TableDependency
{
    public Product() : base(”ProductTableDependency”) { }
}


如果需要自定义CacheDependency,那么创建依赖项的方式又有不同。然而不管是创建SqlCacheDependency对象,还是自 定义的CacheDependency对象,都是将这些依赖项添加到AggregateCacheDependency类中,因而我们也可以为自定义 CacheDependency建立专门的类,只要实现IPetShopCacheDependency接口即可。
4.3.3  CacheDependency工厂
继承了抽象类TableDependency的Product、Category和Item类均需要在调用时创建各自的对象。由于它们的父类 TableDependency实现了接口IPetShopCacheDependency,因而它们也间接实现了 IPetShopCacheDependency接口,这为实现工厂模式提供了前提。
在PetShop 4.0中,依然利用了配置文件和反射技术来实现工厂模式。命名空间PetShop.CacheDependencyFactory中,类DependencyAccess即为创建IPetShopCacheDependency对象的工厂类:

public static class DependencyAccess
{     
    public static IPetShopCacheDependency CreateCategoryDependency()
    {
        return LoadInstance(”Category”);
    }
    public static IPetShopCacheDependency CreateProductDependency()
    {
        return LoadInstance(”Product”);
    }
    public static IPetShopCacheDependency CreateItemDependency()
    {
        return LoadInstance(”Item”);
    }
    private static IPetShopCacheDependency LoadInstance(string className)
    {
        string path = ConfigurationManager.AppSettings[”CacheDependencyAssembly”];
        string fullyQualifiedClass = path + “.” + className;
        return (IPetShopCacheDependency)Assembly.Load(path).CreateInstance(fullyQualifiedClass);
    }
}


整个工厂模式的实现如图4-3所示:


图4-3 CacheDependency工厂

虽然DependencyAccess类创建了实现了IPetShopCacheDependency接口的类Category、Product、 Item,然而我们之所以引入IPetShopCacheDependency接口,其目的就在于获得创建了依赖项的 AggregateCacheDependency类型的对象。我们可以调用对象的接口方法GetDependency(),如下所示:
AggregateCacheDependency dependency = DependencyAccess.CreateCategoryDependency().GetDependency();
为了方便调用者,似乎我们可以对DependencyAccess类进行改进,将原有的CreateCategoryDependency()方法,修改为创建AggregateCacheDependency类型对象的方法。
然而这样的做法扰乱了作为工厂类的DependencyAccess的本身职责,且创建IPetShopCacheDependency接口对象的行为仍然有可能被调用者调用,所以保留原有的DependencyAccess类仍然是有必要的。
在PetShop 4.0的设计中,是通过引入Facade模式以方便调用者更加简单地获得AggregateCacheDependency类型对象。
 
 

 

《解剖PetShop》系列之五 PetShop之业务逻辑层设计

五 PetShop之业务逻辑层设计

业务逻辑层(Business Logic Layer)无疑是系统架构中体现核心价值的部分。它的关注点主要集中在业务规则的制定、业务流程的实现等与业务需求有关的系统设计,也即是说它是与系统所应对的领域(Domain)逻辑有关,很多时候,我们也将业务逻辑层称为领域层。例如Martin Fowler在《Patterns of Enterprise Application Architecture》一书中,将整个架构分为三个主要的层:表示层、领域层和数据源层。作为领域驱动设计的先驱Eric Evans,对业务逻辑层作了更细致地划分,细分为应用层与领域层,通过分层进一步将领域逻辑与领域逻辑的解决方案分离。
业务逻辑层在体系架构中的位置很关键,它处于数据访问层与表示层中间,起到了数据交换中承上启下的作用。由于层是一种弱耦合结构,层与层之间的依赖是向下的,底层对于上层而言是“无知”的,改变上层的设计对于其调用的底层而言没有任何影响。如果在分层设计时,遵循了面向接口设计的思想,那么这种向下的依赖也应该是一种弱依赖关系。因而在不改变接口定义的前提下,理想的分层式架构,应该是一个支持可抽取、可替换的“抽屉”式架构。正因为如此,业务逻辑层的设计对于一个支持可扩展的架构尤为关键,因为它扮演了两个不同的角色。对于数据访问层而言,它是调用者;对于表示层而言,它却是被调用者。依赖与被依赖的关系都纠结在业务逻辑层上,如何实现依赖关系的解耦,则是除了实现业务逻辑之外留给设计师的任务。
5.1  与领域专家合作

设计业务逻辑层最大的障碍不在于技术,而在于对领域业务的分析与理解。很难想象一个不熟悉该领域业务规则和流程的架构设计师能够设计出合乎客户需求的系统架构。几乎可以下定结论的是,业务逻辑层的设计过程必须有领域专家的参与。在我曾经参与开发的项目中,所涉及的领域就涵盖了电力、半导体、汽车等诸多行业,如果缺乏这些领域的专家,软件架构的设计尤其是业务逻辑层的设计就无从谈起。这个结论唯一的例外是,架构设计师同时又是该领域的专家。然而,正所谓“千军易得,一将难求”,我们很难寻觅到这样卓越出众的人才。
领域专家在团队中扮演的角色通常称为Business Consultor(业务咨询师),负责提供与领域业务有关的咨询,与架构师一起参与架构与数据库的设计,撰写需求文档和设计用例(或者用户故事User Story)。如果在测试阶段,还应该包括撰写测试用例。理想的状态是,领域专家应该参与到整个项目的开发过程中,而不仅仅是需求阶段。
领域专家可以是专门聘请的对该领域具有较深造诣的咨询师,也可以是作为需求提供方的客户。在极限编程(Extreme Programming)中,就将客户作为领域专家引入到整个开发团队中。它强调了现场客户原则。现场客户需要参与到计划游戏、开发迭代、编码测试等项目开发的各个阶段。由于领域专家与设计师以及开发人员组成了一个团队,贯穿开发过程的始终,就可以避免需求理解错误的情况出现。即使项目的开发与实际需求不符,也可以在项目早期及时修正,从而避免了项目不必要的延期,加强了对项目过程和成本的控制。正如Steve McConnell在构建活动的前期准备中提及的一个原则:发现错误的时间要尽可能接近引入该错误的时间。需求的缺陷在系统中潜伏的时间越长,代价就越昂贵。如果在项目开发中能够与领域专家充分的合作,就可以最大效果地规避这样一种恶性的链式反应。
传统的软件开发模型同样重视与领域专家的合作,但这种合作主要集中在需求分析阶段。例如瀑布模型,就非常强调早期计划与需求调研。然而这种未雨绸缪的早期计划方式,对架构师与需求调研人员的技能要求非常高,它强调需求文档的精确性,一旦分析出现偏差,或者需求发生变更,当项目开发进入设计阶段后,由于缺乏与领域专家沟通与合作的机制,开发人员估量不到这些错误与误差,因而难以及时作出修正。一旦这些问题像毒瘤一般在系统中蔓延开来,逐渐暴露在开发人员面前时,已经成了一座难以逾越的高山。我们需要消耗更多的人力物力,才能够修正这些错误,从而导致开发成本成数量级的增加,甚至于导致项目延期。当然还有一个好的选择,就是放弃整个项目。这样的例子不胜枚举,事实上,项目开发的“滑铁卢”,究其原因,大部分都是因为业务逻辑分析上出现了问题。
迭代式模型较之瀑布模型有很大地改进,因为它允许变更、优化系统需求,整个迭代过程实际上就是与领域专家的合作过程,通过向客户演示迭代所产生的系统功能,从而及时获取反馈,并逐一解决迭代演示中出现的问题,保证系统向着合乎客户需求的方向演化。因而,迭代式模型往往能够解决早期计划不足的问题,它允许在发现缺陷的时候,在需求变更的时候重新设计、重新编码并重新测试。
无论采用何种开发模型,与领域专家的合作都将成为项目成败与否的关键。这基于一个软件开发的普遍真理,那就是世界上没有不变的需求。一句经典名言是:“没有不变的需求,世上的软件都改动过3次以上,唯一一个只改动过两次的软件的拥有者已经死了,死在去修改需求的路上。”一语道尽了软件开发的残酷与艰辛!
那么应该如何加强与领域专家的合作呢?James Carey和Brent Carlson根据他们在参与的IBM SanFrancisco项目中获得的经验,提出了Innocent Questions模式,其意义即“改进领域专家和技术专家的沟通质量”。在一个项目团队中,如果我们没有一位既能担任首席架构师,同时又是领域专家的人选,那么加强领域专家与技术专家的合作就显得尤为重要了。毕竟,作为一个领域专家而言,可能并不熟悉软件设计方法学,也不具备面向对象开发和架构设计的能力,同样,大部分技术专家很有可能对该项目所涉及的业务领域仅停留在一知半解的地步。如果领域专家与技术专家不能有效沟通,则整个项目的前途就岌岌可危了。
Innocent Questions模式提出的解决方案包括:
(1)选用可以与人和谐相处的人员组建开发团队;
(2)清楚地定义角色和职权;
(3)明确定义需要的交互点;
(4)保持团队紧密;
(5)雇佣优秀的人。
事实上,这已经从技术的角度上升到对团队的管理层次了。就好比篮球运动一样,即使你的球队集合了五名世界上最顶尖最有天赋的球员,如果各自为战,要想取得比赛的胜利依旧是非常困难的。团队精神与权责分明才是取得胜利的保障,软件开发同样如此。
与领域专家合作的基础是保证开发团队中永远保留至少一名领域专家。他可以是系统的客户,第三方公司的咨询师,最理想是自己公司雇佣的专家。如果项目中缺乏这样的一个人,那么我的建议是去雇佣他,如果你不想看到项目遭遇“西伯利亚寒流”的话。
确定领域专家的角色任务与职责。必须要让团队中的每一个人明确领域专家在整个团队中究竟扮演什么样的角色,他的职责是什么。一个合格的领域专家必须对业务领域有足够深入的理解,他应该是一个能够俯瞰整个系统需求、总揽全局的人物。在项目开发过程中,将由他负责业务规则和流程的制定,负责与客户的沟通,需求的调研与讨论,并于设计师一起参与系统架构的设计。编档是领域专家必须参与的工作,无论是需求文档还是设计文档,以及用例的编写,领域专家或者提出意见,或者作为撰写的作者,至少他也应该是评审委员会的重要成员。
规范业务领域的术语和技术术语。领域专家和技术专家必须在保证不产生二义性的语义环境下进行沟通与交流。如果出现理解上的分歧,我们必须及时解决,通过讨论确立术语标准。很难想象两个语言不通的人能够相互合作愉快,解决的办法是加入一位翻译人员。在领域专家与技术专家之间搭建一座语义上的桥梁,使其能够相互理解、相互认同。还有一个办法是在团队内部开展培训活动。尤其对于开发人员而言,或多或少地了解一些业务领域知识,对于项目的开发有很大的帮助。在我参与过的半导体领域的项目开发,团队就专门邀请了半导体行业的专家就生产过程的业务逻辑进行了全方位的介绍与培训。正所谓“磨刀不误砍柴工”,虽然我们消费了培训的时间,但对于掌握了业务规则与流程的开发人员,却能够提升项目开发进度,总体上节约了开发成本。
加强与客户的沟通。客户同时也可以作为团队的领域专家,极限编程的现场客户原则是最好的示例。但现实并不都如此的完美,在无法要求客户成为开发团队中的固定一员时,聘请或者安排一个专门的领域专家,加强与客户的沟通,就显得尤为重要。项目可以通过领域专家获得客户的及时反馈。而通过领域专家去了解变更了的需求,会在最大程度上减少需求误差的可能。
5.2  业务逻辑层的模式应用

Martin Fowler在《企业应用架构模式》一书中对领域层(即业务逻辑层)的架构模式作了整体概括,他将业务逻辑设计分为三种主要的模式:Transaction Script、Domain Model和Table Module。
Transaction Script模式将业务逻辑看作是一个个过程,是比较典型的面向过程开发模式。应用Transaction Script模式可以不需要数据访问层,而是利用SQL语句直接访问数据库。为了有效地管理SQL语句,可以将与数据库访问有关的行为放到一个专门的Gateway类中。应用Transaction Script模式不需要太多面向对象知识,简单直接的特性是该模式全部价值之所在。因而,在许多业务逻辑相对简单的项目中,应用Transaction Script模式较多。
Domain Model模式是典型的面向对象设计思想的体现。它充分考虑了业务逻辑的复杂多变,引入了Strategy模式等设计模式思想,并通过建立领域对象以及抽象接口,实现模式的可扩展性,并利用面向对象思想与身俱来的特性,如继承、封装与多态,用于处理复杂多变的业务逻辑。唯一制约该模式应用的是对象与关系数据库的映射。我们可以引入ORM工具,或者利用Data Mapper模式来完成关系向对象的映射。
与Domain Model模式相似的是Table Module模式,它同样具有面向对象设计的思想,唯一不同的是它获得的对象并非是单纯的领域对象,而是DataSet对象。如果为关系数据表与对象建立一个简单的映射关系,那么Domain Model模式就是为数据表中的每一条记录建立一个领域对象,而Table Module模式则是将整个数据表看作是一个完整的对象。虽然利用DataSet对象会丢失面向对象的基本特性,但它在为表示层提供数据源支持方面却有着得天独厚的优势。尤其是在.Net平台下,ADO.NET与Web控件都为Table Module模式提供了生长的肥沃土壤。
5.3  PetShop的业务逻辑层设计

PetShop在业务逻辑层设计中引入了Domain Model模式,这与数据访问层对于数据对象的支持是分不开的。由于PetShop并没有对宠物网上商店的业务逻辑进行深入,也省略了许多复杂细节的商务逻辑,因而在Domain Model模式的应用上并不明显。最典型地应该是对Order领域对象的处理方式,通过引入Strategy模式完成对插入订单行为的封装。关于这一点,我已在第27章有了详尽的描述,这里就不再赘述。
本应是系统架构设计中最核心的业务逻辑层,由于简化了业务流程的缘故,使得PetShop在这一层的设计有些乏善可陈。虽然在业务逻辑层中,针对B2C业务定义了相关的领域对象,但这些领域对象仅仅是完成了对数据访问层中数据对象的简单封装而已,其目的仅在于分离层次,以支持对各种数据库的扩展,同时将SQL语句排除在业务逻辑层外,避免了SQL语句的四处蔓延。
最能体现PetShop业务逻辑的除了对订单的管理之外,还包括购物车(Shopping Cart)与Wish List的管理。在PetShop的BLL模块中,定义了Cart类来负责相关的业务逻辑,定义如下:

[Serializable]
public class Cart
{
    private Dictionary cartItems = new Dictionary();
    public decimal Total
    {
        get
        {
            decimal total = 0;
            foreach (CartItemInfo item in cartItems.Values)
                total += item.Price * item.Quantity;
            return total;
        }
    }
    public void SetQuantity(string itemId, int qty)
    {
        cartItems[itemId].Quantity = qty;
    }
    public int Count
    {
        get { return cartItems.Count; }
    }
    public void Add(string itemId)
    {
        CartItemInfo cartItem;
        if (!cartItems.TryGetValue(itemId, out cartItem))
        {
            Item item = new Item();
            ItemInfo data = item.GetItem(itemId);
            if (data != null)
            {
                CartItemInfo newItem = new CartItemInfo(itemId, data.ProductName, 1, (decimal)data.Price, data.Name, data.CategoryId, data.ProductId);
                cartItems.Add(itemId, newItem);
            }
        }
        else
            cartItem.Quantity++;
    }
    //其他方法略;
}


Cart类通过一个Dictionary对象来负责对购物车内容的存储,同时定义了Add、Remove、Clear等方法,来实现对购物车内容的管理。
在前面我提到PetShop业务逻辑层中的领域对象仅仅是完成对数据对象的简单封装,但这种分离层次的方法在架构设计中依然扮演了举足轻重的作用。以Cart类的Add()方法为例,在方法内部引入了PetShop.BLL.Item领域对象,并调用了Item对象的GetItem()方法。如果没有在业务逻辑层封装Item对象,而是直接调用数据访问层的Item数据对象,为保证层次间的弱依赖关系,就需要调用工厂对象的工厂方法来创建PetShop.IDAL.IItem接口类型对象。一旦数据访问层的Item对象被多次调用,就会造成重复代码,既不离于程序的修改与扩展,也导致程序结构生长为臃肿的态势。
此外,领域对象对数据访问层数据对象的封装,也有利于表示层对业务逻辑层的调用。在三层式架构中,表示层应该是对于数据访问层是“无知”的,这样既减少了层与层间的依赖关系,也能有效避免“循环依赖”的后果。
值得商榷的是Cart类的Total属性。其值的获取是通过遍历购物车集合,然后累加价格与商品数量的乘积。这里显然简化了业务逻辑,而没有充分考虑需求的扩展。事实上,这种获取购物车总价格的算法,在大多数情况下仅仅是其中的一种策略而已,我们还应该考虑折扣的情况。例如,当总价格超过100元时,可以给与顾客一定的折扣,这是与网站的促销计划相关的。除了给与折扣的促销计划外,网站也可以考虑赠送礼品的促销策略,因此我们有必要引入Strategy模式,定义接口IOnSaleStrategy:

public interface IOnSaleStrategy
{
    decimal CalculateTotalPrice(Dictionary cartItems);
}
如此一来,我们可以为Cart类定义一个有参数的构造函数:
private IOnSaleStrategy m_onSale;
public Cart(IOnSaleStrategy onSale)
{
    m_onSale = onSale;
}


那么Total属性就可以修改为:

public decimal Total
{
    get {return m_onSale.CalculateTotalPrice(cartItems);}
}


如此一来,就可以使得Cart类能够有效地支持网站推出的促销计划,也符合开-闭原则。同样的,这种设计方式也是Domain Model模式的体现。修改后的设计如图5-1所示:


图5-1 引入Strategy模式

作为一个B2C的电子商务架构,它所涉及的业务领域已为大部分设计师与开发人员所熟悉,因而在本例中,与领域专家的合作显得并不那么重要。然而,如果我们要开发一个成功的电子商务网站,与领域专家的合作仍然是必不可少的。以订单的管理而言,如果考虑复杂的商业应用,就需要管理订单的跟踪(Tracking),与网上银行的合作,账户安全性,库存管理,物流管理,以及客户关系管理(CRM)。整个业务过程却涵盖了诸如电子商务、银行、物流、客户关系学等诸多领域,如果没有领域专家的参与,业务逻辑层的设计也许会“败走麦城”。
5.4  与数据访问层的通信

业务逻辑层需要与数据访问层通信,利用数据访问层访问数据库,因此业务逻辑层与数据访问层之间就存在依赖关系。在数据访问层引入接口程序集以及数据工厂的设计前提下,能够做到两者间关系为弱依赖。我们从业务逻辑层的引用程序集中可以看到,BLL模块并没有引用SQLServerDAL和OracleDAL程序集。在业务逻辑层中,有关数据访问层中数据对象的调用,均利用多态原理定义了抽象的接口类型对象,然后利用工厂对象的工厂方法创建具体的数据对象。如PetShop.BLL.PetShop领域对象所示:

namespace PetShop.BLL
{
    public class Product
    {
    //根据工厂对象创建IProduct接口类型实例;
        private static readonly IProduct dal =  PetShop.DALFactory.DataAccess.CreateProduct();     
        //调用IProduct对象的接口方法GetProductByCategory();
  public IList
GetProductsByCategory(string category)
  {
  // 如果为空则新建List对象;
  if(string.IsNullOrEmpty(category))
    return new List();  // 通过数据访问层的数据对象访问数据库;
  return dal.GetProductsByCategory(category);
  }
        //其他方法略;
    }
}


在领域对象Product类中,利用数据访问层的工厂类DALFactory.DataAccess创建PetShop.IDAL.IProduct类型的实例,如此就可以解除对具体程序集SQLServerDAL或OracleDAL的依赖。只要PetShop.IDAL的接口方法不变,即使修改了IDAL接口模块的具体实现,都不会影响业务逻辑层的实现。这种松散的弱耦合关系,才能够最大程度地支持架构的可扩展。
领域对象Product实际上还完成了对数据对象Product的封装,它们暴露在外的接口方法是一致地,正是通过封装,使得表示层可以完全脱离数据库以及数据访问层,表示层的调用者仅需要关注业务逻辑层的实现逻辑,以及领域对象暴露的接口和调用方式。事实上,只要设计合理,规范了各个层次的接口方法,三层式架构的设计完全可以分离开由不同的开发人员同时开发,这就可以有效地利用开发资源,缩短项目开发周期。
5.5  面向接口设计

也许是业务逻辑比较简单地缘故,在业务逻辑层的设计中,并没有秉承在数据访问层中面向接口设计的思想。除了完成对插入订单策略的抽象外,整个业务逻辑层仅以BLL模块实现,没有为领域对象定义抽象的接口。因而PetShop的表示层与业务逻辑层就存在强依赖关系,如果业务逻辑层中的需求发生变更,就必然会影响表示层的实现。唯一可堪欣慰的是,由于我们采用分层式架构将用户界面与业务领域逻辑完全分离,一旦用户界面发生更改,例如将B/S架构修改为C/S架构,那么业务逻辑层的实现模块是可以完全重用的。
然而,最理想的方式仍然是面向接口设计。根据第28章对ASP.NET缓存的分析,我们可以将表示层App_Code下的Proxy类与Utility类划分到业务逻辑层中,并修改这些静态类为实例类,并将这些类中与业务领域有关的方法抽象为接口,然后建立如数据访问层一样的抽象工厂。通过“依赖注入”方式,解除与具体领域对象类的依赖,使得表示层仅依赖于业务逻辑层的接口程序集以及工厂模块。
那么,这样的设计是否有“过度设计”的嫌疑呢?我们需要依据业务逻辑的需求情况而定。此外,如果我们需要引入缓存机制,为领域对象创建代理类,那么为领域对象建立接口,就显得尤为必要。我们可以建立一个专门的接口模块IBLL,用以定义领域对象的接口。以Product领域对象为例,我们可以建立IProduct接口:

public interface IProduct
{
  IListGetProductByCategory(string category);
  IListGetProductByCategory(string[] keywords);
  ProductInfo GetProduct(string productId);
}在BLL模块中可以引入对IBLL程序集的依赖,则领域对象Product的定义如下:
public class Product:IProduct
{
  public IListGetProductByCategory(string category) { //实现略; }
  public IListGetProductByCategory(string[] keywords) { //实现略; }
  public ProductInfo GetProduct(string productId) { //实现略; }
}然后我们可以为代理对象建立专门的程序集BLLProxy,它不仅引入对IBLL程序集的依赖,同时还将依赖于BLL程序集。此时代理对象ProductDataProxy的定义如下:
using PetShop.IBLL;
using PetShop.BLL;
namespace PetShop.BLLProxy
{
  public class ProductDataProxy:IProduct
  {
    public IListGetProductByCategory(string category)
    {
        Product product = new Product();
        //其他实现略;
    }
    public IListGetProductByCategory(string[] keywords) { //实现略; }
    public ProductInfo GetProduct(string productId) { //实现略; }
  }
}


如此的设计正是典型的Proxy模式,其类结构如图5-2所示:


图5-2 Proxy模式

参照数据访问层的设计方法,我们可以为领域对象及代理对象建立抽象工厂,并在web.config中配置相关的配置节,然后利用反射技术创建具体的对象实例。如此一来,表示层就可以仅仅依赖PetShop.IBLL程序集以及工厂模块,如此就可以解除表示层与具体领域对象之间的依赖关系。表示层与修改后的业务逻辑层的关系如图5-3所示:


图5-3 修改后的业务逻辑层与表示层的关系

图5-4则是PetShop 4.0原有设计的层次关系图:


图5-4 PetShop 4.0中表示层与业务逻辑层的关系

通过比较图5-3与图5-4,虽然后者不管是模块的个数,还是模块之间的关系,都相对更加简单,然而Web Component组件与业务逻辑层之间却是强耦合的,这样的设计不利于应对业务扩展与需求变更。通过引入接口模块IBLL与工厂模块BLLFactory,解除了与具体模块BLL的依赖关系。这种设计对于业务逻辑相对比较复杂的系统而言,更符合面向对象的设计思想,有利于我们建立可抽取、可替换的“抽屉”式三层架构。

 

 

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值