《敏捷软件开发——原则、模式与实践》读书笔记

1. 水在前面

开发圈公认的圣经级读物,前言里一句「如果你是一名开发人员,那么请从头到尾读一遍」,一下戳中了我。 既然吃开发这碗饭,也打算一直做下去,索性借回家细读。

罗伯特・C. 马丁《敏捷软件开发 ------ 原则、模式与实践(珍藏版)》,印象里早年翻过一遍,这次算重读,好好再啃一遍。

2. 敏捷实战

2.1. 敏捷宣言

  • **个体交互优先于过程和工具:**优秀的人未必都是顶尖的程序员,他们可以是处于平均水平的程序员,但可以和其他人合作无间。工具的使用从最简单开始,当不够用了再说。
  • **可以工作的软件优先于面面俱到的文档:**文档需要短小精悍,言简意赅,短就是最多只有12到24页,精就是应该描述整体设计的基本原理,只包含高层次的结构。培训新人通过坐在旁边亲自辅导和传授知识。
  • **客户合作优先于合同谈判:**成功的项目需要客户规律和频繁的反馈,保证开发团队和客户能在一起工作才是最好的合同。
  • **响应变化优先于遵循计划:**更好的计划策略是为接下来两周安排详细计划,接下来三个月安排比较粗超的计划,再远就是非常粗略的计划。

2.2. 敏捷原则

  • 我们看重的是,通过及早、持续交付有价值的软件,来满足客户的需求。
  • 欢迎需求有变化,即使在软件开发后期。轻量级的敏捷流程可以驾驭任何有利于提升客户竞争优势的变化。
  • 频繁交付能用起来的软件,频率从两周到两个月,倾向于更短的时限。
  • 业务人员和开发人员必须合作,这样的合作贯穿于整个项目中的每一天。
  • 围绕着主动性强的人来立项。为他们提供必要的环境和支持,同时信任他们能够干成是事情。
  • 开发团队内部以及跨团队之间,最有效和最有效的信息传递方式是,面地面进行对话。
  • 能有起来的软件,就是衡量进度的基本依据。
  • 敏捷流程倡导可持续的开发。发起人、开发人员和用户都能够长期保持一种稳定、可持续的节拍。
  • 持续保持对技术卓越和设计优良的关注,这是强化敏捷能力的前提。
  • 简洁为本,极简是消除浪费的艺术。
  • 最好的架构、需求和设计,是从自组织团队中涌现出来的。
  • 按规定的时间间隔,团队反思提效的方式,进而从行为上做出相应的优化和调整。

3. 极限编程实践

极限编程是一种敏捷方法,前面章节的宣言和原则只是给出了指引,而极限编程则是明明白白告诉我们要怎么做。

  • **客户团队成员:**客户是指定义产品特性并给这些特性排列优先级的人或者团队。XP项目要求客户必须和团队一起工作,如果不行,就指派一名客户代表。
  • **用户故事:**就是需求澄清过程的助记词,是一个计划工具。如果只是做计划,只要对需求了解到足够估算的程度就够了。
  • **短交付周期:**两周交付一次可工作的满足干系人部分需求的工作软件,并进行演示(此间客户不会调整优先级)。3个月发布一次较大的版本,大版本中的需求特性可以根据客户排的优先级进行调整。
  • **验收测试:**验收测试的编写要先与或者和用户故事的实现同步,并且是自动运行的,并且要集成到系统中反复运行。这样保证需求一旦实现,就永远不会被破坏,否则就无法通过验收测试了。
  • **结对编程:**两个人共同工作,保证每队组合每天至少要改变一次,这样就可以促使每个人都能对整个团队的工作有足够了解,甚至代替专家。
  • **测试驱动开发:**先写一个失败的测试,因为它测试的功能不存在,然后实现功能让测试通过,这样就能够让测试集成到系统中反复运行,并且通过单元测试促进代码解耦。
  • **集体所有权:**每一对成员都有权来去和改进任何模块中的代码。
  • **持续集成:**每天多次提交代码,率先提交的人成功提交入库,其他人得合并本地代码才能提交入库。
  • **可持续的开发速度:**绝对不加班,但是在发布前一周该规则例外。
  • **开放的工作空间:**所有人都一起工作,并且随时参与讨论。
  • 规划游戏:每次发布和迭代的开始,开发人员会基于一次迭代或发布的工作量估算出当前的预算。客户挑选出的用户故事其总花销不超过预算上限。
  • **简单设计:**只关注本轮迭代的用户故事,用最简单的方式实现,除非有十分明确的证据表明现在的方法不行才会考虑复杂的办法。
  • **重构:**在不改变代码行为的前提下,进行小步改造从而改进系统,并且无时无刻都要进行重构。
  • **隐喻:**就是为你要实现的功能打个贴切的比喻,让所有人都能够明白这个功能要干什么和你要干什么才能实现个个功能。例如"用户登录"比喻成"入职登记"

