把书读薄 | 《设计模式之美》设计原则(上)(一)

简介: 继续啃《设计模式之美》,本文是 设计原则(15-22)浓缩总结,实战部分(23-26)拆到下节,没做过Web开发,要点时间消化。还是那句话:二手知识加工难免有所纰漏,感兴趣有时间的可自行查阅原文,谢谢。

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)


子类对象能够替换程序中父类对象出现的任何地方,并且保证原来程序的逻辑行为不变及正确性不被破坏。


网络异常,图片无法展示
|


里式替换原则和多态是有区别的!!!


多态是面向对象编程的特性,一种代码实现思路,里式替换是一种 设计原则,用来指导继承关系中的子类该如何设计。多态语法代码实现子类替换父类不报错,不代表就符合 里式替换原则,原则除了子类能替换父类外,还不能改变原有程序逻辑及破坏原有程序的正确性。


一些违反了里式替换原则的例子:


  • 子类违背父类声明要实现的功能 (如父类订单排序函数按金额升序排,子类重写此函数变成了按日期排);


  • 子类违背父类对输入、输出、异常的约定 (如父类某函数输入可以是任何整数,子类实现时输入只允许正整数);


  • 子类违背父类注释中所罗列的任何特殊说明


判断子类设计是否违背里式替换原则的小窍门


拿父类单元测试去验证子类代码,如果某些单元测试运行失败,就有可能说明,子类违背了里式替换原则。


相关文章
|
设计模式 关系型数据库
【设计模式——学习笔记】设计模式简介+七大设计原则介绍(下)
【设计模式——学习笔记】设计模式简介+七大设计原则介绍
150 0
|
16天前
|
设计模式 存储 关系型数据库
「全网最细 + 实战源码案例」设计模式——六大设计原则
本文介绍了面向对象设计中的六大原则,旨在提高软件系统的可维护性、可复用性和可拓展性。这些原则包括:开闭原则(OCP)、里氏代换原则(LSP)、依赖倒转原则(DIP)、接口隔离原则(ISP)、迪米特法则(LoD)和合成复用原则(CARP)。每项原则通过具体示例展示了如何通过抽象、多态、组合等方式降低耦合度,增强系统的灵活性与稳定性,从而提升开发效率并降低成本。
28 10
|
5月前
|
设计模式 Java 测试技术
Java设计模式-UML与设计原则(1)
Java设计模式-UML与设计原则(1)
|
6月前
|
设计模式 前端开发 JavaScript
React开发设计模式及原则概念问题之什么是HOC(Higher-order component),HOC遵循的设计原则都有哪些
React开发设计模式及原则概念问题之什么是HOC(Higher-order component),HOC遵循的设计原则都有哪些
|
7月前
|
设计模式 算法
交易链路设计原则&模式问题之中介者(Mediator)方法设计模式是什么,如何解决
交易链路设计原则&模式问题之中介者(Mediator)方法设计模式是什么,如何解决
|
9月前
|
设计模式 算法 Java
【设计模式系列笔记】设计模式与设计原则
设计模式,是一套被反复使用、多数人知晓的、经过分类编目的、代码设计经验的总结。 设计原则是一些通用的设计指导方针,它们提供了如何设计一个优秀的软件系统的基本思想和规则。指导着设计者如何组织代码以实现高内聚、低耦合、易扩展和易维护的软件系统。
99 4
|
9月前
|
设计模式 Java 数据安全/隐私保护
设计模式之六大设计原则
设计模式之六大设计原则
88 0
|
9月前
|
设计模式 关系型数据库 程序员
【设计模式】设计原则
【1月更文挑战第12天】【设计模式】设计原则
|
设计模式 程序员
设计模式-设计原则
设计模式-设计原则
|
设计模式 存储 Java
JAVA设计模式第一讲:设计原则
JAVA设计模式第一讲:设计原则
127 0

热门文章

最新文章