码迷,mamicode.com
首页 > Web开发 > 详细

记一次.NET代码重构

时间:2016-12-19 11:38:40      阅读:185      评论:0      收藏:0      [点我收藏+]

标签:发送   jsp   吐槽   表示   created   访问   接下来   wget   ppp   

    好久没写代码了,终于好不容易接到了开发任务,一看时间还挺充足的,我就慢慢整吧,若是遇上赶进度,基本上直接是功能优先,完全不考虑设计。你可以认为我完全没有追求,当身后有鞭子使劲赶的时候,神马设计都是浮云,按时上线才是王道,毕竟领导是不会关注过程和代码质量的,领导只看结果,这也许就是国内码农的悲哀。

    需求:是这样的,要开发一个短信发送的模板,不同客户可能会使用不同的模板,而不同的客户使用的变量参数也是不同的。之前为了应急,线上已经完成了一个短信模板发送短信的功能,短信模板表也创建了,而且在表中已经新增了一条记录。我只需要做一个短信模板的增删改查界面就可以了,看上去我的任务挺简单的,老司机应该知道,接了个烂摊子。

    下图所示是原来已经创建好了的表

技术分享

   SQL创建脚本如下:

SET ANSI_NULLS ON
GO

SET QUOTED_IDENTIFIER ON
GO

CREATE TABLE [dbo].[MessageModule](
    [Id] [uniqueidentifier] NOT NULL,
    [Name] [nvarchar](50) NULL,
    [Type] [nvarchar](50) NULL,
    [TypeNo] [nvarchar](50) NULL,
    [Channel] [nvarchar](50) NULL,
    [Param] [nvarchar](50) NULL,
    [Content] [nvarchar](max) NULL,
    [CreatedBy] [uniqueidentifier] NULL,
    [CreatedOn] [datetime] NULL,
    [ModifiedBy] [uniqueidentifier] NULL,
    [ModifiedOn] [datetime] NULL,
    [IsDeleted] [bit] NULL,
    [TypeId] [uniqueidentifier] NULL
) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY]

GO

    在这之前是已经开发了一个发送短信的API接口供客户调用了的,也就是说调用方(客户),不会修改代码,只能我这边来修改。虽然极不情愿接做了一半的任务,但是没办法,不可能给你的开发任务都是从头开始的。