4. 计划

印象中几年前也看过关于敏捷计划的书,也写过一些东西,今天再看这方面的内容,感觉又有了新的感悟。。。当然这里的计划是对XP中规划游戏的描述,与其他敏捷方法类似但不完全一致。

4.1. 探究、分解和速度

项目开始的时候识别出所有真正重要的用户故事,然后把这些用户故事的实现细分成工作任务,并对这些工作任务进行工作量排序。这里的工作量排序并不需要精准到"3天完成"的程度,而是一种相对的排序,例如"用户登录"的工作量比"按照公式计算项目总成本"的工作量少,所以就排在前面。

完成排序后就以最前面的任务为1个单位,后面的任务则根据实际评估增加,例如第二个是1.3等等。这样每个任务都会拥有一个单位值。

然后我们讨论,最前面的任务搞完要多长时间(探究速度),这样再按比例为后面的任务算出时间,这样我们就有了一个大致的工作量评估了。

4.2. 发布计划

根据用户故事的优先级和前面的工作量评估,我们制定一个2-4个月的发布计划,当然这里面的内容是有客户挑选的用户故事,以及这些用户故事之间的粗略顺序。这个计划的内容和顺序可以根据在发布计划内的实际情况进行不断的调整,但是调整的一个原则是必须把一个完整的用户故事放在一次发布里面。

4.3. 迭代计划

一次发布计划是2-4个月,而一个迭代计划是2周。在每个迭代开始前,用户根据实际情况挑选最有价值的用户故事来实现,但是这些故事不能超过当前的速度限制。例如上一个迭代完成了14个单位的故事,那么这个迭代的就不要挑选需要20个单位才能完成的故事。

嗯,这里我也疑惑了,按照这个玩法,用户故事的评估时间只会越来越长,怎么办?

4.4. 任务计划

在每次迭代开始前,我们把挑选好的用户故事和任务挂在墙上,团队成员自己挑选感兴趣的任务去搞(书上说没有人会挑选超过自己上一迭代的任务量,那岂不是大家都越来越偷懒?)。这样能够促进成员掌握整个项目的不同知识领域,提高团队的协作能力。

当迭代进行到一半的时候,我们统计下完成了多少任务,是否需要采取措施去尽可能完成更多的有价值任务。这个时候估计可以硬塞了吧。。。

4.5. 迭代

当迭代结束时,团队向用户演示可运行的程序,让客户进行评价并把反馈写到新的用户故事中,从而对下个迭代进行调整,挑选任务。

5. 测试驱动开发(TDD)

前面已经讲过,先把测试写好,然后通过编写代码使测试通过。这里有几个敲黑板的词:"先"、"写测试" 还有**"通过"**。

一个"写字"告诉我们,测试首先必须是自动化!

5.1. 好处

  • 任何时候,测试都可以告诉我们系统是完好如初的(以前的测试一直在自动化运行)。
  • 先写测试,会使设计出来的软件便于调用。
  • 先写测试,强制我们将软件解耦。
  • 测试,就是一种最好的文档形式。

5.2. 意图编程(Intentional Programming)

