面向对象的设计模式有七大基本原则:
- 开闭原则(Open Closed Principle,OCP)
- 单一职责原则(Single Responsibility Principle, SRP)
- 里氏代换原则(Liskov Substitution Principle,LSP)
- 依赖倒转原则(Dependency Inversion Principle,DIP)
- 接口隔离原则(Interface Segregation Principle,ISP)
- 合成/聚合复用原则(Composite/Aggregate Reuse Principle,CARP)
- 迪米特法则(Law of Demeter,LOD)
开闭原则
开闭原则(Open Closed Principle,OCP)的定义是:一个软件实体如类、模块和函数应该对扩展开放,对修改关闭。模块应尽量在不修改原(是"原",指原来的代码)代码的情况下进行扩展。
开闭原则的意义:
在软件的生命周期内,因为变化、升级和维护等原因需要对软件原有代码进行修改时,可能会给旧代码中引入错误,也可能会使我们不得不对整个功能进行重构,并且需要原有代码经过重新测试。当软件需要变化时,尽量通过扩展软件实体的行为来实现变化,而不是通过修改已有的代码来实现变化。
如何实现对扩展开放,对修改关闭?
要实现对扩展开放,对修改关闭,即遵循开闭原则,需要对系统进行抽象化设计,抽象可以基于抽象类或者接口。一般来说需要做到几点:
- 1、通过接口或者抽象类约束扩展,对扩展进行边界限定,不允许出现在接口或抽象类中不存在的public方法,也就是扩展必须添加具体实现而不是改变具体的方法。
- 2、参数类型、引用对象尽量使用接口或者抽象类,而不是实现类,这样就能尽量保证抽象层是稳定的。
- 3、一般抽象模块设计完成(例如接口的方法已经敲定),不允许修改接口或者抽象方法的定义。
以下是错误代码:
1. public class OpenClosed1 { 2. public static void main(String[] args) { 3. GraphicEditor graphicEditor = new GraphicEditor(); 4. graphicEditor.drawShape(new Rectangle()); 5. graphicEditor.drawShape(new Circle()); 6. graphicEditor.drawShape(new Triangle());//+新增绘制三角形 7. } 8. } 9. 10. //这是一个用于绘图的类 11. class GraphicEditor { 12. //接收 Shape 对象,然后根据 type 来绘制不同的图形 13. public void drawShape(Shape s) { 14. if (s.m_type == 1) 15. drawRectangle(s); 16. else if (s.m_type == 2) 17. drawCircle(s); 18. else if (s.m_type == 3)//+新增绘制三角形 19. drawTriangle(s); 20. } 21. 22. //绘制矩形 23. public void drawRectangle(Shape r) { 24. System.out.println("绘制矩形"); 25. } 26. 27. //绘制圆形 28. public void drawCircle(Shape r) { 29. System.out.println("绘制圆形"); 30. } 31. 32. //+新增绘制三角形 33. public void drawTriangle(Shape r) { 34. System.out.println("绘制三角形"); 35. } 36. } 37. 38. class Shape { 39. int m_type; 40. } 41. 42. //以前就写好的 43. class Rectangle extends Shape { 44. Rectangle() { 45. super.m_type = 1; 46. } 47. } 48. 49. //以前就写好的 50. class Circle extends Shape { 51. Circle() { 52. super.m_type = 2; 53. } 54. } 55. 56. //+新增绘制三角形 57. class Triangle extends Shape { 58. Triangle() { 59. super.m_type = 3; 60. } 61. }
以下是正确示范:
1. public class OpenClosed2 { 2. public static void main(String[] args) { 3. GraphicEditor graphicEditor = new GraphicEditor(); 4. graphicEditor.drawShape(new Rectangle()); 5. graphicEditor.drawShape(new Circle()); 6. graphicEditor.drawShape(new Triangle());//+新增绘制三角形 7. } 8. } 9. 10. //这是一个用于绘图的类 11. class GraphicEditor { 12. //接收 Shape 对象,调用 draw 方法 13. public void drawShape(Shape s) { 14. s.draw(); 15. } 16. } 17. 18. abstract class Shape { 19. int m_type; 20. 21. public abstract void draw(); 22. } 23. 24. //以前就写好的 25. class Rectangle extends Shape { 26. Rectangle() { 27. super.m_type = 1; 28. } 29. 30. @Override 31. public void draw() { 32. System.out.println("绘制矩形"); 33. } 34. } 35. 36. //以前就写好的 37. class Circle extends Shape { 38. Circle() { 39. super.m_type = 2; 40. } 41. 42. @Override 43. public void draw() { 44. System.out.println("绘制圆形"); 45. } 46. } 47. 48. //+新增绘制三角形 49. class Triangle extends Shape { 50. Triangle() { 51. super.m_type = 3; 52. } 53. 54. @Override 55. public void draw() { 56. System.out.println("绘制三角形"); 57. } 58. }
单一职责原则
单一职责原则(Single Responsibility Principle, SRP)的定义是:指一个类或者模块应该有且只有一个改变的原因。如果一个类承担的职责过多,就等于把这些职责耦合在一起了。一个职责的变化可能会削弱或者抑制这个类完成其他职责的能力。这种耦合会导致脆弱的设计,当发生变化时,设计会遭受到意想不到的破坏。而如果想要避免这种现象的发生,就要尽可能的遵守单一职责原则。此原则的核心就是解耦和增强内聚性。
单一职责原则的意义:
单一职责原则告诉我们:一个类不能做太多的东西。在软件系统中,一个类(一个模块、或者一个方法)承担的职责越多,那么其被复用的可能性就会越低。一个很典型的例子就是万能类。其实可以说一句大实话:任何一个常规的MVC项目,在极端的情况下,可以用一个类(甚至一个方法)完成所有的功能。但是这样做就会严重耦合,甚至牵一发动全身。一个类承(一个模块、或者一个方法)担的职责过多,就相当于将这些职责耦合在一起,当其中一个职责变化时,可能会影响其他职责的运作,因此要将这些职责进行分离,将不同的职责封装在不同的类中,即将不同的变化原因封装在不同的类中,如果多个职责总是同时发生改变则可将它们封装在同一类中。
以下是错误示范:
1. public class Service { 2. 3. public UserDTO findUser(String name){ 4. Connection connection = getConnection(); 5. PreparedStatement preparedStatement = connection.prepareStatement("SELECT * FROM t_user WHERE name = ?"); 6. preparedStatement.setObject(1, name); 7. User user = //处理结果 8. UserDTO dto = new UserDTO(); 9. //entity值拷贝到dto 10. return dto; 11. } 12. }
以下是正确示范:
1. public class Service { 2. 3. private Dao dao; 4. 5. public UserDTO findUser(String name){ 6. User user = dao.findUserByName(name); 7. UserDTO dto = new UserDTO(); 8. //entity值拷贝到dto 9. return dto; 10. } 11. } 12. 13. 14. public class Dao{ 15. 16. public User findUserByName(String name){ 17. Connection connection = DataBaseUtils.getConnnection(); 18. PreparedStatement preparedStatement = connection.prepareStatement("SELECT * FROM t_user WHERE name = ?"); 19. preparedStatement.setObject(1, name); 20. User user = //处理结果 21. return user; 22. } 23. }
里氏代换原则
里氏代换原则(Liskov Substitution Principle,LSP)的定义是:所有引用基类的地方必须能透明地使用其子类的对象,也可以简单理解为任何基类可以出现的地方,子类一定可以出现。
里氏代换原则的意义:
只有当衍生类可以替换掉基类,软件单位的功能不受到影响时,基类才能真正被复用,而衍生类也能够在基类的基础上增加新的行为。里氏代换原则是对"开-闭"原则的补充。实现"开-闭"原则的关键步骤就是抽象化。而基类与子类的继承关系就是抽象化的具体实现,所以里氏代换原则是对实现抽象化的具体步骤的规范。当然,如果反过来,软件单位使用的是一个子类对象的话,那么它不一定能够使用基类对象。
子类为什么可以替换基类的位置?
其实原因很简单,只要存在继承关系,基类的所有非私有属性或者方法,子类都可以通过继承获得(白箱复用),反过来不成立,因为子类很有可能扩充自身的非私有属性或者方法,这个时候不能用基类获取子类新增的这些属性或者方法。
里氏代换原则是实现开闭原则的基础,它告诉我们在设计程序的时候进可能使用基类进行对象的定义和引用,在运行时再决定基类的具体子类型。
举个简单的例子,假设一种会呼吸的动物作为父类,子类猪和鸟也有自身的呼吸方式:
1. public abstract class Animal { 2. 3. protected abstract void breathe(); 4. } 5. 6. public class Bird extends Animal { 7. 8. @Override 9. public void breathe() { 10. System.out.println("Bird breathes..."); 11. } 12. } 13. 14. public class Pig extends Animal { 15. 16. @Override 17. public void breathe() { 18. System.out.println("Pig breathes..."); 19. } 20. } 21. 22. public class App { 23. 24. public static void main(String[] args) throws Exception { 25. Animal bird = new Bird(); 26. bird.breathe(); 27. Animal pig = new Pig(); 28. pig.breathe(); 29. } 30. }
依赖倒转原则
依赖倒转原则(Dependency Inversion Principle,DIP)的定义:程序要依赖于抽象接口,不要依赖于具体实现。简单的说就是要求对抽象进行编程,不要对实现进行编程,这样就降低了客户与实现模块间的耦合。
依赖倒转原则的意义:
依赖倒转原则要求我们在程序代码中传递参数时或在关联关系中,尽量引用层次高的抽象层类,即使用接口和抽象类进行变量类型声明、参数类型声明、方法返回类型声明,以及数据类型的转换等,而不要用具体类来做这些事情。为了确保该原则的应用,一个具体类应当只实现接口或抽象类中声明过的方法,而不要给出多余的方法,否则将无法调用到在子类中增加的新方法。在引入抽象层后,系统将具有很好的灵活性,在程序中尽量使用抽象层进行编程,而将具体类写在配置文件中,这样一来,如果系统行为发生变化,只需要对抽象层进行扩展,并修改配置文件,而无须修改原有系统的源代码,在不修改的情况下来扩展系统的功能,满足开闭原则的要求。
1. public class DependenceInversion2 { 2. public static void main(String[] args) { 3. Client2 client = new Client2(); 4. client.receive(new QQImpl()); 5. client.receive(new WXImpl()); 6. } 7. } 8. 9. interface IReceive { 10. public void getUserInfo(Integer uid); 11. } 12. 13. class WXImpl implements IReceive { 14. public void getUserInfo(Integer uid) { 15. System.out.println("WX getUserInfo " + uid); 16. } 17. } 18. 19. class QQImpl implements IReceive { 20. public void getUserInfo(Integer uid) { 21. System.out.println("QQ getUserInfo " + uid); 22. } 23. } 24. 25. class Client2 { 26. public void receive(IReceive iReceive) { 27. iReceive.getUserInfo(774908833); 28. } 29. }
接口隔离原则
接口隔离原则(Interface Segregation Principle,ISP)的定义是客户端不应该依赖它不需要的接口,类间的依赖关系应该建立在最小的接口上。简单来说就是建立单一的接口,不要建立臃肿庞大的接口。也就是接口尽量细化,同时接口中的方法尽量少。
如何看待接口隔离原则和单一职责原则?
单一职责原则注重的是类和接口的职责单一,这里职责是从业务逻辑上划分的,但是在接口隔离原则要求当一个接口太大时,我们需要将它分割成一些更细小的接口,使用该接口的客户端仅需知道与之相关的方法即可。也就是说,我们在设计接口的时候有可能满足单一职责原则但是不满足接口隔离原则。
接口隔离原则的规范:
- 使用接口隔离原则前首先需要满足单一职责原则。
- 接口需要高内聚,也就是提高接口、类、模块的处理能力,少对外发布public的方法。
- 定制服务,就是单独为一个个体提供优良的服务,简单来说就是拆分接口,对特定接口进行定制。
- 接口设计是有限度的,接口的设计粒度越小,系统越灵活,但是值得注意不能过小,否则变成"字节码编程"。
以下为正确示范:
1. public interface ValueOperations<K, V> { 2. 3. void set(K key, V value); 4. void set(K key, V value, long timeout, TimeUnit unit); 5. //.... 6. }
合成/聚合复用原则
合成/聚合复用原则(Composite/Aggregate Reuse Principle,CARP)一般也叫合成复用原则(Composite Reuse Principle, CRP),定义是:尽量使用合成/聚合,而不是通过继承达到复用的目的。
合成/聚合复用原则就是在一个新的对象里面使用一些已有的对象,使之成为新对象的一部分;新的对象通过向内部持有的这些对象的委派达到复用已有功能的目的,而不是通过继承来获得已有的功能。
聚合(Aggregate)的概念:
聚合表示一种弱的"拥有"关系,一般表现为松散的整体和部分的关系,其实,所谓整体和部分也可以是完全不相关的。例如A对象持有B对象,B对象并不是A对象的一部分,也就是B对象的生命周期是B对象自身管理,和A对象不相关。
合成(Composite)的概念:
合成表示一种强的"拥有"关系,一般表现为严格的整体和部分的关系,部分和整体的生命周期是一样的。
1. public class Person 2. { 3. public void sayHello() 4. { 5. Console.WriteLine("打招呼方法的实现"); 6. } 7. } 8. public class User : Person 9. { 10. } 11. public class Operator : Person 12. { 13. }
迪米特法则
迪米特法则(Law of Demeter,LOD),有时候也叫做最少知识原则(Least Knowledge Principle,LKP),它的定义是:一个软件实体应当尽可能少地与其他实体发生相互作用。每一个软件单位对其他的单位都只有最少的知识,而且局限于那些与本单位密切相关的软件单位。迪米特法则的初衷在于降低类之间的耦合。由于每个类尽量减少对其他类的依赖,因此,很容易使得系统的功能模块功能独立,相互之间不存在(或很少有)依赖关系。迪米特法则不希望类之间建立直接的联系。如果真的有需要建立联系,也希望能通过它的友元类(中间类或者跳转类)来转达。
迪米特法则的规则:
- Only talk to your immediate friends(只与直接的朋友通讯),一个对象的"朋友"包括他本身(this)、它持有的成员对象、入参对象、它所创建的对象。
- 尽量少发布public的变量和方法,一旦公开的属性和方法越多,修改的时候影响的范围越大。
- "是自己的就是自己的",如果一个方法放在本类中,既不产生新的类间依赖,也不造成负面的影响,那么次方法就应该放在本类中。
迪米特法则的意义:
迪米特法则的核心观念就是类间解耦,也就降低类之间的耦合,只有类处于弱耦合状态,类的复用率才会提高。所谓降低类间耦合,实际上就是尽量减少对象之间的交互,如果两个对象之间不必彼此直接通信,那么这两个对象就不应当发生任何直接的相互作用,如果其中的一个对象需要调用另一个对象的某一个方法的话,可以通过第三者转发这个调用。简言之,就是通过引入一个合理的第三者来降低现有对象之间的耦合度。但是这样会引发一个问题,有可能产生大量的中间类或者跳转类,导致系统的复杂性提高,可维护性降低。如果一味追求极度解耦,那么最终有可能变成面向字节码编程甚至是面向二进制的0和1编程。