标签: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"; } }
现在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 }
在这个类里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个以下参数。
标签:idt 根据 static 最好 定义 第一个 end dao 假设
原文地址:https://www.cnblogs.com/yorkness/p/14629288.html