在实现之前,现在测试中阐述你的意图,尽可能使其简单已读,并且相信这种简单和清晰能给程序指出不错的结构。只要按照测试所暗示的结构写出的程序就能通过测试。这种方法就是意图编程。

5.3. MOCK OBJECT模式

通过使用mock对象实现单元测试,从而意外获得解耦的效果。

模拟对象,或简称 Mock 对象,是一个在测试场景下用以模仿真实对象功能的伪造体。利用这些对象,开发者能构建出一个控制度极高的测试环境,方便模拟各类场景,为单元测试等提供便利,这样做让测试团队能集中精力于当前的代码片段。【1】

5.4. 验收测试

验收测试也是"写"出来的,通过编写脚本实现自动化的验收测试,从而意外获得架构设计和系统应用文档。

6. 重构

每个软件模块都有三个职责。第一项是运行所完成的功能。第二项是它要应对变化。第三项是要和读的人沟通。

这一章貌似要我读的下一本书《重构------改善既有代码的设计》。。。

7. 敏捷设计

7.1. 设计的臭味

僵化、脆弱、牢固、粘滞、不必要的重复、不必要的复杂性和晦涩。

7.2. 敏捷人员做什么

  1. 他们遵循敏捷实践去发现问题;
  2. 他们使用设计原则去诊断问题;
  3. 他们应用适当的设计模式去解决问题;
  4. 以上三方面互相作用就是软件设计。

8. 单一职责原则SRP

单一职责原则:对于一个类而言,应该仅有一个原因会引起它的变化。

这个类就违反了单一职责原则,它有两个职责,第一种是提供矩形几何形状的数学模型;第二种是把矩形绘制到一个图形化的用户界面上。并不是说两个函数就表示两种职责。在SPA的语境中,把职责定义为"变化的原因"。如果有超过一个动机去改变一个类,那么这个类就具有多种职责。例如上图中绘制和计算面积就是两种变化的动机。另一方面,如果应用程序变化时,都是要两个函数同时变化的,那么这个类就不违反原则了。也就是说,如果变化没法发生的化,那么就无从谈起违反原则了。

9. 开放封闭原则OCP

开放封闭原则:软件实体应该对扩展开放,但是对修改封闭。

cpp 复制代码
enum ShapeType{circle, square}

struct Shape
{
    ShapeType itsType;
};

struct Circle
{
    ShapeType itsType;
    // balabala
};
void DrawCircle(struct* Circle);

struct Square
{
    ShapeType itsType;
    // balabala
};
void DrawCricle(struct* Square);

typedef struct Shape *ShapePointer;

//程序的某几处地方都会出现以下的代码

ShapePointer s;

switch (s->itsType)
{
    case circle:
        DrawCircle((struct Circle*)s);
        break;
    case square:
        DrawSquare((struct Square*)s);
        break;
}

上述的代码就是违反OCP的,因为在程序的后续实现中switch(s->itsType)会反复的出现,当我们要增加一种新的形状Triangle时,除了要修改ShapeType枚举,还要把出现switch的地方都修改一遍。这就违反了对修改封闭的原则。

OCP要求我们适应变化,但并不是说我们在一开始设计的时候就要把所有可能的变化都考虑进去,我们也无法预测所有的可能。敏捷告诉我们,当遇到变化的时候,再开始考虑设计!而有一些方法可以帮助我们提前识别变化:先写测试、短迭代周期、经常展示特性给参与人员、首先开发重要的特性、尽早尽快发布软件。

10.里氏代换原则LSP

里氏代换原则:子类型必须能够替换掉它的基类。

LSP其实可以作为子类定义的一部分,因为LSP比IS-A更能够体现继承的本质。书上的原话是这样说的:***属于IS-A的含义过于宽泛不能作为子类型的定义。子类型的正确定义是"可替换的",这里的可替换性可以通过显式或者隐式的契约来定义。***这里稍微有点绕,还是以矩形和正方形的例子来说明吧。

从直观认识来讲,或者从教科书上说的,正方形是一种特殊的矩形,因此我们下意识的把这种"IS-A"带到了程序设计上,因此我们就会有下面这段设计。

