因收到Google相关通知,网站将会择期关闭。相关通知内容 14 软件设计的单一职责原则:为什么说一个类文件打开最好不要超过一屏? 我在Intel工作期间,曾经接手过一个大数据SQL引擎的开发工作([如何自己开发一个大数据SQL引擎?])。我接手的时候,这个项目已经完成了早期的技术验证和架构设计,能够处理较为简单的标准SQL语句。后续公司打算成立一个专门的小组,开发支持完整的标准SQL语法的大数据引擎,然后进一步将这个产品商业化。 我接手后打开项目一看,吓出一身冷汗,这个项目只有几个类组成,其中最大的一个类,负责SQL语法的处理,有近万行代码。代码中充斥着大量的switch/case,if/else代码,而且方法之间互相调用,各种全局变量传递。 只有输入测试SQL语句的时候,在debug状态下才能理解每一行代码的意思。而这样的代码有1万行,现在只实现了不到10%的SQL语法特性。如果将SQL的全部语法特性都实现了,那么这个类该有多么大!逻辑有多么复杂!维护有多么困难!而且还要准备一个团队来合作开发!想想看,几个人在这样一个大文件里提交代码,想想都酸爽。 这是当时这个SQL语法处理类中的一个方法,而这样的方法有上百个。 /** * Digest all Not Op and merge into subq or normal filter semantics * After this process there should not be any NOT FB in the FB tree. */ private void digestNotOp(FilterBlockBase fb, FBPrepContext ctx) { // recursively digest the not op in a top down manner if (fb.getType() == FilterBlockBase.Type.LOGIC_NOT) { FilterBlockBase child = fb.getOnlyChild(); FilterBlockBase newOp = null; switch (child.getType()) { case LOGIC_AND: case LOGIC_OR: { // not (a and b) -> (not a) or (not b) newOp = (child.getType() == Type.LOGIC_AND) ? new OpORFilterBlock() : new OpANDFilterBlock(); FilterBlockBase lhsNot = new OpNOTFilterBlock(); FilterBlockBase rhsNot = new OpNOTFilterBlock(); lhsNot.setOnlyChild(child.getLeftChild()); rhsNot.setOnlyChild(child.getRightChild()); newOp.setLeftChild(lhsNot); newOp.setRightChild(rhsNot); break; } case LOGIC_NOT: newOp = child.getOnlyChild(); break; case SUBQ: { switch (((SubQFilterBlock) child).getOpType()) { case ALL: { ((SubQFilterBlock) child).setOpType(OPType.SOMEANY); SqlASTNode op = ((SubQFilterBlock) child).getOp(); // Note: here we directly change the original SqlASTNode revertRelationalOp(op); break; } case SOMEANY: { ((SubQFilterBlock) child).setOpType(OPType.ALL); SqlASTNode op = ((SubQFilterBlock) child).getOp(); // Note: here we directly change the original SqlASTNode revertRelationalOp(op); break; } case RELATIONAL: { SqlASTNode op = ((SubQFilterBlock) child).getOp(); // Note: here we directly change the original SqlASTNode revertRelationalOp(op); break; } case EXISTS: ((SubQFilterBlock) child).setOpType(OPType.NOTEXISTS); break; case NOTEXISTS: ((SubQFilterBlock) child).setOpType(OPType.EXISTS); break; case IN: ((SubQFilterBlock) child).setOpType(OPType.NOTIN); break; case NOTIN: ((SubQFilterBlock) child).setOpType(OPType.IN); break; case ISNULL: ((SubQFilterBlock) child).setOpType(OPType.ISNOTNULL); break; case ISNOTNULL: ((SubQFilterBlock) child).setOpType(OPType.ISNULL); break; default: // should not come here assert (false); } newOp = child; break; } case NORMAL: // we know all normal filters are either UnCorrelated or // correlated, don't have both case at present NormalFilterBlock nf = (NormalFilterBlock) child; assert (nf.getCorrelatedFilter() == null || nf.getUnCorrelatedFilter() == null); CorrelatedFilter cf = nf.getCorrelatedFilter(); UnCorrelatedFilter ucf = nf.getUnCorrelatedFilter(); // It's not likely to result in chaining SqlASTNode // as any chaining NOT FB has been collapsed from top down if (cf != null) { cf.setRawFilterExpr( SqlXlateUtil.revertFilter(cf.getRawFilterExpr(), false)); } if (ucf != null) { ucf.setRawFilterExpr( SqlXlateUtil.revertFilter(ucf.getRawFilterExpr(), false)); } newOp = child; break; default: } fb.getParent().replaceChildTree(fb, newOp); } if (fb.hasLeftChild()) { digestNotOp(fb.getLeftChild(), ctx); } if (fb.hasRightChild()) { digestNotOp(fb.getRightChild(), ctx); } } 我当时就觉得,我太难了。 单一职责原则 软件设计有两个基本准则:低耦合和高内聚。我在前面讲到过的设计原则和后面将要讲的设计模式大多数都是关于如何进行低耦合设计的。而内聚性主要研究组成一个模块或者类的内部元素的功能相关性。 设计类的时候,我们应该把强相关的元素放在一个类里,而弱相关性的元素放在类的外边。保持类的高内聚性。具体设计时应该遵循这样一个设计原则: 一个类,应该只有一个引起它变化的原因。 这就是软件设计的单一职责原则。如果一个类承担的职责太多,就等于把这些职责都耦合在一起。这种耦合会导致类很脆弱:当变化发生的时候,会引起类不必要的修改,进而导致bug出现。 职责太多,还会导致类的代码太多。一个类太大,它就很难保证满足开闭原则,如果不得不打开类文件进行修改,大堆大堆的代码呈现在屏幕上,一不小心就会引出不必要的错误。 所以关于编程有这样一个最佳实践:一个类文件打开后,最好不要超过屏幕的一屏。这样做的好处是,一方面代码少,职责单一,可以更容易地进行复用和扩展,更符合开闭原则。另一方面,阅读简单,维护方便。 一个违反单一职责原则的例子 如何判断一个类的职责是否单一,就是看这个类是否只有一个引起它变化的原因。 我们看这样一个设计: 正方形类Rectangle有两个方法,一个是绘图方法draw(),一个是计算面积方法area()。有两个应用需要依赖这个Rectangle类,一个是几何计算应用,一个是图形界面应用。 绘图的时候,程序需要计算面积,但是计算面积的时候呢,程序又不需要绘图。而在计算机屏幕上绘图又是一件非常麻烦的事情,所以需要依赖一个专门的GUI组件包。 这样就会出现一个尴尬的情形:当我需要开发一个几何计算应用程序的时候,我需要依赖Rectangle类,而Rectangle类又依赖了GUI包,一个GUI包可能有几十M甚至数百M。本来几何计算程序作为一个纯科学计算程序,主要是一些数学计算代码,现在程序打包完,却不得不把一个不相关的GUI包也打包进来。本来程序包可能只有几百K,现在变成了几百M。 Rectangle类的设计就违反了单一职责原则。Rectangle承担了两个职责,一个是几何形状的计算,一个是在屏幕上绘制图形。也就是说,Rectangle类有两个引起它变化的原因,这种不必要的耦合不仅会导致科学计算应用程序庞大,而且当图形界面应用程序不得不修改Rectangle类的时候,还得重新编译几何计算应用程序。 比较好的设计是将这两个职责分离开来,将Rectangle类拆分成两个类: 将几何面积计算方法拆分到一个独立的类GeometricRectangle,这个类负责图形面积计算area()。Rectangle只保留单一绘图职责draw(),现在绘制长方形的时候可以使用计算面积的方法,而几何计算应用程序则不需要依赖一个不相关的绘图方法以及一大堆的GUI组件。 从Web应用架构演进看单一职责原则 事实上,Web应用技术的发展、演化过程,也是一个不断进行职责分离,实现单一职责原则的过程。在十几年前,互联网应用早期的时候,业务简单,技术落后,通常是一个类负责处理一个请求处理。 以Java为例,就是一个Servlet完成一个请求处理。 这种技术方案有一个比较大的问题是,请求处理以及响应的全部操作都在Servlet里,Servlet获取请求数据,进行逻辑处理,访问数据库,得到处理结果,根据处理结果构造返回的HTML。这些职责全部都在一个类里完成,特别是输出HTML,需要在Servlet中一行一行输出HTML字符串,类似这样: response.getWriter().println("