码迷,mamicode.com
首页 > 其他好文 > 详细

编写可维护软件的不朽代码随想-5

时间:2021-04-26 14:00:52      阅读:0      评论:0      收藏:0      [点我收藏+]

标签:idt   根据   static   最好   定义   第一个   end   dao   假设   

保持代码单元的接口简单

限制每个代码单元的参数不能超过4将多个参数提取成对象

为了保持代码的可维护性,需要限制参数的个数,避免使用过多的参数(也称为代码单元接口

之前的JPacman项目中,BoardPanel类的render方法,拥有许多参数的典型,此方法在一个由x,y,w,h表示的矩形中绘制一个方块以及方块的占有者(例如表示一个幽灵或一个豆丸)

/// <summary>
        /// 在一个指定的矩形上绘制一个方块
        /// </summary>
        /// <param name="squere">要绘制的方块</param>
        /// <param name="g">需要进行绘制的图形上下文</param>
        /// <param name="x">开始绘制的x位置</param>
        /// <param name="y">开始绘制的y位置</param>
        /// <param name="w">方块的宽度</param>
        /// <param name="h">方块的高度</param>
        private void Render(Square squere, Graphics g, int x, int y, int w, int h)
        {
            squere.Sprite.Draw(g, x, y, w, h);
            foreach(Unit unit in squere.Occupants)
            {
                uint.Sprite.Draw(g, x, y, w, h);
            }
        }

方法超过了4个参数,由于后四个参数x,y,w,h都是相关联的,并且Render方法没有单独去操作每个变量,所以应将它们封装为一个对象,下面代码展示了重构的Render方法

public class Point
    {
        public int X { get; set; }
        public int Y { get; set; }
    }
    public class Rectangle
    {
        public Point Position { get; set; }
        public int Width { get; set; }
        public int Height { get; set; }

        public Rectangle(Point position, int width, int height)
        {
            this.Position = position;
            this.Width = width;
            this.Height = height;
        }
    }
private void Render(Square squere, Graphics g, Rectangle r)
        {
            Point position = r.Position;
            squere.Sprite.Draw(g, position.X, position.Y, r.Width, r.Height);
            foreach (Unit unit in squere.Occupants)
            {
                uint.Sprite.Draw(g, position.X, position.Y, r.Width, r.Height);
            }
        }

相比之前的6个参数,限制render方法只有3个参数,进一步简化Draw方法的接口参数

private void Render(Square squere, Graphics g, Rectangle r)
    {
        squere.Sprite.Draw(g, r);
        foreach (Unit unit in squere.Occupants)
        {
            uint.Sprite.Draw(g, r);
        }
    }

接口参数较少的方法能够保持简单的上下文,更容易被人理解。并不过于依赖来自外部的输入,也更易于重用和修改。

短接口更易于理解和重用:随着代码库规模的增长,核心类会逐渐成为其他代码所依赖的API,为了避免代码量迅速膨胀以及开发速度下降,核心类必须有一个清晰、短的接口定义,
假设想在数据库中存储一个ProductOrder对象,你是喜欢这个ProductOrderDao.store(ProductOrder order),还是喜欢这个ProductOrderDao.store(ProductOrder order,string databaseUser,string databaseName, bool validateBeforeStore, bool closeDbConnection)

短接口的方法更易于修改:过长的接口不仅让方法变得难以理解,还表示它承担着多重的职责(尤其是当你感觉真的无法将这些对象组合在一起的时候)。接口长短与代码单元大小和复杂度有关,如果你的某个方法有8个参数,方法内部有很多代码,你就很难将它拆分成多个独立的部分,但是一旦拆分成功,这几个方法就能够各司其职,并且每个方法只有很少的几个参数,这样可以很容易定位到需要修改的代码。

根据SIG经验,参数个数上限为4比较合理,在演示如何将接口从长变短之前,应该清楚,过长的接口本身不是问题,代码中可能存在设计不合理的数据模型

假设有9个参数的方法用来构造并发送一封电子邮件:

public void DoBuildAndSendMail(MailMan m,string firstName, string lastName, string division,string subject, MailFont font, string message1, string message2, string message3)
    {
        //格式化电子邮件地址
        string mId = $"{firstName[0]}.{lastName.Substring(0, 7)}" + $"@{division.Substring(0, 5)}.coma.ny";
        //根据指定的内容类型和原始消息进行格式化
        MailMessage mMessage = FormatMessage(font, message1 + message2 + message3);
        //发送消息
        m.Send(mId, subject, mMessage);
    }

这个方法拥有太多的职责,生成邮件地址与发送具体邮件没有关系,重构后的代码如下:

技术图片
public void DoBuildAndSendMail(MailMan m, MailAddress mAddress, MailBody mBody)
    {
        //创建电子邮件
        Mail mail = new Mail(mAddress, mBody);
        //发送消息
        m.Send(mail);
    }
    public class Mail
    {
        public MailAddress Address { get; set; }
        public MailBody Body { get; set; }
        public Mail(MailAddress mAddress, MailBody mBody)
        {
            this.Address = mAddress;
            this.Body = mBody;
        }
    }
    public class MailBody
    {
        public string Subject { get; set; }
        public MailMessage Message { get; set; }
        public MailBody(string subject, MailMessage message)
        {
            this.Subject = subject;
            this.Message = message;
        }
    }
    public class MailAddress
    {
        public string MsgId { get; set; }
        public MailAddress(string firstName, string lastName, string division)
        {
            this.MsgId = $"{firstName[0]}.{lastName.Substring(0, 7)}" + $"@{division.Substring(0, 5)}.coma.ny";
        }
    }
View Code

现在DoBuildAndSendMail方法的复杂度降低了很多,这里的程序都将参数包装成了对象,通常被称为数据传输对象(DTO)

这些实际上代表了对应的领域对象,一个点,一个长度和宽度表示了一个矩形,同样一个姓,一个名,一个地区表示了一个地址,抽取成了名为MailAddress的类。

封装这些类不只是减少方法参数的数量,还因为它们都是对实际领域对象的通用抽象,在代码中被频繁地重用。

 

如果方法的各个参数无法组合在一起,仍然可以封装成一个类,但是这个类可能只能使用一次,例如,正在创建一个可以在Drawing.Graphics画布上绘制图表的库,柱形图和饼图,需要绘制的区域大小、横纵轴的配置信息,以及实际的数据集等,一种提供这些信息的方式如下:

public static void DrawBarChart(Graphics g, 
        CategoryItemRendererState state, 
        Rectangle graphArea, 
        CategoryPlot plot, 
        CategoryAxis domainAxis, 
        ValueAxis rangeAxis, 
        CategoryDataset dataset)
    {
        //..
    }

这个方法已经有7个参数,对此方法的调用都需要提供这7个参数,如果希望图表库提供默认值,一种实现方式是重载方法,定义一个只有2个参数的DrawBarChart方法

public static void DrawBarChart(Graphics g, CategoryDataset dataset)
    {
        Charts.DrawBarChart(g, 
            CategoryItemRendererState.DEFAULT, 
            new Rectangle(new Point(0, 0), 100, 100), 
            CategoryPlot.DEFAULT, 
            CategoryAxis.DEFAULT, 
            ValueAxis.DEFAULT, 
            dataset);
    }

但是还是存在着7个参数的方法,另一种解决方式是使用之前提到过的“使用方法对象替换方法”,首先定义一个BarChart类

技术图片
public class BarChart
    {
        private CategoryItemRendererState state = CategoryItemRendererState.DEFAULT;
        private Rectangle graphArea = new Rectangle(new Point(0, 0), 100, 100);
        private CategoryPlot plot = CategoryPlot.DEFAULT;
        private CategoryAxis domainAxis = CategoryAxis.DEFAULT;
        private ValueAxis rangeAxis = ValueAxis.DEFAULT;
        private CategoryDataset dataset = CategoryDataset.DEFAULT;

        public BarChart Draw(Graphics g)
        {
            //..
            return this;
        }

        public ValueAxis GetValueAxis()
        {
            return rangeAxis;
        }
        public BarChart SetRangeAxis(ValueAxis rangeAxis)
        {
            this.rangeAxis = rangeAxis;
            return this;
        }
        //更多的getter和setter
    }
View Code

在这个类里DrawBarChart被替换成了Draw,原先的7个参数就只剩下了1个,其余6个都转换为了类的私有成员,且有默认值。所有的setter方法都返回this,从而可以创建一个流式的接口,一种级联方式来进行调用,比如:

private void ShowMyBarChart()
    {
        Graphics g = this.CreateGraphics();
        BarChart b = new BarChart().SetRangeAxis(myValueAxis).SetDateset(myDataset).Draw(g);
    }

 

常见反对意见:

构造参数对象过于复杂:如果一切顺利,你已经在重构过长接口的过程中,把一组参数封装到了一个对象中,这里可能会出现该对象拥有很多参数的情况,通常意味着需要在对象内部进行更细粒度的划分。拿第一个例子Rectangle的重构来说,虽然定义矩形的参数都已经被组合到一起,但我们没有采用一个四个参数的构造函数,而是将x和y封装到了一个Point对象中,因此,你应当引入另一个参数对象,更应该思考一下这个对象的结构以及它与其他代码的关系。

重构长接口并没有带来任何改善:想要避免长接口不是一件容易的事,都不是仅靠重构方法就能解决,应该持续取分离方法中的各个职责,只在需要时去访问最主要的几个参数,比如,Render方法经过重构后,由于Draw方法的原因,需要访问Rectangle对象中的所有参数,当然你也可以对Draw方法进行重构,在方法体访问x,y,w,h参数,这样也许会更好,你不需要在开始绘制前去实际操纵他的类成员变量,只需向Render方法传递一个Rectangle对象就可以了。

一些框架或库已经规定了长参数列表的接口:实现或者重载这些方法会让你的代码中也出现长列表参数的方法,无法避免,但是可以限制它们的影响,最好的办法是使用包装类或适配器类对它们进行隔离。

 

SIG评估代码单元的接口程度,根据系统中每个代码单元的参数数量,划分4个风险分类,超过7个参数,5个以上参数,3个以上参数,2个以下参数。

 

编写可维护软件的不朽代码随想-5

标签:idt   根据   static   最好   定义   第一个   end   dao   假设   

原文地址:https://www.cnblogs.com/yorkness/p/14629288.html

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