我举一个例子,陀螺是个程序喵,创办了一个生产猫粮的公司——跑码场,手下有个小徒弟叫招财,写了一个下单的逻辑。
开闭原则是指一个软件实体(模块、类、方法等)应该对扩展开放,对修改关闭
我举一个例子,陀螺是个程序喵,创办了一个生产猫粮的公司——跑码场,手下有个小徒弟叫招财,写了一个下单的逻辑。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
/** * @author 蝉沐风 * @description 原始代码 * @date 2022/2/8 */ public class PaoMaChangV1 { public void order(String flavor) { if (flavor.equals("毛血旺")) { orderMaoXueWangCatFood(); } else if (flavor.equals("鱼香肉丝")) { orderFishCatFood(); } } private void orderMaoXueWangCatFood() { System.out.println("售卖一袋「毛血旺」风味猫粮"); } private void orderFishCatFood() { System.out.println("售卖一袋「鱼香肉丝」风味猫粮"); } } |
逻辑本身很简单,核心业务逻辑主要是order()
函数,客户需要传入相应的猫粮口味flavor
进行下单。
现在跑码场扩展了业务,新增了一种「大肠刺身」口味的猫粮,而且支持用户自定义猫粮购买数量(毕竟这种口味可能会供不应求)。在以上代码的基础上,招财做了如下修改:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
/** * @author 蝉沐风 * @description 原始代码功能扩展 * @date 2022/2/8 */ public class PaoMaChangV1Expand { public void order(String flavor, Integer count) { if (flavor.equals("毛血旺")) { orderMaoXueWangCatFood(count); } else if (flavor.equals("鱼香肉丝")) { orderFishCatFood(count); } // 更改1:添加口味的逻辑判断 else if (flavor.equals("大肠刺身")) { orderDaChangFood(count); } } private void orderMaoXueWangCatFood(Integer count) { System.out.println("售卖" + count + "袋「毛血旺」风味猫粮"); } private void orderFishCatFood(Integer count) { System.out.println("售卖" + count + "袋「鱼香肉丝」风味猫粮"); } // 更改2:添加售卖逻辑 private void orderDaChangFood(Integer count) { System.out.println("售卖" + count + "一袋「大肠刺身」风味猫粮"); } } |
这种修改方式确实能解决目前的业务问题,但同时也存在很多问题。
首先,修改了order()
方法,添加了一个参数,相应的客户端调用必须修改;其次,每当有新的口味猫粮产品诞生时,都必须在order()
方法中添加口味的判断,同时需要添加该产品的售卖逻辑。这些操作都是通过「修改」来实现新功能的,不符合「开闭原则」。
如果我们要遵循「开闭原则」,必须对修改关闭,对扩展开放。
我们重构一下初始代码,主要做以下两方面的修改:
- 创建
CatFood
基类,然后创建对应口味的猫粮继承基类; - 将每种口味猫粮的售卖逻辑写在具体类中。
- 修改客户调用的
order
方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
/** * @author 蝉沐风 * @description 猫粮基类 * @date 2022/2/8 */ public abstract class CatFood { public abstract void order(); } /** * @author 蝉沐风 * @description 「毛血旺」猫粮 * @date 2022/2/8 */ public class MaoXueWangCatFood extends CatFood { @Override public void order() { System.out.println("售卖一袋「毛血旺」风味猫粮"); } } /** * @author 蝉沐风 * @description 「鱼香肉丝」猫粮 * @date 2022/2/8 */ public class FishCatFood extends CatFood { @Override public void order() { System.out.println("售卖一袋「鱼香肉丝」风味猫粮"); } } |
order()
方法修改如下
1 2 3 4 5 6 7 8 9 10 11 12 |
/** * @author 蝉沐风 * @description 遵循「开闭原则」之后的代码 * @date 2022/2/8 */ public class PaoMaChangV2 { public void order(CatFood catFood) { catFood.order(); } } |
重构之后的客户端调用方式如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
/** * @author 蝉沐风 * @description 客户端调用 * @date 2022/2/8 */ public class ClientV2 { public static void main(String[] args) { PaoMaChangV2 paoMaChang = new PaoMaChangV2(); // 创建对应口味的猫粮 FishCatFood fish = new FishCatFood(); paoMaChang.order(fish); } } |
现在我们再来看,基于重构之后的代码,我们要实现刚才讲到的业务需求,我们需要进行怎样的改动。主要的修改内容有如下:
CatFood
基类中添加属性count
,为子类添加构造函数;- 添加新类
DaChangCatFood
;
扩展之后的代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 |
/** * @author 蝉沐风 * @description 猫粮类 * @date 2022/2/8 */ public abstract class CatFood { //订购数量 private Integer count; public abstract void order(); public Integer getCount() { return count; } public void setCount(Integer count) { this.count = count; } public CatFood(Integer count) { this.count = count; } public CatFood() { } } /** * @author 蝉沐风 * @description 「毛血旺」猫粮 * @date 2022/2/8 */ public class MaoXueWangCatFood extends CatFood { public MaoXueWangCatFood(Integer count) { this.setCount(count); } @Override public void order() { System.out.println("售卖" + this.getCount() + "袋「毛血旺」风味猫粮"); } } /** * @author 蝉沐风 * @description 「鱼香肉丝」猫粮 * @date 2022/2/8 */ public class FishCatFood extends CatFood { public FishCatFood(Integer count) { this.setCount(count); } @Override public void order() { System.out.println("售卖" + this.getCount() + "袋「鱼香肉丝」风味猫粮"); } } /** * @author 蝉沐风 * @description 「大肠刺身」猫粮 * @date 2022/2/8 */ public class DaChangCatFood extends CatFood { public DaChangCatFood(Integer count) { this.setCount(count); } @Override public void order() { System.out.println("售卖" + this.getCount() + "袋「大肠刺身」风味猫粮"); } } |
客户端调用方式变为
1 2 3 4 5 6 7 8 9 |
public class ClientV2 { public static void main(String[] args) { PaoMaChangV2 paoMaChang = new PaoMaChangV2(); // 创建对应口味的猫粮 DaChangCatFood dachang = new DaChangCatFood(2); paoMaChang.order(dachang); } } |
重构之后的代码在扩展上更加的灵活
- 如果有了新口味的猫粮产品,只需创建新的class对象,重写
order()
方法就可以了,不需要改动其他的代码; - 如果
order
方法中需要其他参数,可以根据实际情况,在CatFood
中添加相关属性。
是不是修改代码就违背开闭原则?
你可能会有疑问,我们为了完成新业务功能,不仅在CatFood
类中添加了count
属性,而且还添加了getter/setter
方法,这难道不算修改代码吗?
首先我们需要认识到,添加新功能的时候,我们不可能一点代码都不修改!其次,「开闭原则」的定义是软件实体(模块、类、方法等)应该对扩展开放,对修改关闭。对于count
属性的添加而言,在模块或类的粒度下,可以被认为是修改,但是在方法的粒度下,我们并没有修改之前存在的方法和属性,因此可以被认为是扩展。
实际编码过程中怎么遵守开闭原则?
我的理解是不需要刻意遵守。
你只需要头脑中有这个印象就行了,你需要知道的就是你的代码需要具有一定的扩展性。所有的设计原则都只有一个最终归宿——不破坏原有代码的正常运行,方便扩展。
随着你的理论知识和实战经验的提高,同时对业务有了足够了解,你在设计代码结构时会很自然地向未来靠拢(这需要稍加练习,这种技能不是单纯靠工作时长就能获得的),识别出未来可能会发生的扩展点。
但是想识别出所有可能的扩展点既不可能也没必要,最合理的做法是对一些比较确定的、短期内可能会发生的需求进行扩展设计。
还是那句话,设计原则和设计模式不是金科玉律,只要适合当前需求,并具备一定弹性的设计就是好设计。要平衡代码扩展性和可读性,切勿滥用设计原则和设计模式,牺牲代码的可读性。