[转载]petshop解析Dr.Wang

同步/异步处理策略接口(实现在bll根据配置反射选择)

13

MessagingFactory

PetShop.MessagingFactory

异时处理消息队列的抽象工厂

14

IMessaging

PetShop.IMessaging

异时处理消息队列接口定义

15

MSMQMessaging

PetShop.MsmqMessaging

异时处理消息队列的实现

16

Profile

PetShop.Profile

Profile的数据访问层

17

ProfileDALFactory

PetShop.ProfileDALFactory

ProfileDAL的工厂类(反射创建ProfileDAL)

18

IProfileDAL

PetShop.IProfileDAL

Profile的数据访问层接口定义

19

OracleProfileDAL

PetShop.OracleProfileDAL

Oracle的ProfileProviders

做用户状态管理

20

SQLProfileDAL

PetShop.SQLProfileDAL

SQLServer的ProfileProviders

21

Membership

PetShop.Membership

22

OrderProcessor

PetShop.OrderProcessor

后台处理进程,处理订单队列

一、PetShop的系统架构设计

在软件体系架构设计中,分层式结构是最常见,也是最重要的一种结构。微软推荐的分层式结构一般分为三层,从下至上分别为:数据访问层、业务逻辑层(又或成为领域层)、表示层,如图所示:

图一:三层的分层式结构

数据访问层:有时候也称为是持久层,其功能主要是负责数据库的访问。简单的说法就是实现对数据表的Select,Insert,Update,Delete的操作。如果要加入ORM的元素,那么就会包括对象和数据表之间的mapping,以及对象实体的持久化。在PetShop的数据访问层中,并没有使用ORM,从而导致了代码量的增加,可以看作是整个设计实现中的一大败笔。

松散耦合的好处是显而易见的。如果一个系统没有分层,那么各自的逻辑都紧紧纠缠在一起,彼此间相互依赖,谁都是不可替换的。一旦发生改变,则牵一发而动全身,对项目的影响极为严重。降低层与层间的依赖性,既可以良好地保证未来的可扩展,在复用性上也是优势明显。每个功能模块一旦定义好统一的接口,就可以被各个模块所调用,而不用为相同的功能进行重复地开发。

进行好的分层式结构设计,标准也是必不可少的。只有在一定程度的标准化基础上,这个系统才是可扩展的,可替换的。而层与层之间的通信也必然保证了接口的标准化。

“金无足赤,人无完人”,分层式结构也不可避免具有一些缺陷:1、降低了系统的性能。这是不言而喻的。如果不采用分层式结构,很多业务可以直接造访数据库,以此获取相应的数据,如今却必须通过中间层来完成。2、有时会导致级联的修改。这种修改尤其体现在自上而下的方向。如果在表示层中需要增加一个功能,为保证其设计符合分层式结构,可能需要在相应的业务逻辑层和数据访问层中都增加相应的代码。

前面提到,PetShop的表示层是用ASP.Net设计的,也就是说,它应是一个BS系统。在.Net中,标准的BS分层式结构如下图所示:

图二:.Net中标准的BS分层式结构

随着PetShop版本的更新,其分层式结构也在不断的完善,例如PetShop2.0,就没有采用标准的三层式结构,如图三:

图三:PetShop2.0的体系架构

从图中我们可以看到,并没有明显的数据访问层设计。这样的设计虽然提高了数据访问的性能,但也同时导致了业务逻辑层与数据访问的职责混乱。一旦要求支持的数据库发生变化,或者需要修改数据访问的逻辑,由于没有清晰的分层,会导致项目作大的修改。而随着硬件系统性能的提高,以及充分利用缓存、异步处理等机制,分层式结构所带来的性能影响几乎可以忽略不计。

PetShop3.0纠正了此前层次不明的问题,将数据访问逻辑作为单独的一层独立出来:

图四:PetShop3.0的体系架构

PetShop4.0基本上延续了3.0的结构,但在性能上作了一定的改进,引入了缓存和异步处理机制,同时又充分利用了ASP.Net2.0的新功能MemberShip,因此PetShop4.0的系统架构图如下所示:

图五:PetShop4.0的体系架构

比较3.0和4.0的系统架构图,其核心的内容并没有发生变化。在数据访问层(DAL)中,仍然采用DALInterface抽象出数据访问逻辑,并以DALFactory作为数据访问层对象的工厂模块。对于DALInterface而言,分别有支持MS-SQL的SQLServerDAL和支持Oracle的OracleDAL具体实现。而Model模块则包含了数据实体对象。其详细的模块结构图如下所示:

图六:数据访问层的模块结构图

可以看到,在数据访问层中,完全采用了“面向接口编程”思想。抽象出来的IDAL模块,脱离了与具体数据库的依赖,从而使得整个数据访问层利于数据库迁移。DALFactory模块专门管理DAL对象的创建,便于业务逻辑层访问。SQLServerDAL和OracleDAL模块均实现IDAL模块的接口,其中包含的逻辑就是对数据库的Select,Insert,Update和Delete操作。因为数据库类型的不同,对数据库的操作也有所不同,代码也会因此有所区别。

此外,抽象出来的IDAL模块,除了解除了向下的依赖之外,对于其上的业务逻辑层,同样仅存在弱依赖关系,如下图所示:

图七:业务逻辑层的模块结构图

图七中BLL是业务逻辑层的核心模块,它包含了整个系统的核心业务。在业务逻辑层中,不能直接访问数据库,而必须通过数据访问层。注意图中对数据访问业务的调用,是通过接口模块IDAL来完成的。既然与具体的数据访问逻辑无关,则层与层之间的关系就是松散耦合的。如果此时需要修改数据访问层的具体实现,只要不涉及到IDAL的接口定义,那么业务逻辑层就不会受到任何影响。毕竟,具体实现的SQLServerDAL和OracalDAL根本就与业务逻辑层没有半点关系。

因为在PetShop4.0中引入了异步处理机制。插入订单的策略可以分为同步和异步,两者的插入策略明显不同,但对于调用者而言,插入订单的接口是完全一样的,所以PetShop4.0中设计了IBLLStrategy模块。虽然在IBLLStrategy模块中,仅仅是简单的IOrderStategy,但同时也给出了一个范例和信息,那就是在业务逻辑的处理中,如果存在业务操作的多样化,或者是今后可能的变化,均应利用抽象的原理。或者使用接口,或者使用抽象类,从而脱离对具体业务的依赖。不过在PetShop中,由于业务逻辑相对简单,这种思想体现得不够明显。也正因为此,PetShop将核心的业务逻辑都放到了一个模块BLL中,并没有将具体的实现和抽象严格的按照模块分开。所以表示层和业务逻辑层之间的调用关系,其耦合度相对较高:

图八:表示层的模块结构图

在图五中,各个层次中还引入了辅助的模块,如数据访问层的Messaging模块,是为异步插入订单的功能提供,采用了MSMQ(MicrosoftMessagingQueue)技术。而表示层的CacheDependency则提供缓存功能。

如果使用IoC+O/RMapping,不但各层之间的耦合降低到最小,而且持久层的代码也会大大精简。使用Spring.Net/Castle+NHibernate来重构一次,那么petshop将会真正的优雅起来

1.分层式结构不一定会导致性能问题,相反,在一个结合了WorkUnit的系统中,还可能会提供系统的性能。性能问题主要取决于具体软件在分层架构中所采用的技术实现和机制,与分层架构本身是没有任何关系的

2.一个实现良好的分层结构并不会导致所谓的级联修改问题。对于一个业务层、表现层、数据访问层使用一一对应式的“蹩脚”实现,确实是会出现级联修改问题。但是这大多是因为实现者仍然是用分层结构形式来做他的两层系统。分层结构本身的三个层次在设计、逻辑、接口上的粒度是完全不同的,从设计角度上看根本不存在所谓级联问题,因为他的粗细颗粒不同,一个细粒度的DAC方法又怎么会导致一个粗粒度的BIZ方法的接口修改呢?表现层在实现良好的系统中往往会考虑模板化的数据显示(诸如XSLT等技术),这些表现技术也能很好的剥离业务逻辑、数据含义与表现层的耦合性,所以也不会出现级联修改问题

并没有使用ORM,从而导致了代码量的增加,可以看作是整个设计实现中的一大败笔。我不同意作者的这个说法,ormapping不用代码相对来说是增加了,但是不能说是败笔虽说ormapping能使代码的编写量减少但是同时带来的是性能上的下降,对象与数据表的映射全部交于系统自动处理是很恐怖的一件事.

在系列一中,我从整体上分析了PetShop的架构设计,并提及了分层的概念。从本部分开始,我将依次对各层进行代码级的分析,以求获得更加细致而深入的理解。在PetShop4.0中,由于引入了ASP.Net2.0的一些新特色,所以数据层的内容也更加的广泛和复杂,包括:数据库访问、Messaging、MemberShip、Profile四部分。在系列二中,我将介绍有关数据库访问的设计。

在PetShop中,系统需要处理的数据库对象分为两类:一是数据实体,对应数据库中相应的数据表。它们没有行为,仅用于表现对象的数据。这些实体类都被放到Model程序集中,例如数据表Order对应的实体类OrderInfo,其类图如下:

这些对象并不具有持久化的功能,简单地说,它们是作为数据的载体,便于业务逻辑针对相应数据表进行读/写操作。虽然这些类的属性分别映射了数据表的列,而每一个对象实例也恰恰对应于数据表的每一行,但这些实体类却并不具备对应的数据库访问能力。

由于数据访问层和业务逻辑层都将对这些数据实体进行操作,因此程序集Model会被这两层的模块所引用。

