0x1、SOLID原则
并非单纯的一个原则,而是由下述五个设计原则组成,看到几个有趣的图片顺便贴上,来源:SOLID Development Principles – In Motivational Pictures
① 单一职责原则 (SRP,Single Responsibility Principle)
一个类或模块只负责完成一个职责(或功能)
,就是说:不要设计大而全的类,要设计粒度小、功能单一的类。
举个例子:
一个类中即包含了订单的操作,又包含了用户的操作,而订单和用户是两个独立的业务领域模型(DDD),将两个不相干的功能放在一个类中,就违反了单一职责原则,可以将类拆分成粒度更小、功能更单一的两个类:订单类 和 用户类。
但,大多数时候,类是否职责单一,却很难拿捏,比如:
public class UserInfo { private long userId; private String userName; private String avatarUrl; private String email; private String telephone; private long createTime; private long lastLoginTime; private String provinceOfAddress; //省 private String cityOfAddress; // 市 private String regionOfAddress; // 区 private String detailedAddress; // 详细地址 // ...省略其他属性和方法 }
两种不同的观点:
- UserInfo类包含的都是跟用户相关的信息,都属于用户这个业务模型,满足单一职责;
- 地址信息在类中占比较高,可以将其拆到一个独立的UserAddress类中,拆分后的两个类职责更加单一;
哪种观点更对?
答:要根据具体的应用场景,如果地址信息和其他信息一样只是单纯用来展示,现在的设计就是合理的。
如果其他功能模块也会用到用户的地址信息,最好还是拆一拆。
持续重构:
不要一开始就想着拆多细,可以先写一个粗粒度的类,满足业务需求,随着业务的发展,粗粒度的类越来越庞大,代码越来越多,此时,再来将这个类拆分成多个更细粒度的类。
判断类是否职责单一的几个技巧:
- 类中代码行数、函数或属性过多 → 会影响代码的可读性和可维护性,考虑拆下;
- 类依赖的其他类过多,或依赖类的其他类过多 → 不符合高内聚低耦合,考虑拆下;
- 私有方法过多 → 考虑下能否独立到新的类中,设置为public方法,供更多类使用,提高复用性;
- 给类命名时困难 → 很难用一个业务名词概括,说明类的职责定义得可能不够清晰;
- 类中大量的方法都是几种操作类中的某几个属性 → 如上述例子,若有半数方法都在操作address,考虑拆下;
类是不是拆得越细越好?
不是,单一职责原则通过避免设计大而全的类,避免将不相关的功能耦合在一起,以此提高类内聚性,降低代码耦合性。但如果拆分过细,反而会适得其反,降低内聚性,影响代码的可维护性。
谨记:应用设计原则/模式的最终目的:提高代码的可读性、可扩展性、复用性、可维护性 等。
② 开闭原则 (OCP,Open Closed Principle)
对扩展开放、对修改关闭
,就是说:添加一个新功能,应该是在代码基础上扩展,而非修改已有代码。
写个简单例子:
public class OcpTest { public static void main(String[] args) { GraphicEditor editor = new GraphicE-ditor(); editor.drawShape(new Rectangle()); editor.drawShape(new Circle()); } } // 绘图类 class GraphicEditor { public void drawShape(Shape shape) { if (shape.type == 1) drawRectangle(shape); else if(shape.type == 2) drawCircle(shape); } public void drawRectangle(Shape s) { System.out.println("画矩形"); } public void drawCircle(Shape s) { System.out.println("画圆形"); } } // 图形类,只有一个type属性代表类别 class Shape { int type; } class Rectangle extends Shape { Rectangle() { super.type = 1; } } class Circle extends Shape { Circle() { super.type = 2; } }
如果想新增一个画三角形的功能,需要对上述代码进行修改:
// 绘图类 class GraphicEditor { public void drawShape(Shape shape) { if (shape.type == 1) drawRectangle(shape); else if(shape.type == 2) drawCircle(shape); else if(shape.type == 3) drawTriangle(shape); // 改动③ } public void drawRectangle(Shape s) { System.out.println("画矩形"); } public void drawCircle(Shape s) { System.out.println("画圆形"); } public void drawTriangle(Shape s) { System.out.println("画三角形"); } // 改动② } // 图形类 class Shape { int type; } class Rectangle extends Shape { Rectangle() { super.type = 1; } } class Circle extends Shape { Circle() { super.type = 2; } } class Triangle extends Shape { Triangle() { super.type = 3; } } // 改动①
一下子改动了三处,这里的绘图类可以看做 上游系统(调用方),图形类则是 下游系统(提供方),开闭原则的愿景:
对提供方可扩展,对调用方修改关闭(不改动或少改动)
所以这里明显是违背开闭原则的,那应该怎么做:
将可变部分封装起来,隔离变化,提供抽象化的不可变接口,供上游系统调用。当具体实现发生改变时,只需基于相同的抽象接口,扩展一个新的实现,替换掉旧实现即可,上游系统的代码几乎不需要修改。
按照这样的思想,我们来改动上面的代码,变化的是draw()方法,我们将Shape类改进为抽象类,并定义此方法,然后让子类实现。
// 绘图类 class GraphicEditor { public void drawShape(Shape shape) { shape.draw(); // 新增图形也无需修改代码 } } // 图形类 abstract class Shape { int type; public abstract void draw(); // 将变化部分抽象出来 } class Rectangle extends Shape { Rectangle() { super.type = 1; } @Override public void draw() { System.out.println("画矩形"); } } class Circle extends Shape { Circle() { super.type = 2; } @Override public void draw() { System.out.println("画圆形"); } } class Triangle extends Shape { Triangle() { super.type = 3; } @Override public void draw() { System.out.println("画三角形"); } }
现在如果想新增一个椭圆形,只需集成Shape,重写draw()方法,GraphicEditor无需任何改动。
如何做到 "对扩展开放、修改关闭":
时刻具备 扩展、抽象、封装意识,写代码时多思考下,这段代码未来可能有哪些需求变更,如何设计代码结构,实现留好扩展点,以便在未来需求变更的时候,在不改动代码整体结构、做到最小代码改动的情况下,将新代码灵活地插入到扩展点上。
③ 里式替换原则 (LSP,Liskov Substitution Principle)
子类对象能够替换程序中父类对象出现的任何地方,并且保证原来程序的逻辑行为不变及正确性不被破坏。
里式替换原则和多态是有区别的!!!
多态是面向对象编程的特性,一种代码实现思路,里式替换是一种 设计原则,用来指导继承关系中的子类该如何设计。多态语法代码实现子类替换父类不报错,不代表就符合 里式替换原则,原则除了子类能替换父类外,还不能改变原有程序逻辑及破坏原有程序的正确性。
一些违反了里式替换原则的例子:
- ① 子类违背父类声明要实现的功能 (如父类订单排序函数按金额升序排,子类重写此函数变成了按日期排);
- ② 子类违背父类对输入、输出、异常的约定 (如父类某函数输入可以是任何整数,子类实现时输入只允许正整数);
- ③ 子类违背父类注释中所罗列的任何特殊说明;
判断子类设计是否违背里式替换原则的小窍门:
拿父类单元测试去验证子类代码,如果某些单元测试运行失败,就有可能说明,子类违背了里式替换原则。