cpp 复制代码
public class Rectangle
{
    public:
       void setWidth(double w);
       void setHeight(double h);
    private:
       double width;
       double height;
}

public class Square : public Rectangle
{
    public:
       void setLength(double l);
    private:
       double length;
}

上面这段代码的问题在于,当我们用一个子类替换父类的时候就会出现迷惑的行为,在这里如果求面积的话,父类长方形大概率算了个0值,这就是问题所在。当然你可以取个巧绕开这个问题如下:

cpp 复制代码
public class Square : public Rectangle
{
    public:
        void setLength(double l)
        {
            setWidth(l);
            setHeight(l); //大聪明如我
        }
    
}

Square sq = new Square();
Rectangle re = sq;
sq.setWidth(3);
sq.setHeight(2); //这时候期望的面积是6,但是算出来的其实算出来的却是4!!!!!

当你发现无论怎么弄子类都无法替代父类的时候,其实就已经说明了,这不是一种继承关系!我们唯一需要做的就是果断结束这种父子关系,这就是里氏代换原则的精髓!

11.依赖倒置原则DIP

依赖倒置原则:高层次模块不应该依赖于低层次模块,两者都应该依赖于抽象,抽象不应该依赖于细节,细节应该依赖于抽象。

高层次模块:定义了"做什么"(What)

低层次模块:负责"怎么做"(How)

书上说这里的倒置指的是面向对象的思路是和传统结构化编程方式的依赖方式相反的。这里的高层次模块个人理解的就是越接近用户的模块就越高层。具体以按钮与台灯的关系为例:

如果使用结构化编程的方式,则当Lamb修改时,Button也要进行修改,而使用面向对象的方式思考,Button和Lamb都应该依赖于抽象的Device,无论Lamb怎么修改,Button都可以保持不变,这就是所谓的依赖倒置。

12.接口隔离原则ISP

接口隔离原则:客户端程序不应该被迫依赖于它们不需要的方法。

个人理解这个原则跟单一职责原则说的是同一个原理吧,SRP是从被依赖者的角度看问题,而ISP是从依赖者的角度看问题。通俗的来说就是,当一个接口包含了很多组方法,而当这些方案并不全是在同一个客户端所需要时,那么我们就应该把这些方案拆开,避免客户端因为自己根本用不着的方法变化时被迫改变。(嗯,人就应该为自己而活,不要在意别人的看法。。。)

在这个例子中,因为并不是所有的Door类型都要用到Timer Client接口,所以通过一个多继承方法把Timer Client的方案隔离开来,避免因为其变化导致所有的Door都发生变化。

13.命令模式

书上说命令模式是最简单的模式,个人理解这只是一种调侃。因为上面第七章说到过:"用设计原则来诊断问题,用设计模式来解决问题。"那么很自然的想法就是,命令模式是用来解决什么问题的?它对应了哪一种设计原则?

在传统编程中,请求发送者(如一个 "开机按钮" 类)会直接调用接收者(如 "电视" 类)的具体方法(如 turnOn())。这种设计使发送者和请求者高度耦合,并且难以扩展。例如要增加一种开关,这需要在发送方增加相关代码,违反 "开放封闭原则(OCP)"(对扩展开放、对修改关闭)。

命令模式将 "请求"(如 "开机""静音")封装成独立的命令对象 (实现统一的 Command 接口,包含 execute() 方法),发送者只需持有 Command 接口的引用,无需知道具体接收者是谁。

当然,命令模式也能方便实现事务回滚、日志记录等需求(需要与组合模式和备忘录模式结合使用)

14.模板方法模式和策略模式

模板方法和策略模式都解决了从具体的上下文中分离出通用算法的问题,区别是模板方法模式使用了继承,策略模式使用了委托。

14.1模板方法模式

最近我在写一个从串口读取传感器数据的程序,程序的流程是先打开特定串口,读取和解析数据,然后把数据存储到数据库中。很简单的需求,一开始的代码是这样写的:

java 复制代码
public class ProPort{
    public static void main(String[] args){
        openPort();
        readData();
        saveData();
    }
}

刚开始只有一种传感器,我这个程序运行的挺好的,一直到老板要求增加第2种,第3中传感器的时候,我才发现这段代码是多么的难看。因为我需要在每个函数中增加传感器类型的判断,不同的传感器数据解析的方法还不一样。。。我才发现这段代码违反 "开放封闭原则(OCP)"(对扩展开放、对修改关闭)。

于是,我使用模板方法模式把代码改成了这种样子:

java 复制代码
public interface Handler{
    public void openPort();
    public void readData();
    public void saveData();
    public void closePort();
}

public class SensorA implements Handler{
    @Override
    public void openPort(){
        // do something;
    }
    @Override
    public void readData(){
        // do something;
    }
    @Override
    public void saveData(){
        // do something;
    }
    @Override
    public void closePort(){
        // do something;
    }
}

代码行数多了,但是,我再也不用担心增加传感器类型后,修改代码会不小心把原来的正常的功能搞错了。。。

14.2策略模式

后来写着写着,我发现上面的模板方法里,其实只有readData()会不同,其他的函数都是一个实现方式,嗯用专业的语言表达就是"有臭味",违反了DRY ( Don't Repeat Yourself**)原则。**

好吧,那我就把接口改成抽象类,然后重复的方法实现提取到抽象类中实现,只留下一个readData()作为抽象方法,让子类实现。嗯,这样的话对每个子类来说关心的只是一个函数而已了。那感觉子类的职责是不是太多了,本来就只是读数而已,是否违反了SRP(单一职责原则)?。。。不如把子类也干掉,把readData()提出来自己当一个对象。这不就是策略模式嘛。

java 复制代码
public class Handler{
    public void openPort(){
        // do something;
    }
    public void readData(){
        // do something;
    }
    public void saveData( DataReader dr){ // 把继承变成委托!
        dr.run();
    }
    public void closePort(){
        // do something;
    }
}

public Interface DataReader{
    public void run(); // 你爱怎么实现都行
} 

15.外观模式和中介者模式

查了些资料,个人认为外观模式和中介者模式不应该放在一起讨论,外观模式是对已有接口的重新封装和暴露,中介者模式则是解决不同对象之间的通信问题。可能是我理解不够透彻吧。。。

15.1外观模式

还是最近那个传感器的项目,这回说的是服务器端了,为了读写数据库,我写了InstrumentService 和SensorService两个类处理业务。后来遇到需要结合instrument和sensor来处理的事情,一开始我都放在InstrumentServie里面了,即把SensorService作为关联整个引入,但是这样做无论从语义上还是设计上都不合适,明显违反了ISP(接口隔离原则)

经过一番操作,最后增加了InstrumentSensorService来做这件事情,把原来的service层拆分为low-leval service 和 high-level service。 再问了下AI,这妥妥的就是外观模式的应用了。后来再细思,其实我们的原始分层里面,把mapper类封装到service类里面,本来就已经体现了外观模式的思想。

15.2中介者模式

吹过了嵌入式和服务器,这回说说前端的问题。这块代码我感觉就是一坨屎,一开始我把html标记和js逻辑处理都放一个文件,后来发现行数实在太多了,就拆开了html和js两个文件,现在看来js的问题不单是长,还有乱。。。主要的内容都集中在控件的交互上,例如select控件选值变了,表格和图表的内容都要及时更新;timepicker控件选值变了表格和图表的内容也要及时更新。。。反正就是网状关系图憋。

其实认真思考这个问题,会发现每个控件都是只需要完成自己的工作就好了,不应该知道别的控件的状态,现在这个状况,明显违反了SRP(单一职责原则)迪米特法则(一个对象应尽量少地了解其他对象,仅与直接关联的 "朋友" 通信,避免不必要的依赖。)。

今天看到中介者模式的介绍,居然典型应用就是UI的交互控制,通过中介类来统一处理不同控件间的逻辑处理,使得每个控件只知道自己和中介,从而实现不同控件之间的解耦。再深挖突然发现怎么这种处理方式有点熟悉,这不就是MVVM中ViewModel其中的一项职责么!看来前端有重构的必要了,还不知道老板能不能多给几天,我想直接把框架都改成基于vue的,现在的js都得改。。。

16.单例模式和单状态模式

看到这个标题,我还特意去翻了《设计模式:可复用面向对象软件的基础》,真没有提到单状态模式,啥时多出来的?

从单例模式的实现和作用来看,应该是体现了单一职责原则(SRP)。

16.1单例模式

在 Spring Boot(以及整个 Spring 框架)中,@Component 及其派生注解(如 @Service@Controller@Repository)标记的 Bean 默认采用 **单例(Singleton)作用域。**查了下资料,发现这一设计是基于对性能、资源效率、使用场景的综合考量。通过减少资源消耗、适配无状态组件、简化容器管理,满足了绝大多数企业级应用的性能和易用性需求。

记得初次接触单例模式时,还觉得它只是面向对象编程的一种炫技,无非是将构造函数私有化,并通过静态成员变量来获取实例。直到真正投入工作后才发现,几乎所有的框架底层都在使用单例模式。。。

java 复制代码
public class SingletonHungry {
    // 1. 私有静态变量
    private static final SingletonHungry INSTANCE = new SingletonHungry();

    // 2. 私有构造函数,禁止外部创建实例
    private SingletonHungry() {}

    // 3. 公开静态方法,提供全局访问点
    public static SingletonHungry getInstance() {
        return INSTANCE;
    }
   
    public void doSomething() {
        //do something
    }
}

16.2单状态模式

单例模式有个非常不好的缺点,就是没有一个好的方法来销毁单例对象或者接触器职责。而单状态模式就能弥补这个缺点。

单例模式是把实例做成静态成员,而单状态模式则是把成员变量做成静态成员变量,对使用使用者来说两者的效果是一致的,但是单状态模式却能够定义良好的创建和销毁方法。

java 复制代码
import java.util.concurrent.atomic.AtomicInteger;

public class MonostateExample {
    // 共享状态(所有实例共享)
    private static AtomicInteger sharedState = new AtomicInteger(0);
        
    
    // 构造函数(允许创建多个实例)
    public MonostateExample(String name) {
        this.instanceName = name;
    }
    
    // 获取共享状态
    public int getSharedState() {
        return sharedState.get();
    }
    
    // 设置共享状态(所有实例共享这个值)
    public void setSharedState(int value) {
        sharedState.set(value);
    }   
    
}

17.空对象模式

又是一个《设计模式》里没有提及的名词,但是却很好理解。我们经常在代码中写出这样的内容:

java 复制代码
if (null == User){
    //show exception or return;
}

这代码其实也没有什么大逆不道的地方,就是不太美观,而容易忘记判断,如果非要给它定个罪,我决定应该是违反了DIP依赖倒置原则 吧。其实在JAVA8+之后,我们一般用Optional 容器类来弄。书上给出了这玩意一种很优雅的解决方式,就是当User是空值的时候,并也返回一个特殊的User对象。为什么说特殊呢,就是它是一个User对象,也有User的所有方法,只是这些方法都不干活。

java 复制代码
Public interface User{
    public void work();
    public static final User nullUser = new User(){
        public void work(){
            //donothing
        }
    };
}

我们不直接返回User的空值,而是返回User.nullUser, 这样就可以不适用null == User判断了。

18.包的设计模式

我还真的分别去豆包和ds问了包和模块有什么区别,模块一般是只一个文件,包是一个目录(包含了模块)。书中的原文是:"通过将不同的类分组到不同的包中,我们可以在更高层次的抽象中研究软件的设计。我们还可以用这些包来对软件的开发和发布进行管理"。

18.1包的内聚性原则

包的内聚性原则,可以帮助开发人员决定如何将不同的类划分到不同的包中。

**重用发布等价原则(REP):**要么一个软件包中所有的类都可以复用,要么他们都不可以被复用。

**共同重用原则(CRP):**没有一定依赖关系的类不应该被放在同一个包中。

**共同封闭原则(CCP):**一个包不应该有多个可被改变的原因。

18.2包的耦合性原则

包的耦合性原则,告诉我们包之间是怎么依赖的。

**无环依赖原则(ADP):**包之间的依赖关系不应该存在环形。

**稳定依赖原则(SDP):**向着稳定的方向依赖。

**稳定抽象原则(SAP):**稳定包应该是抽象的,不稳定的包应该是具体的。

19.工厂模式

"实际上,可以说任何使用new关键字的代码都违反了DIP(依赖倒置)原则,有些时候,虽然违背了DIP,但也是无害且非常常见的。"

又让我回忆起了那些年拼搏奋斗的日子。那位大牛对我说:"设计的目的是什么,为什么要用设计模式?因为我们的需求发生了变化,因为我们要用尽量小的代码去应对需求的变化。"

如果你确定了代码永远不会变化,客户永远不会提出新的需求,其实违反了DIP又如何呢,这也正是我最近迷茫的所在。现在AI已经可以帮我们写大部分的代码了,过两年可能AI将会完全替代程序员去写全部代码,那么我们现在学的设计模式、架构还有什么用呢,反正AI可以把100人天变成1天,代价就是电费而已,只要代码能跑起来,机器才不会管你代码是否优雅。

扯远了,回到正题,工厂模式的作用就是用来代替new关键字的。既然说new是违反了DIP,那我们就造一个抽象的工厂来替代new。

举个栗子,我的netty服务器里面有一个地方是decode协议的,一开始只有一种协议的时候,直接解码就好了。后来协议类型越来越多,在处理的时候就要根据不同的协议类型采取不同的解码方式,也就是很多if-else出现了。然后把每一种协议抽象成一个协议类,每一个类代表一种协议类型,这样就只剩下一个if-else了(每次new 的时候就要if-else)。然后就会发现增加协议类都要修改这个new,所以我们用工厂来替换new, 把工厂的实现抽象出来,这样以后要增加协议,只修改这个工程就行了,以前的代码不需要修改。这就是工厂模式的本质作用。

20. 组合模式

当你要同时处理很多不同的类,而这些类的行为其实是差不多一致的,这时候,组合模式就是很好的选择,而且,组合模式除了能够代表这些类,还能够代表这些类的容器。

从这个角度来看,如果不使用组合模式(以我的尿性来写代码),就是直接把这些类写出来,然后逐个对象去NEW,最后客户端直接耦合引用这些类。当需要修改某个类的实现时,需要把整个代码都修改一遍,这样明显是违反了DIP原则的

java 复制代码
public interface Shape{
    public void draw();
}

public class CompositeShape implements Shape{
    private Vector itsShapes = new Vector();
    public void add(Shape s){
        itsShapes.add(s);
    }
    publid void draw(){
        for (int i=0; i<itsShapes.size(); i++){
            Shape shape = (Shape)itsShape.elementAt(i);
            shape.draw();
        }
    }
}

21. 观察者模式

"如果你熟悉设计模式,那么在面临一个设计问题的时候,脑子里很可能会浮现出一个模式。随后的问题就是直接实现这个模式呢,还是通过一系列小步骤不断地演进呢?"本章提出了我一直很纠结的问题,同时也给出了正确的答案:根据实际情况决定。

Netty 中观察者模式的典型场景是异步结果监听ChannelFuture)。还有MVVM模式中,「数据驱动、双向绑定、UI 自动刷新」,也是观察者模式的体现。

