分层思想的一个核心就是部件化,各个层之间是相互独立的,每一层可以随便抽取换成一个其他语言的版本,但只要与相应的接口吻合就行。
我用的三层架构大致是这样的,基本的三层就不说了,然后分别为业务逻辑层和数据访问层定义一个接口,由具体的那个层来实现,问题产生了,由谁来指定程序使用哪个具体的对象来实现相应接口?
为解决这个问题,我应用的是抽象工厂模式。分别为业务逻辑层和数据访问层添加一个抽象工厂。具体架构还是看下图吧。
这里的Utility是一个工具类,在下文中会提到。
学过设计模式的人都应该听过反射技术,但是一个系统中用到的类很多,需要对每一个类进行实例化,如果仅利用抽象工厂+反射模式,重复的代码比较多,如果哪一天整个DAL层发生变更,那么就要在代码中修改每一个用到的地方,不仅不容易维护,而且还很容易出错,未解决这个问题,对程序作了一个优化——用到依赖注入。还是看看代码吧。
1、先看看依赖注入的容器:这里我把这个注入容器放到了工具类中,刚开始学习设计模式,不知道是否合理,欢迎高手们指点。
- Imports System.Configuration
- Imports System.Reflection
- Public Class DependencyInjector
- ''' <summary>
- ''' 利用反射机制,取得数据访问层对象
- ''' </summary>
- ''' <param name="className">传入数据访问层中要实例化的类的名称</param>
- ''' <returns>指定的数据访问层的类</returns>
- ''' <remarks></remarks>
- Public Function GetDALObject(ByVal className As String) As Object
- Dim dal As Object
- Dim dalName As String
- Dim fullClassName As String
- Dim dalObj As Object
- '通过配置文件的指定要应用的DAL层
- dal = System.Configuration.ConfigurationManager.AppSettings("DAL")
- 'dalName就是常说的用用程序的名称
- dalName = dal.ToString
- '命名空间+类的名称,指明要实例化的类的路径
- fullClassName = dalName + "." + className
- '通过反射,取得数据访问层对象
- dalObj = Assembly.Load(dalName).CreateInstance(fullClassName)
- '返回指定的对象
- Return dalObj
- End Function
- ''' <summary>
- '''取得指定业务逻辑层的指定类
- ''' </summary>
- ''' <param name="className">要应用的业务逻辑层的具体类的名称</param>
- ''' <returns>指定的业务逻辑层的类(对象)</returns>
- ''' <remarks></remarks>
- Public Function GetBLLObject(ByVal className As String) As Object
- Dim bll As Object
- Dim bllName As String
- Dim bllObj As Object
- Dim fullClassName As String
- '从配置文件中读取业务逻辑名称
- bll = System.Configuration.ConfigurationManager.AppSettings("BLL")
- bllName = bll.ToString
- fullClassName = bllName + "." + className
- '利用反射取得业务逻辑层对象
- bllObj = Assembly.Load(bllName).CreateInstance(fullClassName)
- Return bllObj
- End Function
- End Class
2、相关配置文件:
- <appSettings>
- <add key="connStr" value="Persist Security Info=true;Data Source=*****;Initial Catalog=Charge_Sys_SelfDesign;User ID=sa;PWD=****;" />
- <add key="DAL" value="DAL" />
- <add key="BLL" value="BLL" />
- </appSettings>
3、业务逻辑层工厂:这里以在工厂中生产一个UserBLL类为例,在工厂中添加CreateUserBLL()方法,理论上讲,业务逻辑层有多少个类在此工厂中就要有多少个相应的方法,但是针对不同语言写的或者是不同的程序员用同一种语言写的同一层的代码(但都实现了程序指定的接口),我们在给类起名字的时候,只要用相同的类名就可以通过仅修改配置文件,达到换层的目的,而无需在工厂中改动任何代码。比如说,我现在要把DAL层换成AccessDAL,那么仅需要做如下修改即可。
- <add key="DAL" value="AccessDAL" />
这就是分层的好处,当然也是面向对象思想的优势了。
上面主要是解释了一下配置文件,来看看业务逻辑工厂代码:
- Imports IBLL
- Imports Utility
- Imports System.Configuration
- Imports System.Reflection
- Imports System.Windows.Forms
- Public Class CreateBLL
- Private dependecy As New Utility.DependencyInjector
- ''' <summary>
- ''' 生成用户业务逻辑层访问对象
- ''' </summary>
- ''' <returns></returns>
- ''' <remarks></remarks>
- Public Function CreateUserBLL() As IBLL.IUserBLL
- Try
- '如果不用注入,需要重复N次BLL,
- 'Return CType(Assembly.Load("BLL").CreateInstance("BLL.UserBLL"), IBLL.IUserBLL)
- '优化后只需指明具体类名,如果要换层,只需到配置文件中做简单修改即可,程序代码清晰,不宜出错。
- Return CType(dependecy.GetBLLObject("UserBLL"), IBLL.IUserBLL)
- Catch ex As Exception
- MsgBox(ex.Message)
- Return Nothing
- End Try
- End Function
- End Class
4、数据访问层工厂代码类似:
- Imports Utility
- Imports System.Configuration
- Imports System.Reflection
- Imports IDAL
- Imports System.Windows.Forms
- Public Class CreateDAL
- Private dependency As New Utility.DependencyInjector
- ''' <summary>
- ''' 生成用户指定的数据访问层对象
- ''' </summary>
- ''' <returns></returns>
- ''' <remarks></remarks>
- Public Function CreateUserDAL() As IDAL.IUserDAL
- Try
- 'Return CType(Assembly.Load("DAL").CreateInstance("DAL.UserDAL"), IDAL.IUserDAL)
- Return CType(dependency.GetDALObject("UserDAL"), IDAL.IUserDAL)
- Catch ex As Exception
- MessageBox.Show(ex.Message)
- Return Nothing
- End Try
- End Function
- End Class
5、业务逻辑层接口代码比较简单,只需要定义一些相关接口方法即可,但是这里面体现了系统的架构,看似代码简单,实则反应程序员的架构水平。
- Public Interface IUserBLL
- '返回用户登录的身份级别,在接口的实现中要验证用户名和密码
- Function LogIn(ByVal modelUser As Model.User) As String
- End Interface
6、数据访问层接口:
- Public Interface IUserDAL
- '获取用户ID
- Function GetID(ByVal modelUser As Model.User) As String
- '获取用户密码
- Function GetPwd(ByVal modelUser As Model.User) As String
- '获取用户级别
- Function GetLevel(ByVal modelUser As Model.User) As String
- End Interface
7、业务逻辑层:
- Imports DALFactory
- Imports IBLL
- Imports IDAL
- Imports Model
- Imports System.Data.SqlClient
- Imports System.Collections.Generic
- Public Class UserBLL
- Implements IBLL.IUserBLL
- Private dalFactory As New DALFactory.CreateDAL
- ''' <summary>
- ''' 先判断用户名和密码是否正确,然后返回用户级别
- ''' </summary>
- ''' <param name="modelUser">用户实体类</param>
- ''' <returns>用户级别</returns>
- ''' <remarks></remarks>
- Public Function LogIn(ByVal modelUser As Model.User) As String Implements IBLL.IUserBLL.LogIn
- Dim userLevel As String
- Try
- If dalFactory.CreateUserDAL.GetID(modelUser) = "" Then
- MsgBox("用户名错误!")
- Return Nothing
- Exit Function
- End If
- If dalFactory.CreateUserDAL.GetPwd(modelUser) = "" Then
- MsgBox("密码名错误!")
- Return Nothing
- Exit Function
- End If
- '通过数据访问层工厂指定实现数据访问层接口的具体的数据访问层以及该层的具体类
- userLevel = dalFactory.CreateUserDAL.GetLevel(modelUser)
- Catch ex As Exception
- Return Nothing
- End Try
- Return userLevel
- End Function
- End Class
8、数据访问层:
- Imports System.Data.SqlClient
- Imports Utility
- Imports System.Windows.Forms
- Public Class UserDAL
- '实现数据访问层的接口
- Implements IDAL.IUserDAL
- Private sqlHelp As New Utility.SQLServerDALHelp
- ''' <summary>
- ''' 读取用户ID
- ''' </summary>
- ''' <param name="modelUser">用户实体类</param>
- ''' <returns>用户ID</returns>
- ''' <remarks></remarks>
- Public Function GetID(ByVal modelUser As Model.User) As String Implements IDAL.IUserDAL.GetID
- Dim User_ID As String
- Dim conn As New SqlConnection(sqlHelp.connStr)
- Dim spName As String
- spName = "proc_GetUserID"
- Dim cmd As New SqlCommand(spName, conn)
- cmd.CommandType = CommandType.StoredProcedure
- Dim Param As SqlParameter
- Param = New SqlParameter("@User_ID", SqlDbType.VarChar)
- Param.Value = modelUser.User_ID
- cmd.Parameters.Add(Param)
- Try
- conn.Open()
- User_ID = cmd.ExecuteScalar.ToString
- Return User_ID
- Catch ex As Exception
- MsgBox(ex.Message)
- Throw New Exception(ex.Message)
- Finally
- conn.Close()
- cmd.Dispose()
- cmd = Nothing
- End Try
- Return User_ID
- End Function
- ''' <summary>
- ''' 读取用户密码
- ''' </summary>
- ''' <param name="modelUser">用户实体类</param>
- ''' <returns>密码</returns>
- ''' <remarks></remarks>
- Public Function GetPwd(ByVal modelUser As Model.User) As String Implements IDAL.IUserDAL.GetPwd
- Dim user_Pwd As String
- Dim spName As String
- spName = "proc_GetUserPwd"
- Dim conn As New SqlConnection(sqlHelp.connStr)
- Dim cmd As New SqlCommand(spName, conn)
- cmd.CommandType = CommandType.StoredProcedure
- Dim Param As SqlParameter
- Param = New SqlParameter("@User_Pwd", SqlDbType.VarChar)
- Param.Value = modelUser.User_Pwd
- cmd.Parameters.Add(Param)
- Param = New SqlParameter("@User_ID", SqlDbType.VarChar)
- Param.Value = modelUser.User_Pwd
- cmd.Parameters.Add(Param)
- Try
- conn.Open()
- user_Pwd = cmd.ExecuteScalar.ToString
- Catch ex As Exception
- MsgBox(ex.Message)
- Throw New Exception(ex.Message)
- Finally
- conn.Close()
- cmd.Dispose()
- cmd = Nothing
- End Try
- Return user_Pwd
- End Function
- ''' <summary>
- ''' 读取用户身份级别
- ''' </summary>
- ''' <param name="modelUser">用户实体类</param>
- ''' <returns>身份级别</returns>
- ''' <remarks></remarks>
- Public Function GetLevel(ByVal modelUser As Model.User) As String Implements IDAL.IUserDAL.GetLevel
- Dim User_Level As String
- Dim spName As String
- spName = "proc_GetUserLevel"
- Dim conn As New SqlConnection(sqlHelp.connStr)
- Dim cmd As New SqlCommand(spName, conn)
- cmd.CommandType = CommandType.StoredProcedure
- Dim Param As SqlParameter
- Param = New SqlParameter("@User_ID", SqlDbType.VarChar)
- Param.Value = modelUser.User_ID
- cmd.Parameters.Add(Param)
- Try
- conn.Open()
- User_Level = cmd.ExecuteScalar.ToString
- Catch ex As Exception
- MsgBox(ex.Message)
- Throw New Exception(ex.Message)
- Finally
- conn.Close()
- cmd.Dispose()
- cmd = Nothing
- End Try
- Return User_Level
- End Function
- End Class
9、总算到UI层了:注意这里还没有添加针对用户输入合法性的验证,如,用户名、密码输入是否为空,是否符合指定格式等。
- Imports Model
- Imports IBLL
- Imports System.Collections.Generic
- Imports BLLFactory
- Public Class frmLogIn
- Private bllFactory As New BLLFactory.CreateBLL
- Private Sub btnLogIn_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btnLogIn.Click
- Dim modelUser As New Model.User
- Dim userLevel As String
- modelUser.User_ID = txtUserID.Text
- modelUser.User_Pwd = txtUserPwd.Text
- '通过业务逻辑工厂指定业务逻辑接口要使用的具体的业务逻辑层中的具体类
- userLevel = bllFactory.CreateUserBLL.LogIn(modelUser)
- Select Case userLevel
- Case "一般用户"
- Case "操作员"
- Case "管理员"
- frmMDIParentForm.Show()
- Case Else
- MsgBox("未知错误!")
- Exit Sub
- End Select
- End Sub
- Private Sub btnExit_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btnExit.Click
- txtUserID.Text = ""
- txtUserPwd.Text = ""
- Me.Close()
- End Sub
- End Class
到此为止,一个简单的三层架构就算实现了,隐约感觉设计上有些地方不合理,但说不好存在于哪些地方,希望由此路过的大牛们,多多指教,另外,这里的数据访问层代码冗余较多,在后续的博客中,会通过编写一个SQLHelp来实现优化,还有UML包图也会在后续博客中天上。
最后说说我自己的感触。
针对架构:用到了两个抽象工厂,刚开始是将两个工厂写到了一层中,这层的名称就叫Factory,而且封装注入的类也一并放到了这层中,但是在调试程序的时候出现DAL层依赖循环调用错误,不知道是代码还是设计上的原因?
针对接口设计:不太清楚业务逻辑层的方法设计是否合理,现在的一个问题就是如果用户名或者密码出错的话,并不是由UI层向用户提供反馈信息,而是由业务逻辑层担当了此任务,暂且就这么写吧,估计到后面写的多了就找到合适的方法了。