1. 第1部分
该文档的 PDF 版本可在此处获取 |HERE|。
文档中的示例已发布 |HERE|。
1.1. Introduction
本文目标:
- 编写一个三层Web应用程序 [interface utilisateur, métier, accès aux données]
- 使用 Spring 配置应用程序 IOC
- 通过更改三层中的任意一层实现来编写不同版本。
使用的工具:
- 开发使用 Visual Studio.net —— 详见附录第 3.1 节;
- Cassini Web 服务器用于运行——参见附录第 3.2 节;
- Nunit 用于单元测试——详见附录第 3.4 节;
- Spring:用于Web应用程序各层的集成与配置——详见附录第3.3节;
按初级-中级-高级的难度等级划分,本文档属于 [intermédiaire-avancé] 级别。理解本文需要具备多种先决条件。其中部分内容可通过我撰写的其他文档获取。若涉及此类情况,我会予以注明。当然,这仅为建议,读者亦可使用自己偏好的文档。
- VB.net 语言:[通过实例学习VB.NET语言];
- VB.net 中的 Web 编程:[使用 ASP.NET 1.1 进行 Web 开发 (2004)];
- Spring的IoC组件应用:[Spring IoC for .NET (2005)];
- Spring.net文档:[Spring.NET | Homepage ]
本文延续了为 Java 编写的文档 [基于Struts、Spring和Java的三层架构与MVC架构] 的主线。我们将使用 VB.NET 构建一个用 Java 编写的三层 Web 应用程序 MVC。 本文旨在阐明:Java与.NET开发平台之间具有高度相似性,因此在其中一个领域掌握的技能可直接应用于另一个领域。
目前似乎尚无广受认可的 MVC ASP.NET 开发方案。以下方案采用了 [使用 ASP.NET 1.1 进行 Web 开发 (2004)] 文档中介绍的方法。 虽然该方法的优点在于采用了J2EE开发中普遍的概念,但必须将其视为其本来的样子,即c.a.d。它只是众多MVC开发方法中的一种。 一旦某种MVC开发方法在ASP.NET中获得广泛认可,就应采用该方法。目前正在开发的Spring .NET版本,很可能成为首个解决方案。
1.2. webarticles 应用程序
本文介绍了一个简化的电子商务Web应用程序的组成部分。该应用程序将允许Web用户:
- 浏览来自数据库的商品列表
- 将部分商品放入电子购物车
- 确认购物车。此确认操作仅会更新数据库中已购商品的库存。
向用户展示的视图包括:
- “LISTE”视图,展示待售商品列表 ![]() | - 视图 [INFOS],提供产品的详细信息: ![]() |
- 视图 [PANIER] 和 [PANIERVIDE],用于显示客户购物车中的内容
![]() | ![]() |
- 视图 [ERREURS],用于报告应用程序中的任何错误

1.3. 应用程序的总体架构
我们希望构建一个具有以下三层结构的应用程序:
![]() |
- 通过使用接口,使这三层相互独立
- 各层之间的集成由Spring实现
- 每层均使用独立的命名空间:web(UI层)、domain(业务层)和dao(数据访问层)。
该应用程序将遵循 MVC 架构(模型-视图-控制器)。若参照上述分层图,MVC 架构将按以下方式融入其中:
![]() |
处理客户端请求的流程如下:
- 客户端向控制器发送请求。此处的控制器将是一个 .aspx 页面,它将承担特殊角色。该页面处理所有客户端请求,是应用程序的入口,即 MVC 架构中的 C 层。
- 控制器处理该请求。为此,它可能需要业务层的协助,即MVC架构中的M模型。
- 控制器从业务层收到响应。客户端的请求已处理完毕。这可能导致多种可能的响应。一个典型的例子是
- 如果请求无法正确处理,则显示错误页面
- 否则则返回确认页面
- 控制器选择要发送给客户端的响应(即视图)。该响应通常是一个包含动态元素的页面。控制器将这些元素提供给视图。
- 视图被发送给客户端。这就是MVC中的“V”。
1.4. 模型
这里我们研究的是 MVC 中的 M。该模型由以下元素组成:
- 业务类
- 数据访问类
- 数据库
1.4.1. 数据库
该数据库仅包含一个名为 ARTICLES 的表。该表是通过以下 SQL 命令生成的:
CREATE TABLE ARTICLES (
ID INTEGER NOT NULL,
NOM VARCHAR(20) NOT NULL,
PRIX NUMERIC(15,2) NOT NULL,
STOCKACTUEL INTEGER NOT NULL,
STOCKMINIMUM INTEGER NOT NULL
);
/* 约束 */
ALTER TABLE ARTICLES ADD CONSTRAINT CHK_ID check (ID>0);
ALTER TABLE ARTICLES ADD CONSTRAINT CHK_PRIX check (PRIX>=0);
ALTER TABLE ARTICLES ADD CONSTRAINT CHK_STOCKACTUEL check (STOCKACTUEL>=0);
ALTER TABLE ARTICLES ADD CONSTRAINT CHK_STOCKMINIMUM check (STOCKMINIMUM>=0);
ALTER TABLE ARTICLES ADD CONSTRAINT CHK_NOM check (NOM<>'');
ALTER TABLE ARTICLES ADD CONSTRAINT UNQ_NOM UNIQUE (NOM);
/* 主键 */
ALTER TABLE ARTICLES ADD CONSTRAINT PK_ARTICLES PRIMARY KEY (ID);
唯一标识商品的主键 | |
商品名称 | |
其价格 | |
当前库存 | |
库存低于该数值时需下达补货订单 |
1.4.2. 模型的命名空间
此处提供的模型 M 包含两个命名空间:
- istia.st.articles.dao:包含 [dao] 层的数据访问类
- istia.st.articles.domain:包含 [domain] 层面的业务类
每个命名空间都将生成一个专属的“程序集”文件:
contenu | rôle | |
- [IArticlesDao]:访问 [dao] 层的接口。 这是 [domain] 层唯一可见的接口。它看不到其他接口。 - [Article]:定义商品的类 - [ArticlesDaoArrayList]:实现 [IArticlesDao] 接口的实现类,其中包含一个 [ArrayList] | 数据访问层 - 完全位于 [dao] Web 应用程序 | |
- [IArticlesDomain]:访问 [domain] 层的接口。这是 Web 层唯一可见的接口,它看不到其他接口。 - [AchatsArticles]:一个实现 [IArticlesDomain] 的类 - [Achat]:表示客户购买记录的类 - [Panier]:表示客户所有购买记录的类 | 代表了 - 完全位于 [domain] 层中 三层Web应用程序 |
1.4.3. [dao]层
[dao]层包含以下组件:
-
[IArticlesDao]:访问 [dao] 层的接口
-
[Article]:定义文章的类
-
[ArticlesDaoArrayList]:通过类 [ArrayList] 实现接口 [IArticlesDao] 的类
[dao] 层中的 [Visual Studio] 项目结构如下:

备注:
- 项目 [dao] 属于 [bibliothèque de classes] 类型
- 类已被放置在以文件夹 [istia] 为根的树形结构中。它们均位于命名空间 [istia.st.articles.dao] 中。
1.4.3.1. 类 [Article]
定义文章的类如下:
Imports System
Namespace istia.st.articles.dao
Public Class Article
' 私有字段
Private _id As Integer
Private _nom As String
Private _prix As Double
Private _stockactuel As Integer
Private _stockminimum As Integer
' 商品ID
Public Property id() As Integer
Get
Return _id
End Get
Set(ByVal Value As Integer)
If Value <= 0 Then
Throw New Exception("Le champ id [" + Value.ToString + "] est invalide")
End If
Me._id = Value
End Set
End Property
' 商品名称
Public Property nom() As String
Get
Return _nom
End Get
Set(ByVal Value As String)
If Value Is Nothing OrElse Value.Trim.Equals("") Then
Throw New Exception("Le champ nom [" + Value + "] est invalide")
End If
Me._nom = Value
End Set
End Property
' 商品价格
Public Property prix() As Double
Get
Return _prix
End Get
Set(ByVal Value As Double)
If Value < 0 Then
Throw New Exception("Le champ prix [" + Value.ToString + "] est invalide")
End If
Me._prix = Value
End Set
End Property
' 商品当前库存
Public Property stockactuel() As Integer
Get
Return _stockactuel
End Get
Set(ByVal Value As Integer)
If Value < 0 Then
Throw New Exception("Le champ stockActuel [" + Value.ToString + "] est invalide")
End If
Me._stockactuel = Value
End Set
End Property
' 商品最低库存
Public Property stockminimum() As Integer
Get
Return _stockminimum
End Get
Set(ByVal Value As Integer)
If Value < 0 Then
Throw New Exception("Le champ stockMinimum [" + Value.ToString + "] est invalide")
End If
Me._stockminimum = Value
End Set
End Property
' 默认制造商
Public Sub New()
End Sub
' 带属性的制造商
Public Sub New(ByVal id As Integer, ByVal nom As String, ByVal prix As Double, ByVal stockactuel As Integer, ByVal stockminimum As Integer)
Me.id = id
Me.nom = nom
Me.prix = prix
Me.stockactuel = stockactuel
Me.stockminimum = stockminimum
End Sub
' 商品识别方法
Public Overrides Function ToString() As String
Return "[" + id.ToString + "," + nom + "," + prix.ToString + "," + stockactuel.ToString + "," + stockminimum.ToString + "]"
End Function
End Class
End Namespace
该类提供:
- 一个构造函数,用于设置文章的5项信息:[id, nom, prix, stockactuel, stockminimum]
- 用于读取和写入这5项信息的公共属性。
- 对商品中插入的数据进行验证。若数据有误,将抛出异常。
- 一个方法 toString,用于以字符串形式获取条目的值。这在应用程序调试中通常非常有用。
1.4.3.2. [IArticlesDao] 接口
接口 [IArticlesDao] 定义如下:
Imports System
Imports System.Collections
Namespace istia.st.articles.dao
Public Interface IArticlesDao
' 所有商品列表
Function getAllArticles() As IList
' 添加商品
Function ajouteArticle(ByVal unArticle As Article) As Integer
' 删除商品
Function supprimeArticle(ByVal idArticle As Integer) As Integer
' 修改商品
Function modifieArticle(ByVal unArticle As Article) As Integer
' 搜索商品
Function getArticleById(ByVal idArticle As Integer) As Article
' 删除所有商品
Sub clearAllArticles()
' 更改商品库存
Function changerStockArticle(ByVal idArticle As Integer, ByVal mouvement As Integer) As Integer
End Interface
End Namespace
接口中各方法的作用如下:
返回数据源中的所有文章 | |
清空数据源 | |
返回由主键标识的对象 [Article] | |
允许向数据源添加一条记录 | |
允许修改数据源中的商品 | |
用于从数据源中删除商品 | |
允许修改数据源中某商品的库存 |
该接口向客户端程序提供了一系列仅由其签名定义的方法。它不涉及这些方法的实际实现方式。这为应用程序带来了灵活性。客户端程序调用的是接口,而非其具体的实现。
![]() |
具体实现的选择将通过 Spring 配置文件来完成。 为了说明在测试Web应用程序时,仅需关注数据访问接口而非其实现类,我们将首先通过一个简单的对象[ArrayList]来实现数据源。后续我们将介绍基于SGBD的解决方案。
1.4.3.3. 实现类 [ArticlesDaoArrayList]
实现类 [ArticlesDaoArrayList] 定义如下:
Imports System
Imports System.Collections
Namespace istia.st.articles.dao
Public Class ArticlesDaoArrayList
Implements istia.st.articles.dao.IArticlesDao
Private articles As New ArrayList
Private Const nbArticles As Integer = 4
' 默认制造商
Public Sub New()
' 正在编写几篇文章
For i As Integer = 1 To nbArticles
articles.Add(New Article(i, "article" + i.ToString, i * 10, i * 10, i * 10))
Next
End Sub
' 显示所有文章列表
Public Function getAllArticles() As IList Implements IArticlesDao.getAllArticles
' 返回文章列表
SyncLock Me
Return articles
End SyncLock
End Function
' 删除所有文章
Public Sub clearAllArticles() Implements IArticlesDao.clearAllArticles
' 清空商品列表
SyncLock Me
articles.Clear()
End SyncLock
End Sub
' 根据键值获取某条记录
Public Function getArticleById(ByVal idArticle As Integer) As Article Implements IArticlesDao.getArticleById
' 在列表中搜索商品
SyncLock Me
Dim ipos As Integer = posArticle(articles, idArticle)
If ipos <> -1 Then
Return CType(articles(ipos), Article)
Else
Return Nothing
End If
End SyncLock
End Function
' 将商品添加到商品列表
Public Function ajouteArticle(ByVal unArticle As Article) As Integer Implements IArticlesDao.ajouteArticle
' 将商品添加到商品列表中
SyncLock Me
' 验证该商品是否已存在
Dim ipos As Integer = posArticle(articles, unArticle.id)
If ipos <> -1 Then
Throw New Exception("L'article d'id [" + unArticle.id.ToString + "] existe déjà")
End If
' 添加该条目
articles.Add(unArticle)
' 返回结果
Return 1
End SyncLock
End Function
' 修改商品
Public Function modifieArticle(ByVal articleNouveau As Article) As Integer Implements IArticlesDao.modifieArticle
' 修改文章
SyncLock Me
' 检查是否存在
Dim ipos As Integer = posArticle(articles, articleNouveau.id)
' 若不存在
If ipos = -1 Then Return 0
' 若存在 - 则修改
articles(ipos) = articleNouveau
' 返回结果
Return 1
End SyncLock
End Function
' 删除通过其键标识的商品
Public Function supprimeArticle(ByVal idArticle As Integer) As Integer Implements IArticlesDao.supprimeArticle
' 删除商品
SyncLock Me
' 检查是否存在
Dim ipos As Integer = posArticle(articles, idArticle)
' 若不存在
If ipos = -1 Then Return 0
' 若存在 - 则删除
articles.RemoveAt(ipos)
' 返回结果
Return 1
End SyncLock
End Function
' 根据商品键修改商品库存
Public Function changerStockArticle(ByVal idArticle As Integer, ByVal mouvement As Integer) As Integer Implements IArticlesDao.changerStockArticle
' 更改某商品的库存
SyncLock Me
' 检查是否存在
Dim ipos As Integer = posArticle(articles, idArticle)
' 若不存在
If ipos = -1 Then Return 0
' 若存在 - 且条件允许则修改其库存
Dim unArticle As Article = CType(articles(ipos), Article)
' 仅在库存充足时才修改
If unArticle.stockactuel + mouvement >= 0 Then
unArticle.stockactuel += mouvement
Return 1
Else
Return 0
End If
End SyncLock
End Function
' 根据其键查找商品
Private Function posArticle(ByVal listArticles As ArrayList, ByVal idArticle As Integer) As Integer
' 返回商品 [idArticle] 在列表中的位置,若未找到则返回 -1
Dim unArticle As Article
For i As Integer = 0 To listArticles.Count - 1
unArticle = CType(listArticles(i), Article)
If unArticle.id = idArticle Then
Return i
End If
Next
' 未找到
Return -1
End Function
End Class
End Namespace
注释:
- 数据源由私有字段 [articles] 模拟,其类型为 [ArrayList]
- 该类的构造函数默认在数据源中创建 4 条记录。
- 所有数据访问方法均已进行同步,以避免数据源的并发访问问题。在任何给定时刻,仅有一个线程可访问某个特定方法。
- 方法 [posArticle] 用于根据编号查询数据源 [ArrayList] 中某条记录的位置 [0..N]。 如果该文章不存在,该方法返回位置 -1。其他方法会反复调用此方法。
- 方法 [ajouteArticle] 用于在商品列表中添加商品。它返回插入的商品数量:1。如果该商品已存在,则抛出异常。
- 方法 [modifieArticle] 用于修改现有商品。该方法返回修改的商品数量:若商品存在则返回 1,否则返回 0。
- 方法 [supprimeArticle] 用于删除现有商品。该方法返回被删除的商品数量:若商品存在则返回 1,否则返回 0。
- 方法 [getAllArticles] 返回所有文章的列表
- 方法 [getArticleById] 用于根据编号获取特定商品。若该商品不存在,则返回值 [nothing]。
- 该代码本身并不复杂。我们留给读者自行阅读并理解。
1.4.3.4. 生成 [dao] 层程序集
Visual Studio 项目已配置为生成 [webarticles-dao.dll] 程序集。该程序集生成在项目的 [bin] 文件夹中:
![]() | ![]() |
1.4.3.5. [dao] 层的 NUnit 测试
在 Java 中,类通过 [Junit] 框架进行测试。在 .NET 中,Nunit 框架提供了相同的单元测试功能:

测试 Visual Studio 项目的结构如下:

注释:
- 项目 [tests] 属于 [bibliothèque de classes] 类型
- 测试 [NUnit] 需要引用程序集 [nunit.framework.dll]
- 测试类 [NUnit] 通过 Spring 获取待测对象的实例。因此,在
- 在 [bin] 文件夹中,Spring 的类文件
- 在 [References] 中,有一个对文件夹 [bin] 中组件 [Spring-Core.dll] 的引用
- 在 [bin] 中,有一个 Spring 的配置文件
- 测试类需要 [dao] 层中的 [webarticles-dao.dll] 程序集。该程序集已放置在 [bin] 文件夹中,并将其引用添加到项目引用中。
测试类 [NUnit] 需要访问 [NUnit.Framework] 命名空间中的类。因此,其中包含以下导入语句:
命名空间 [NUnit.Framework] 位于“程序集” [nunit.framework.dll] 中,需将其添加到项目引用中:
![]() | ![]() |
如果已安装 [Nunit],则 [nunit.framework.dll] 应出现在建议列表中。只需双击该程序集即可将其添加到项目中:

[dao] 层中的测试类 [NUnit] 可能如下所示:
Imports System
Imports System.Collections
Imports NUnit.Framework
Imports istia.st.articles.dao
Imports System.Threading
Imports Spring.Objects.Factory.Xml
Imports System.IO
Namespace istia.st.articles.tests
<TestFixture()> _
Public Class NunitTestArticlesArrayList
' 待测试对象
Private articlesDao As IArticlesDao
<SetUp()> _
Public Sub init()
' 获取 Spring 对象生成器的实例
Dim factory As XmlObjectFactory = New XmlObjectFactory(New FileStream("spring-config.xml", FileMode.Open))
' 请求实例化 articlesdao 对象
articlesDao = CType(factory.GetObject("articlesdao"), IArticlesDao)
End Sub
<Test()> _
Public Sub testGetAllArticles()
' 目视检查
listArticles()
End Sub
<Test()> _
Public Sub testClearAllArticles()
' 删除所有文章
articlesDao.clearAllArticles()
' 查询所有文章
Dim articles As IList = articlesDao.getAllArticles
' 验证:数量应为 0
Assert.AreEqual(0, articles.Count)
End Sub
<Test()> _
Public Sub testAjouteArticle()
' 删除所有商品
articlesDao.clearAllArticles()
' 验证:商品表应为空
Dim articles As IList = articlesDao.getAllArticles
Assert.AreEqual(0, articles.Count)
' 添加两条商品
articlesDao.ajouteArticle(New Article(3, "article3", 30, 30, 3))
articlesDao.ajouteArticle(New Article(4, "article4", 40, 40, 4))
' 验证:应有两条商品
articles = articlesDao.getAllArticles
Assert.AreEqual(2, articles.Count)
' 目视检查
listArticles()
End Sub
<Test()> _
Public Sub testSupprimeArticle()
' 删除所有商品
articlesDao.clearAllArticles()
' 验证:商品表应为空
Dim articles As IList = articlesDao.getAllArticles
Assert.AreEqual(0, articles.Count)
' 添加两件商品
articlesDao.ajouteArticle(New Article(3, "article3", 30, 30, 3))
articlesDao.ajouteArticle(New Article(4, "article4", 40, 40, 4))
' 验证:应有 2 条商品
articles = articlesDao.getAllArticles
Assert.AreEqual(2, articles.Count)
' 删除第4个商品
articlesDao.supprimeArticle(4)
' 验证:应剩余1个商品
articles = articlesDao.getAllArticles
Assert.AreEqual(1, articles.Count)
' 目视检查
listArticles()
End Sub
<Test()> _
Public Sub testModifieArticle()
' 删除所有项目
articlesDao.clearAllArticles()
' 核对
Dim articles As IList = articlesDao.getAllArticles
Assert.AreEqual(0, articles.Count)
' 添加2个项目
articlesDao.ajouteArticle(New Article(3, "article3", 30, 30, 3))
articlesDao.ajouteArticle(New Article(4, "article4", 40, 40, 4))
' 验证
articles = articlesDao.getAllArticles
Assert.AreEqual(2, articles.Count)
' 搜索第3项
Dim unArticle As Article = articlesDao.getArticleById(3)
' 验证
Assert.AreEqual(unArticle.nom, "article3")
' 搜索第4项
unArticle = articlesDao.getArticleById(4)
' 验证
Assert.AreEqual(unArticle.nom, "article4")
' 修改第4条
articlesDao.modifieArticle(New Article(4, "article4", 44, 44, 44))
' 核对
unArticle = articlesDao.getArticleById(4)
Assert.AreEqual(unArticle.prix, 44, 0.000001)
' 目视检查
listArticles()
End Sub
<Test()> _
Public Sub testGetArticleById()
' 删除条目
articlesDao.clearAllArticles()
' 核查
Dim articles As IList = articlesDao.getAllArticles
Assert.AreEqual(0, articles.Count)
' 新增2项
articlesDao.ajouteArticle(New Article(3, "article3", 30, 30, 3))
articlesDao.ajouteArticle(New Article(4, "article4", 40, 40, 4))
' 核对
articles = articlesDao.getAllArticles
Assert.AreEqual(2, articles.Count)
' 第3项搜索
Dim unArticle As Article = articlesDao.getArticleById(3)
' 验证
Assert.AreEqual(unArticle.nom, "article3")
' 第4条搜索
unArticle = articlesDao.getArticleById(4)
' 验证
Assert.AreEqual(unArticle.nom, "article4")
End Sub
' 屏幕列表
Private Sub listArticles()
Dim articles As IList = articlesDao.getAllArticles
For i As Integer = 0 To articles.Count - 1
Console.WriteLine(CType(articles(i), Article).ToString)
Next
End Sub
<Test()> _
Public Sub testArticleAbsent()
' 删除所有商品
articlesDao.clearAllArticles()
' 商品搜索 1
Dim article As article = articlesDao.getArticleById(1)
' 验证
Assert.IsNull(article)
' 修改不存在的商品
Dim i As Integer = articlesDao.modifieArticle(New article(1, "1", 1, 1, 1))
' 未修改任何行
Assert.AreEqual(i, 0)
' 删除不存在的商品
i = articlesDao.supprimeArticle(1)
' 未删除任何行
Assert.AreEqual(0, i)
End Sub
<Test()> _
Public Sub testChangerStockArticle()
' 删除所有条目
articlesDao.clearAllArticles()
' 添加了一篇文章
Dim nbArticles As Integer = articlesDao.ajouteArticle(New Article(3, "article3", 30, 101, 3))
Assert.AreEqual(nbArticles, 1)
' 添加了一篇文章
nbArticles = articlesDao.ajouteArticle(New Article(4, "article4", 40, 40, 4))
Assert.AreEqual(nbArticles, 1)
' 创建了 100 个线程
Dim taches(99) As Thread
For i As Integer = 0 To taches.Length - 1
' 创建主题 i
taches(i) = New Thread(New ThreadStart(AddressOf décrémente))
' 设置线程名称
taches(i).Name = "tache_" & i
' 启动线程 i 的执行
taches(i).Start()
Next
' 等待所有线程结束
For i As Integer = 0 To taches.Length - 1
taches(i).Join()
Next
' 验证 - 商品3的库存必须为1
Dim unArticle As Article = articlesDao.getArticleById(3)
Assert.AreEqual(unArticle.nom, "article3")
Assert.AreEqual(1, unArticle.stockactuel)
' 将商品4的库存减1
Dim erreur As Boolean = False
Dim nbLignes As Integer = articlesDao.changerStockArticle(4, -100)
' 验证:其库存不应发生变化
Assert.AreEqual(0, nbLignes)
' 目视检查
listArticles()
End Sub
Public Sub décrémente()
' 启动线程
System.Console.Out.WriteLine(Thread.CurrentThread.Name + " lancé")
' 线程减少库存
articlesDao.changerStockArticle(3, -1)
' 线程结束
System.Console.Out.WriteLine(Thread.CurrentThread.Name + " terminé")
End Sub
End Class
End Namespace
注释:
- 我们希望编写一个测试接口 [IArticlesDao] 的程序,且该程序应与该接口的实现类无关。因此,我们使用 Spring 来隐藏实现类的名称,使其对测试程序不可见。
- 属性方法 <Setup()> 从 Spring 中获取待测试对象 [articlesdao] 的引用。该对象在以下文件 [spring-config.xml] 中定义:
<?xml version="1.0" encoding="iso-8859-1" ?>
<!DOCTYPE objects PUBLIC "-//SPRING//DTD OBJECT//EN"
"http://www.springframework.net/dtd/spring-objects.dtd">
<objects>
<object id="articlesdao" type="istia.st.articles.dao.ArticlesDaoArrayList, webarticles-dao"/>
</objects>
该文件指明了接口 [IArticlesDao] 的实现类 [istia.st.articles.dao.ArticlesDaoArrayList] 的名称,以及其所在位置 [ webarticles-dao.dll]。由于实例化不需要参数,因此此处未定义任何参数。
- 大多数测试用例都易于理解。建议读者阅读相关注释。
- 方法 [testChangerStockArticle] 需要稍作说明。它创建了 100 个线程,负责递减指定商品的库存。
<Test()> _
Public Sub testChangerStockArticle()
' 删除所有商品
articlesDao.clearAllArticles()
' 添加商品
Dim nbArticles As Integer = articlesDao.ajouteArticle(New Article(3, "article3", 30, 101, 3))
Assert.AreEqual(nbArticles, 1)
' 添加商品
nbArticles = articlesDao.ajouteArticle(New Article(4, "article4", 40, 40, 4))
Assert.AreEqual(nbArticles, 1)
' 创建 100 个线程
Dim taches(99) As Thread
For i As Integer = 0 To taches.Length - 1
' 创建主题 i
taches(i) = New Thread(New ThreadStart(AddressOf décrémente))
' 设置线程名称
taches(i).Name = "tache_" & i
' 启动线程 i 的执行
taches(i).Start()
Next
' 等待所有线程结束
For i As Integer = 0 To taches.Length - 1
taches(i).Join()
Next
' 验证 - 商品3的库存应为1
Dim unArticle As Article = articlesDao.getArticleById(3)
Assert.AreEqual(unArticle.nom, "article3")
Assert.AreEqual(1, unArticle.stockactuel)
' 减少商品4的库存
Dim erreur As Boolean = False
Dim nbLignes As Integer = articlesDao.changerStockArticle(4, -100)
' 验证:其库存不应发生变化
Assert.AreEqual(0, nbLignes)
' 目视检查
listArticles()
End Sub
此处旨在测试对数据源的并发访问。负责更新库存的方法如下:
Public Sub décrémente()
' 启动线程
System.Console.Out.WriteLine(Thread.CurrentThread.Name + " lancé")
' 线程减少库存
articlesDao.changerStockArticle(3, -1)
' 线程结束
System.Console.Out.WriteLine(Thread.CurrentThread.Name + " terminé")
End Sub
它将第3号商品的库存减少1。如果参考方法代码[testChangerStockArticle],可以看到:
- 第3号商品的库存初始值为101
- 100 个线程将各自将该库存减少 1
- 因此,在所有线程执行完毕后,库存应为1
此外,在该方法中,我们还尝试将第4号商品的库存设置为负值。此操作应会失败。
为了测试 [dao] 层,我们在 [tests] 项目的 [bin] 文件夹中生成 DLL 和 [tests-webarticles-dao.dll]:
![]() | ![]() |
然后,使用应用程序 [Nunit-Gui],我们加载该 DLL 并执行测试:

在左侧窗口中,可以看到已测试的方法列表。每个方法名称前面的圆点颜色表示该方法的测试结果:成功(绿色)或失败(红色)。 在屏幕上查看本文档的读者将看到所有测试均已通过。接下来,我们将认为我们拥有一个可运行的 [dao] 层。
1.4.4. [domain]层
[domain]层包含以下组件:
-
[IArticlesDomain]:访问 [domain] 层的接口
-
[Achat]:定义采购的类
-
[Panier]:定义购物车的类
-
[AchatsArticles]:[IArticlesDomain] 接口的实现类
[domain] 层的 [Visual Studio] 解决方案结构如下:

备注:
- 项目 [domain] 属于 [bibliothèque de classes] 类型
- 类已放置在以文件夹 [istia] 为根的树形结构中。它们均位于命名空间 [istia.st.articles.domain] 中。
- [dao]层中的DLL已放置在新项目的[bin]文件夹中。此外,DLL已作为引用添加到该项目中。
1.4.4.1. 接口 [IArticlesDomain]
接口 [IArticlesDomain] 将 [métier] 层与 [web] 层解耦。 后者通过该接口访问 [métier/domain] 层,而无需关心实际实现该接口的类。该接口定义了以下用于访问业务层的操作:
Imports Article = istia.st.articles.dao.Article
Namespace istia.st.articles.domain
Public Interface IArticlesDomain
' 方法
Sub acheter(ByVal panier As Panier)
Function getAllArticles() As IList
Function getArticleById(ByVal idArticle As Integer) As Article
ReadOnly Property erreurs() As ArrayList
End Interface
End Namespace
返回关联数据源的对象列表 [Article] | |
返回由 [idArticle] 标识的对象 [Article] | |
通过将已购商品的库存减去购买数量来验证客户购物车——如果库存不足,可能会失败 | |
返回发生的错误列表——若无错误则为空 |
1.4.4.2. 类 [Achat]
类 [Achat] 表示客户的购买:
Imports istia.st.articles.dao
Namespace istia.st.articles.domain
Public Class Achat
' 私有字段
Private _article As article
Private _qte As Integer
' 默认构造函数
Public Sub New()
End Sub
' 带参数的构造函数
Public Sub New(ByVal unArticle As article, ByVal qte As Integer)
' 通过属性访问
Me.article = unArticle
Me.qte = qte
End Sub
' 已购商品
Public Property article() As article
Get
Return _article
End Get
Set(ByVal Value As article)
_article = Value
End Set
End Property
' 购买数量
Public Property qte() As Integer
Get
Return _qte
End Get
Set(ByVal Value As Integer)
If Value < 0 Then
Throw New Exception("Quantité [" + Value.ToString + "] invalide")
End If
_qte = Value
End Set
End Property
' 采购总额
Public ReadOnly Property totalAchat() As Double
Get
Return _qte * _article.prix
End Get
End Property
' 身份
Public Overrides Function ToString() As String
Return "[" + _article.ToString + "," + _qte.ToString + "]"
End Function
End Class
End Namespace
注释:
- 类 [Achat] 具有以下属性和方法:
已购买的商品 | |
购买数量 | |
购买金额 | |
对象标识符 |
- 它有一个构造函数,用于初始化定义购买的属性 [article, qte]。
1.4.4.3. 类 [Panier]
类 [Panier] 表示客户的全部购买记录:
Namespace istia.st.articles.domain
Public Class Panier
' 私有字段
Private _achats As New ArrayList
Private _totalPanier As Double = 0
' 默认制造商
Public Sub New()
End Sub
' 购买清单
Public ReadOnly Property achats() As ArrayList
Get
Return _achats
End Get
End Property
' 购物总额
Public ReadOnly Property totalPanier() As Double
Get
Return _totalPanier
End Get
End Property
' 方法
Public Sub ajouter(ByVal unAchat As Achat)
' 检查采购是否已存在
Dim iAchat As Integer = posAchat(unAchat.article.id)
If iAchat <> -1 Then
' 已找到
Dim achatCourant As Achat = CType(_achats(iAchat), Achat)
achatCourant.qte += unAchat.qte
Else
' 未找到
_achats.Add(unAchat)
End If
' 增加购物车总额
_totalPanier += unAchat.totalAchat
End Sub
' 移除一件商品
Public Sub enlever(ByVal idAchat As Integer)
' 正在搜索该商品
Dim iachat As Integer = posAchat(idAchat)
' 若已找到,则移除
If iachat <> -1 Then
Dim achatCourant As Achat = CType(_achats(iachat), Achat)
' 从购物车中移除
_achats.RemoveAt(iachat)
' 减少购物车总金额
_totalPanier -= achatCourant.totalAchat
End If
End Sub
Private Function posAchat(ByVal idArticle As Integer) As Integer
' 在购买列表中查找购买记录
' 返回其在列表中的位置,若未找到则返回-1
Dim achatCourant As Achat
Dim trouvé As Boolean = False
Dim i As Integer = 0
While Not trouvé AndAlso i < _achats.Count
' 当前购买
achatCourant = CType(_achats(i), Achat)
' 与搜索商品进行比对
If achatCourant.article.id = idArticle Then
Return i
End If
'下一笔采购
i += 1
End While
' 未找到
Return -1
End Function
' 身份验证
Public Overrides Function ToString() As String
Return _achats.ToString
End Function
End Class
End Namespace
注释:
- 类 [Panier] 具有以下属性和方法:
客户采购清单 - 类型为 [Achat] 的对象列表 | |
将一项购买添加到购物清单中 | |
从商品列表中删除该商品 idAchat | |
购物车总金额 | |
返回购物车的标识符字符串 |
- 方法 [posAchat] 是一个实用方法,用于根据所购商品的编号获取该商品在购物清单中的位置。 购物清单的管理机制确保同一商品即使多次购买,在列表中也仅占据一个位置。因此,可以通过所购商品的编号来识别该笔购买。如果未找到所查询的购买记录,方法 [posAchat] 将返回 -1。
- 方法 [ajouter] 将新的采购记录添加到采购清单中。这意味着:如果所购商品尚未存在于清单中,则向清单添加新条目;如果已存在,则增加其采购数量。
- 方法 [enlever] 用于从采购清单中移除一个由编号标识的采购条目。如果该采购条目不存在,该方法将不返回任何信息且不执行任何操作。
- [totalPanier] 方法会随着购物条目的增删,持续更新购物总额。
1.4.4.4. 类 [AchatsArticles]
接口 [IArticlesDomain] 将由以下类 [AchatsArticles] 实现:
Imports istia.st.articles.dao
Namespace istia.st.articles.domain
Public Class AchatsArticles
Implements IArticlesDomain
'私有字段
Private _articlesDao As IArticlesDao
Private _erreurs As ArrayList
' 构造函数
Public Sub New(ByVal articlesDao As IArticlesDao)
_articlesDao = articlesDao
End Sub
' 错误列表
Public ReadOnly Property erreurs() As ArrayList Implements IArticlesDomain.erreurs
Get
Return _erreurs
End Get
End Property
' 文章列表
Public Function getAllArticles() As IList Implements IArticlesDomain.getAllArticles
' 所有商品列表
Try
Return _articlesDao.getAllArticles
Catch ex As Exception
_erreurs = New ArrayList
_erreurs.Add("Erreur d'accès aux données : " + ex.Message)
End Try
End Function
' 按编号获取某条目
Public Function getArticleById(ByVal idArticle As Integer) As Article Implements IArticlesDomain.getArticleById
' 特定商品
Try
Return _articlesDao.getArticleById(idArticle)
Catch ex As Exception
_erreurs = New ArrayList
_erreurs.Add("Erreur d'accès aux données : " + ex.Message)
End Try
End Function
' 购买购物车
Public Sub acheter(ByVal panier As Panier) Implements IArticlesDomain.acheter
' 购买购物车 - 所购商品的库存需相应减少
_erreurs = New ArrayList
Dim achat As achat
Dim achats As ArrayList = panier.achats
For i As Integer = achats.Count - 1 To 0 Step -1
' 减少商品 i 的库存
achat = CType(achats(i), achat)
Try
If _articlesDao.changerStockArticle(achat.article.id, -achat.qte) = 0 Then
' 操作失败
_erreurs.Add("L'achat " + achat.ToString + " n'a pu se faire - Vérifiez les stocks")
Else
' 操作成功 - 从购物车中移除该商品
panier.enlever(achat.article.id)
End If
Catch ex As Exception
_erreurs = New ArrayList
_erreurs.Add("Erreur d'accès aux données : " + ex.Message)
End Try
Next
End Sub
End Class
End Namespace
注释:
- 该类实现了接口 [IArticlesDomain] 的四个方法。它有两个私有字段:
数据访问对象 | |
可能出现的错误列表。可通过公共属性 [erreurs] 访问 |
- 来构建该类的实例,必须提供用于访问数据的对象:
- 方法 [getAllArticles] 和 [getArticleById] 基于 [dao] 层中同名的方法
- 方法 [acheter] 用于验证购物车的购买操作。该验证仅需对已购商品的库存进行减量处理。 只有当商品库存充足时,才能进行购买。若库存不足,购买将被拒绝:商品仍保留在购物车中,并在列表 [erreurs] 中报告错误。已确认的购买将从购物车中移除,且相应商品的库存将减少购买数量。
1.4.4.5. 生成 [domain] 层程序集
Visual Studio 项目已配置为生成 [webarticles-domain.dll] 程序集。该程序集生成在项目的 [bin] 文件夹中:
![]() | ![]() |
1.4.4.6. NUnit 层面的测试(基于 [domain] 层)
测试 Visual Studio 项目的结构如下:

备注:
- 项目 [tests] 属于 [bibliothèque de classes] 类型
- [NUnit] 测试需要引用 [nunit.framework.dll] 程序集
- 测试类 [NUnit] 通过 Spring 获取待测对象的实例。因此,在
- 在 [bin] 文件夹中,Spring 的类文件
- 在 [References] 中,有一个对文件夹 [bin] 内 [Spring-Core.dll] 组件的引用
- 在 [bin] 中,有一个 Spring 的配置文件
- 该测试类需要 [dao] 层中的 [webarticles-dao.dll] 程序集以及 [domain] 层中的 [webarticles-domain.dll] 程序集。 这些组件已放置在 [bin] 文件夹中,并将其引用添加到了项目引用中。
来自 [domain] 层的测试类 NUnit 可能如下所示:
Imports NUnit.Framework
Imports istia.st.articles.dao
Imports istia.st.articles.domain
Imports Spring.Objects.Factory.Xml
Imports System.IO
Namespace istia.st.articles.tests
<TestFixture()> _
Public Class NunitTestArticlesDomain
' 待测试对象
Private articlesDomain As IArticlesDomain
Private articlesDao As IArticlesDao
<SetUp()> _
Public Sub init()
' 获取 Spring 对象生成器的实例
Dim factory As XmlObjectFactory = New XmlObjectFactory(New FileStream("spring-config.xml", FileMode.Open))
' 请求实例化 articles dao 对象
articlesDao = CType(factory.GetObject("articlesdao"), IArticlesDao)
' 随后实例化商品领域对象
articlesDomain = CType(factory.GetObject("articlesdomain"), IArticlesDomain)
End Sub
<Test()> _
Public Sub getAllArticles()
' 目视检查
listArticles()
End Sub
<Test()> _
Public Sub getArticleById()
' 删除商品
articlesDao.clearAllArticles()
' 验证
Dim articles As IList = articlesDomain.getAllArticles
Assert.AreEqual(0, articles.Count)
' 添加2个商品
articlesDao.ajouteArticle(New Article(3, "article3", 30, 30, 3))
articlesDao.ajouteArticle(New Article(4, "article4", 40, 40, 4))
' 核对
articles = articlesDomain.getAllArticles
Assert.AreEqual(2, articles.Count)
' 第3条目搜索
Dim unArticle As Article = articlesDomain.getArticleById(3)
' 验证
Assert.AreEqual(unArticle.nom, "article3")
' 搜索第4条
unArticle = articlesDao.getArticleById(4)
' 验证
Assert.AreEqual(unArticle.nom, "article4")
End Sub
<Test()> _
Public Sub acheterPanier()
' 删除条目
articlesDao.clearAllArticles()
' 验证
Dim articles As IList = articlesDomain.getAllArticles
Assert.AreEqual(0, articles.Count)
' 添加2个条目
articlesDao.ajouteArticle(New Article(3, "article3", 30, 30, 3))
articlesDao.ajouteArticle(New Article(4, "article4", 40, 40, 4))
' 验证
articles = articlesDomain.getAllArticles
Assert.AreEqual(2, articles.Count)
' 创建包含两件商品的购物车
Dim panier As New panier
panier.ajouter(New Achat(New Article(3, "article3", 30, 30, 3), 10))
panier.ajouter(New Achat(New Article(4, "article4", 40, 40, 4), 10))
' 验证
Assert.AreEqual(700, panier.totalPanier, 0.000001)
Assert.AreEqual(2, panier.achats.Count)
' 购物车确认
articlesDomain.acheter(panier)
' 验证
Assert.AreEqual(0, articlesDomain.erreurs.Count)
Assert.AreEqual(0, panier.achats.Count)
' 商品搜索 3
Dim unArticle As Article = articlesDomain.getArticleById(3)
' 验证
Assert.AreEqual(unArticle.stockactuel, 20)
' 商品搜索 4
unArticle = articlesDao.getArticleById(4)
' 验证
Assert.AreEqual(unArticle.stockactuel, 30)
' 新建购物车
panier.ajouter(New Achat(New Article(3, "article3", 30, 30, 3), 100))
' 确认购物车
articlesDomain.acheter(panier)
' 验证
Assert.AreEqual(1, articlesDomain.erreurs.Count)
' 商品搜索 3
unArticle = articlesDomain.getArticleById(3)
' 验证
Assert.AreEqual(unArticle.stockactuel, 20)
End Sub
<Test()> _
Public Sub testRetirerAchats()
' 删除 ARTICLES 的内容
articlesDao.clearAllArticles()
' 读取表 ARTICLES
Dim articles As IList = articlesDao.getAllArticles()
Assert.AreEqual(0, articles.Count)
' 插入
Dim article3 As New Article(3, "article3", 30, 30, 3)
articlesDao.ajouteArticle(article3)
Dim article4 As New Article(4, "article4", 40, 40, 4)
articlesDao.ajouteArticle(article4)
' 读取表 ARTICLES
articles = articlesDomain.getAllArticles()
Assert.AreEqual(2, articles.Count)
' 创建包含两件商品的购物车
Dim monPanier As New Panier
monPanier.ajouter(New Achat(article3, 10))
monPanier.ajouter(New Achat(article4, 10))
' 验证
Assert.AreEqual(700.0, monPanier.totalPanier, 0.000001)
Assert.AreEqual(2, monPanier.achats.Count)
' 添加已购买的商品
monPanier.ajouter(New Achat(article3, 10))
' 验证
' 总金额应调整为1000
Assert.AreEqual(1000.0, monPanier.totalPanier, 0.000001)
' 购物车中仍有 2 件商品
Assert.AreEqual(2, monPanier.achats.Count)
' 商品3的数量应调整为20
Dim unAchat As Achat = CType(monPanier.achats(0), Achat)
Assert.AreEqual(20, unAchat.qte)
' 从购物车中移除商品3
monPanier.enlever(3)
' 验证
' 总金额应调整为400
Assert.AreEqual(400.0, monPanier.totalPanier, 0.000001)
' 购物车中仅剩1件商品
Assert.AreEqual(1, monPanier.achats.Count)
' 这应该是第4件商品
Assert.AreEqual(4, CType(monPanier.achats(0), Achat).article.id)
End Sub
' 屏幕列表
Private Sub listArticles()
Dim articles As IList = articlesDomain.getAllArticles
For i As Integer = 0 To articles.Count - 1
Console.WriteLine(CType(articles(i), Article).ToString)
Next
End Sub
End Class
End Namespace
注释:
- 我们希望编写一个用于测试接口 [IArticlesDomain] 的测试程序,且该程序应与该接口的实现类无关。因此,我们使用 Spring 来隐藏实现类的名称,使其对测试程序不可见。
- 属性方法 <Setup()> 从 Spring 中获取待测试对象 [articlesdomain] 和 [articlesdao] 的引用。这些对象在以下 [spring-config.xml] 文件中定义:
<?xml version="1.0" encoding="iso-8859-1" ?>
<!DOCTYPE objects PUBLIC "-//SPRING//DTD OBJECT//EN"
"http://www.springframework.net/dtd/spring-objects.dtd">
<objects>
<object id="articlesdao" type="istia.st.articles.dao.ArticlesDaoArrayList, webarticles-dao" />
<object id="articlesdomain" type="istia.st.articles.domain.AchatsArticles, webarticles-domain">
<constructor-arg index="0">
<ref object="articlesdao" />
</constructor-arg>
</object>
</objects>
该文件显示:
- (续)
- 对于单例 [articlesdao],其实现类名为 [istia.st.articles.dao.ArticlesDaoArrayList],且位于 [ webarticles-dao.dll]。由于实例化无需参数,此处未定义任何参数。
- 对于单例 [articlesdomain],其实现类名为 [istia.st.articles.domain.AchatsArticles],且位于 [ webarticles-domain.dll]。 类 [AchatsArticles] 有一个单参数构造函数:管理对 [dao] 层访问的单例。在此,该单例被定义为之前定义的单例 [articlesdao]。
- 测试类获取了待测类 [articlesdomain] 的实例,以及数据访问类 [articlesdao] 的实例。最后这一点存在争议。 理论上,测试类本不应访问 [dao] 层,因为它甚至不应该知道该层的存在。在此,我们打破了这一“规范”——若要遵守该规范,我们本需在 [IArticlesDomain] 接口中创建新的方法。
为了测试 [domain] 层,我们在 [tests] 项目的 [bin] 文件夹中生成 DLL 和 [tests-webarticles-domain.dll]:
![]() | ![]() |
然后,使用应用程序 [Nunit-Gui],我们加载该 DLL 并执行测试:

