引言
本文是新开设的MSDN软件设计基础专栏的第一篇文章。我的目的是以不局限于某种特定工具或者某个(软件工程)周期方法(lifecycle methodology)的方式来讨论设计的模式和原则。换言之,我计划讨论一些可以引导你使用任何技术,或者在任何项目中更好地进行设计的基础知识。
我喜欢以讨论开闭原则和其他由 Robert C.Martin 在其著作《敏捷软件开发,原则,模式和实践》中所倡导的相关主题作为开始。不要因为在标题中出现“敏捷”一词就把书合上了,因为这本书实际上完全是关于如何竭力进行优良软件设计的。
问下你自己:有多少次你是从零开始去写一个全新的应用程序?又有多少次你是通过将新功能添加到现有代码库(codebase)中来作为开始?恐怕大多数的情况下,你是花费了更多的时间将新功能添加到现有代码库中吧。
然后再问自己另一个问题:写全新的代码容易还是对现有代码进行修改容易?通常对我来说写全新的方法和类要比深入旧代码中,找出我想要修改的部分容易得多。修改旧有代码增添了破坏已有功能的风险。对于新代码来说,你通常只需要测试下新实现的功能就可以了。而当你修改旧有代码时,你不得不既要测试你更改的部分,还要进行一系列的兼容测试,以保证你没有破坏任何的旧有代码。
所以,你通常基于现有的代码库进行工作,可是写全新的代码又比修改旧的代码容易得多。你难道不想像写全新代码一样多产、轻松地去对现有的代码库进行扩展么?这就是开闭原则一展身手的地方了。我来解释一下开闭原则,它的意思是:软件实体应该对于扩展是开放的,而对于修改是关闭的。
从字面上看这好像是矛盾的,实际并非如此。它的全部含义就是你应该这样去构建一个应用程序:可以在对现有代码做最小修改的同时添加新的功能。我曾经认为开闭原则仅仅是意味着使用插件(plugins),但并不是这么简单。
你应该避免一个小小的改动就波及了你应用程序中的多个类。这样会使程序更加脆弱,更倾向于产生向下兼容的问题,并使扩展付出更高的代价。为了隔离变化,你会想要以一种一旦写好了就再也不需要修改的方式去写类和方法。
然而你如何构建代码以实现隔离变化呢?我想说的第一步就是遵循单一责任原则。
单一责任原则
在遵循开闭原则的过程中,我期望能够写出一个类或者方法,在以后我回过头读它的时候,会很舒服地看到它能完成它的工作并且我也不需要再修改它。你永远也达不到真正的开闭天堂,但是通过严格地遵循与之相关的单一责任原则:一个类应该有并且只有一个更改的理由,你可以非常靠近地接近它。
写那些永远也不需要进行修改的类的最简单方法就是写一些只能做一件事情的类。通过这种方式,一个类只有在它所确切负责的那件事更改时它才需要更改。代码1演示了没有遵循单一责任原则的一个例子。我真的怀疑你正在像这样设计一个系统,但是最好记得为什么我们不应该这样去构建代码。
代码1. 这个类负责了太多的事
1 public class OrderProcessingModule { 2 public void Process(OrderStatusMessage orderStatusMessage) { 3 // 从配置文件中读取连接字符串 4 string connectionString = 5 ConfigurationManager.ConnectionStrings["Main"].ConnectionString; 6 7 Order order = null; 8 9 using (SqlConnection connection = 10 new SqlConnection(connectionString)) { 11 // 从数据库中获取一些数据 12 order = fetchData(orderStatusMessage, connection); 13 } 14 15 // 向来自于OrderStatusMessage的订单提交变更 16 updateTheOrder(order); 17 18 // 国际订单有一些特定的规则 19 if (order.IsInternational) { 20 processInternationalOrder(order); 21 } 22 23 // 对于大批量订单我们需要特别处理 24 else if (order.LineItems.Count > 10) { 25 processLargeDomesticOrder(order); 26 } 27 // 小的国内订单也需要区别处理 28 else { 29 processRegularDomesticOrder(order); 30 } 31 32 // 如果订单准备好了就发货 33 if (order.IsReadyToShip()) { 34 ShippingGateway gateway = new ShippingGateway(); 35 36 // 将订单对象提交运送 37 ShipmentMessage message = createShipmentMessageForOrder(order); 38 gateway.SendShipment(message); 39 } 40 }