第二类数据库对象则是数据的业务逻辑对象。这里所指的业务逻辑,并非业务逻辑层意义上的领域(domain)业务逻辑(从这个意义上,我更倾向于将业务逻辑层称为“领域逻辑层”),一般意义上说,这些业务逻辑即为基本的数据库操作,包括Select,Insert,Update和Delete。由于这些业务逻辑对象,仅具有行为而与数据无关,因此它们均被抽象为一个单独的接口模块IDAL,例如数据表Order对应的接口IOrder:

以Order为例,在SQLServerDAL、OracleDAL两个模块中,有不同的实现,但它们同时又都实现了IOrder接口,如图:

从数据库的实现来看,PetShop体现出了没有ORM框架的臃肿与丑陋。由于要对数据表进行Insert和Select操作,以SQLServer为例,就使用了SqlCommand,SqlParameter,SqlDataReader等对象,以完成这些操作。尤其复杂的是Parameter的传递,在PetShop中,使用了大量的字符串常量来保存参数的名称。此外,PetShop还专门为SQLServer和Oracle提供了抽象的Helper类,包装了一些常用的操作,如ExecuteNonQuery、ExecuteReader等方法。

在没有ORM的情况下,使用Helper类是一个比较好的策略,利用它来完成数据库基本操作的封装,可以减少很多和数据库操作有关的代码,这体现了对象复用的原则。PetShop将这些Helper类统一放到DBUtility模块中,不同数据库的Helper类暴露的方法基本相同,只除了一些特殊的要求,例如Oracle中处理bool类型的方式就和SQLServer不同,从而专门提供了OraBit和OraBool方法。此外,Helper类中的方法均为static方法,以利于调用。OracleHelper的类图如下:

对于数据访问层来说,最头疼的是SQL语句的处理。在早期的CS结构中,由于未采用三层式架构设计,数据访问层和业务逻辑层是紧密糅合在一起的,因此,SQL语句遍布与系统的每一个角落。这给程序的维护带来极大的困难。此外,由于Oracle使用的是PL-SQL,而SQLServer和Sybase等使用的是T-SQL,两者虽然都遵循了标准SQL的语法,但在很多细节上仍有区别,如果将SQL语句大量的使用到程序中,无疑为可能的数据库移植也带来了困难。

最好的方法是采用存储过程。这种方法使得程序更加整洁,此外,由于存储过程可以以数据库脚本的形式存在,也便于移植和修改。但这种方式仍然有缺陷。一是存储过程的测试相对困难。虽然有相应的调试工具,但比起对代码的调试而言,仍然比较复杂且不方便。二是对系统的更新带来障碍。如果数据库访问是由程序完成,在.Net平台下,我们仅需要在修改程序后,将重新编译的程序集xcopy到部署的服务器上即可。如果使用了存储过程,出于安全的考虑,必须有专门的DBA重新运行存储过程的脚本,部署的方式受到了限制。

SQL语句的使用无法避免,如何更好的应用SQL语句也无定论,但有一个原则值得我们遵守,就是“应该尽量让SQL语句尽存在于数据访问层的具体实现中”。

当然,如果应用ORM,那么一切就变得不同了。因为ORM框架已经为数据访问提供了基本的Select,Insert,Update和Delete操作了。例如在NHibernate中,我们可以直接调用ISession对象的Save方法,来Insert(或者说是Create)一个数据实体对象:publicvoidInsert(OrderInfoorder){ISessions=Sessions.GetSession();ITransactiontrans=null;try{trans=s.BeginTransaction();s.Save(order);trans.Commit();}finally{s.Close();}}

没有SQL语句,也没有那些烦人的Parameters,甚至不需要专门去考虑事务。此外,这样的设计,也是与数据库无关的,NHibernate可以通过Dialect(方言)的机制支持不同的数据库。唯一要做的是,我们需要为OrderInfo定义hbm文件。

还是回到对PetShop的讨论上来。现在我们已经有了数据实体,数据对象的抽象接口和实现,可以说有关数据库访问的主体就已经完成了。留待我们的还有两个问题需要解决:1、数据对象创建的管理2、利于数据库的移植

在PetShop中,要创建的数据对象包括Order,Product,Category,Inventory,Item。在前面的设计中,这些对象已经被抽象为对应的接口,而其实现则根据数据库的不同而有所不同。也就是说,创建的对象有多种类别,而每种类别又有不同的实现,这是典型的抽象工厂模式的应用场景。而上面所述的两个问题,也都可以通过抽象工厂模式来解决。标准的抽象工厂模式类图如下:

例如,创建SQLServer的Order对象如下:PetShopFactoryfactory=newSQLServerFactory();IOrderorder=factory.CreateOrder();

要考虑到数据库的可移植性,则factory必须作为一个全局变量,并在主程序运行时被实例化。但这样的设计虽然已经达到了“封装变化”的目的,但在创建PetShopFactory对象时,仍不可避免的出现了具体的类SQLServerFactory,也即是说,程序在这个层面上产生了与SQLServerFactory的强依赖。一旦整个系统要求支持Oracle,那么还需要修改这行代码为:PetShopFactoryfactory=newOracleFactory();

DataAccess类完全取代了前面创建的工厂类体系,它是一个sealed类,其中创建各种数据对象的方法,均为静态方法。之所以能用这个类达到抽象工厂的目的,是因为配置文件和反射的运用,如下的代码片断所示:publicsealedclassDataAccess{//LookuptheDALimplementationweshouldbeusingprivatestaticreadonlystringpath=ConfigurationManager.AppSettings[”WebDAL”];privatestaticreadonlystringorderPath=ConfigurationManager.AppSettings[”OrdersDAL”];

publicstaticPetShop.IDAL.IOrderCreateOrder(){stringclassName=orderPath+“.Order”;return(PetShop.IDAL.IOrder)Assembly.Load(orderPath).CreateInstance(className);}}