假设,我们不适用观察者模式,那么当被观察者的实现修改了,主体代码必须修改,这违反了OCP原则。 同时因为主体是定义做什么的,而被观察者是定义做什么的,主体代码依赖被观察者,就意味着违反了DIP原则。

22. 抽象服务器模式、适配器模式、桥接模式

三种模式,是OCP和DIP的层层递进。

22.1. 抽象服务器模式

抽象服务器模式是将服务的契约(接口 / 抽象)与具体实现彻底分离,让客户端仅依赖抽象接口而非具体实现类,从而实现解耦、可扩展、可维护的系统架构。

本质是依赖倒置原则(DIP)的直接体现,同时完美契合开闭原则(OCP) 和里氏替换原则(LSP)。

22.2. 适配器模式

适配器模式,是在抽象服务器模式的基础上,把具体实现进一步解耦出来(当具体实现也存在多种方式时),特别是,我们无法把具体实现彻底抽象时,甚至我们压根就只是使用了某个插件包。这时候,我们需要的,是委托实现。

22.3. 桥接模式

接着适配器模式,当我们发现连Switch 都会存在多种实现时,上面我们可以把它进一步抽象(当然,你也可以进一步委托。。。。)。

23. 代理模式

代理模式的意图是为其他对象提供一种代理以控制这个对象的访问。