在屏幕上查看此文档的读者将看到所有测试均已通过。接下来,我们将认为已有一个可运行的 [domain] 层。
1.4.5. 结论
需要提醒的是,我们计划构建以下三层Web应用程序:
![]() |
我们应用程序的 M 模型 MVC 现已编写并经过测试。它由两个文件 DLL 和 [webarticles-dao.dll, webarticles-domain.dll] 提供。 我们可以转到最后一层,即包含控制器 C 和视图 V 的 [web] 层。首先,我们将探讨 [Développement WEB avec ASP.NET 1.1 ] 文档中介绍的一种方法
- 控制器 C 由两个文件 [global.asax, main.aspx] 实现
- 视图 V 由 aspx 页面实现
1.5. [web]层
该 Web 应用程序的架构 MVC 将如下所示:
![]() |
业务类 [domain]、数据访问类 [dao] 以及数据源 | |
页面 ASPX | |
所有来自客户端的请求 HTTP 均通过以下两个控制器: global.asax:处理与应用程序初始启动相关的事件 main.aspx:单独处理每个客户端的请求 |
1.5.1. 视图
这些视图与文档开头介绍的视图一致:
liste.aspx | 这些视图汇总在应用程序的 [vues] 文件夹中 ![]() | |
infos.aspx | ||
panier.aspx | ||
paniervide.aspx | ||
erreurs.aspx |
1.5.2. 控制器
如前所述,控制器将由两个部分组成:
- [global.asax,global.asax.vb]:主要用于初始化应用程序,并将所有需在不同客户端之间共享的数据放入应用程序上下文中
- [main.aspx, main.aspx.vb]:真正的控制器,负责处理来自客户端的 HTTP 请求。
来自客户端的各种请求将发送至控制器 [main.aspx],并包含一个名为 [action] 的参数,该参数指定了客户端请求的操作:
请求 | 含义 | 控制器操作 | 可能的响应 |
客户想要 商品列表 | - 向业务层请求商品列表 业务层 | - [LISTE] - [ERREURS] | |
客户要求 关于视图中显示的 视图中显示的 [LISTE] | - 向业务层请求该商品 | - [INFOS] - [ERREURS] | |
客户购买商品 | - 向业务层请求商品 并将商品加入客户购物车 | - [INFOS](若数量出错) - [LISTE] 若无数量错误 | |
客户希望删除 商品 | - 从会话中获取购物车 并进行修改 | - [PANIER] - [PANIERVIDE] - [ERREURS] | |
客户希望查看其 购物车 | - 从会话中获取购物车 | - [PANIER] - [PANIERVIDE] - [ERREURS] | |
客户已完成购物 并进入支付阶段 | - 在数据库中更新 已购买商品的库存 - 将客户购物车中 - 清空客户购物车中 | - [LISTE] - [ERREURS] |
1.5.3. 应用程序配置
我们将努力配置应用程序,使其在面对以下变更时尽可能灵活:
- 不同视图中 URL 的变更
- 实现 [IArticlesDao] 和 [IArticlesDomain] 接口的类发生变更
- SGBD、数据库及商品表的变更
1.5.3.1. URL的变更
视图的名称 URL 将与其他一些参数一起放入应用程序配置文件 [web.config] 中:
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
..
<appSettings>
<add key="urlMain" value="/webarticles/main.aspx"/>
<add key="urlInfos" value="vues/infos.aspx"/>
<add key="urlErreurs" value="vues/erreurs.aspx"/>
<add key="urlListe" value="vues/liste.aspx"/>
<add key="urlPanier" value="vues/panier.aspx"/>
<add key="urlPanierVide" value="vues/paniervide.aspx"/>
</appSettings>
</configuration>
1.5.3.2. 接口实现类的变更
根据三层架构的理念,各层之间应相互隔离。这种隔离通过以下方式实现:
- 各层之间通过接口而非具体类进行通信
- 某一层级的代码绝不会直接实例化另一层级的类来使用它。它只需向外部工具(此处为 Spring)请求所需使用层级接口的实现实例。为此,我们知道它无需知道实现类的名称,只需知道其希望获取引用所对应的 Spring 单例的名称即可。
在我们的应用程序中,Spring将在Web应用程序的[web.config]文件中按以下方式进行配置:
<?xml version="1.0" encoding="iso-8859-1" ?>
<configuration>
<configSections>
<sectionGroup name="spring">
<section name="context" type="Spring.Context.Support.ContextHandler, Spring.Core" />
<section name="objects" type="Spring.Context.Support.DefaultSectionHandler, Spring.Core" />
</sectionGroup>
</configSections>
<spring>
<context type="Spring.Context.Support.XmlApplicationContext, Spring.Core">
<resource uri="config://spring/objects" />
</context>
<objects>
<object id="articlesDao" type="istia.st.articles.dao.ArticlesDaoArrayList, webarticles-dao" />
<object id="articlesDomain" type="istia.st.articles.domain.AchatsArticles, webarticles-domain">
<constructor-arg index="0">
<ref object="articlesDao" />
</constructor-arg>
</object>
</objects>
</spring>
<appSettings>
<add key="urlMain" value="/webarticles/main.aspx"/>
<add key="urlInfos" value="vues/infos.aspx"/>
<add key="urlErreurs" value="vues/erreurs.aspx"/>
<add key="urlListe" value="vues/liste.aspx"/>
<add key="urlPanier" value="vues/panier.aspx"/>
<add key="urlPanierVide" value="vues/paniervide.aspx"/>
</appSettings>
</configuration>
为了访问 [métier] 层,[web] 层中的某个类可以请求单例 [articlesDomain]。 Spring 随后将实例化一个 [istia.st.articles.domain.AchatsArticles] 类型的对象。为此,它需要一个 [articlesDao] 类型的对象,即一个 [istia.st.articles.dao.ArticlesDaoArrayList] 类型的对象。 Spring 随后将实例化该对象。操作完成后,请求了单例 [articlesDomain] 的 [web] 层便拥有了连接其与数据源的完整链:
![]() |
1.5.3.3. 与 SGBD 或数据库相关的变更
此处将忽略该点,因为我们当前处于一个不包含 SGBD 的测试应用程序中。我们将在后续阶段讨论基于 SGBD 实现 [dao] 层的方法。
1.5.4. <asp:> 标签库
考虑显示错误列表的 [ERREURS] 视图:

