设计原则,是学习设计模式的基础。但是并非每种设计模式都是“六边形展示”,并非严格遵守每一个设计原则。

设计原则不是金科玉律,在实际开发过程中,我们要考虑人力、时间、成本、质量,不是刻意追求完美,生搬硬套,要在适当的场景遵循设计原则,体现的是一种平衡取舍,帮助我们设计出更加优雅的代码结构。

开闭原则(Open-Closed Priciple,OCP)是指一个软件实体(模块、类、方法等)应该对扩展开放,对修改关闭

我举一个例子,陀螺是个程序喵,创办了一个生产猫粮的公司——跑码场,手下有个小徒弟叫招财,写了一个下单的逻辑。

  1. /**
  2. * @author 蝉沐风
  3. * @description 原始代码
  4. * @date 2022/2/8
  5. */
  6. public class PaoMaChangV1 {
  7. public void order(String flavor) {
  8. if (flavor.equals("毛血旺")) {
  9. orderMaoXueWangCatFood();
  10. } else if (flavor.equals("鱼香肉丝")) {
  11. orderFishCatFood();
  12. }
  13. }
  14. private void orderMaoXueWangCatFood() {
  15. System.out.println("售卖一袋「毛血旺」风味猫粮");
  16. }
  17. private void orderFishCatFood() {
  18. System.out.println("售卖一袋「鱼香肉丝」风味猫粮");
  19. }
  20. }

逻辑本身很简单,核心业务逻辑主要是order()函数,客户需要传入相应的猫粮口味flavor进行下单。

现在跑码场扩展了业务,新增了一种「大肠刺身」口味的猫粮,而且支持用户自定义猫粮购买数量(毕竟这种口味可能会供不应求)。在以上代码的基础上,招财做了如下修改:

  1. /**
  2. * @author 蝉沐风
  3. * @description 原始代码功能扩展
  4. * @date 2022/2/8
  5. */
  6. public class PaoMaChangV1Expand {
  7. public void order(String flavor, Integer count) {
  8. if (flavor.equals("毛血旺")) {
  9. orderMaoXueWangCatFood(count);
  10. } else if (flavor.equals("鱼香肉丝")) {
  11. orderFishCatFood(count);
  12. }
  13. // 更改1:添加口味的逻辑判断
  14. else if (flavor.equals("大肠刺身")) {
  15. orderDaChangFood(count);
  16. }
  17. }
  18. private void orderMaoXueWangCatFood(Integer count) {
  19. System.out.println("售卖" + count + "袋「毛血旺」风味猫粮");
  20. }
  21. private void orderFishCatFood(Integer count) {
  22. System.out.println("售卖" + count + "袋「鱼香肉丝」风味猫粮");
  23. }
  24. // 更改2:添加售卖逻辑
  25. private void orderDaChangFood(Integer count) {
  26. System.out.println("售卖" + count + "一袋「大肠刺身」风味猫粮");
  27. }
  28. }

这种修改方式确实能解决目前的业务问题,但同时也存在很多问题。

首先,修改了order()方法,添加了一个参数,相应的客户端调用必须修改;其次,每当有新的口味猫粮产品诞生时,都必须在order()方法中添加口味的判断,同时需要添加该产品的售卖逻辑。这些操作都是通过「修改」来实现新功能的,不符合「开闭原则」。

如果我们要遵循「开闭原则」,必须对修改关闭,对扩展开放。

我们重构一下初始代码,主要做以下两方面的修改:

  1. 创建CatFood基类,然后创建对应口味的猫粮继承基类;
  2. 将每种口味猫粮的售卖逻辑写在具体类中。
  3. 修改客户调用的order方法
  1. /**
  2. * @author 蝉沐风
  3. * @description 猫粮基类
  4. * @date 2022/2/8
  5. */
  6. public abstract class CatFood {
  7. public abstract void order();
  8. }
  9. /**
  10. * @author 蝉沐风
  11. * @description 「毛血旺」猫粮
  12. * @date 2022/2/8
  13. */
  14. public class MaoXueWangCatFood extends CatFood {
  15. @Override
  16. public void order() {
  17. System.out.println("售卖一袋「毛血旺」风味猫粮");
  18. }
  19. }
  20. /**
  21. * @author 蝉沐风
  22. * @description 「鱼香肉丝」猫粮
  23. * @date 2022/2/8
  24. */
  25. public class FishCatFood extends CatFood {
  26. @Override
  27. public void order() {
  28. System.out.println("售卖一袋「鱼香肉丝」风味猫粮");
  29. }
  30. }

order()方法修改如下

  1. /**
  2. * @author 蝉沐风
  3. * @description 遵循「开闭原则」之后的代码
  4. * @date 2022/2/8
  5. */
  6. public class PaoMaChangV2 {
  7. public void order(CatFood catFood) {
  8. catFood.order();
  9. }
  10. }

