从一些老生常谈的事情开始说起来吧,优秀的代码应符合以下特质:
1,可维护性
2,可扩展性
3,模块化
如果代码在其生命周期内保持易于维护、扩展和模块化,那么就上面列出的特性而言,这意味着代码高于平均水平。
下面所示的代码示例更接近Java和C#,但它们对于面向对象编程领域的任何开发者都是有帮助的。
以下是完整的原则列表:
1.Single Responsibility Principle (SOLID) 单一责任性原则
2. High Cohesion (GRASP) 高内聚
3. Low Coupling (GRASP) 低耦合
4. Open Closed Principle (SOLID) 开闭原则
5. Liskov Substitution principle (SOLID) 里氏替换原则
6. Interface Segregation Principle (SOLID) 接口分离原则
7. Dependency Inversion Principle (SOLID) 依赖倒置原则
8. Program to an Interface, not to an Implementation 面向接口编程
9. Hollywood Principle 好莱坞原则
10. Polymorphism (GRASP) 多态原则
11. Information Expert (GRASP) 信息专家模式
12. Creator (GRASP) 创造者原则
13. Pure Fabrication (GRASP) 纯虚构原则
14. Controller (GRASP) 控制器原则
15. Favor composition over inheritance 组合优于继承
16. Indirection (GRASP) 间接原则
单一责任原则
单一责任原则(SRP)规定:
每个类应该只负责一种单一的功能
一个类使用其函数或契约(以及数据成员帮助函数)来履行其职责。
我们来看下面的这个类:
这个类处理两个职责。第一类是加载仿真数据,第二类是执行仿真算法(使用Simulate和ConvertParams函数)。
类使用一个或多个函数履行职责。在上面的示例中,加载模拟数据是一种责任,执行模拟是另一种责任。加载模拟数据(即LoadSimulationFile)需要一个函数。剩下的两个功能需要执行模拟。
那么如何分辨自己的类有哪些功能呢考功能的定义短语为“改变的原因”。因此,寻找一个类改变的所有原因,如果有一个以上的理由需要改动这个类,那么这意味着这个类并没有遵守单一功能原则
上面的示例中,这个类不应该包含LoadSimulationFile函数(或者装载仿真数据的功能)。如果我们创建一个单独的类来加载模拟数据,那么这个类就不再违反SRP原则了。
一个类只能有一个功能,那么在设计软件的时候我们如何去遵守这个严格的规则/span>
让我们来考虑一下另一个与SRP密切相关的原则:高内聚性。高内聚力会给你一个主观的尺度,而不是客观的尺度,就像SRP那样。非常低的内聚力意味着一个类要履行许多职责。例如,一个类负责的职责超过10个。低内聚意味着一个类要履行5项职责,中等内聚意味着一个类要履行3项职责。高内聚意味着履行一个单一的责任。因此,设计时的经验法则是力求高内聚。
另一个需要在这里讨论的原则是低耦合。这个原则表明一个类应该独立完成特定的功能,使得类之间保持低依赖性。再次审视上面的示例类,在应用SRP和高内聚规则之后,我们决定创建一个独立的类来处理模拟数据文件。这样,我们就创建了两个互相依赖的类。
看起来采用高内聚似乎和低耦合原则相抵触了。因为原则是最小化耦合,并不是使耦合为零,因此这种程度的耦合是可以接受的。对于创建一个通过对象之间协作完成任务的面向对象的程序设计来说,一定程度的耦合是正常的。
另一方面,考虑一个链接数据库的GUI类,通过HTTP协议链接远程客户端并处理屏幕布局。这个GUI类依赖了太多的类,很明显违反了低耦合原则。如果不包含所有的相关类则该类不能被重用,任何对数据库组件的改变都将改变这个GUI类。
开闭原则
开闭原则描述为:
一个软件模块(类或者方法)应该对拓展开放而对修改关闭
换句话说,不能更新已经为项目编写的代码,但可以向项目添加新代码。
以下则是使用继承应用开放原则的示例:
在这个示例中,客户端读取(ds.Read())来自于 络数据流。如果我想要扩展这个客户端的功能使之能够读取其他数据流的内容,例如PCI数据流,那么我需要添加另外继承自DataStream的子类,如下所示:
在这种情况下,客户端代码的运行没有任何错误。客户端认识基类,因此可以传递DataStream两个子类中的任何一个的对象,这样,客户端可以在未知子类的情况下读取数据。这是在不修改任何现有代码的情况下实现的。
然而,你必须将这个原则应用于每一段代码吗然不是了,因为大部分的代码其实是不怎么变动的,你只需要战略性的将这个原则应用到那些你预计将来会有变动的代码片上即可。
里氏替换原则
Liskov替代原则指出:
子类应当可以替换父类并出现在父类能够出现的任何地方
查看此定义的另一种方法是抽象类(接口或抽象类)对于客户端应该足够了。
为了详细说明,让我们考虑一个例子,如下:
这个例子是数据采集装置的抽象。数据采集装置按其接口类型不同而区分,它能够使用USB接口, 络接口(TCP 或者 UDP),PIC接口或者另外的计算机接口。然而,客户端设备不需要知道与其链接的是何种数据采集装置。为了在不改变客户端代码的情况下适应新的采集装置,这就需要程序员给接口提供极大的灵活性。
让我们回顾一下实现IDevice接口的两个具体类的历史,如下所示:
这三个方法(打开、读取和关闭)对于处理设备传入的数据已经足够了。然后,假设需要添加基于USB接口的另一个数据采集设备。
USB设备的问题在于,当你打开连接时,来自先前连接的数据仍保留在缓冲区中。因此,在对USB设备的第一次读取调用时,返回来自上一个会话的数据。有针对性的采集行为会破坏这些数据。幸运的是,基于USB的设备驱动程序提供刷新功能,预先清除了基于USB的采集设备中的缓冲区。那么如何在代码中实现这个功能,并使代码的改动最小/span>
一个简单但是草率的解决方案是更新代码,通过标识识别是否调用USB设备,如下:
在这个解决方案中,客户端代码直接使用具体类以及接口(或抽象)。这意味着抽象不能够让客户履行其职责。
另一种陈述方式是基类无法满足需求(刷新操作),但是子类可以,实际上,子类有该项行为。因此,派生类和基类不兼容且子类不能被代替。所以,该解决方案违反了里氏替换原则。
下面这个示例中,客户端依赖于更多的实体(iDevices 和 USB Devices),一个实体的任何一点改变都将影响其他实体。因此,违反LSP原则将导致类之间的互相依赖。
下面是遵循LSP这个问题的解决方案:
声明:本站部分文章及图片源自用户投稿,如本站任何资料有侵权请您尽早请联系jinwei@zod.com.cn进行处理,非常感谢!