视图 [ERREURS] 负责显示错误列表,该列表由控制器 [main.aspx] 以名称 [context.Items("erreurs")] 放置在请求上下文中。编写此类页面有多种方法。 本文仅关注错误显示部分。
需要提醒的是,ASPX页面包含一个呈现部分(HTML)和一个代码部分(NET),后者负责准备呈现部分需要显示的数据。 这两个部分可以位于同一个文件 [aspx] 中(方案 WebMatrix),也可以分别位于两个文件中:[aspx] 用于呈现,[aspx.vb] 用于代码。后者是 Visual Studio 采用的方案。 更复杂的是,HTML 布局部分本身也可能包含 .NET 代码,这往往会模糊 [contrôleur] 和 [présentation] 视图的分离。 通常强烈不建议采用这种方案。为了从 [présentation] 部分移除所有代码,需要创建标签库。这些标签库通过类似 HTML 标签的外观来“隐藏”代码。 针对 [ERREURS] 页面,我们提供两种可能的解决方案。
我们的第一种方案是在页面的 [présentation] 部分使用 .NET 代码。ASPX 页面在其控制器部分 [erreurs.aspx.vb] 中获取请求中的错误列表:
Protected erreurs As ArrayList
Private Sub Page_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
...
' 检索错误
erreurs = CType(context.Items("erreurs"), ArrayList)
End Sub
然后将其显示在 [présentation, erreurs.aspx] 部分:
<h2>Les erreurs suivantes se sont produites :</h2>
<ul>
<%
for i as integer=0 to erreurs.count-1
response.write("<li>" & erreurs(i).ToString & "</li>")
next
%>
</ul>
第二种解决方案使用ASP.NET的<asp:>标签库中的<asp:repeater>标签。 如果通过图形化方式构建页面 ASPX,该标签将以服务器组件的形式呈现,可直接拖放到设计表单中。如果手动编写代码 ASPX,则可将其视为标签库。
借助 <asp:> 标签库,前一个视图 [ERREURS] 的代码 ASPX 将变为如下形式:
<asp:Repeater id="rptErreurs" runat="server">
<HeaderTemplate>
<h3>Les erreurs suivantes se sont produites :
</h3>
<ul>
</HeaderTemplate>
<ItemTemplate>
<li>
<%# Container.DataItem %>
</li>
</ItemTemplate>
<FooterTemplate>
</ul>
</FooterTemplate>
</asp:Repeater>
标签
用于在数据源的不同元素上重复一个模式 HTML。其各个元素如下:
在显示数据源元素之前需显示的样式 HTML | |
在数据源的每个元素上重复显示的图案 HTML。表达式 [<%# Container.DataItem %>] 用于显示数据源当前元素的值 | |
在显示完数据源中的所有元素后,显示模式 HTML |
数据源通常在页面的 [contrôleur] 部分与标签相关联:
Protected WithEvents rptErreurs As System.Web.UI.WebControls.Repeater
Private Sub Page_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
..
' 将错误关联到 rptErreurs
With rptErreurs
.DataSource = context.Items("erreurs")
.DataBind()
End With
End Sub
如果数据源已知(例如现有数据库),也可以在页面设计阶段建立这种关联。
在视图中,我们将使用另一个标签:<asp:datagrid>,它允许以表格形式显示数据源。
1.5.5. [webarticles] 应用程序的 Visual Studio 解决方案结构
Web 应用程序就像一个由众多元素组成的拼图。采用 MVC 架构通常会增加这些元素的数量。[webarticles] 应用程序在 [Visual Studio] 下的结构如下:
![]() | ![]() | ![]() |
![]() |
备注:
- 项目 [web] 的类型为 [bibliothèque de classes],而非按逻辑预期应有的 [Application web ASP.NET]。 类型 [Application web ASP.NET] 要求在开发机或远程机器上存在 Web 服务器 IIS。 IIS 服务器在 Windows XP 家庭版系统中默认并不存在。然而,许多 PC 系统正是搭载此版本销售的。 为了让使用 Windows XP 的读者能够实现本教程中的应用程序,我们将使用微软免费提供的 Cassini Web 服务器(参见附录),并将项目 [Application web ASP.NET] 替换为项目 [bibliothèque de classes]。 这会带来一些不便,具体说明详见附录。
- 该应用程序使用的DLL文件如下:
包含数据访问层的类 | |
包含业务层的类 | |
包含 Spring 类,用于集成 Web、Domain 和 DAO 层 | |
日志类——由Spring使用 |
这些 DLL 文件被放置在 [bin] 文件夹中,并添加到项目引用中。
1.5.6. 视图 ASPX
如前所述,我们将在 ASPX 视图中使用 <asp:> 标签库。
1.5.6.1. 用户组件 [entete.ascx]
为了使不同视图保持一致性,它们将共享同一个页眉,该页眉显示应用程序名称及菜单:
![]() | ![]() |
该菜单是动态的,由控制器确定。控制器会在发送给页面 ASPX 的请求中,添加一个名为“actions”的键属性,其关联值为一个 Hashtable() 类型的元素数组。该数组中的每个元素都是一个字典,用于生成页眉菜单中的一个选项。 每个字典包含两个键:
- href:与菜单选项关联的URL
- lien:菜单的文本
我们将页眉转换为用户控件。用户控件将页面的一部分(包括布局和相关代码)封装成一个组件,以便在其他页面中重复使用。在此,我们希望在应用程序的其他视图中复用 [entete] 组件。 布局代码将位于 [entete.ascx] 中,相关控件代码则位于 [entete.ascx.vb] 中。布局代码将使用 <asp:repeater> 组件来显示菜单选项列表:
![]() |
n° | type | nom | rôle |
1 | 中继器 | rptMenu 数据源:一个包含两个键的字典数组 包含两个键:href、链接 | 显示菜单选项 |
页面呈现代码如下:
注释:
- 组件 [repeater] 在第 6-14 行中定义
- 与重复器关联的数据源中的每个元素都是一个包含两个键的字典:href(第9行)和lien(第10行)
相关的控制代码如下:
注释:
- 类型为 [EnteteWebArticles] 的组件具有一个只读的公共属性 [actions] - 第 7 行
- 该属性用于将名为 [rptMenu] 的 <asp:repeater> 组件(第 10 行)与应用程序控制器计算出的选项数组(第 11-12 行)相关联。
应用程序的其他视图将使用由 [entete.ascx] 定义的页眉。例如,页面 [erreurs.aspx] 将通过以下代码包含该页眉:
注释:
- 第 1 行声明 <WA:entete> 标签应与由文件 [entete.ascx] 定义的组件相关联。属性 [TagPrefix] 和 [TagName] 可自由设置。
- 完成上述设置后,第9行将该组件插入到页面的呈现代码中。在运行时,该标签将使包含它的页面ASPX的代码中,包含页面[entete.ascx]的代码。 控制代码 [erreurs.aspx.vb] 将负责初始化该组件。其实现方式如下:
注释:
- 第 6 行创建了一个类型为 [EnteteWebArticles] 的对象,该类型即为所创建组件的类型
- 第 11 行初始化了该对象的 [actions] 属性
1.5.6.2. 视图 [liste.aspx]
1.5.6.2.1. Introduction
该视图显示了可供销售的商品列表:
![]() | ![]() |
该视图在执行 /main?action=liste 或 /main?action=validationpanier 请求后显示。控制器请求的参数如下:
Hashtable() 对象 - 菜单选项数组 | |
ArrayList 类型为 [Article] 的对象 | |
String 对象 - 页面底部显示的消息 |
文章表 HTML 中的每个链接 [Infos] 都有一个 URL,其形式为 [?action=infos&id=ID],其中 ID 是所显示文章的 id 字段。
1.5.6.2.2. 页面组件
![]() |
n° | type | nom | rôle |
用户组件 | 页眉 | 显示页眉 | |
DataGrid | DataGridArticles 3 - 关联列:表头:姓名,字段:姓名 4 - 关联列:标题:价格,字段:价格 5 - 超链接列:文本:信息,URL 字段:id, 格式 URL:/webarticles/main.aspx?action=infos&id={0} | 显示在售商品 | |
标签 | lblMessage | 显示消息 |
回顾一下如何设置这些属性:
- 在 Visual Studio 中,选择 [DataGrid] 以访问其属性表:

- 使用上方的链接 [Mise en forme automatique] 来管理显示表格的样式
- 并使用链接 [Générateur de propriétés] 来管理其内容
1.5.6.2.3. 展示代码 [liste.aspx]
注释:
- 第 9 行定义了页面的页眉
- 第 12-24 行定义了 [DataGrid] 的特征
- 第26行定义了标签[lblMessage]
1.5.6.2.4. 控制器代码 [liste.aspx.vb]
注释:
- 页面组件出现在第13-15行。需要注意的是,我们需要创建一个带有操作符[new]的对象[EnteteWebArticles],而对于其他组件则无需如此。 若未显式创建该对象,将出现运行错误,提示对象 [entete] 未引用任何内容。这一问题值得深入探究,但目前尚未进行深入分析。
- 页眉菜单的选项数组是从上下文中获取的,用于初始化页面中的组件 [entete] —— 第 20 行
- 从上下文中获取商品列表 - 第 22 行
- 用于初始化组件 [DataGridArticles] - 第 24-27 行
- 组件 [lblMessage] 通过上下文中的消息进行初始化 - 第 29 行
1.5.6.3. 视图 [infos.aspx]
1.5.6.3.1. Introduction
该视图显示商品信息,并支持购买操作:

当购买数量有误时,该视图会在执行 /main?action=infos&id=ID 或 /main?action=achat&id=ID 请求后显示。控制器请求的参数如下:
Hashtable() 对象 - 菜单选项数组 | |
类型为 [Article] 的对象 - 要显示的商品 | |
String 对象 - 数量出错时显示的提示信息 | |
字符串对象 - 在输入字段中显示的值[Qte] |
当数量输入出错时,将使用字段 [msg] 和 [qte]:

该页面包含一个表单,该表单通过按钮 [Acheter] 提交。 POST 的目标 URL 是 [?action=achat&id=ID],其中 ID 是所购商品的 ID。
1.5.6.3.2. 页面组件
![]() |
n° | type | nom | rôle |
用户组件 | 页眉 | 显示页眉 | |
字面量 | litID | 显示商品编号 | |
DataGrid | DataGridArticle 3 - 关联列:表头:姓名,字段:姓名 4 - 关联列:标题:价格,字段:价格 5 - 关联列:标题:当前库存,字段:stockActuel 6 - 关联列:标题:最低库存,字段:stockMinimum | 显示商品 | |
HTML 提交 | 提交表单 | ||
HTML 输入 runat=server | txtQte | 输入购买数量 | |
标签 | lblMsgQte | 可能出现的错误信息 |
1.5.6.3.3. 展示代码 [infos.aspx]
备注:
- 页眉已包含在页面中 - 第 9 行
- 字符串 [litId] 定义在第 10 行
- DataGrid 和 [DataGridArticles] 在第 12-34 行定义
- 表单在第36-46行定义。其类型为POST。
- POST 的目标由变量 [strAction] 提供——第 36 行。该变量需由控制器定义。
- 购买数量的输入字段定义在第41行。这是一个服务器端组件(runat=server),类型为HTML。在代码端,可通过一个对象访问该字段。
- 第 42 行定义了标签 [lblMsgQte],该标签将用于显示有关输入数量的错误信息(如有)
1.5.6.3.4. 验证代码 [infos.aspx.vb]
注释:
- 页面组件在第10-14行中定义
- 该类定义了一个公共属性 [strAction],用于指定表单中 POST 的目标——第 17-25 行
- 要显示的文章是从应用程序上下文中获取的——第30行
- 页眉菜单的选项数组从上下文中获取,用于初始化页面的 [entete] 组件——第 32 行
- 第 33-39 行,[DataGridArticle] 组件与类型为 [ArrayList] 的数据源相关联,该数据源仅包含第 30 行获取的商品
- [lblMsgQte, txtQte] 组件使用上下文中的信息进行初始化 - 第 42-45 行
- 属性 [straction] 同样使用上下文中的信息进行初始化——第 47 行。该变量用于生成页面中表单 HTML 的属性 [action]:
1.5.6.4. 视图 [panier.aspx]
1.5.6.4.1. Introduction
此视图显示购物车内容:

该页面是在执行 /main?action=panier 或 /main?action=retirerachat&id=ID 请求后显示的。控制器请求的参数如下:
Hashtable() 对象——菜单选项数组 | |
类型为 [Panier] 的对象——待显示的购物车 |
购物车购买列表 HTML 中的每个链接 [Retirer] 都包含一个 URL,其形式为 [?action=retirerachat&id=ID],其中 ID 是 [id]字段,该字段对应要从购物车中移除的商品。
1.5.6.4.2. 页面组件
![]() |
n° | type | nom | rôle |
用户组件 | 页眉 | 显示页眉 | |
DataGrid | DataGridAchats 3- 关联列 - 表头:商品,字段:名称 4 - 关联列 - 表头:数量,字段:数量 5 - 关联列 - 列头:价格,字段:价格 6 - 关联列 - 标题:总计,字段:总计,格式 {0:C} 7 - 超链接列 - 文本:退货,URL:id,URL格式:/webarticles/main.aspx?action=retirerachat&id={0} | 显示已购商品列表 | |
标签 | lblTotal | 显示应付金额 |
1.5.6.4.3. 展示代码 [panier.aspx]
备注
- 第 9 行包含标题
- 第 12-27 行,定义了组件 [DataGridAchats]
- 第 29 行,定义了组件 [lblTotal]
1.5.6.4.4. 控制代码 [panier.aspx.vb]
注释:
- 页面组件在第 11-13 行中声明
- [entete]组件的初始化与之前分析过的页面中的初始化方式相同——第17行
- 待显示的购物车数据从会话中获取——第19行
- 使用组件 [DataGridAchats] 显示该购物车时遇到问题。困难源于该组件的初始化。回顾该组件的列:
- 列 [Article] 关联数据源中的字段 [nom]
- 列 [Qté] 关联数据源中的字段 [qte]
- 列 [Prix] 关联到数据源中的字段 [prix]
- 列 [Total] 关联于数据源中的字段 [total]
我们拥有的数据源是购物车及其购物清单。后者将成为 [DataGrid] 的数据源。 只有那些将填充 [DataGrid] 行数据的 [Achat] 对象,不具备 [DataGrid] 所期望的 [nom, qte, prix, total] 属性。 因此,我们在此专门为 [DataGrid] 创建了一个数据源,其元素具有 [DataGrid] 所期望的特征。 这些元素将属于 [LigneAchat] 类型,这是一个为此专门创建的类,并继承自 [Achat] 类(第 36-66 行)
- 定义类 [LigneAchat] 后,[DataGridAchats] 的数据源基于会话中找到的购物车构建 - 第 20-30 行
- 通过类 [Panier] 的属性 [totalPanier] 显示购物金额 - 第 32 行
1.5.6.5. 视图 [paniervide.aspx]
1.5.6.5.1. Introduction
该视图显示了购物车为空的信息:

该视图在执行 /main?action=panier 或 /main?action=retirerachat&id=ID 请求后显示。控制器请求的参数如下:
Hashtable() 对象 - 菜单选项数组 |
1.5.6.5.2. 页面组件
![]() |
n° | type | nom | rôle |
用户组件 | 页眉 | 显示页眉 |
1.5.6.5.3. 呈现代码 [paniervide.aspx]
注释:
- 页眉包含在第 9 行
1.5.6.5.4. 控制代码 [paniervide.aspx.vb]
注释:
- 仅初始化页面中唯一的动态组件 - 第10行
1.5.6.6. 视图 [erreurs.aspx]
1.5.6.6.1. Introduction
出现错误时将显示此视图:

该视图在任何导致错误的请求后显示,但数量错误的购买操作除外,该情况由视图 [INFOS] 处理。控制器请求的元素如下:
Hashtable() 对象 - 菜单选项数组 | |
ArrayList 对象,其中 [String] 表示要显示的错误消息 |
1.5.6.6.2. 页面组件
![]() |
n° | type | nom | rôle |
用户组件 | 页眉 | 显示页眉 | |
重复器 | rptErreurs | 显示错误列表 |
1.5.6.6.3. 呈现代码 [erreurs.aspx]
注释:
- 标题在第 9 行定义
- 组件 [rptErreurs] 定义在第 13-19 行。其内容来自数据源,该数据源的类型为 [ArrayList],包含 [String] 对象。
1.5.6.6.4. 控制代码 [erreurs.aspx.vb]
注释:
- [entete]组件按惯例初始化,见第9行和第14行
- 组件 [rptErreurs] 使用在上下文中找到的 [ArrayList] 类型的错误列表进行初始化——第 16-19 行
1.5.7. 控制器 global.asax、main.aspx
接下来需要编写我们 Web 应用程序的核心——控制器。其作用在于:
- 获取客户端的请求,
- 借助业务类处理客户端请求的操作,
- 返回相应的视图。
1.5.7.1. 控制器 [global.asax.vb]
当应用程序接收到首个请求时,将执行文件 [global.asax.vb] 中的过程 [Application_Start]。这仅会发生一次。 [Application_Start] 过程旨在初始化 Web 应用程序所需的对象,这些对象将作为只读资源由所有客户端线程共享。这些共享对象可放置在两个位置:
- 控制器中的私有字段
- 应用程序的运行上下文(Application)
应用程序 [global.asax.vb] 中的方法 [Application_Start] 将执行以下操作:
- 检查文件 [web.config] 中是否包含应用程序正常运行所需的参数。这些参数已在第 1.5.3 节中描述。
- 将可能出现的错误列表以 [ArrayList erreurs] 对象的形式放入应用程序上下文中。若无错误,该列表将为空,但仍会存在。
- 若发生错误,[Application_Start]方法即在此终止。否则,它将请求一个类型为[IArticlesDomain]的单例引用,该对象将成为控制器用于处理业务需求的业务对象。 如 1.5.3.2 节所述,控制器将向 Spring 框架请求该单例。此实例化操作可能引发各种错误。若发生错误,这些错误将再次被记录在应用程序上下文中的 [erreurs] 对象中。
控制器 [global.asax.vb] 包含一个名为 [Session_Start] 的过程,该过程会在每次有新客户访问时执行。在此过程中,将为该客户创建一个空购物车。该购物车将在该特定客户的后续请求中持续保留。相关代码可能如下所示:
注释:
- [web.config] 中所需的参数在数组中定义——第 18 行
- 在 [web.config] 中检索这些参数。若存在,则将其存储在应用程序上下文中;否则,会在 [erreurs] 的错误列表中记录一条错误(第 21-33 行)
- 若无错误,则向 Spring 请求单例 [articlesDomain] 的引用,该单例负责管理对应用程序 [domain] 层的访问 - 第 35-47 行。 可能出现的错误将记录在 [erreurs] 中。
- 错误记录在应用程序上下文中——第49行
- 若发生错误则退出该过程——第 51 行
- 创建一个包含三个字典的数组。每个字典都有两个键:href 和 lien。该数组表示三种可能的菜单选项——第 52-71 行
- 该数组存储在应用程序上下文中 - 第 73 行
- 每次有新客户时,都会执行 [Session_Start] 过程。在此过程中,会在客户的会话中创建一个空购物车 - 第 78-81 行
1.5.7.2. 控制器 [main.aspx.vb]
控制器 [main.aspx.vb] 会处理所有来自客户的请求。实际上,这些请求都采用 [/webarticles/main.aspx?action=XX] 的格式。请求将按以下方式处理:
- 将验证应用程序上下文中的 [erreurs] 对象。若该对象不为空,则表示应用程序初始化时发生错误,导致无法运行。此时将返回视图 [ERREURS] 作为响应。
- 将获取并验证请求中的参数 [action]。如果该参数不对应已知的操作,则发送视图 [ERREURS] 并附上相应的错误信息。
- 如果参数 [action] 有效,则将客户端的请求传递给该操作的特定处理程序进行处理:
方法 | 请求 | 处理 | 可能的响应 |
GET /main?action=liste | - 查询 业务类 - 显示 | [LISTE] 或 [ERREURS] | |
GET /main?action=infos&id=ID | - 向 业务类 - 显示 | [INFOS] 或 [ERREURS] | |
POST /main?action=achat&id=ID - 购买数量属于提交的参数之一 | - 向 业务类中 - 将其加入购物车,并 客户会话中 | [LISTE] 或 [INFOS] 或 [ERREURS] | |
GET /main?action=退货&id=ID | - 从 购物车中的 客户会话 | [PANIER] | |
GET /main?action=panier | - 显示 客户会话 | [PANIER] 或 [PANIERVIDE] | |
GET /main?action=validationpanier | - 在数据库中减少 库存 库存 客户会话 | [LISTE] 或 [ERREURS] |
控制器 [main.aspx.vb] 的骨架代码可能如下:
注释:
- 该类有两个私有字段,将在方法之间共享——第 15-16 行:
- articlesDomain:访问 [domain] 层的单例
- options:菜单选项字典数组
- 过程 [Page_Load]:
- 将初始化该类的两个私有字段
- 从请求中获取参数 [action],并执行该操作的处理方法。
1.5.7.3. 方法 [Page_Load]
这是页面上发生的第一个事件。代码如下:
注释:
- 每次加载页面时,需确保由 [global.asax] 执行的应用程序初始化已顺利完成。
- 为此,从应用程序上下文中获取由 [global.asax] 放置的错误列表(第 4 行)
- 如果该列表不为空,则显示视图 [ERREURS] - 第 6-10 行
- 从应用程序上下文中获取由 [global.asax] 放置的单例 [articlesDomain],并将其存储在私有字段 [articlesDomain] 中,以便该类中的各种方法都能使用它 - 第 12 行
- 对菜单选项数组进行类似操作 - 第 14 行
- 从请求中获取参数 [action] —— 第 16 行
- 执行与请求操作对应的方法。未预设的操作被视为操作 [liste] - 第 16-36 行
1.5.7.4. 处理操作 [liste]
此操作用于显示物料清单:

代码如下:
备注:
- 将可能出现的错误记录在 [ArrayList] 中 - 第 4 行
- 向单例 [articlesDomain] 请求文章列表 - 第 5-12 行
- 若出现错误,则发送视图 [ERREURS] - 第 13-19 行
- 否则发送视图 [LISTE] - 第 20-24 行
1.5.7.5. 处理操作 [infos]
客户查询了某件商品的信息:

代码如下:
备注:
- 将可能出现的错误放入 [ArrayList] - 第 4 行
- 从请求中获取所请求文章的标识符 - 第6行
- 验证该标识符。它必须存在且必须为整数。若不符合条件,则发送视图 [ERREURS] 并附带相应的错误消息 - 第 7-27 行
- 验证ID后,向单例[articlesDomain]查询该商品。若发生异常,则返回视图[ERREURS] - 第29-39行
- 如果未找到该商品,则发送视图 [ERREURS] - 第 41-48 行
- 如果找到了该条目,它将被放入用户的会话中,然后显示在视图 [INFOS] 的第 50-56 行
1.5.7.6. 处理操作 [achat]
客户购买了显示在视图 [INFOS] 中的商品。

代码如下:
备注:
- 从会话中检索商品 - 第 5 行
- 如果该文章不存在(会话可能已过期),则显示视图 [LISTE] - 第 7-10 行
- 从查询中获取购买数量 - 第12行
- 验证其有效性 - 第13-29行
- 若无效,根据具体情况,发送视图 [LISTE] - 第 16 行 或视图 [INFOS] - 第 24-28 行
- 若一切正常,将商品添加至购物车 - 第31-32行
- 随后发送视图 [LISTE] - 第 34 行
1.5.7.7. 处理操作 [panier]
客户进行了多项购买,并要求查看购物车:

代码如下:
注释:
- 在第4行从会话中获取购物车。此处未验证是否成功获取数据。应进行此验证,因为会话可能已过期。
- 如果购物车为空,则发送视图 [PANIERVIDE] - 第 6-10 行
- 否则,发送视图 [PANIER] - 第 11-14 行
1.5.7.8. 处理操作 [retirerachat]
客户希望从购物车中移除一件商品:

代码如下:
注释:
- 在第4行从会话中获取购物车。此处未验证是否成功获取数据。应进行此验证,因为会话可能已过期。
- 从请求中获取待移除商品的ID [id] - 第8行。
- 将对应的购买项从购物车中移除——第10行
- 此处未验证已购商品ID的有效性。若ID类型无效,将触发异常并在第11-13行进行处理。若ID有效但不存在,则第10行的[panier.enlever]方法不执行任何操作。
- 显示新的购物车 - 第16行
1.5.7.9. 处理操作 [validerpanier]
客户希望确认购物车:

代码如下:
注释:
- 在第6行从会话中获取购物车。此处未验证是否成功获取数据。应进行此验证,因为会话可能已过期。
- 若会话已过期,将获得指向购物车的指针 [nothing],此时第 9 行中的方法 [acheter] 将抛出异常,并返回视图 [ERREURS]。 但该错误信息对用户而言可能不够直观。
- 第 8-16 行,尝试验证从会话中获取的购物车。如果所请求的数量超过了所选商品的库存,某些购买可能无法通过验证。这些情况由方法 [acheter] 记录在错误列表中,该列表在第 11 行被获取。
- 若存在错误,则发送视图 [ERREURS] —— 第 18-23 行
- 否则将发送视图 [LISTE] — 第 25-26 行
1.6. Conclusion
我们在此开发了一个基于 MVC 模型的应用程序。目前(2005年4月)似乎尚不存在像 Java 领域(Struts、Spring、 ……)。[Spring.net]项目预计将很快推出此类框架。在此之前,前述方法可为中等规模的应用程序提供可行的MVC开发方案。































