面向对象编程的十大原则
众所周知,面向对象的设计原则(Object-Oriented Design Principles)是面向对象编程(OOP)的核心。但是,如今有许多Java程序员在追求诸如Singleton、Decorator或Observer等设计模式的同时,却忽略了面向对象的分析和设计。我们除了要学习诸如抽象、封装、多态和继承之类的基础知识,还需要了解面向对象的设计原则。据此,我们可以创建出简洁的模块化设计,以便后期轻松地开展测试,调试和维护。
不知您是否听说过OOP的SOLID设计原则(请参见-- https://javarevisited.blogspot.com/2018/02/top-5-java-design-pattern-courses-for-developers.html)?作为一种面向对象设计原则,它具体包括如下十个部分。
1.DRY(Don’t repeat yourself)
顾名思义,DRY表示不要编写重复的代码,应尽量使用抽象类(Abstraction)来抽象出目标事物。例如:如果您有两个以上的代码块,那么就应当考虑使其成为一个单独的方法。如果您多次用到某个硬编程(hard-coded)的值,那么就应当将它们设为public final constant(请参见-- http://javarevisited.blogspot.com/2011/12/final-variable-method-class-java.html)。显然,这样的面向对象设计原则将有益于后期的维护。
不过,在您使用标准化的代码去验证OrderId和SSN(译者注:美国社会安全号码),这两种不同的功能或事物时,您需要避免将它们联系得过于紧密,否则当OrderId更改其格式时,SSN的验证代码将会受阻。这就是我们常说的耦合。请不要合并任何使用了相似代码,但并实际联系的事物。您可以在Udemy(译者注:一家开放式的在线教育网站)上的“Java课程”中进一步学习《软件架构和设计模式的基础知识》,以了解有关编写正确的代码和在设计系统时,需要遵循的最佳实践。
2.封装变更(Encapsulate What Changes)
资深码农经常会仰天长叹:“在软件领域,唯一不变的就是变化!”可见,您应当尽量封装那些您期望,或可能在将来变更的代码。此举的好处同样是易于后期的测试与维护。
如果您使用Java进行编程,那么请在默认情况下将各种变量和方法设为私有,之后可逐步增加访问权限。同时在Java中,有不少设计模式都使用到了封装。其中Factory设计模式(Factory design pattern,请参见--http://javarevisited.blogspot.com/2011/12/factory-design-pattern-java-example.html)就是一个很好的例子。它通过封装对象的代码创建,提供了在不影响现有代码的情况下,在后期引入新功能的灵活性。
在Pluralsight(译者注:一个软件开发的在线教育网站平台)上的《设计模式库》课程(请参见--https://pluralsight.pxf.io/c/1193463/424552/7490?u=https%3A%2F%2Fwww.pluralsight.com%2Fcourses%2Fpatterns-library),可谓最好的设计模式集。它同时提供了有关如何在真实环境中使用的建议。
3.开放式封闭设计原则(Open Closed Design Principle)
根据OOP的设计原则:“类、方法或功能都应当为新功能的扩展而开放,但是要为修改而封闭(防止他人更改已经测试过的代码)。”在理想情况下,我们只需要测试那些能够带来新功能的程序代码。这便是开放式封闭设计原则的目标。此处的Open-Closed正是SOLID设计原则中的字母“O”的缩略。
我们来看一个违反“开放式封闭设计原则”的Java示例:在某段代码中,GraphicEditor与Shape紧密结合,如果需要新的Shape,则需要在drawShape(Shape s)方法的内部,修改已通过测试的系统。显然,这样既容易出错,也不可取。
您可以在Udemy那里学习到《面向对象设计和架构的SOLID原则》的相关课程,以加深对该原则的理解。
4.单一责任原则(Single Responsibility Principle,SRP)
单一责任原则要求:导致类发生变更的原因不应多于一个,或者说在一个级别上应始终只实现一项功能。其好处在于:有效地减少了软件的各个组件与代码之间的耦合。此处的SRP便是SOLID设计原则中的字母“S”的缩写。
例如,如果您在Java的一个类中设置了一个以上的功能,那么就会导致两个功能之间产生耦合。只要您更改了一个功能,那么就可能破坏耦合,进而引发新的一轮测试,以避免在生产环境中出现任何异常。
您可以在Udemy那里学习到《从0到1:设计模式》的相关课程,以加深对该原则的理解。
5.依赖性注入或反转原则(Dependency Injection or Inversion Principle)
该原则要求:不要自行增加依赖性,请把它交给框架。例如:作为编写实际应用最流行的Java框架之一,Spring框架就提供了各种依赖性。该设计原则的优点在于:由DI框架注入的任何类,都易于使用模拟对象来进行测试,且易于后期维护。由于对象的创建代码集中于框架之中,因此客户端的代码则不会被散落在各处。该此Dependency Injection原则便是SOLID中字母“D”的缩写。
我们可以用多种方法来实现依赖项注入,例如:使用似于AspectJ的面向切面编程(Aspect Oriented Programming,AOP)框架,进行字节码检测;或通过使用Spring中的各种代理来实现。
我们来看一个违反了依赖性注入原则的Java代码示例:EventLogWriter与AppManager有着紧密的耦合关系。如果您需要使用其他的方式(例如:通过推送短信或邮件通知)来通知客户端的话,则需要更改AppManager类。为此,我们可以通过使用依赖关系反转原则,来予以解决。也就是说,为了避免AppManager去请求EventLogWriter,我们可以使用由框架注入或提供的AppManager。
您可以在Udemy那里学习到《使用SOLID原则写更好的代码—速成班》的相关课程,以加深对该原则的理解。
6. 优先使用(对象)组合,而非(类)继承(Favor Composition over Inheritance)
继承和合成是两种通用的方法,可用于重用已编写好的代码。两者虽然各有优、缺点,但是如果可能的话,您应当尽量使用组合而不是继承。毕竟组合比继承要灵活得多。
此处的组合是指:通过设置属性,以更改运行时(run-time)类的行为,并使用各种接口来组成一个类。这就是我们常说的多态性(polymorphism)。它可以随时、且灵活地替换成为更好的实现方式。
如果您有兴趣学习更多有关组合、继承、关联、聚合等面向对象编程的概念和知识,请在Coursera(译者注:一个与世界顶尖大学合作的大型公开在线课程项目)上的《Java面向对象编程》课程中深入学习。
7. Liskov替代原则(Liskov Substitution Principle,LSP)
根据Liskov替换原则,子类型必须可以替代父类型。也就是说,那些使用父类型的方法或函数,必须能够与子类的对象无障碍地协作。即:派生类或子类必须在父类的基础上增强功能,而不是减少功能。相反,如果一个类具有比其子类更多的功能,那么它就不应该支持这些更多的功能,也就是违反了LSP。其实,LSP与单一职责原则(Single responsibility principle)、以及接口隔离原则(Interface Segregation Principle,见下文)有着密切的相关性。而且,LSP正是SOLID中字母“L”的缩写。
我们来看一个违反了Liskov替换原则的Java代码示例:如果您设计了一个area(Rectangle r)方法来计算Rectangle的面积,那么当您传入Square时,由于Square并非真正的Rectangle,该代码就会产生中断。
如果您对更多真实环境的示例感兴趣的话,请在Pluralsight上的《面向对象设计的SOLID原则》课程中进行深入学习。
8.接口隔离原则(Interface Segregation Principle,ISP)
接口隔离原则规定:如果一个接口包含了多个功能,而某个客户端只需要其中的一项功能,那么我们就不应该去实现那些用不到的接口。
毫无疑问,接口设计是一项比较棘手的工作。毕竟我们一旦发布了某个接口,就无法在不破坏其现有实现的情况下,对其进行更改。ISP在Java中的另一个好处是:如果不同的类都需要使用某个接口去实现各种方法的话,不如用更少的方法去实现单一的功能。
如果您对接口编程感兴趣的话,请参考博文--《Java接口的实战用法》,以了解更多的信息。
9.为接口编程,而非为实现编程(Programming for Interface not implementation)
程序员应该始终为接口编程,而不是为实现而编程。根据该原则所创建的代码,将能够灵活地被用于接口的任何一种新的实现上。
确切地说,我们应该在各种变量上使用接口类型、方法的返回类型、以及类似于Java的参数类型。例如:您可以使用SuperClasstype来存储对象,而不必使用SubClass。同时,您可以用List numbers= getNumbers();来代替ArrayList numbers = getNumbers();。当然,诸如:《Java高效编程(Effective Java)》(请参见-- https://www.amazon.com/Effective-Java-3rd-Joshua-Bloch/dp/0134685997/?tag=javamysqlanta-20)和《入浅出的设计模式(Head First design pattern)》(请参见-- http://www.amazon.com/dp/0596007124/?tag=javamysqlanta-20)之类的Java书籍也是这么建议的。
如果您对提高程序的代码质量感兴趣的话,我建议您学习Udemy上的《设计模式重构》课程。该课程将教会您如何使用C#中的重构技术,以及设计模式来改进内部设计。
10.委托原则(Delegation Principles)
委托原则建议:请不要自己做所有的事,要学会将不同的实现委托给相应的类。该原则的经典示例是Java中的equals()和hashCode()方法(请参见-- http://javarevisited.blogspot.com/2011/02/how-to-write-equals-method-in-java.html)。事件委托(Event delegation)则是另一种示例,它将事件委托给处理程序(handlers)进行处理。可见,此设计原则的主要好处是:无需重复编写代码,便可轻松地修改程序的行为。
总结与其他资源
可以说,上述所有面向对象的设计原则,都会有利于程序代码的高内聚和低耦合性,也都有助于您编写出灵活、简洁的代码。您需要通过反复的练习,来实践这些理论原则,进而解决应用程序开发和软件工程中的各种常见问题。
与此同时,您可以从Apache和Google处寻找各种开源代码,以便学习Java和OOP的设计原则。另外,Java开发工具包(Java Development Kit,JDK)也包含有许多设计原则,例如:BorderFactory类中的Factory模式(请参见--http://javarevisited.blogspot.sg/2011/12/factory-design-pattern-java-example.html#axzz51cvxH5kW)、java.lang.Runtime类中的Singleton模式(请参见--https://javarevisited.blogspot.com/2014/05/double-checked-locking-on-singleton-in-java.html)、以及各种java.io类中的Decorator模式(请参见--http://www.java67.com/2013/07/decorator-design-pattern-in-java-real-life-example-tutorial.html)。
如果您对于学习面向对象的原则和模式兴趣不减的话,我推荐您阅读《深入浅出学习面向对象的分析和设计(Head First Object-Oriented Analysis and Design)》一书。