实体类代码如下:

     [Table("dbo.MessageModule")]
    public class MessageModule : DTO
    {
        public string Type { get; set; } //业务类型
        public string TypeNo { get; set; } //业务编号
        public string Channel { get; set; } //使用渠道
        public string Name { get; set; } //名称模版
        public string Content { get; set; } //短信内容
    }

    DOT类:

    public class DTO
    {
        public virtual Guid Id { get; set; }
        public virtual DateTime? CreatedOn { get; set; }
        public virtual Guid? CreatedBy { get; set; }
        public virtual DateTime? ModifiedOn { get; set; }
        public virtual Guid? ModifiedBy { get; set; }
        public virtual bool IsDeleted { get; set; }
    }

    这是之前的代码,业务实体类MessageModuleBusiness.cs代码如下:

    public class MessageModuleBusiness : GenericRepository<Model.MessageModule>, IMessageModuleBusiness
    {
        private UnitOfWork.UnitOfWork unitOfWork = new UnitOfWork.UnitOfWork();

        #region old code
        /// <summary>
        /// 获取模版内容
        /// </summary>
        /// <param name="crowd"></param>
        /// <returns></returns>
        public string GetContent(MessageContext messageContext)
        {
            string messageContent = "";
            string TypeCode = string.IsNullOrEmpty(messageContext.serviceCode) ? "001" : messageContext.serviceCode;
            try
            {
                var Module = unitOfWork.MessageModule.Get(c => c.Type == messageContext.channel && c.TypeNo == TypeCode).FirstOrDefault();
//Content的内容:【一应生活】您有一件单号为expressNumbers company,已到communityName收发室,请打开一应生活APP“收发室”获取取件码进行取件。点击下载http://a.app.qq.com/o/simple.jsp?pkgname=com.ening.life
if (!string.IsNullOrEmpty(Module.Content)) { var content = Module.Content; content = content.Replace("company", messageContext.company); content = content.Replace("expressNumbers", messageContext.expressNumbers); content = content.Replace("communityName", messageContext.communityName); content = content.Replace("Id", messageContext.Id); content = content.Replace("receiveTime", messageContext.receiveTime); content = content.Replace("fetchCode", messageContext.fetchCode); messageContent = content; } return messageContent; } catch (Exception ex) { } return ""; } #endregion }

    MessageContext类,这个是客户端传输过来调用的一个实体对象。对象里面存在许多类似于短信的动态标签变量。

    public class MessageContext
    {
        /// <summary>
        /// 手机号码
        /// </summary>
        public string phone { get; set; }

        /// <summary>
        /// 发送信息
        /// </summary>
        public string message { get; set; }

        /// <summary>
        /// 签名
        /// </summary>
        public string sign { get; set; }

        /// <summary>
        /// 渠道
        /// </summary>
        public string channel { get; set; }

        /// <summary>
        /// 内容
        /// </summary>
        public string content { get; set; }
        /// <summary>
        /// 取件码
        /// </summary>
        public string fetchCode { get; set; }
        /// <summary>
        /// 快递公司
        /// </summary>
        public string company { get; set; }
        /// <summary>
        /// 快递单号
        /// </summary>
        public string expressNumbers { get; set; }
        /// <summary>
        /// 社区名称
        /// </summary>
        public string communityName { get; set; }
        /// <summary>
        /// 到件时间
        /// </summary>
        public string receiveTime { get; set; }
        /// <summary>
        /// 序号
        /// </summary>
        public string Id { get; set; }
        /// <summary>
        /// 业务代码
        /// </summary>
        public string serviceCode { get; set; }
    }

    控制器方法externalMerchantSendMessage,这是供外部调用的

        /// <summary>
        /// 外部商户发送信息
        /// </summary>
        /// <returns></returns>
        public ActionResult externalMerchantSendMessage(MessageContext param)
        {
            logger.Info("[externalMerchantSendMessage]param:" + param);
            bool isAuth = authModelBusiness.isAuth(param.channel, param.phone, param.sign);
            if (!isAuth)
            {
                return Json(new Result<string>()
                {
                    resultCode = ((int)ResultCode.NoPermission).ToString(),
                    resultMsg = "签名或无权限访问"
                }, JsonRequestBehavior.AllowGet);
            }


            var meaage = messageModuleBusiness.GetContent(param);

            if (string.IsNullOrEmpty(meaage))
            {
                return Json(new Result<string>()
                {
                    resultCode = ((int)ResultCode.failure).ToString(),
                    resultMsg = "发送失败"
                }, JsonRequestBehavior.AllowGet);
            }

            SMSHelper helper = new SMSHelper();
            helper.SendSMS(meaage, param.phone);
            return Json(new Result<string>()
            {
                resultCode = ((int)ResultCode.success).ToString(),
                resultMsg = "发送成功"
            }, JsonRequestBehavior.AllowGet);

        }

    以上是我接收开发任务之前已经实现了的功能。看上去我的任务挺简单的,可是多年的开发经验告诉我,这里需要重构,如果我现在啥都不管,就只管做一个短信模板的正删改查界面的话,后面维护的人一定会抓狂。

    看出什么问题没有?

    这个接口方法externalMerchantSendMessage是给所有客户调用,而不同客户使用不同的短信模板,不同的模板,又存在不同的变量参数。而现在所有的变量参数都封装在了类MessageContext中,问题是我们无法一下子把所有的变量参数全部确定下来,并保持不变。那么,也就是说一旦需要添加变量参数,类MessageContext中的代码就必须修改,而且GetContent方法中的代码是硬编的,一样需要跟着修改。这样就形成了一个循环,不断加变量参数,不断改代码,不断发布接口版本.......

    时间充裕的情况下,我自然是一个有节操的程序猿,那么就开始重构吧。

    在重构之前,在脑海浮现的并不是各种设计模式,而是面向对象设计的基本原则。各种设计模式就好比各种武学套路或者招式,习武之人应该像张无忌练习太极剑一样,先学会各种套路,然后忘记所有套路,从而融会贯通。因为招式是死的,人是活得,有招就有破绽,根本没有必胜招式存在,就好像没有万能的设计模式一样,任何设计模式都存在缺点。

    面向对象设计的核心思想就是封装变化,那么先找出变化点。从上面的分析中,我们已经发现了变化点,那就是短信模板中的变量参数,而这些变量参数都是客户调用方传过来的,不同客户传递的参数变量又可能是不一样的。我们先来看一下,客户传递过来的是什么?我们看下客户调用代码:

        function sendMsg() {
            var appParam ="phone=15914070649&sign=78a7ce797cf757916c2c7675b6865b54&channel=weijiakeji&content=&fetchCode=1&company=%E9%A1%BA%E4%B8%B0%E5%BF%AB%E9%80%92&expressNumbers=123456
&communityName=%E9%95%BF%E5%9F%8E%E4%B8%80%E8%8A%B1%E5%9B%AD&receiveTime=5&Id=1231"; Get("/Message/externalMerchantSendMessage?" + appParam, {}); }

    可见客户传递的是一个键值对集合,就是一个JSON格式的对象。根据前面的代码 bool isAuth = authModelBusiness.isAuth(param.channel, param.phone, param.sign);,可以分析出有三个参数是所有调用客户都必须传递过来的,那就是:channel,phone,sign,而其它的参数就是短信模板的变量参数和参数值。那么方法externalMerchantSendMessage(MessageContext param)中的参数就是一个可变对象。在C#4.0种存在一个dynamic不正式用来描述可变对象吗?

那么第一步修改传入参数类型:

 public ActionResult externalMerchantSendMessage(dynamic param)

接下来是GetContent方法,此方法的目的很简单,就是要根据客户传递的模板变量参数键值对和短信模板内容,拼装成最后的短信发送内容,之前此方法里面是硬编码的,现在我们需要变成动态获取。

短信模板的内容示例:

【一应生活】您有一件单号为expressNumbers company,已到communityName收发室,请打开一应生活APP“收发室”获取取件码进行取件。点击下载http://a.app.qq.com/o/simple.jsp?pkgname=com.ening.life

我发现这样的模板内容有问题,模板中的变量参数是直接用的英文单词表示的,而我们的短信内容中可能有时候也会存在英文单词,那么我就给所有的变量参数加上{}。修改后如下:

【一应生活】您有一件单号为{expressNumbers} {company},已到{communityName}收发室,请打开一应生活APP“收发室”获取取件码进行取件。点击下载http://a.app.qq.com/o/simple.jsp?pkgname=com.ening.life

我们需要根据客户传递过来的对象,将短信模板中的变量参数,替换成变量参数对应的值。那么我们首先就要解析这个对象中的键值对信息。

        /// <summary>
        /// 把object对象的属性反射获取到字典列表中
        /// </summary>
        /// <param name="data">object对象</param>
        /// <returns>返回Dictionary(属性名,属性值)列表</returns>
         static Dictionary<string, string> GetProperties(object data)
        {
            Dictionary<string, string> dict = new Dictionary<string, string>();

            Type type = data.GetType();
            string[] propertyNames = type.GetProperties().Select(p => p.Name).ToArray();
            foreach (var prop in propertyNames)
            {
                object propValue = type.GetProperty(prop).GetValue(data, null);
                string value = (propValue != null) ? propValue.ToString() : "";
                if (!dict.ContainsKey(prop))
                {
                    dict.Add(prop, value);
                }
            }
            return dict;
        }

接下来是通过正则表达式来匹配短信模板内容。

        /// <summary>
        /// 多个匹配内容
        /// </summary>
        /// <param name="sInput">输入内容</param>
        /// <param name="sRegex">表达式字符串</param>
        /// <param name="sGroupName">分组名, ""代表不分组</param>
        static List<string> GetList(string sInput, string sRegex, string sGroupName)
        {
            List<string> list = new List<string>();
            Regex re = new Regex(sRegex, RegexOptions.IgnoreCase | RegexOptions.IgnorePatternWhitespace | RegexOptions.Multiline);
            MatchCollection mcs = re.Matches(sInput);
            foreach (Match mc in mcs)
            {
                if (sGroupName != "")
                {
                    list.Add(mc.Groups[sGroupName].Value);
                }
                else
                {
                    list.Add(mc.Value);
                }
            }
            return list;
        }
        public static string ReplaceTemplate(string template, object data)
        {
            var regex = @"\{(?<name>.*?)\}";
            List<string> itemList = GetList(template, regex, "name"); //获取模板变量对象

            Dictionary<string, string> dict = GetProperties(data);
            foreach (string item in itemList)
            {
                //如果属性存在,则替换模板,并修改模板值
                if (dict.ContainsKey(item))
                {
                    template = template.Replace("{"+item+"}", dict.First(x => x.Key == item).Value);
                }
            }

            return template;
        }

这样就讲客户传递的对象和我们的解析代码进行了解耦,客户传递的对象不再依赖于我们的代码实现,而是依赖于我们数据表中模板内容的配置。

这几个方法我是写好了,顺便弄个单元测试来验证一下是不是我要的效果,可怜的是,这个项目中根本就没用到单元测试,没办法,我自己创建一个单元测试

    [TestClass]
    public class MatchHelperTest
    {
        [TestMethod]
        public void ReplaceTemplate()
        {
            //模板文本
            var template = "【一应生活】您有一件单号为{expressNumbers} {company},已到{communityName}收发室,请打开一应生活APP“收发室”获取取件码进行取件。点击下载http://a.app.qq.com/o/simple.jsp?pkgname=com.ening.life";
            //数据对象
            var data = new { expressNumbers = "2016", company = "长城", communityName = "长怡花园"};
            string str = "【一应生活】您有一件单号为2016 长城,已到长怡花园收发室,请打开一应生活APP“收发室”获取取件码进行取件。点击下载http://a.app.qq.com/o/simple.jsp?pkgname=com.ening.life";
            string str1=MatchHelper.ReplaceTemplate(template, data);

            Assert.AreEqual(str1,str);

            //重复标签的测试
            template = "【一应生活】您有一件单号为{expressNumbers} {company},已到{communityName}收发室,单号:{expressNumbers}";
            str = "【一应生活】您有一件单号为2016 长城,已到长怡花园收发室,单号:2016";
            str1=MatchHelper.ReplaceTemplate(template, data);
            Assert.AreEqual(str1, str);
        }
    }

说到单元测试,我相信在许多公司都没有用起来,理由太多。我也觉得如果业务简单的话,根本没必要写单元测试,国内太多创业型公司项目进度都非常赶,如果说写单元测试不费时间,那绝对是骗人的,至于说写单元测试能提高开发效率,减少返工率,个人感觉这个还真难说,因为即便不写单元测试也还是可以通过许多其它手段来弥补的,个人观点,勿喷。

接下来修改GetContent方法如下:

        public string GetContent(dynamic messageContext)
        {
            string strMsg = "";
            string TypeCode = string.IsNullOrEmpty(messageContext.serviceCode) ? "001" : messageContext.serviceCode;
            string channel = messageContext.channel;
            try
            {
                var Module = unitOfWork.MessageModule.Get(c => c.Type == channel && c.TypeNo == TypeCode).FirstOrDefault();
                if (!string.IsNullOrEmpty(Module.Content))
                {
                    var content = Module.Content;
                    strMsg = MatchHelper.ReplaceTemplate(content, messageContext);
                }

                return strMsg;
            }
            catch (Exception ex)
            {
                strMsg = ex.Message;
            }
            return strMsg;
        } 

(话外:先吐槽一下之前这个变量命名,MessageContext messageContext 和string messageContent,长得太像了,一开始我重构的时候害我弄错了,建议不要在同一个方法中使用相似的变量名称,以免弄混淆。妈蛋,老司机的我又被坑了,愤怒,无可忍受,果断重命名。)

原来控制器调用业务逻辑代码是直接这样的

MessageModuleBusiness messageModuleBusiness = new MessageModuleBusiness()

依赖于具体类的实现,而我们知道,具体是不稳定的,抽象才是稳定的,我们应该面向接口编程。今天是发送短信,明天可能就是发邮件,又或者要加日志记录等等等。

    public interface IMessageModuleBusiness
    {
        /// <summary>
        /// 组装消息内容
        /// </summary>
        /// <param name="messageContext">动态参数对象</param>
        /// <returns>组装后的消息内容</returns>
        string GetContent(dynamic messageContext);
    }

然后调用的代码修改为:

   private IMessageModuleBusiness messageModuleBusiness = new MessageModuleBusiness();

这样的话,即便日后通过反射或者IOC来再次解耦也方便。

好了,通过这样一步一步的重构,在不修改原有表结构和不影响客户调用的情况下,我已经将变化点进行了封装,当客户的模板参数变量变化的时候,再也不需要变更代码,只需要修改表中的模板内容就可以了。

 技术分享

重构时,画类图是一个非常耗的习惯,代码结构一目了然,这里我附上类图。

记一次.NET代码重构

标签:发送   jsp   吐槽   表示   created   访问   接下来   wget   ppp   

原文地址:http://www.cnblogs.com/jiekzou/p/6187932.html

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!