重构之后的客户端调用方式如下

  1. /**
  2. * @author 蝉沐风
  3. * @description 客户端调用
  4. * @date 2022/2/8
  5. */
  6. public class ClientV2 {
  7. public static void main(String[] args) {
  8. PaoMaChangV2 paoMaChang = new PaoMaChangV2();
  9. // 创建对应口味的猫粮
  10. FishCatFood fish = new FishCatFood();
  11. paoMaChang.order(fish);
  12. }
  13. }

现在我们再来看,基于重构之后的代码,我们要实现刚才讲到的业务需求,我们需要进行怎样的改动。主要的修改内容有如下:

  1. CatFood基类中添加属性count,为子类添加构造函数;
  2. 添加新类DaChangCatFood;

扩展之后的代码如下

  1. /**
  2. * @author 蝉沐风
  3. * @description 猫粮类
  4. * @date 2022/2/8
  5. */
  6. public abstract class CatFood {
  7. //订购数量
  8. private Integer count;
  9. public abstract void order();
  10. public Integer getCount() {
  11. return count;
  12. }
  13. public void setCount(Integer count) {
  14. this.count = count;
  15. }
  16. public CatFood(Integer count) {
  17. this.count = count;
  18. }
  19. public CatFood() {
  20. }
  21. }
  22. /**
  23. * @author 蝉沐风
  24. * @description 「毛血旺」猫粮
  25. * @date 2022/2/8
  26. */
  27. public class MaoXueWangCatFood extends CatFood {
  28. public MaoXueWangCatFood(Integer count) {
  29. this.setCount(count);
  30. }
  31. @Override
  32. public void order() {
  33. System.out.println("售卖" + this.getCount() + "袋「毛血旺」风味猫粮");
  34. }
  35. }
  36. /**
  37. * @author 蝉沐风
  38. * @description 「鱼香肉丝」猫粮
  39. * @date 2022/2/8
  40. */
  41. public class FishCatFood extends CatFood {
  42. public FishCatFood(Integer count) {
  43. this.setCount(count);
  44. }
  45. @Override
  46. public void order() {
  47. System.out.println("售卖" + this.getCount() + "袋「鱼香肉丝」风味猫粮");
  48. }
  49. }
  50. /**
  51. * @author 蝉沐风
  52. * @description 「大肠刺身」猫粮
  53. * @date 2022/2/8
  54. */
  55. public class DaChangCatFood extends CatFood {
  56. public DaChangCatFood(Integer count) {
  57. this.setCount(count);
  58. }
  59. @Override
  60. public void order() {
  61. System.out.println("售卖" + this.getCount() + "袋「大肠刺身」风味猫粮");
  62. }
  63. }

客户端调用方式变为

  1. public class ClientV2 {
  2. public static void main(String[] args) {
  3. PaoMaChangV2 paoMaChang = new PaoMaChangV2();
  4. // 创建对应口味的猫粮
  5. DaChangCatFood dachang = new DaChangCatFood(2);
  6. paoMaChang.order(dachang);
  7. }
  8. }

设计原则(1)| 开闭原则 - 图1

重构之后的代码在扩展上更加的灵活

  1. 如果有了新口味的猫粮产品,只需创建新的class对象,重写order()方法就可以了,不需要改动其他的代码;
  2. 如果order方法中需要其他参数,可以根据实际情况,在CatFood中添加相关属性。

是不是修改代码就违背开闭原则?

你可能会有疑问,我们为了完成新业务功能,不仅在CatFood类中添加了count属性,而且还添加了getter/setter方法,这难道不算修改代码吗?

首先我们需要认识到,添加新功能的时候,我们不可能一点代码都不修改!其次,「开闭原则」的定义是软件实体(模块、类、方法等)应该对扩展开放,对修改关闭。对于count属性的添加而言,在模块或类的粒度下,可以被认为是修改,但是在方法的粒度下,我们并没有修改之前存在的方法和属性,因此可以被认为是扩展。

实际编码过程中怎么遵守开闭原则?

我的理解是不需要刻意遵守。

你只需要头脑中有这个印象就行了,你需要知道的就是你的代码需要具有一定的扩展性。所有的设计原则都只有一个最终归宿——不破坏原有代码的正常运行,方便扩展

随着你的理论知识和实战经验的提高,同时对业务有了足够了解,你在设计代码结构时会很自然地向未来靠拢(这需要稍加练习,这种技能不是单纯靠工作时长就能获得的),识别出未来可能会发生的扩展点。

但是想识别出所有可能的扩展点既不可能也没必要,最合理的做法是对一些比较确定的、短期内可能会发生的需求进行扩展设计。

还是那句话,设计原则和设计模式不是金科玉律,只要适合当前需求,并具备一定弹性的设计就是好设计。要平衡代码扩展性和可读性,切勿滥用设计原则和设计模式,牺牲代码的可读性。

2. 依赖倒置原则(Dependence Inversion Principle,DIP)

3. 单一职责原则(Simple Responsibility Pinciple,SRP)

4. 接口隔离原则(Interface Segregation Principle,ISP)

5. 迪米特法则(Law of Demeter,LoD)

6. 里式替换原则(Liskov Substitution Principle,LSP)

7. 合成复用原则(Composite/Aggregate Reuse Principle,CARP)