代理模式最经典的应用场景之一便是数据库访问代理。以订单新增方法 void addOrder(String name, Double price) 为例:若直接编写该方法,内部需要实例化订单对象、拼接 SQL 语句并执行数据库操作。这种写法虽然实现简单,却存在明显缺陷:一方面,addOrder 同时承载业务逻辑与数据访问逻辑,违背了单一职责原则 (SRP) ;另一方面,代码强依赖具体数据库实现,也不符合依赖倒置原则 (DIP)。而借助代理模式,能够有效解决上述两类问题。

24. 访问者模式

当系统已存在稳定的类层次结构,且需要为每个类添加一套可变的功能时(这些功能在不同类中有不同实现,并且功能本身也可能存在多种变体,如适配不同操作系统的情况),访问者模式是最佳解决方案。

访问者模式的核心优势在于:无需修改现有类层次结构,就能扩展其功能访问能力。该模式通过"双重分发"技术巧妙地解决了上述需求,这是其实现的关键机制。

25. 装饰模式

动态扩展类功能的核心在于不修改原有类的代码,而是通过外部手段为其添加新功能或属性。这种方法遵循开闭原则(OCP),即对扩展开放,对修改关闭。

26. 状态模式

状态模式是一种行为设计模式,它通过将不同状态的行为封装到独立的类中,来替代传统的基于条件语句的状态管理方式。这种模式主要解决了以下核心问题:

  • 消除复杂的条件判断 传统的状态管理通常会导致冗长的switch/case或if/else语句块,随着状态数量的增加,代码会变得难以阅读和维护。状态模式通过将每个状态的行为封装到单独的类中,实现了更清晰的结构。

  • 遵循开闭原则(OCP) 当需要添加新状态时,只需创建新的状态类而不需要修改现有代码,这完全符合对扩展开放、对修改关闭的原则。

  • 符合单一职责原则(SRP) 每个状态类只负责该状态下的行为逻辑,使得每个类的职责更加单一和明确

​​​​​​​

27. 最后再说两句

这本书记竟断断续续读了两年,连带这篇学习笔记也写了两年光景,今天总算画上了句号。回想起来实在汗颜,原本三个月就能完成的事,硬是拖成了漫长的马拉松。说到底还是惰性作祟,每逢假期总信誓旦旦要一鼓作气读完,结果不是沉迷小说就是追剧度日......

如今全书总算啃完,却仍有几分忐忑。书中几个实战案例并未完全消化,否则定会留下详细心得笔记,看来日后还得回头精读这部分内容。

合上书本才惊觉,自己对设计模式的理解实在肤浅。计划重新系统学习,这次要站在全局视角来审视这些模式。

恰逢完成之际,LLM技术突然席卷而来,软件行业首当其冲。前几日与同事闲聊,谈及程序员正被AI取代的现状,我补充道:何止编码岗位,设计师和架构师的地位同样岌岌可危。细想之下,**软件设计工具的诞生本就是为了应对需求变更------说白了就是用最小成本适应变化,本质是种经济行为。**当LLM能随时重写代码时,所谓的设计与架构,莫非只剩炫技的价值?