zoukankan      html  css  js  c++  java
  • MVC 实用构架实战(一)——项目结构搭建

    一、前言

      在《上篇》中,已经把项目整体结构规划做了个大概的规划。在本文中,将使用代码的方式来一一解说各个层次。由于要搭建一个基本完整的结构,可能文章会比较长。另外,本系列主要出于实用的目的,因而并不会严格按照传统的三层那样进行非常明确的层次职能划分。

    二、需求说明

      在本系列中,为方便大家理解,将以一个账户管理的小系统来进行解说,具体需求如下:

    1. 用户信息分主要信息与扩展信息,一个用户可以有(或没有)一个用户扩展信息。
    2. 记录用户的登录记录,一个用户可以有多条登录记录,但登录记录所属用户唯一。
    3. 一个用户可以有多个角色,一个角色也可以分配给多个用户。

    三、架构基础

     (一) 功能返回值

      对于一个操作性业务功能(比如添加,修改,删除),通常我们处理返回值的做法是使用简单类型,通常会有如下几种方案:

    1. 直接返回void,即什么也不返回,在操作过程中抛出异常,只要没有异常抛出,就认为是操作成功了
    2. 返回是否操作成功的bool类型的返回值
    3. 返回操作变更后的新数据信息
    4. 返回表示各种结果的状态码的返回值
    5. 返回一个自定义枚举来表示操作的各种结果
    6. 如果要返回多个值,还要使用 out 来添加返回参数

      这样做有什么不妥之处呢,我们来逐一分析:

    1. 靠抛异常的方式来终止系统的运行,异常是沿调用堆栈逐层向上抛出的,会造成很大的性能问题
    2. bool值太死板,无法表示出业务操作中的各种情况
    3. 返回变更后的数据,还要与原始数据来判断才能得到是否操作成功
    4. 用状态码解决了2的问题,但各种状态码的维护成本也会非常高
    5. 用枚举值一定程序上解决了翻译的问题,但还是要把枚举值翻译成各种情况的文字描述
    6. !@#¥%……&

      综上,我们到底需要一个怎样的业务操作结果呢?

    1. 要能表示操作的成功失败(废话)
    2. 要能快速表示各种操作场景(如参数错误,查询数据不存在,数据状态不满足操作要求等)
    3. 能返回附加的返回信息(如更新成功后有后续操作,需要使用更新后的新值)
    4. 最好在调用方能使用统一的代码进行返回值处理
    5. 最好能自定义返回的文字描述信息
    6. 最好能把返回给用户的信息与日志记录的信息分开

      再综上,显然简单类型的返回值满足不了需求了,那就需要定义一个专门用来封装返回值信息的返回值类,这里定义如下: 

    复制代码
     1     /// <summary>
     2     ///     业务操作结果信息类,对操作结果进行封装
     3     /// </summary>
     4     public class OperationResult
     5     {
     6         #region 构造函数
     7 
     8         /// <summary>
     9         ///     初始化一个 业务操作结果信息类 的新实例
    10         /// </summary>
    11         /// <param name="resultType">业务操作结果类型</param>
    12         public OperationResult(OperationResultType resultType)
    13         {
    14             ResultType = resultType;
    15         }
    16 
    17         /// <summary>
    18         ///     初始化一个 定义返回消息的业务操作结果信息类 的新实例
    19         /// </summary>
    20         /// <param name="resultType">业务操作结果类型</param>
    21         /// <param name="message">业务返回消息</param>
    22         public OperationResult(OperationResultType resultType, string message)
    23             : this(resultType)
    24         {
    25             Message = message;
    26         }
    27 
    28         /// <summary>
    29         ///     初始化一个 定义返回消息与附加数据的业务操作结果信息类 的新实例
    30         /// </summary>
    31         /// <param name="resultType">业务操作结果类型</param>
    32         /// <param name="message">业务返回消息</param>
    33         /// <param name="appendData">业务返回数据</param>
    34         public OperationResult(OperationResultType resultType, string message, object appendData)
    35             : this(resultType, message)
    36         {
    37             AppendData = appendData;
    38         }
    39 
    40         /// <summary>
    41         ///     初始化一个 定义返回消息与日志消息的业务操作结果信息类 的新实例
    42         /// </summary>
    43         /// <param name="resultType">业务操作结果类型</param>
    44         /// <param name="message">业务返回消息</param>
    45         /// <param name="logMessage">业务日志记录消息</param>
    46         public OperationResult(OperationResultType resultType, string message, string logMessage)
    47             : this(resultType, message)
    48         {
    49             LogMessage = logMessage;
    50         }
    51 
    52         /// <summary>
    53         ///     初始化一个 定义返回消息、日志消息与附加数据的业务操作结果信息类 的新实例
    54         /// </summary>
    55         /// <param name="resultType">业务操作结果类型</param>
    56         /// <param name="message">业务返回消息</param>
    57         /// <param name="logMessage">业务日志记录消息</param>
    58         /// <param name="appendData">业务返回数据</param>
    59         public OperationResult(OperationResultType resultType, string message, string logMessage, object appendData)
    60             : this(resultType, message, logMessage)
    61         {
    62             AppendData = appendData;
    63         }
    64 
    65         #endregion
    66 
    67         #region 属性
    68 
    69         /// <summary>
    70         ///     获取或设置 操作结果类型
    71         /// </summary>
    72         public OperationResultType ResultType { get; set; }
    73 
    74         /// <summary>
    75         ///     获取或设置 操作返回信息
    76         /// </summary>
    77         public string Message { get; set; }
    78 
    79         /// <summary>
    80         ///     获取或设置 操作返回的日志消息,用于记录日志
    81         /// </summary>
    82         public string LogMessage { get; set; }
    83 
    84         /// <summary>
    85         ///     获取或设置 操作结果附加信息
    86         /// </summary>
    87         public object AppendData { get; set; }
    88 
    89         #endregion
    90     }
    复制代码

       再定义一个表示业务操作结果的枚举,枚举项上有一个DescriptionAttribute的特性,用来作为当上面的Message为空时的返回结果描述。

    复制代码
     1     /// <summary>
     2     ///     表示业务操作结果的枚举
     3     /// </summary>
     4     [Description("业务操作结果的枚举")]
     5     public enum OperationResultType
     6     {
     7         /// <summary>
     8         ///     操作成功
     9         /// </summary>
    10         [Description("操作成功。")]
    11         Success,
    12 
    13         /// <summary>
    14         ///     操作取消或操作没引发任何变化
    15         /// </summary>
    16         [Description("操作没有引发任何变化,提交取消。")]
    17         NoChanged,
    18 
    19         /// <summary>
    20         ///     参数错误
    21         /// </summary>
    22         [Description("参数错误。")]
    23         ParamError,
    24 
    25         /// <summary>
    26         ///     指定参数的数据不存在
    27         /// </summary>
    28         [Description("指定参数的数据不存在。")]
    29         QueryNull,
    30 
    31         /// <summary>
    32         ///     权限不足
    33         /// </summary>
    34         [Description("当前用户权限不足,不能继续操作。")]
    35         PurviewLack,
    36 
    37         /// <summary>
    38         ///     非法操作
    39         /// </summary>
    40         [Description("非法操作。")]
    41         IllegalOperation,
    42 
    43         /// <summary>
    44         ///     警告
    45         /// </summary>
    46         [Description("警告")]
    47         Warning,
    48 
    49         /// <summary>
    50         ///     操作引发错误
    51         /// </summary>
    52         [Description("操作引发错误。")]
    53         Error,
    54     }
    复制代码

     (二) 实体基类

      对于业务实体,有一些相同的且必要的信息,比如信息的创建时间,总是必要的;再比如想让数据库有一个“回收站”的功能,以给数据删除做个缓冲,或者很多数据并非想从数据库中彻底删除掉,只是暂时的“禁用”一下,添加个逻辑删除的标记也是必要的。再有就是想给所有实体数据仓储操作来个类型限定,以防止传入了其他非实体类型。基于以上理由,就有了下面这个实体基类:

    复制代码
     1     /// <summary>
     2     ///     可持久到数据库的领域模型的基类。
     3     /// </summary>
     4     [Serializable]
     5     public abstract class Entity
     6     {
     7         #region 构造函数
     8 
     9         /// <summary>
    10         ///     数据实体基类
    11         /// </summary>
    12         protected Entity()
    13         {
    14             IsDeleted = false;
    15             AddDate = DateTime.Now;
    16         }
    17 
    18         #endregion
    19 
    20         #region 属性
    21 
    22         /// <summary>
    23         ///     获取或设置 获取或设置是否禁用,逻辑上的删除,非物理删除
    24         /// </summary>
    25         public bool IsDeleted { get; set; }
    26 
    27         /// <summary>
    28         ///     获取或设置 添加时间
    29         /// </summary>
    30         [DataType(DataType.DateTime)]
    31         public DateTime AddDate { get; set; }
    32 
    33         /// <summary>
    34         ///     获取或设置 版本控制标识,用于处理并发
    35         /// </summary>
    36         [ConcurrencyCheck]
    37         [Timestamp]
    38         public byte[] Timestamp { get; set; }
    39 
    40         #endregion
    41     }
    复制代码

       这里要补充一下,本来实体基类中是可以定义一个表示“实体编号”的Id属性的,但有个问题,如果定义了,就限定了Id属性的数据类型了,但实际需求中可能有些实体使用自增的int类型,有些实体使用的是易于数据合并的guid类型,因此为灵活方便,不在此限制住 Id的数据类型。

    四、架构分层

      具体的架构分层如下图所示:

     (一) 核心业务层

       根据 需求说明 中定义的需求,简单起见,这里只实现一个简单的用户登录功能:

      用户信息实体:

    复制代码
     1     /// <summary>
     2     ///     实体类——用户信息
     3     /// </summary>
     4     [Description("用户信息")]
     5     public class Member : Entity
     6     {
     7         /// <summary>
     8         /// 获取或设置 用户编号
     9         /// </summary>
    10         public int Id { get; set; }
    11 
    12         /// <summary>
    13         /// 获取或设置 用户名
    14         /// </summary>
    15         [Required]
    16         [StringLength(20)]
    17         public string UserName { get; set; }
    18 
    19         /// <summary>
    20         /// 获取或设置 密码
    21         /// </summary>
    22         [Required]
    23         [StringLength(32)]
    24         public string Password { get; set; }
    25 
    26         /// <summary>
    27         /// 获取或设置 用户昵称
    28         /// </summary>
    29         [Required]
    30         [StringLength(20)]
    31         public string NickName { get; set; }
    32 
    33         /// <summary>
    34         /// 获取或设置 用户邮箱
    35         /// </summary>
    36         [Required]
    37         [StringLength(50)]
    38         public string Email { get; set; }
    39 
    40         /// <summary>
    41         /// 获取或设置 用户扩展信息
    42         /// </summary>
    43         public virtual MemberExtend Extend { get; set; }
    44 
    45         /// <summary>
    46         /// 获取或设置 用户拥有的角色信息集合
    47         /// </summary>
    48         public virtual ICollection<Role> Roles { get; set; }
    49 
    50         /// <summary>
    51         /// 获取或设置 用户登录记录集合
    52         /// </summary>
    53         public virtual ICollection<LoginLog> LoginLogs { get; set; }
    54     }
    复制代码

      核心业务契约:注意接口的返回值使用了上面定义的返回值类

    复制代码
     1     /// <summary>
     2     ///     账户模块核心业务契约
     3     /// </summary>
     4     public interface IAccountContract
     5     {
     6         /// <summary>
     7         /// 用户登录
     8         /// </summary>
     9         /// <param name="loginInfo">登录信息</param>
    10         /// <returns>业务操作结果</returns>
    11         OperationResult Login(LoginInfo loginInfo);
    12     }
    复制代码

       核心业务实现:核心业务实现类为抽象类,因没有数据访问功能,这里使用了一个Members字段来充当数据源,业务功能的实现为虚方法,必要时可以在具体的客户端(网站、桌面端,移动端)相应的派生类中进行重写。请注意具体实现中对于返回值的处理。这里登录只负责最核心的登录业务操作,不涉及比如Http上下文状态的操作。

    复制代码
     1     /// <summary>
     2     ///     账户模块核心业务实现
     3     /// </summary>
     4     public abstract class AccountService : IAccountContract
     5     {
     6         private static readonly Member[] Members = new[]
     7         {
     8             new Member { UserName = "admin", Password = "123456", Email = "admin@gmfcn.net", NickName = "管理员" },
     9             new Member { UserName = "gmfcn", Password = "123456", Email = "mf.guo@qq.com", NickName = "郭明锋" }
    10         };
    11 
    12         private static readonly List<LoginLog> LoginLogs = new List<LoginLog>();
    13 
    14         /// <summary>
    15         /// 用户登录
    16         /// </summary>
    17         /// <param name="loginInfo">登录信息</param>
    18         /// <returns>业务操作结果</returns>
    19         public virtual OperationResult Login(LoginInfo loginInfo)
    20         {
    21             PublicHelper.CheckArgument(loginInfo, "loginInfo");
    22             Member member = Members.SingleOrDefault(m => m.UserName == loginInfo.Access || m.Email == loginInfo.Access);
    23             if (member == null)
    24             {
    25                 return new OperationResult(OperationResultType.QueryNull, "指定账号的用户不存在。");
    26             }
    27             if (member.Password != loginInfo.Password)
    28             {
    29                 return new OperationResult(OperationResultType.Warning, "登录密码不正确。");
    30             }
    31             LoginLog loginLog = new LoginLog { IpAddress = loginInfo.IpAddress, Member = member };
    32             LoginLogs.Add(loginLog);
    33             return new OperationResult(OperationResultType.Success, "登录成功。", member);
    34         }
    35     }
    复制代码

    (二) 站点业务层

      站点业务契约:站点业务契约继承核心业务契约,即可拥有核心层定义的业务功能。站点登录验证使用了Forms的Cookie验证,这里的退出不涉及核心层的操作,因而核心层没有退出功能

    复制代码
     1     /// <summary>
     2     ///     账户模块站点业务契约
     3     /// </summary>
     4     public interface IAccountSiteContract : IAccountContract
     5     {
     6         /// <summary>
     7         ///     用户登录
     8         /// </summary>
     9         /// <param name="model">登录模型信息</param>
    10         /// <returns>业务操作结果</returns>
    11         OperationResult Login(LoginModel model);
    12 
    13         /// <summary>
    14         ///     用户退出
    15         /// </summary>
    16         void Logout();
    17     }
    复制代码

      站点业务实现:站点业务实现继承核心业务实现与站点业务契约,负责把从UI中接收到的视图模型信息转换为符合核心层定义的参数,并处理与网站状态相关的Session,Cookie等Http相关业务。

      在这里需要注意的是,目前的项目中并没有加入IOC组件来对层与层之间进行解耦,在上层调用下层的时候,我们仍然以如下方式来进行实例化:

    1 IAccountSiteContract accountContract = new AccountSiteService();

      这会造成层与层之间紧耦合,在后面的文章中,会加入.NET自带的MEF组件进行层之间的解耦,到时层对象实现化的工作将由MEF来完成,就需要把 AccountSiteService 类的可访问性由 public 修改为 internal,以防止出现上面的实例化代码出现。

    复制代码
     1     /// <summary>
     2     ///     账户模块站点业务实现
     3     /// </summary>
     4     public class AccountSiteService : AccountService, IAccountSiteContract
     5     {
     6         /// <summary>
     7         ///     用户登录
     8         /// </summary>
     9         /// <param name="model">登录模型信息</param>
    10         /// <returns>业务操作结果</returns>
    11         public OperationResult Login(LoginModel model)
    12         {
    13             PublicHelper.CheckArgument(model, "model");
    14             LoginInfo loginInfo = new LoginInfo
    15             {
    16                 Access = model.Account,
    17                 Password = model.Password,
    18                 IpAddress = HttpContext.Current.Request.UserHostAddress
    19             };
    20             OperationResult result = base.Login(loginInfo);
    21             if (result.ResultType == OperationResultType.Success)
    22             {
    23                 Member member = (Member)result.AppendData;
    24                 DateTime expiration = model.IsRememberLogin
    25                     ? DateTime.Now.AddDays(7)
    26                     : DateTime.Now.Add(FormsAuthentication.Timeout);
    27                 FormsAuthenticationTicket ticket = new FormsAuthenticationTicket(1, member.UserName, DateTime.Now, expiration,
    28                     true, member.NickName, FormsAuthentication.FormsCookiePath);
    29                 HttpCookie cookie = new HttpCookie(FormsAuthentication.FormsCookieName, FormsAuthentication.Encrypt(ticket));
    30                 if (model.IsRememberLogin)
    31                 {
    32                     cookie.Expires = DateTime.Now.AddDays(7);
    33                 }
    34                 HttpContext.Current.Response.Cookies.Set(cookie);
    35                 result.AppendData = null;
    36             }
    37             return result;
    38         }
    39 
    40         /// <summary>
    41         ///     用户退出
    42         /// </summary>
    43         public void Logout()
    44         {
    45             FormsAuthentication.SignOut();
    46         }
    47     }
    复制代码

    (三) 站点展现层

      MVC控制器:Action提供统一风格的代码来对业务操作结果OperationResult进行处理

    复制代码
     1     public class AccountController : Controller
     2     {
     3         public AccountController()
     4         {
     5             AccountContract = new AccountSiteService();
     6         }
     7 
     8         #region 属性
     9 
    10         public IAccountSiteContract AccountContract { get; set; }
    11 
    12         #endregion
    13 
    14         #region 视图功能
    15 
    16         public ActionResult Login()
    17         {
    18             string returnUrl = Request.Params["returnUrl"];
    19             returnUrl = returnUrl ?? Url.Action("Index", "Home", new { area = "" });
    20             LoginModel model = new LoginModel
    21             {
    22                 ReturnUrl = returnUrl
    23             };
    24             return View(model);
    25         }
    26 
    27         [HttpPost]
    28         public ActionResult Login(LoginModel model)
    29         {
    30             try
    31             {
    32                 OperationResult result = AccountContract.Login(model);
    33                 string msg = result.Message ?? result.ResultType.ToDescription();
    34                 if (result.ResultType == OperationResultType.Success)
    35                 {
    36                     return Redirect(model.ReturnUrl);
    37                 }
    38                 ModelState.AddModelError("", msg);
    39                 return View(model);
    40             }
    41             catch (Exception e)
    42             {
    43                 ModelState.AddModelError("", e.Message);
    44                 return View(model);
    45             }
    46         }
    47 
    48         public ActionResult Logout( )
    49         {
    50             string returnUrl = Request.Params["returnUrl"];
    51             returnUrl = returnUrl ?? Url.Action("Index", "Home", new { area = "" });
    52             if (User.Identity.IsAuthenticated)
    53             {
    54                 AccountContract.Logout();
    55             }
    56             return Redirect(returnUrl);
    57         }
    58 
    59         #endregion
    60     }
    复制代码

       MVC 视图:

    复制代码
    @model GMF.Demo.Site.Models.LoginModel
    @{
        ViewBag.Title = "Login";
        Layout = "~/Views/Shared/_Layout.cshtml";
    }
    <h2>Login</h2>
    @using (Html.BeginForm()) {
        @Html.AntiForgeryToken()
        @Html.ValidationSummary(true)
        <fieldset>
            <legend>LoginModel</legend>
            <div class="editor-label">
                @Html.LabelFor(model => model.Account)
            </div>
            <div class="editor-field">
                @Html.EditorFor(model => model.Account)
                @Html.ValidationMessageFor(model => model.Account)
            </div>
            <div class="editor-label">
                @Html.LabelFor(model => model.Password)
            </div>
            <div class="editor-field">
                @Html.EditorFor(model => model.Password)
                @Html.ValidationMessageFor(model => model.Password)
            </div>
            <div class="editor-label">
                @Html.LabelFor(model => model.IsRememberLogin)
            </div>
            <div class="editor-field">
                @Html.EditorFor(model => model.IsRememberLogin)
                @Html.ValidationMessageFor(model => model.IsRememberLogin)
            </div>
            @Html.HiddenFor(m => m.ReturnUrl)
            <p>
                <input type="submit" value="登录" />
            </p>
        </fieldset>
    }
    <div>
        @Html.ActionLink("Back to List", "Index", "Home")
    </div>
    @section Scripts {
        @Scripts.Render("~/bundles/jqueryval")
    }
    复制代码

      至此,整个项目构架搭建完成,运行结果如下:
      

    在本篇中,网站的Controller是依赖于站点业务实现与核心业务实现的,在下一篇中,将使用.net 4.0自带的MEF作为IOC对层与层之间的依赖进行解耦。

    五、源码下载

      GMFrameworkForBlog.zip

      为了让大家能第一时间获取到本架构的最新代码,也为了方便我对代码的管理,本系列的源码已加入微软的开源项目网站 http://www.codeplex.com,地址为:

      https://gmframework.codeplex.com/

      可以通过下列途径获取到最新代码:

    • 如果你是本项目的参与者,可以通过VS自带的团队TFS直接连接到 https://tfs.codeplex.com:443/tfs/TFS17 获取最新代码
    • 如果你安装有SVN客户端(亲测TortoiseSVN 1.6.7可用),可以连接到 https://gmframework.svn.codeplex.com/svn 获取最新代码
    • 如果以上条件都不满足,你可以进入页面 https://gmframework.codeplex.com/SourceControl/latest 查看最新代码,也可以点击页面上的 Download 链接进行压缩包的下载,你还可以点击页面上的 History 链接获取到历史版本的源代码
      • 如果你想和大家一起学习MVC,学习EF,欢迎加入Q群:5008599(群发言仅限技术讨论,拒绝闲聊,拒绝酱油,拒绝广告)
      • 如果你想与我共同来完成这个开源项目,可以随时联系我。
  • 相关阅读:
    normalize.css 中文版
    [转载]自适应高度输入框
    【转载】H5页面列表的无线滚动加载(前端分页)
    CSS设置table下tbody滚动条与thead对齐的方法
    [转载]Jquery mobiscroll 移动设备(手机)wap日期时间选择插件以及滑动、滚动插件
    wordpress 目录、数据结构和解析原理
    WordPress基础知识:条件判断标签及用法大全
    主题如何添加tag标签页面
    WordPress进阶:[2]不同页面显示不同的侧边栏
    WordPress进阶:[1]怎样用tag标签做导航菜单
  • 原文地址:https://www.cnblogs.com/haiyabtx/p/3656418.html
Copyright © 2011-2022 走看看