在PetShop中,这种依赖配置文件和反射创建对象的方式极其常见,包括IBLLStategy、CacheDependencyFactory等等。这些实现逻辑散布于整个PetShop系统中,在我看来,是可以在此基础上进行重构的。也就是说,我们可以为整个系统提供类似于“ServiceLocator”的实现:publicstaticclassServiceLocator{privatestaticreadonlystringdalPath=ConfigurationManager.AppSettings[”WebDAL”];privatestaticreadonlystringorderPath=ConfigurationManager.AppSettings[”OrdersDAL”];//……privatestaticreadonlystringorderStategyPath=ConfigurationManager.AppSettings[”OrderStrategyAssembly”];

publicstaticobjectLocateDALObject(stringclassName){stringfullPath=dalPath+“.”+className;returnAssembly.Load(dalPath).CreateInstance(fullPath);}publicstaticobjectLocateDALOrderObject(stringclassName){stringfullPath=orderPath+“.”+className;returnAssembly.Load(orderPath).CreateInstance(fullPath);}publicstaticobjectLocateOrderStrategyObject(stringclassName){stringfullPath=orderStategyPath+“.”+className;returnAssembly.Load(orderStategyPath).CreateInstance(fullPath);}//……}

publicvoidSend(OrderInfoorderMessage){//ThismethoddoesnotinvolveindistributedtransactionandoptimizesperformanceusingSingletypebase.transactionType=MessageQueueTransactionType.Single;base.Send(orderMessage);}所以,最后的类图应该如下:

注意在Order类的Receive()方法中,是用new关键字而不是override关键字来重写其父类PetShopQueue的Receive()虚方法。因此,如果是实例化如下的对象,将会调用PetShopQueue的Receive()方法,而不是子类Order的Receive()方法:PetShopQueuequeue=newOrder();queue.Receive();从设计上来看,由于PetShop采用“面向接口设计”的原则,如果我们要创建Order对象,应该采用如下的方式:IOrderorder=newOrder();order.Receive();考虑到IOrder的实现有可能的变化,PetShop仍然利用了工厂模式,将IOrder对象的创建用专门的工厂模块进行了封装:

Orderorder=newOrder();while(true){//queuetimeoutvariablesTimeSpandatetimeStarting=newTimeSpan(DateTime.Now.Ticks);doubleelapsedTime=0;

intprocessedItems=0;

ArrayListqueueOrders=newArrayList();

using(TransactionScopets=newTransactionScope(TransactionScopeOption.Required,tsTimeout)){//Receivetheordersfromthequeuefor(intj=0;j

//updateelapsedtimeelapsedTime=newTimeSpan(DateTime.Now.Ticks).TotalSeconds-datetimeStarting.TotalSeconds;}catch(TimeoutException){//exitloopbecausenomoremessagesarewaitingj=batchSize;}}//processthequeuedordersfor(intk=0;k

//batchcompleteorMSMQreceivetimedoutts.Complete();}

Console.WriteLine("(ThreadId"+Thread.CurrentThread.ManagedThreadId+")batchfinished,"+processedItems+"items,in"+elapsedTime.ToString()+"seconds.");}}首先,它会通过PetShop.BLL.Order类的公共方法ReceiveFromQueue()来获取消息队列中的订单数据,并将其放入到一个ArrayList对象中,然而再调用PetShop.BLL.Order类的Insert方法将其插入到Order和Inventory数据库中。在PetShop.BLL.Order类中,并不是直接执行插入订单的操作,而是调用了IOrderStrategy接口的Insert()方法:publicvoidInsert(OrderInfoorder){//CallcreditcardprocesorProcessCreditCard(order);

//Inserttheorder(a)synchrounouslybasedonconfigurationorderInsertStrategy.Insert(order);}在这里,运用了一个策略模式,类图如下所示:

在PetShop.BLL.Order类中,仍然利用配置文件来动态创建IOrderStategy对象:privatestaticreadonlyPetShop.IBLLStrategy.IOrderStrategyorderInsertStrategy=LoadInsertStrategy();privatestaticPetShop.IBLLStrategy.IOrderStrategyLoadInsertStrategy(){//Lookupwhichstrategytousefromconfigfilestringpath=ConfigurationManager.AppSettings[”OrderStrategyAssembly”];stringclassName=ConfigurationManager.AppSettings[”OrderStrategyClass”];

//Usingtheevidencegivenintheconfigfileloadtheappropriateassemblyandclassreturn(PetShop.IBLLStrategy.IOrderStrategy)Assembly.Load(path).CreateInstance(className);}由于OrderProcessor是一个单独的应用程序,因此它使用的配置文件与PetShop不同,是存放在应用程序的App.config文件中,在该文件中,对IOrderStategy的配置为:因此,以异步方式插入订单的流程如下图所示:

MicrosoftMessagingQueue(MSMQ)技术除用于异步处理以外,它主要还是一种分布式处理技术。分布式处理中,一个重要的技术要素就是有关消息的处理,而在System.Messaging命名空间中,已经提供了Message类,可以用于承载消息的传递,前提上消息的发送方与接收方在数据定义上应有统一的接口规范。MSMQ在分布式处理的运用,在我参与的项目中已经有了实现。在为一个汽车制造商开发一个大型系统时,分销商Dealer作为.Net客户端,需要将数据传递到管理中心,并且该数据将被Oracle的EBS(E-BusinessSystem)使用。由于分销商管理系统(DMS)采用的是C/S结构,数据库为SQLServer,而汽车制造商管理中心的EBS数据库为Oracle。这里就涉及到两个系统之间数据的传递。实现架构如下:

首先Dealer的数据通过MSMQ传递到MSMQServer,此时可以将数据插入到SQLServer数据库中,同时利用FTP将数据传送到专门的文件服务器上。然后利用IBM的EAI技术(企业应用集成,EnterpriseApplicationItegration)定期将文件服务器中的文件,利用接口规范写入到EAI数据库服务器中,并最终写道EBS的Oracle数据库中。上述架构是一个典型的分布式处理结构,而技术实现的核心就是MSMQ和EAI。由于我们已经定义了统一的接口规范,在通过消息队列形成文件后,此时的数据就已经与平台无关了,使得在.Net平台下的分销商管理系统能够与Oracle的EBS集成起来,完成数据的处理。

如果对微型计算机硬件系统有足够的了解,那么我们对于Cache这个名词一定是耳熟能详的。在CPU以及主板的芯片中,都引入了这种名为高速缓冲存储器(Cache)的技术。因为Cache的存取速度比内存快,因而引入Cache能够有效的解决CPU与内存之间的速度不匹配问题。硬件系统可以利用Cache存储CPU访问概率高的那些数据,当CPU需要访问这些数据时,可以直接从Cache中读取,而不必访问存取速度相对较慢的内存,从而提高了CPU的工作效率。软件设计借鉴了硬件设计中引入缓存的机制以改善整个系统的性能,尤其是对于一个数据库驱动的Web应用程序而言,缓存的利用是不可或缺的,毕竟,数据库查询可能是整个Web站点中调用最频繁但同时又是执行最缓慢的操作之一,我们不能被它老迈的双腿拖缓我们前进的征程。缓存机制正是解决这一缺陷的加速器。

4.1ASP.NET缓存概述

作为.Net框架下开发Web应用程序的主打产品,ASP.NET充分考虑了缓存机制。通过某种方法,将系统需要的数据对象、Web页面存储在内存中,使得Web站点在需要获取这些数据时,不需要经过繁琐的数据库连接、查询和复杂的逻辑运算,就可以“触手可及”,如“探囊取物”般容易而快速,从而提高整个Web系统的性能。

ASP.NET提供了两种基本的缓存机制来提供缓存功能。一种是应用程序缓存,它允许开发者将程序生成的数据或报表业务对象放入缓存中。另外一种缓存机制是页输出缓存,利用它,可以直接获取存放在缓存中的页面,而不需要经过繁杂的对该页面的再次处理。

应用程序缓存其实现原理说来平淡无奇,仅仅是通过ASP.NET管理内存中的缓存空间。放入缓存中的应用程序数据对象,以键/值对的方式存储,这便于用户在访问缓存中的数据项时,可以根据key值判断该项是否存在缓存中。

放入在缓存中的数据对象其生命周期是受到限制的,即使在整个应用程序的生命周期里,也不能保证该数据对象一直有效。ASP.NET可以对应用程序缓存进行管理,例如当数据项无效、过期或内存不足时移除它们。此外,调用者还可以通过CacheItemRemovedCallback委托,定义回调方法使得数据项被移除时能够通知用户。

在.NetFramework中,应用程序缓存通过System.Web.Caching.Cache类实现。它是一个密封类,不能被继承。对于每一个应用程序域,都要创建一个Cache类的实例,其生命周期与应用程序域的生命周期保持一致。我们可以利用Add或Insert方法,将数据项添加到应用程序缓存中,如下所示:Cache[”First”]=“FirstItem”;Cache.Insert(”Second”,“SecondItem”);

我们还可以为应用程序缓存添加依赖项,使得依赖项发生更改时,该数据项能够从缓存中移除:string[]dependencies={”Second”};Cache.Insert(”Third”,“ThirdItem”,newSystem.Web.Caching.CacheDependency(null,dependencies));

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

如此一来,ASP.NET会对txtCity控件的值进行判断,只有输入的值与缓存值相同,才从缓存中取出相应的值。这就有效地避免了因为值的不同而导致输出错误的数据。

利用缓存的机制对性能的提升非常明显。通过ACT(ApplicationCenterTest)的测试,可以发现设置缓存后执行的性能比未设置缓存时的性能足足提高三倍多。

引入缓存看来是提高性能的“完美”解决方案,然而“金无足赤,人无完人”,缓存机制也有缺点,那就是数据过期的问题。一旦应用程序数据或者页面结果值发生的改变,那么在缓存有效期范围内,你所获得的结果将是过期的、不准确的数据。我们可以想一想股票系统利用缓存所带来的灾难,当你利用错误过期的数据去分析股市的风云变幻时,你会发现获得的结果真可以说是“失之毫厘,谬以千里”,看似大好的局面就会像美丽的泡沫一样,用针一戳,转眼就消失得无影无踪。

那么我们是否应该为了追求高性能,而不顾所谓“数据过期”所带来的隐患呢?显然,在类似于股票系统这种数据更新频繁的特定场景下,数据过期的糟糕表现甚至比低效的性能更让人难以接受。故而,我们需要在性能与数据正确性间作出权衡。所幸的是,.NetFramework2.0引入了一种新的缓存机制,它为我们的“鱼与熊掌兼得”带来了技术上的可行性。

.Net2.0引入的自定义缓存依赖项,特别是基于MS-SQLServer的SqlCacheDependency特性,使得我们可以避免“数据过期”的问题,它能够根据数据库中相应数据的变化,通知缓存,并移除那些过期的数据。事实上,在PetShop4.0中,就充分地利用了SqlCacheDependency特性。

4.2SqlCacheDependency特性

4.2.1利用aspnet_regsql工具

aspnet_regsql工具位于Windows\Microsoft.NET\Framework\[版本]文件夹中。如果直接双击该工具的执行文件,会弹出一个向导对话框,提示我们完成相应的操作:

图4-1aspnet_regsql工具

如图4-1所示中的提示信息,说明该向导主要用于配置SQLServer数据库,如membership,profiles等信息,如果要配置SqlCacheDependency,则需要以命令行的方式执行。以PetShop4.0为例,数据库名为MSPetShop4,则命令为:aspnet_regsql-Slocalhost-E-dMSPetShop4-ed

以下是该工具的命令参数说明:-显示该工具的帮助功能;-S后接的参数为数据库服务器的名称或者IP地址;-U后接的参数为数据库的登陆用户名;-P后接的参数为数据库的登陆密码;-E当使用windows集成验证时,使用该功能;-d后接参数为对哪一个数据库采用SqlCacheDependency功能;-t后接参数为对哪一个表采用SqlCacheDependency功能;-ed允许对数据库使用SqlCacheDependency功能;-dd禁止对数据库采用SqlCacheDependency功能;-et允许对数据表采用SqlCacheDependency功能;-dt禁止对数据表采用SqlCacheDependency功能;-lt列出当前数据库中有哪些表已经采用sqlcachedependency功能。

图4-2AspNet_SqlCacheTablesForChangeNotification数据表

其中,AspNet_SqlCacheUpdateChangeIdStoredProcedure即是工具添加的一组存储过程中的一个。当对Product数据表执行Insert、Update或Delete等操作时,就会激活触发器,然后执行AspNet_SqlCacheUpdateChangeIdStoredProcedure存储过程。其执行的过程就是修改AspNet_SqlCacheTablesForChangeNotification数据表的changeId字段值:CREATEPROCEDUREdbo.AspNet_SqlCacheUpdateChangeIdStoredProcedure@tableNameNVARCHAR(450)ASBEGINUPDATEdbo.AspNet_SqlCacheTablesForChangeNotificationWITH(ROWLOCK)SETchangeId=changeId+1WHEREtableName=@tableNameENDGO

4.2.2利用SqlCacheDependencyAdmin类

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

DisableNotifications

为特定数据库禁用SqlCacheDependency对象更改通知

DisableTableForNotifications

为数据库中的特定表禁用SqlCacheDependency对象更改通知

EnableNotifications

为特定数据库启用SqlCacheDependency对象更改通知

EnableTableForNotifications

为数据库中的特定表启用SqlCacheDependency对象更改通知

GetTablesEnabledForNotifications

返回启用了SqlCacheDependency对象更改通知的所有表的列表

表4-1SqlCacheDependencyAdmin类的主要方法

假设我们定义了如下的数据库连接字符串:conststringconnectionStr=“Server=localhost;Database=MSPetShop4″;

那么为数据库MSPetShop4启用SqlCacheDependency对象更改通知的实现为:protectedvoidPage_Load(objectsender,EventArgse){if(!IsPostBack){SqlCacheDependencyAdmin.EnableNotifications(connectionStr);}}

为数据表Product启用SqlCacheDependency对象更改通知的实现则为:SqlCacheDependencyAdmin.EnableTableForNotifications(connectionStr,“Product”);

虽然说编程方式赋予了程序员更大的灵活性,但aspnet_regsql工具却提供了更简单的方法实现对SqlCacheDependency的配置与管理。PetShop4.0采用的正是aspnet_regsql工具的办法,它编写了一个文件名为InstallDatabases.cmd的批处理文件,其中包含了对aspnet_regsql工具的执行,并通过安装程序去调用该文件,实现对SQLServer的配置。

4.3在PetShop4.0中ASP.NET缓存的实现

PetShop作为一个B2C的宠物网上商店,需要充分考虑访客的用户体验,如果因为数据量大而导致Web服务器的响应不及时,页面和查询数据迟迟得不到结果,会因此而破坏客户访问网站的心情,在耗尽耐心的等待后,可能会失去这一部分客户。无疑,这是非常糟糕的结果。因而在对其进行体系架构设计时,整个系统的性能就显得殊为重要。然而,我们不能因噎废食,因为专注于性能而忽略数据的正确性。在PetShop3.0版本以及之前的版本,因为ASP.NET缓存的局限性,这一问题并没有得到很好的解决。PetShop4.0则引入了SqlCacheDependency特性,使得系统对缓存的处理较之以前大为改观。

4.3.1CacheDependency接口

在PetShop4.0的命名空间PetShop.ICacheDependency中,定义了名为IPetShopCacheDependency接口,它仅包含了一个接口方法:publicinterfaceIPetShopCacheDependency{AggregateCacheDependencyGetDependency();}

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

4.3.2CacheDependency实现

CacheDependency的实现正是为Category、Product和Item数据表建立了对应的SqlCacheDependency类型的依赖项,如代码所示:publicabstractclassTableDependency:IPetShopCacheDependency{//Thisistheseparatorthat’susedinweb.configprotectedchar[]configurationSeparator=newchar[]{‘,’};

protectedAggregateCacheDependencydependency=newAggregateCacheDependency();protectedTableDependency(stringconfigKey){stringdbName=ConfigurationManager.AppSettings[”CacheDatabaseName”];stringtableConfig=ConfigurationManager.AppSettings[configKey];string[]tables=tableConfig.Split(configurationSeparator);

foreach(stringtableNameintables)dependency.Add(newSqlCacheDependency(dbName,tableName));}publicAggregateCacheDependencyGetDependency(){returndependency;}}

需要建立依赖项的数据库与数据表都配置在web.config文件中,其设置如下:

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

如果需要自定义CacheDependency,那么创建依赖项的方式又有不同。然而不管是创建SqlCacheDependency对象,还是自定义的CacheDependency对象,都是将这些依赖项添加到AggregateCacheDependency类中,因而我们也可以为自定义CacheDependency建立专门的类,只要实现IPetShopCacheDependency接口即可。

4.3.3CacheDependency工厂

继承了抽象类TableDependency的Product、Category和Item类均需要在调用时创建各自的对象。由于它们的父类TableDependency实现了接口IPetShopCacheDependency,因而它们也间接实现了IPetShopCacheDependency接口,这为实现工厂模式提供了前提。

在PetShop4.0中,依然利用了配置文件和反射技术来实现工厂模式。命名空间PetShop.CacheDependencyFactory中,类DependencyAccess即为创建IPetShopCacheDependency对象的工厂类:publicstaticclassDependencyAccess{publicstaticIPetShopCacheDependencyCreateCategoryDependency(){returnLoadInstance(”Category”);}publicstaticIPetShopCacheDependencyCreateProductDependency(){returnLoadInstance(”Product”);}publicstaticIPetShopCacheDependencyCreateItemDependency(){returnLoadInstance(”Item”);}privatestaticIPetShopCacheDependencyLoadInstance(stringclassName){stringpath=ConfigurationManager.AppSettings[”CacheDependencyAssembly”];stringfullyQualifiedClass=path+“.”+className;return(IPetShopCacheDependency)Assembly.Load(path).CreateInstance(fullyQualifiedClass);}}整个工厂模式的实现如图4-3所示:

图4-3CacheDependency工厂

虽然DependencyAccess类创建了实现了IPetShopCacheDependency接口的类Category、Product、Item,然而我们之所以引入IPetShopCacheDependency接口,其目的就在于获得创建了依赖项的AggregateCacheDependency类型的对象。我们可以调用对象的接口方法GetDependency(),如下所示:AggregateCacheDependencydependency=DependencyAccess.CreateCategoryDependency().GetDependency();

为了方便调用者,似乎我们可以对DependencyAccess类进行改进,将原有的CreateCategoryDependency()方法,修改为创建AggregateCacheDependency类型对象的方法。

然而这样的做法扰乱了作为工厂类的DependencyAccess的本身职责,且创建IPetShopCacheDependency接口对象的行为仍然有可能被调用者调用,所以保留原有的DependencyAccess类仍然是有必要的。

在PetShop4.0的设计中,是通过引入Facade模式以方便调用者更加简单地获得AggregateCacheDependency类型对象。

4.3.4引入Facade模式

利用Facade模式可以将一些复杂的逻辑进行包装,以方便调用者对这些复杂逻辑的调用。就好像提供一个统一的门面一般,将内部的子系统封装起来,统一为一个高层次的接口。一个典型的Facade模式示意图如下所示:

图4-4Facade模式

Facade模式的目的并非要引入一个新的功能,而是在现有功能的基础上提供一个更高层次的抽象,使得调用者可以直接调用,而不用关心内部的实现方式。以CacheDependency工厂为例,我们需要为调用者提供获得AggregateCacheDependency对象的简便方法,因而创建了DependencyFacade类:publicstaticclassDependencyFacade{privatestaticreadonlystringpath=ConfigurationManager.AppSettings[”CacheDependencyAssembly”];publicstaticAggregateCacheDependencyGetCategoryDependency(){if(!string.IsNullOrEmpty(path))returnDependencyAccess.CreateCategoryDependency().GetDependency();elsereturnnull;}publicstaticAggregateCacheDependencyGetProductDependency(){if(!string.IsNullOrEmpty(path))returnDependencyAccess.CreateProductDependency().GetDependency();elsereturnnull;}publicstaticAggregateCacheDependencyGetItemDependency(){if(!string.IsNullOrEmpty(path))returnDependencyAccess.CreateItemDependency().GetDependency();elsereturnnull;}}

比起直接调用DependencyAccess类的GetDependency()方法而言,除了方法更简单之外,同时它还对CacheDependencyAssembly配置节进行了判断,如果其值为空,则返回null对象。

在PetShop.Web的App_Code文件夹下,静态类WebUtility的GetCategoryName()和GetProductName()方法调用了DependencyFacade类。例如GetCategoryName()方法:publicstaticstringGetCategoryName(stringcategoryId){Categorycategory=newCategory();if(!enableCaching)returncategory.GetCategory(categoryId).Name;

stringcacheKey=string.Format(CATEGORY_NAME_KEY,categoryId);

//检查缓存中是否存在该数据项;stringdata=(string)HttpRuntime.Cache[cacheKey];if(data==null){//通过web.config的配置获取duration值;intcacheDuration=int.Parse(ConfigurationManager.AppSettings[”CategoryCacheDuration”]);//如果缓存中不存在该数据项,则通过业务逻辑层访问数据库获取;data=category.GetCategory(categoryId).Name;//通过Facade类创建AggregateCacheDependency对象;AggregateCacheDependencycd=DependencyFacade.GetCategoryDependency();//将数据项以及AggregateCacheDependency对象存储到缓存中;HttpRuntime.Cache.Add(cacheKey,data,cd,DateTime.Now.AddHours(cacheDuration),Cache.NoSlidingExpiration,CacheItemPriority.High,null);}returndata;}

GetCategoryName()方法首先会检查缓存中是否已经存在CategoryName数据项,如果已经存在,就通过缓存直接获取数据;否则将通过业务逻辑层调用数据访问层访问数据库获得CategoryName,在获得了CategoryName后,会将新获取的数据连同DependencyFacade类创建的AggregateCacheDependency对象添加到缓存中。

WebUtility静态类被表示层的许多页面所调用,例如Product页面:publicpartialclassProducts:System.Web.UI.Page{protectedvoidPage_Load(objectsender,EventArgse){Page.Title=WebUtility.GetCategoryName(Request.QueryString[”categoryId”]);}}

显示页面title的逻辑是放在Page_Load事件方法中,因而每次打开该页面都要执行获取CategoryName的方法。如果没有采用缓存机制,当Category数据较多时,页面的显示就会非常缓慢。

4.3.5引入Proxy模式

以PetShop.BLL.Product业务对象为例,PetShop为其建立了代理对象ProductDataProxy,并在GetProductByCategory()等方法中,引入了缓存机制,例如:publicstaticclassProductDataProxy{

privatestaticreadonlyintproductTimeout=int.Parse(ConfigurationManager.AppSettings[”ProductCacheDuration”]);privatestaticreadonlyboolenableCaching=bool.Parse(ConfigurationManager.AppSettings[”EnableCaching”]);publicstaticIListGetProductsByCategory(stringcategory){Productproduct=newProduct();

if(!enableCaching)returnproduct.GetProductsByCategory(category);

stringkey=“product_by_category_”+category;IListdata=(IList)HttpRuntime.Cache[key];

//Checkifthedataexistsinthedatacacheif(data==null){data=product.GetProductsByCategory(category);

//CreateaAggregateCacheDependencyobjectfromthefactoryAggregateCacheDependencycd=DependencyFacade.GetProductDependency();

//Storetheoutputinthedatacache,andAddthenecessaryAggregateCacheDependencyobjectHttpRuntime.Cache.Add(key,data,cd,DateTime.Now.AddHours(productTimeout),Cache.NoSlidingExpiration,CacheItemPriority.High,null);}returndata;}}

引入Proxy模式,实现了在缓存级别上对业务对象的封装,增强了对业务对象的控制。由于暴露在对象外的方法是一致的,因而对于调用方而言,调用代理对象与真实对象并没有实质的区别。

从职责分离与分层设计的角度分析,我更希望这些Proxy对象是被定义在业务逻辑层中,而不像在PetShop的设计那样,被划分到表示层UI中。此外,如果需要考虑程序的可扩展性与可替换性,我们还可以为真实对象与代理对象建立统一的接口或抽象类。然而,单以PetShop的表示层调用来看,采用静态类与静态方法的方式,或许更为合理。我们需要谨记,“过度设计”是软件设计的警戒线。

如果需要对UI层采用缓存机制,将应用程序数据存放到缓存中,就可以调用这些代理对象。以ProductsControl用户控件为例,调用方式如下:productsList.DataSource=ProductDataProxy.GetProductsByCategory(categoryKey);

productsList对象属于自定义的CustomList类型,这是一个派生自System.Web.UI.WebControls.DataList控件的类,它的DataSource属性可以接受IList集合对象。不过在PetShop4.0的设计中,对于类似于ProductsControl类型的控件而言,采用的缓存机制是页输出缓存。我们可以从ProductsControl.ascx页面的Source代码中发现端倪:<%@OutputCacheDuration="100000"VaryByParam="page;categoryId"%>

采用页输出缓存,并且利用ControlCachePolicy设置输出缓存,能够将业务数据与整个页面放入到缓存中。这种方式比起应用程序缓存而言,在性能上有很大的提高。同时,它又通过引入的SqlCacheDependency特性有效地避免了“数据过期”的缺点,因而在PetShop4.0中被广泛采用。相反,之前为Product、Category、Item业务对象建立的代理对象则被“投闲散置”,仅仅作为一种设计方法的展示而“幸存”与整个系统的源代码中。

五PetShop之业务逻辑层设计

业务逻辑层在体系架构中的位置很关键,它处于数据访问层与表示层中间,起到了数据交换中承上启下的作用。由于层是一种弱耦合结构,层与层之间的依赖是向下的,底层对于上层而言是“无知”的,改变上层的设计对于其调用的底层而言没有任何影响。如果在分层设计时,遵循了面向接口设计的思想,那么这种向下的依赖也应该是一种弱依赖关系。因而在不改变接口定义的前提下,理想的分层式架构,应该是一个支持可抽取、可替换的“抽屉”式架构。正因为如此,业务逻辑层的设计对于一个支持可扩展的架构尤为关键,因为它扮演了两个不同的角色。对于数据访问层而言,它是调用者;对于表示层而言,它却是被调用者。依赖与被依赖的关系都纠结在业务逻辑层上,如何实现依赖关系的解耦,则是除了实现业务逻辑之外留给设计师的任务。

5.1与领域专家合作设计业务逻辑层最大的障碍不在于技术,而在于对领域业务的分析与理解。很难想象一个不熟悉该领域业务规则和流程的架构设计师能够设计出合乎客户需求的系统架构。几乎可以下定结论的是,业务逻辑层的设计过程必须有领域专家的参与。在我曾经参与开发的项目中,所涉及的领域就涵盖了电力、半导体、汽车等诸多行业,如果缺乏这些领域的专家,软件架构的设计尤其是业务逻辑层的设计就无从谈起。这个结论唯一的例外是,架构设计师同时又是该领域的专家。然而,正所谓“千军易得,一将难求”,我们很难寻觅到这样卓越出众的人才。

领域专家在团队中扮演的角色通常称为BusinessConsultor(业务咨询师),负责提供与领域业务有关的咨询,与架构师一起参与架构与数据库的设计,撰写需求文档和设计用例(或者用户故事UserStory)。如果在测试阶段,还应该包括撰写测试用例。理想的状态是,领域专家应该参与到整个项目的开发过程中,而不仅仅是需求阶段。

传统的软件开发模型同样重视与领域专家的合作,但这种合作主要集中在需求分析阶段。例如瀑布模型,就非常强调早期计划与需求调研。然而这种未雨绸缪的早期计划方式,对架构师与需求调研人员的技能要求非常高,它强调需求文档的精确性,一旦分析出现偏差,或者需求发生变更,当项目开发进入设计阶段后,由于缺乏与领域专家沟通与合作的机制,开发人员估量不到这些错误与误差,因而难以及时作出修正。一旦这些问题像毒瘤一般在系统中蔓延开来,逐渐暴露在开发人员面前时,已经成了一座难以逾越的高山。我们需要消耗更多的人力物力,才能够修正这些错误,从而导致开发成本成数量级的增加,甚至于导致项目延期。当然还有一个好的选择,就是放弃整个项目。这样的例子不胜枚举,事实上,项目开发的“滑铁卢”,究其原因,大部分都是因为业务逻辑分析上出现了问题。

迭代式模型较之瀑布模型有很大地改进,因为它允许变更、优化系统需求,整个迭代过程实际上就是与领域专家的合作过程,通过向客户演示迭代所产生的系统功能,从而及时获取反馈,并逐一解决迭代演示中出现的问题,保证系统向着合乎客户需求的方向演化。因而,迭代式模型往往能够解决早期计划不足的问题,它允许在发现缺陷的时候,在需求变更的时候重新设计、重新编码并重新测试。

无论采用何种开发模型,与领域专家的合作都将成为项目成败与否的关键。这基于一个软件开发的普遍真理,那就是世界上没有不变的需求。一句经典名言是:“没有不变的需求,世上的软件都改动过3次以上,唯一一个只改动过两次的软件的拥有者已经死了,死在去修改需求的路上。”一语道尽了软件开发的残酷与艰辛!

那么应该如何加强与领域专家的合作呢?JamesCarey和BrentCarlson根据他们在参与的IBMSanFrancisco项目中获得的经验,提出了InnocentQuestions模式,其意义即“改进领域专家和技术专家的沟通质量”。在一个项目团队中,如果我们没有一位既能担任首席架构师,同时又是领域专家的人选,那么加强领域专家与技术专家的合作就显得尤为重要了。毕竟,作为一个领域专家而言,可能并不熟悉软件设计方法学,也不具备面向对象开发和架构设计的能力,同样,大部分技术专家很有可能对该项目所涉及的业务领域仅停留在一知半解的地步。如果领域专家与技术专家不能有效沟通,则整个项目的前途就岌岌可危了。

InnocentQuestions模式提出的解决方案包括:(1)选用可以与人和谐相处的人员组建开发团队;(2)清楚地定义角色和职权;(3)明确定义需要的交互点;(4)保持团队紧密;(5)雇佣优秀的人。

事实上,这已经从技术的角度上升到对团队的管理层次了。就好比篮球运动一样,即使你的球队集合了五名世界上最顶尖最有天赋的球员,如果各自为战,要想取得比赛的胜利依旧是非常困难的。团队精神与权责分明才是取得胜利的保障,软件开发同样如此。

与领域专家合作的基础是保证开发团队中永远保留至少一名领域专家。他可以是系统的客户,第三方公司的咨询师,最理想是自己公司雇佣的专家。如果项目中缺乏这样的一个人,那么我的建议是去雇佣他,如果你不想看到项目遭遇“西伯利亚寒流”的话。

确定领域专家的角色任务与职责。必须要让团队中的每一个人明确领域专家在整个团队中究竟扮演什么样的角色,他的职责是什么。一个合格的领域专家必须对业务领域有足够深入的理解,他应该是一个能够俯瞰整个系统需求、总揽全局的人物。在项目开发过程中,将由他负责业务规则和流程的制定,负责与客户的沟通,需求的调研与讨论,并于设计师一起参与系统架构的设计。编档是领域专家必须参与的工作,无论是需求文档还是设计文档,以及用例的编写,领域专家或者提出意见,或者作为撰写的作者,至少他也应该是评审委员会的重要成员。

加强与客户的沟通。客户同时也可以作为团队的领域专家,极限编程的现场客户原则是最好的示例。但现实并不都如此的完美,在无法要求客户成为开发团队中的固定一员时,聘请或者安排一个专门的领域专家,加强与客户的沟通,就显得尤为重要。项目可以通过领域专家获得客户的及时反馈。而通过领域专家去了解变更了的需求,会在最大程度上减少需求误差的可能。

5.2业务逻辑层的模式应用MartinFowler在《企业应用架构模式》一书中对领域层(即业务逻辑层)的架构模式作了整体概括,他将业务逻辑设计分为三种主要的模式:TransactionScript、DomainModel和TableModule。

TransactionScript模式将业务逻辑看作是一个个过程,是比较典型的面向过程开发模式。应用TransactionScript模式可以不需要数据访问层,而是利用SQL语句直接访问数据库。为了有效地管理SQL语句,可以将与数据库访问有关的行为放到一个专门的Gateway类中。应用TransactionScript模式不需要太多面向对象知识,简单直接的特性是该模式全部价值之所在。因而,在许多业务逻辑相对简单的项目中,应用TransactionScript模式较多。

DomainModel模式是典型的面向对象设计思想的体现。它充分考虑了业务逻辑的复杂多变,引入了Strategy模式等设计模式思想,并通过建立领域对象以及抽象接口,实现模式的可扩展性,并利用面向对象思想与身俱来的特性,如继承、封装与多态,用于处理复杂多变的业务逻辑。唯一制约该模式应用的是对象与关系数据库的映射。我们可以引入ORM工具,或者利用DataMapper模式来完成关系向对象的映射。

与DomainModel模式相似的是TableModule模式,它同样具有面向对象设计的思想,唯一不同的是它获得的对象并非是单纯的领域对象,而是DataSet对象。如果为关系数据表与对象建立一个简单的映射关系,那么DomainModel模式就是为数据表中的每一条记录建立一个领域对象,而TableModule模式则是将整个数据表看作是一个完整的对象。虽然利用DataSet对象会丢失面向对象的基本特性,但它在为表示层提供数据源支持方面却有着得天独厚的优势。尤其是在.Net平台下,ADO.NET与Web控件都为TableModule模式提供了生长的肥沃土壤。

5.3PetShop的业务逻辑层设计PetShop在业务逻辑层设计中引入了DomainModel模式,这与数据访问层对于数据对象的支持是分不开的。由于PetShop并没有对宠物网上商店的业务逻辑进行深入,也省略了许多复杂细节的商务逻辑,因而在DomainModel模式的应用上并不明显。最典型地应该是对Order领域对象的处理方式,通过引入Strategy模式完成对插入订单行为的封装。关于这一点,我已在第27章有了详尽的描述,这里就不再赘述。

Cart类通过一个Dictionary对象来负责对购物车内容的存储,同时定义了Add、Remove、Clear等方法,来实现对购物车内容的管理。

在前面我提到PetShop业务逻辑层中的领域对象仅仅是完成对数据对象的简单封装,但这种分离层次的方法在架构设计中依然扮演了举足轻重的作用。以Cart类的Add()方法为例,在方法内部引入了PetShop.BLL.Item领域对象,并调用了Item对象的GetItem()方法。如果没有在业务逻辑层封装Item对象,而是直接调用数据访问层的Item数据对象,为保证层次间的弱依赖关系,就需要调用工厂对象的工厂方法来创建PetShop.IDAL.IItem接口类型对象。一旦数据访问层的Item对象被多次调用,就会造成重复代码,既不离于程序的修改与扩展,也导致程序结构生长为臃肿的态势。

此外,领域对象对数据访问层数据对象的封装,也有利于表示层对业务逻辑层的调用。在三层式架构中,表示层应该是对于数据访问层是“无知”的,这样既减少了层与层间的依赖关系,也能有效避免“循环依赖”的后果。

如此一来,我们可以为Cart类定义一个有参数的构造函数:privateIOnSaleStrategym_onSale;publicCart(IOnSaleStrategyonSale){m_onSale=onSale;}

那么Total属性就可以修改为:publicdecimalTotal{get{returnm_onSale.CalculateTotalPrice(cartItems);}}

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

图5-1引入Strategy模式

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

5.4与数据访问层的通信业务逻辑层需要与数据访问层通信,利用数据访问层访问数据库,因此业务逻辑层与数据访问层之间就存在依赖关系。在数据访问层引入接口程序集以及数据工厂的设计前提下,能够做到两者间关系为弱依赖。我们从业务逻辑层的引用程序集中可以看到,BLL模块并没有引用SQLServerDAL和OracleDAL程序集。在业务逻辑层中,有关数据访问层中数据对象的调用,均利用多态原理定义了抽象的接口类型对象,然后利用工厂对象的工厂方法创建具体的数据对象。如PetShop.BLL.PetShop领域对象所示:namespacePetShop.BLL{publicclassProduct{//根据工厂对象创建IProduct接口类型实例;privatestaticreadonlyIProductdal=PetShop.DALFactory.DataAccess.CreateProduct();//调用IProduct对象的接口方法GetProductByCategory();publicIListGetProductsByCategory(stringcategory){//如果为空则新建List对象;if(string.IsNullOrEmpty(category))returnnewList();

//通过数据访问层的数据对象访问数据库;returndal.GetProductsByCategory(category);}//其他方法略;}}

在领域对象Product类中,利用数据访问层的工厂类DALFactory.DataAccess创建PetShop.IDAL.IProduct类型的实例,如此就可以解除对具体程序集SQLServerDAL或OracleDAL的依赖。只要PetShop.IDAL的接口方法不变,即使修改了IDAL接口模块的具体实现,都不会影响业务逻辑层的实现。这种松散的弱耦合关系,才能够最大程度地支持架构的可扩展。

5.5面向接口设计也许是业务逻辑比较简单地缘故,在业务逻辑层的设计中,并没有秉承在数据访问层中面向接口设计的思想。除了完成对插入订单策略的抽象外,整个业务逻辑层仅以BLL模块实现,没有为领域对象定义抽象的接口。因而PetShop的表示层与业务逻辑层就存在强依赖关系,如果业务逻辑层中的需求发生变更,就必然会影响表示层的实现。唯一可堪欣慰的是,由于我们采用分层式架构将用户界面与业务领域逻辑完全分离,一旦用户界面发生更改,例如将B/S架构修改为C/S架构,那么业务逻辑层的实现模块是可以完全重用的。

然而,最理想的方式仍然是面向接口设计。根据第28章对ASP.NET缓存的分析,我们可以将表示层App_Code下的Proxy类与Utility类划分到业务逻辑层中,并修改这些静态类为实例类,并将这些类中与业务领域有关的方法抽象为接口,然后建立如数据访问层一样的抽象工厂。通过“依赖注入”方式,解除与具体领域对象类的依赖,使得表示层仅依赖于业务逻辑层的接口程序集以及工厂模块。

那么,这样的设计是否有“过度设计”的嫌疑呢?我们需要依据业务逻辑的需求情况而定。此外,如果我们需要引入缓存机制,为领域对象创建代理类,那么为领域对象建立接口,就显得尤为必要。我们可以建立一个专门的接口模块IBLL,用以定义领域对象的接口。以Product领域对象为例,我们可以建立IProduct接口:publicinterfaceIProduct{IListGetProductByCategory(stringcategory);IListGetProductByCategory(string[]keywords);ProductInfoGetProduct(stringproductId);}

在BLL模块中可以引入对IBLL程序集的依赖,则领域对象Product的定义如下:publicclassProduct:IProduct{publicIListGetProductByCategory(stringcategory){//实现略;}publicIListGetProductByCategory(string[]keywords){//实现略;}publicProductInfoGetProduct(stringproductId){//实现略;}}

然后我们可以为代理对象建立专门的程序集BLLProxy,它不仅引入对IBLL程序集的依赖,同时还将依赖于BLL程序集。此时代理对象ProductDataProxy的定义如下:usingPetShop.IBLL;usingPetShop.BLL;namespacePetShop.BLLProxy{publicclassProductDataProxy:IProduct{publicIListGetProductByCategory(stringcategory){Productproduct=newProduct();//其他实现略;}publicIListGetProductByCategory(string[]keywords){//实现略;}publicProductInfoGetProduct(stringproductId){//实现略;}}}

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

图5-2Proxy模式

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

图5-4则是PetShop4.0原有设计的层次关系图:

图5-4PetShop4.0中表示层与业务逻辑层的关系

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

对于Microsoft.netPetShop程序中的购物车和订单处理模块,文中主要分析两种技术的应用:

1.Profile技术在PetShop程序中用于三处:

1)购物车ShoppingCart-下面的例子围绕购物车流程进行

2)收藏WishList

3)用户信息AccountInfo

注册新用户NewUser.aspx:使用的是CreateUserWizard控件,基于MemberShip机制,在数据库MSPetShop4Services的表aspnet_Users中创建用户

修改用户注册信息UserProfile.aspx:基于Profile技术,在数据库MSPetShop4Profile的表Profiles和Account中创建用户信息

2.异步消息处理技术运用于订单处理

Profile可以利用数据库存储关于用户的个性化信息,有点象session对象,但session对象是有生存期的,在生存期后,session对象自动失效了。而profile不同,除非显式移除它。要实现profile功能,必须先在web.config中进行定义。

在web.congfig中,将会定义一些属性/值,分别存贮将要保存的变量和值,比如language属性,定义其值是string类型,如此类推。而<group>标签,则是将一些相同或类似功能的变量值放在一起。

程序中使用方法:Profile.language=ddlLanguage.SelectedItem.Value;

2.ShoppingCart.aspx文件处理:在init方法之前处理

protectedvoidPage_PreInit(objectsender,EventArgse){

if(!IsPostBack){

stringitemId=Request.QueryString["addItem"];

if(!string.IsNullOrEmpty(itemId)){

Profile.ShoppingCart.Add(itemId);//注意ShoppingCart的类型是PetShop.BLL.Cart

//Save方法将修改后的配置文件属性值写入到数据源,如ShoppingCart属性已经改变

Profile.Save();

//Redirecttopreventduplictationsinthecartifuserhits"Refresh"

//防止刷新造成多次提交

Response.Redirect("~/ShoppingCart.aspx",true);//将客户端重定向到新的URL。指定新的URL并指定当前页的执行是否应终止。

}

3.PetShop.BLL.Cart类

//Dictionary:key/value

privateDictionarycartItems=newDictionary();

///

///Addanitemtothecart.

///WhenItemIdtobeaddedhasalreadyexisted,thismethodwillupdatethequantityinstead.

///

///ItemIdofitemtoadd

publicvoidAdd(stringitemId){

CartItemInfocartItem;

if(!cartItems.TryGetValue(itemId,outcartItem)){

Itemitem=newItem();

ItemInfodata=item.GetItem(itemId);

if(data!=null){

CartItemInfonewItem=newCartItemInfo(itemId,data.ProductName,1,(decimal)data.Price,data.Name,data.CategoryId,data.ProductId);

cartItems.Add(itemId,newItem);

else

cartItem.Quantity++;

4.更新Profile

如何更新:

根据配置中的ShoppingCartProvider类型PetShop.Profile.PetShopProfileProvider。

ASP.NET配置文件提供对用户特定属性的持久性存储和检索。配置文件属性值和信息按照由ProfileProvider实现确定的方式存储在数据源中。

每个用户配置文件在数据库的Profiles表中进行唯一标识。该表包含配置文件信息,如应用程序名称和上次活动日期。

CREATETABLEProfiles(UniqueIDAutoIncrementNOTNULLPRIMARYKEY,UsernameText(255)NOTNULL,ApplicationNameText(255)NOTNULL,IsAnonymousYesNo,LastActivityDateDateTime,LastUpdatedDateDateTime,CONSTRAINTPKProfilesUNIQUE(Username,ApplicationName))

5.PetShop.Profile.PetShopProfileProvider类,继承自ProfileProvider

//创建PetShop.SQLProfileDAL.PetShopProfileProvider类-数据库操作

privatestaticreadonlyIPetShopProfileProviderdal

=DataAccess.CreatePetShopProfileProvider();

///设置指定的属性设置组的值

publicoverridevoidSetPropertyValues(SettingsContextcontext,SettingsPropertyValueCollectioncollection){

stringusername=(string)context["UserName"];

CheckUserName(username);

boolisAuthenticated=(bool)context["IsAuthenticated"];

intuniqueID=dal.GetUniqueID(username,isAuthenticated,false,ApplicationName);

if(uniqueID==0)

uniqueID=dal.CreateProfileForUser(username,isAuthenticated,ApplicationName);

foreach(SettingsPropertyValuepvincollection){

if(pv.PropertyValue!=null){

switch(pv.Property.Name){

casePROFILE_SHOPPINGCART://ShoppingCart

SetCartItems(uniqueID,(Cart)pv.PropertyValue,true);

break;

casePROFILE_WISHLIST:

SetCartItems(uniqueID,(Cart)pv.PropertyValue,false);

casePROFILE_ACCOUNT:

if(isAuthenticated)

SetAccountInfo(uniqueID,(AddressInfo)pv.PropertyValue);

default:

thrownewApplicationException(ERR_INVALID_PARAMETER+"name.");

UpdateActivityDates(username,false);

//Updatecart

privatestaticvoidSetCartItems(intuniqueID,Cartcart,boolisShoppingCart){

dal.SetCartItems(uniqueID,cart.CartItems,isShoppingCart);

6.PetShop.SQLProfileDAL.PetShopProfileProvider类

使用事务:包含两个sql动作,先删除,再插入

///Updateshoppingcartforcurrentuser

///Userid

///Collectionofshoppingcartitems

///Shoppingcartflag

publicvoidSetCartItems(intuniqueID,ICollectioncartItems,boolisShoppingCart){

stringsqlDelete="DELETEFROMCartWHEREUniqueID=@UniqueIDANDIsShoppingCart=@IsShoppingCart;";

SqlParameter[]parms1={

newSqlParameter("@UniqueID",SqlDbType.Int),

newSqlParameter("@IsShoppingCart",SqlDbType.Bit)};

parms1[0].Value=uniqueID;

parms1[1].Value=isShoppingCart;

if(cartItems.Count>0){

//updatecartusingSqlTransaction

stringsqlInsert="INSERTINTOCart(UniqueID,ItemId,Name,Type,Price,CategoryId,ProductId,IsShoppingCart,Quantity)VALUES(@UniqueID,@ItemId,@Name,@Type,@Price,@CategoryId,@ProductId,@IsShoppingCart,@Quantity);";

SqlParameter[]parms2={

newSqlParameter("@IsShoppingCart",SqlDbType.Bit),

newSqlParameter("@ItemId",SqlDbType.VarChar,10),

newSqlParameter("@Name",SqlDbType.VarChar,80),

newSqlParameter("@Type",SqlDbType.VarChar,80),

newSqlParameter("@Price",SqlDbType.Decimal,8),

newSqlParameter("@CategoryId",SqlDbType.VarChar,10),

newSqlParameter("@ProductId",SqlDbType.VarChar,10),

newSqlParameter("@Quantity",SqlDbType.Int)};

parms2[0].Value=uniqueID;

parms2[1].Value=isShoppingCart;

SqlConnectionconn=newSqlConnection(SqlHelper.ConnectionStringProfile);

conn.Open();

SqlTransactiontrans=conn.BeginTransaction(IsolationLevel.ReadCommitted);

try{

SqlHelper.ExecuteNonQuery(trans,CommandType.Text,sqlDelete,parms1);

foreach(CartItemInfocartItemincartItems){

parms2[2].Value=cartItem.ItemId;

parms2[3].Value=cartItem.Name;

parms2[4].Value=cartItem.Type;

parms2[5].Value=cartItem.Price;

parms2[6].Value=cartItem.CategoryId;

parms2[7].Value=cartItem.ProductId;

parms2[8].Value=cartItem.Quantity;

SqlHelper.ExecuteNonQuery(trans,CommandType.Text,sqlInsert,parms2);

trans.Commit();

catch(Exceptione){

trans.Rollback();

thrownewApplicationException(e.Message);

finally{

conn.Close();

//deletecart

SqlHelper.ExecuteNonQuery(SqlHelper.ConnectionStringProfile,CommandType.Text,sqlDelete,parms1);

订单处理技术:――分布式事务

1)同步:直接在事务中将订单插入到数据库中,同时更新库存

2)异步:订单-》消息队列(使用MSMQ)-》后台处理

开启MSDTC服务支持分布式事务.TostarttheMSDTCservice,openAdministrativeTools|ServicesandstarttheDistributedTransactionCoordinatorservice

1)引用队列

引用队列有三种方法,通过路径、格式名和标签引用队列,这里我只介绍最简单和最常用的方法:通过路径引用队列。队列路径的形式为machinename\queuename。指向队列的路径总是唯一的。下表列出用于每种类型的队列的路径信息:

如果是发送到本机上,还可以使用”.”代表本机名称。

2)消息的创建

不过要使用MSMQ开发你的消息处理程序,必须在开发系统和使用程序的主机上安装消息队列。消息队列的安装属于Windows组件的安装,和一般的组件安装方法类似。

往系统中添加队列十分的简单,打开[控制面板]中的[计算机管理],展开[服务和应用程序],找到并展开[消息队列](如果找不到,说明你还没有安装消息队列,安装windows组件),右击希望添加的消息队列的类别,选择新建队列即可。

消息接收服务位于System.Messaging中,在初始化时引用消息队列的代码很简单,如下所示:

MessageQueueMq=newMessageQueue(“.\\private$\\jiang”);

通过Path属性引用消息队列的代码也十分简单:

MessageQueueMq=newMessageQueue();

Mq.Path=”.\\private$\\jiang”;

使用Create方法可以在计算机上创建队列:

System.Messaging.MessageQueue.Create(@".\private$\jiang");

3)发送和接收消息

过程:消息的创建-》发送-》接收-》阅读-》关闭

简单消息的发送示例如下:

Mq.Send(1000);//发送整型数据

Mq.Send(“Thisisatestmessage!”);//发送字符串

接收消息由两种方式:通过Receive方法接收消息同时永久性地从队列中删除消息;通过Peek方法从队列中取出消息而不从队列中移除该消息。如果知道消息的标识符(ID),还可以通过ReceiveById方法和PeekById方法完成相应的操作。

接收消息的代码很简单:

Mq.Receive();//或Mq.ReceiveById(ID);

Mq.Peek();//或Mq.PeekById(ID);

阅读消息

接收到的消息只有能够读出来才是有用的消息,因此接收到消息以后还必须能读出消息,而读出消息算是最复杂的一部操作了。消息的序列化可以通过VisualStudio和.NETFramework附带的三个预定义的格式化程序来完成:XMLMessageFormatter对象(MessageQueue组件的默认格式化程序设置)、BinaryMessageFormatter对象、ActiveXMessageFormatter对象。由于后两者格式化后的消息通常不能为人阅读,所以我们经常用到的是XMLMessageFormatter对象。

使用XMLMessageFormatter对象格式化消息的代码如下所示:

string[]types={"System.String"};

((XmlMessageFormatter)mq.Formatter).TargetTypeNames=types;

Messagem=mq.Receive(newTimeSpan(0,0,3));

将接收到的消息传送给消息变量以后,通过消息变量m的Body属性就可以读出消息了:

MessageBox.Show((string)m.Body);

关闭消息队列

消息队列的关闭很简单,和其他对象一样,通过Close函数就可以实现了:

Mq.Close();

默认程序使用同步消息处理,直接操作数据库插入订单,更新库存类

1)Web程序中调用PetShop.BLL.Order类方法:Insert(OrderInfoorder);

2)PetShop.BLL.Order类

//IOrderStrategy接口中只有一个插入订单方法:voidInsert(PetShop.Model.OrderInfoorder);

//得到PetShop.BLL.OrderAsynchronous类

privatestaticreadonlyPetShop.IBLLStrategy.IOrderStrategyorderInsertStrategy=LoadInsertStrategy();

//IOrder接口中有两种方法:Send()与Receive()-消息队列

privatestaticreadonlyPetShop.IMessaging.IOrderorderQueue

=PetShop.MessagingFactory.QueueAccess.CreateOrder();

publicvoidInsert(OrderInfoorder){

//Callcreditcardprocesor,采用随机化方法设置订单认证数字

ProcessCreditCard(order);

//Inserttheorder(a)synchrounouslybasedonconfiguration

orderInsertStrategy.Insert(order);//调用PetShop.BLL.OrderAsynchronous类

3)PetShop.BLL.OrderAsynchronous类

//CreateOrder()方法得到PetShop.MSMQMessaging.Order类的实例

privatestaticreadonlyPetShop.IMessaging.IOrderasynchOrder

publicvoidInsert(PetShop.Model.OrderInfoorder){

asynchOrder.Send(order);//调用PetShop.MSMQMessaging.Order类

4)PetShop.MSMQMessaging项目-关键(发送/接收消息)

PetShopQueue基类:创建消息队列,发送和接收消息

Order类:继承自PetShopQueue类

publicnewOrderInfoReceive(){//从队列中接收消息

base.transactionType=MessageQueueTransactionType.Automatic;

return(OrderInfo)((Message)base.Receive()).Body;

publicvoidSend(OrderInfoorderMessage){//发送消息到队列

base.transactionType=MessageQueueTransactionType.Single;

base.Send(orderMessage);

5)PetShop.OrderProcessor项目-后台处理订单,将它们插入到数据库中

Program类:多线程后台订单处理程序,可写成一个控制台程序,作为windows服务开启

处理队列中的批量异步订单,在事务范围内把它们提交到数据库

THE END
1.宠物店加盟宠物连锁店开宠物店中国宠物连锁品牌派多格是进入宠物行业的企业之一,业务覆盖宠物店连锁加盟、宠物美容技术培训、训犬技术培训、宠物店运营管理培训、宠物用品研发及销售、宠物活体销售、宠物行业信息化建设等。http://www.apetdog.com/
2.Java毕业设计网上宠物店系统在系统流程分析当中调查分析它是比较重要的环节,因为在这个系统当中它都涉及到每个环节的业务流程,所以从Java+SpringBoot+Vue实现的网上宠物店系统的设计的整体设计上要保证各个信息的正确输入和输出以及对数据储存的完整,并结合实际的操作步骤来绘制出具体的流程图。具体流程图如下图所示: ...https://www.jianshu.com/p/9b4fba74f8eb
3.基于Springboot的网上宠物店系统的设计与实现毕业论文.doc基于Springboot的网上宠物店系统的设计与实现毕业论文.doc,PAGE 毕业设计(论文) 网上宠物店系统设计与实现 PAGE 30 摘要 传统办法管理信息首先需要花费的时间比较多,其次数据出错率比较高,而且对错误的数据进行更改也比较困难,最后,检索数据费事费力。因此,在计https://max.book118.com/html/2023/0806/8126070134005116.shtm
1.宠物店管理系统设计一套符合店铺经营的管理系统,第一可以提高宠物店的销售效率,通过管理系统对库存、销售记录等进行收集,减少人为的错误和漏洞;第二可以优化服务流程对宠物医疗、美容等服务项目进行全面、精准、高效的管理和监控,从而优化服务流程,提高服务质量和客户满意度;第三可以进行数据分析和决策支持,帮助宠物店管理者更好的了解...https://blog.csdn.net/2401_86117024/article/details/143836864
2.宠物店自助自助洗澡机模式系统APP开发(现成案例),公司宣布将开展一项创新性项目——开发一款针对宠物店的自助洗澡机系统软件。以下是对该项目的详细说明,体现了微都科技在解决行业痛点、提升服务体验方面的能力和创新精神。 项目背景: 随着宠物市场的快速扩张,宠物洗澡服务需求日益旺盛。然而,传统洗澡服务在效率、便捷性等方面存在不足。微都科技针对这一现状,提出了...https://176939816.b2b.11467.com/m/news/8959499.asp
3.创新创业网上店铺计划书(通用6篇)创新创业网上店铺计划书3 1导言 1.1概述:在现代社会,家庭中的宠物对于主人而言完全是一种情感需要,人们养宠物是为了排解心中的寂寞。越来越多的家庭将宠物作为自己家庭的一部分,于此产生的宠物消费也是越来越丰富。伴随宠物的不断增加,出现了宠物美容,医疗,用品等宠物产业。作为新兴的产业,宠物店的发展前景也是非常乐观...https://www.yjbys.com/chuangye/ziliao/chuangyejihuashu/632753.html
4.宠物店项目计划书(推荐10篇)宠物店项目计划书 篇1 一、市场分析 1、国际宠物产业现状分析 宠物作为一个产业在发达国家已经有一百二十年的历史,形成了养殖、培训、用品用具、医疗、医药、贸易的产业链。不仅法律法规严格,责任明确,政府与宠物组织相互合作,而且宠物产业的发展也在系统化和规范化下运行,形成国民经济的一部分。在国外,“宠物经济”...https://www.zwlu.com/word/841380.html
5.产品设计毕业选题(通用8篇)基于网络的通用考务系统-网上报名子系统 基于网络环境的库存管理系统的设计与实现 集成客户关系管理的企业网站的设计与开发 计算机实验室教学管理系统的设计与实现 计算机学院图书管理系统 计算机组成原理教学网站 家电销售管理系统的设计与实现 家教交流平台的实现 ...https://www.360wenmi.com/f/file7avd8w79.html
6.宠物店结算系统有哪些项目宠物店结算系统有哪些项目,宠物店结算系统包含几个项目,其中包括宠物商品销售模块、宠物服务模块和宠物寄养模块等。在宠物商品销售模块中,可以进行商品录入、出库、退货、补货等操作,并可进行实时库存监控和采购管理;宠物服务模块中,可以进行服务预约、服务开单、服务https://h.chanjet.com/ask/39f1d2dbf2682.html
7.基于java的宠物店管理系统javaweb宠物管理系统项目基于java的宠物店管理系统 javaweb宠物管理系统项目 完整动物商店小项目实现 整体项目要求: 实现前后端数据的交互 实现用户的登录和注册 能够实现宠物库的内容的查询和展示 能够完成宠物的添加、修改、删除、排序操作 涉及知识 掌握Maven项目的创建配置、mybatis的配置使用、前端基础知识、servlet请求等...https://blog.51cto.com/u_16213721/7018103
8.矿山抢险救援系统简介.ppt动漫城翻新项目补充条款 宠物店改造施工协议 保龄球场地暖安装协议模板 教育培训项目居间合同 推荐文档 2016年高考英语北京卷(阅读、七选五、完型) 2021年夏季食品安全提示3篇范文 XX公司绩效考核管理办法 车辆管理台帐 2017-2018学年贵州省遵义市七年级(下)期末语文试卷 2014年武汉蔡甸项目开发可行性建议报告_60P 2019学...https://m.renrendoc.com/paper/101527204.html
9.大学生创新创业项目计划书大学生创新创业项目计划书(优秀26篇)我们的项目是做服装行业,开个女式服装店。 一、店名:(衣拉客、唯衣、衣新衣异、衣衣布舍) 群体目标 国内成年服装年龄段分类基本为:18-30岁,30-45岁,45-65岁,65岁以上。 1、18-30岁:该年龄段的消费群体是服装消费的最主要的群体,是消费群体中服装购买频率最多,总体购买金额较多的群体,其中女性消费的频率高于...https://www.shubaoc.com/zhichang/fanwen/43g0.html
10.宠易萌宠物店管理系统收银软件小程序会员系统宠易萌是一款专门为宠物行业设计的宠物门店管理软件,宠物店管理、宠物店收银软件、宠物店会员系统,宠物店小程序等。广泛适用于宠物收银,宠物美容、宠物门店经营,具有强大的会员管理,寄养管理、连锁分店管理、库存预警、新零售管理同步等功能,方便的票据打印,简洁实用https://www.chongyimeng.com/
11.飞机系统简介.ppt货运流程自动化系统 41页 小班安全教育目标 10页 2024 2025学年新教材高中物理课时分层作业1电.. 5页 宠物店锦集八篇 7页 安全培训考试试题答案全面 12页 2024 2025学年新教材高中物理第9章静电场及其.. 7页 音响设备创新技术 52页 太阳开水器项目评估报告 34页 大数据项目评估报告 52页 大学生感恩节演讲稿 ...https://m.taodocs.com/p-135930978.html
12.权威!国内top3人气火爆的宠物店选品陈列进修中心有哪些害怕自己不懂宠物美容知识?慕名宠物美容培训为你解惑。系统的课程、专业的教师,让小白快速提升。成为行业大牛,就在这里,快来开启你的辉煌人生。以上就是小编整理了权威!国内top3人气火爆的宠物店选品陈列进修中心有哪些的相关资讯,如果大家希望了解更多关于课程价格和校区的情况,可以留言在线客服、或者17637878575加老师微信...https://www.jiaoxue123.cn/news/show-488381.html
13.开店计划书范文(精选10篇)一、项目背景 四川的火锅发展到今天,从火锅的品种、档次、规模、制作、调配、风味等方面已经呈现出了多元化的特点,使各种从事火锅制作并提供火锅消费的企业迅速发展,也引起了从事和即将投资火锅产生的企业和经营者的极大热情和兴趣。但是,如何开一家正宗的四川火锅店,却是门不小的学问。 https://www.fwsir.com/fanwen/html/fanwen_20160518091529_337275.html
14.全国12315平台您发现违反市场监管法律法规的行为 我要举报 您需要了解市场监管有关法律法规 我要咨询 网站导航 国家市场监督管理总局 国家企业信用信息公示系统 中国政府网 主办:中华人民共和国国家市场监督管理总局 京ICP备16053442号-2 地址:北京市西城区三里河东路八号 邮政编码:100820 National Platform of Consumer Dispute...https://www.12315.cn/
15.创业策划书开家宠物店,首先要拿出1万元左右,参加培训一个月左右并拿到“宠物美容师”资格证。除啦营养品跟上,还应当有专门的美容项目,所以,还应该拿出1万元投资宠物美容设备:洗澡池、一把电剪(20xx元左右)和五个刀头(1000元)、三四把美容剪(20xx元)、吹水机、电吹风、热水器等。两万元用于进货,在宠物用品所触及的吃、洗...https://www.ruiwen.com/cehuashu/4046779.html