敏捷开发笔记(第9章节)--开放-封闭原则(OCP)

目录

1:PDF上传链接

9.1 开放-封闭原则(OCP)

9.2 描述

9.3 关键是抽象

9.3.1 shape应用程序

9.3.2 违反OCP

糟糕的设计

9.3.3 遵循OCP

9.3.4 是的,我说谎了

9.3.5 预测变化和“贴切的”结构

9.3.6 放置吊钩

1.只受一次愚弄

2.刺激变化

9.3.7 使用抽象获得显示封闭

9.3.8 使用“数据驱动”的方法获取封闭性

9.4 结论


1:PDF上传链接

【免费】敏捷软件开发(原则模式与实践)资源-CSDN文库

        Ivar Jacobson曾说过,“任何系统在其生命周期中都会发生变化。如果我们期望开发出的系统不会再第一版后就被抛弃你就必须牢牢记住一点”。Bertrand Meyer在1988年提出著名的开发-封闭原则(The Open - closed Principle,简称OCP)为我们提供了指引。

9.1 开放-封闭原则(OCP)

        软件实体(类、模块、函数等等)应该是可以扩展的,但是不可修改的。

        如果程序中的一处改动就会产生连锁反应,导致一系列相关模块的改动,那么设计就具有僵化性的臭味。OCP建议我们应该对系统进行重构,这样以后对系统在进行那样的改动时,就不会导致更多的修改。如果正确地应用OCP,那么以后再进行同样的改动,就只需要添加新的代码,而不必改动已经正常运行的代码。

        也许,这看起来像是重所周知的可望而不可及的美好理想-----然后,事实上却有一些相对简单并且有效的策略可以帮助接近这个理想。

9.2 描述

        遵循开放-封闭原则设计出的模块具有两个主要的特征。它们是:

        1:对于扩展是开放的(Open for extension)

                这意味着模块的行为是可以扩展的,当应用的需求改变时,我们可以对模块进行扩展,使其具有满足那些改变的新行为,换句话说,我们可以改变模块的功能。

        2:对更改是封闭的(closed for modification)

                对模块行为进行扩展时,不必改动模块的源代码或者二进制代码。模块的二进制可执行版本,无论是可链接的库、dll或者Java的jar文件都无需改动。

        这个两个特征好像是相互矛盾的,扩展模块行为的通常方式就是修改该模块的源代码。不允许修改的模块常常都被认为具有固定的行为。

        怎么可能在不改动模块源代码的情况下,去更改它的行为呢?怎么才能在无需对模块进行改动情况下就改变它的功能呢?

9.3 关键是抽象

        在C++、Java或者其他任何的OOPL(面向对象编程语言)中,可以创建出固定去能够描述一组任意个可能行为的抽象体。这个抽象体就是抽象类。而这一组任意个可能得行为则表现为可能得派生类。

        模块可以操作一个抽象体。由于模块依赖于一个固定的抽象体,所以他对于更改可以是关闭的,同时,通过从这个抽象体派生,也可以扩展此模块的行为。

       

9.3.1 shape应用程序

        下面的例子在许多讲述OOD(面向对象的设计)的书中都提过。他就是声名狼藉的“shape”样列。它常常被用来展示多态的工作原理。不过,这次我们将使用它来阐明OCP。

        我们有一个需要再标准的GUI上面绘制圆和正方形的应用程序。圆和正方形必须要按照特定的顺序绘制。我们将创建一个列表,列表由按照适当的顺序排列的圆和正方形组成,程序遍历该列表,依次绘制出每个圆和正方形。

9.3.2 违反OCP

        如果使用C语言,并采用不遵循OCP的过程化方法,我们也许会得到程序9.1中所示的解决方法。其中,我们看到了一组的数据结构,它们的第一个成员都相同,但是其余的成员都不同。每个结构中的第一个成员都是一个用来标识该结构是代表圆或者正方形的类型码。DrawAllShapes函数遍历一个数组,该数组的元素是指向这些数据结构的指针,DrawAllShapes函数先检查类型码,然后根据类型码调用对应的函数(DrawCircle或者DrawSquare)。

程序9.1 Square/Circle问题的过程化解决方案
--shape.h --
enum ShapeType {circle,square
};    
struct Shape {ShapeType itsType;
}
-circle.h ---
struct Circle {ShapeType itsType;double itsRadius;Point itsCenter;
}
-square.h ----
struct Square {ShapeType itsType;double itsside;Point itsTopLeft;
}:
--drawA11 Shapes.cc----------------
typedef struct Shape *Shapepointer;void DrawAllShapes(ShapePointer list[], int n)
{int i;for (i = 0; i < n; i++){struct Shape *s = list[i];switch (s->itsType) {case square:DrawSquare((struct Square*)s);break;case circle:Drawcircle((struct circle*)s);Break;default:break;}}
}

        DrawAllShapes函数不符合OCP,因为它对于新的形状类型的添加不是封闭的。如果希望这个函数能够绘制包含有三角形的列表,就必须得更改这个函数。事实上,每增加一种新的形状类型,都必须要更改这个函数。
        当然这只是一个简单的例子。在实际程序中,类似DrawAllShapes函数中的switch语句会在应用程序的各个函数中重复不断地出现,每个函数中switch语句负责完成的工作差别甚微。这些函数中,可能有负责拖曳形状对象的,有负责拉伸形状对象的,有负责移动形状对象的,有负责删除形状对象的,等等。在这样的应用程序中增加一种新的形状类型,就意味着要找出所有包含上述switch语句(或者链式if/else语句)的函数,并在每一处都添加对新增的形状类型的判断。
        更糟的是,并不是所有的switch语句和if/else链都像DrawAllShapes中的那样有比较好的结构。更有可能的情形是,if语句中的判断条件由逻辑操作符组合而成,或者是处理方式相同的case语句被成组处理。在一些极端错误的实现中,会有一些函数对于Square的处理竞然和对于circle的处理一样。在这样的函数中,甚至根本就没有switch/case语句或者if/else链。这样,要发现和理解所有的需要增加对新的形状类型进行判断的地方,恐怕就非常的困难了。
        同样,在进行上述改动时,我们必须要在ShapeType enum中添加·个新的成员。由于所有不同种类的形状都依赖于这个eum的声明,所以我们必须要重新编译所有的形状模块。并且也必须要重新编译所有依赖于Shape类的模块。

糟糕的设计

        再来回顾一下。程序9.1中的解决方法是僵化的,这是因为增加Triangle会导致Shape、Square、Circle以及DrawAllShapes的重新编译和重新部署。该方法是脆弱的,因为有许多其他的即难以查找又难以理解的switch/case或者lse语句。该方法是牢固的,因为想在另一个程序中复用DrawAllShapes时,都必须要附带上Square和Circle,即使那个新程序不需要它们。因此,在程序9.1中展示了许多糟糕设计的臭味。

9.3.3 遵循OCP

        程序9.2中展示了一个square/circle问题的符合OCP的解决方案。在这个方案中,我们编写了一个名为Shape的抽象类。这个抽象类仅有一个名为Draw的抽象方法。Circle和Square都从Shape类派生。

程序9.2问题的OoD解决方案class Shape {public:virtual void Draw () const = 0;
};class Square: public Shape {public:virtual void Draw() const = 0;
};class circle: public Shape{public:virtual void Draw () const = 0;
};void DrawAllShapes(vector<Shape*> & list)
{vector<Shape*>::iterator I;for (i = list.begin(); i != list.end(); i++) {(*i)->Draw ();}
}

        可以看到,如果我们想要扩展程序9.2中DrawAllShapes函数的行为,使之能够绘制一种新的形状,我们只需要增加一个新的Shape类的派生类.DrawAllShapes函数并不需要改变,这样DrawAllShapes就符合了OCP。无需改动自身代码,就可以扩展它的行为。实际上,增加一个Triangle类对于这里展示的任何模块完全没有影响。很明显,为了能够处理Triangle类,必须要改动系统中的某些部分,但是这里展示的所有代码都无需改动。
      在实际的应用程序中,Sape类可能会有更多的方法。但是在应用程序中增加一种新的形状类型依然非常简单,因为所需要做的工作只是创建Sape类的新的派生类,并实现它的所有函数。再也不需要为了找出需要更改的地方而在应用程序的所有地方进行搜寻。这个解决方案不再是脆弱的。
        同时,这个方案也不再是僵化的。在增加一个新的形状类型时,现有的所有模块的源码都无需改动,并且现有的所有二进制模块都无需进行重新构建(rebuild)。只有一个例外,那就是实际创建Shape类新的派生类实例的模块必须被改动。通常情况下,创建Shape类新的派生类实例的工作要么是在main中或者被main调用的一些函数中完成,要么是在被main创建的一些对象的方法中完成。
        最后,这个方案也不再是牢固的。现在,在任何应用程序中重用DrawAllShapes时,都无需再附带上Square和Circle。因而,这个解决方案就不再具有前面提及的任何糟糕设计的特征。
        这个程序是符合OCP的。对它的改动是通过增加新代码进行的,而不是更改现有的代码。因此,它就不会引起像不遵循OCP的程序那样的连锁改动。所需要的改动仅仅是增加新的模块,以及为了能够实例化新类型的对象而进行的围绕main的改动。

9.3.4 是的,我说谎了

        上面的例子其实并非是100%封闭的!如果我们要求所有的圆必须在正方形之前绘制,那么程序9.2中的DrawAllShapes函数会怎样呢?DrawAllShapes函数无法对这种变化做到封闭。要实现这个需求,我们必须要修改DrawAllShapes的实现,使它首先扫描列表中所有的圆,然后再扫描所有的正方形。

9.3.5 预测变化和“贴切的”结构

        如果我们预测到了这种变化,那么就可以设计一个抽象来隔离它。我们在程序92中所选定的抽象对于这种变化来说反倒成为一种障碍。可能你会觉得奇怪:还有什么比定义一个Shape类,并从它派生出Square类和Cice类更贴切的结构呢?为何这个贴切的模型不是最优的呢?很明显,这个模型对于一个形状的顺序比形状类型具有更重要意义的系统来说,就不再是贴切的了。
        这就导致了一个麻烦的结果,一般而言,无论模块是多么的“封闭”,都会存在一些无法对之封闭的变化。没有对于所有的情况都贴切的模型。
        既然不可能完全封闭,那么就必须有策略地对待这个问题。也就是说,设计人员必须对于他设计的模块应该对哪种变化封闭做出选择。他必须先猜测出最有可能发生的变化种类,然后构造抽象来隔离那些变化。

        这需要设计人员具备一些从经验中获得的预测能力。有经验的设计人员希望自己对用户和应用领域很了解,能够以此来判断各种变化的可能性。然后,他可以让设计对于最有可能发生的变化遵循OCP原则。
        这一点不容易做到。因为它意味着要根据经验猜测那些应用程序在生长历程中有可能遭受的变化。如果开发人员猜测正确,他们就获得成功。如果他们猜测错误,他们会遭受失败。并且在大多数情况下,他们都会猜测错误。
        同时,遵循OCP的代价也是昂贵的。创建正确的抽象是要花费开发时间和精力的。同时,那些抽象也增加了软件设计的复杂性。开发人员有能力处理的抽象的数量也是有限的。显然,我们希望把OCP的应用限定在可能会发生的变化上。
        我们如何知道哪个变化有可能发生呢?我们进行适当的调查,提出正确的问题,并且使用我们的经验和一般常识。最终,我们会一直等到变化发生时才采取行动。

9.3.6 放置吊钩

        我们怎样去隔离变化呢?在上个世纪,我们常常说的一句话是,我们会在我们认为可能发生变化的地方放置吊钩(hook)。我们觉得这样做会使软件灵活一些。
        然而,我们放置的吊钩常常是错误的。更糟的是,即使不使用这些吊钩,也必须要去支持和维护它们,从而就具有了不必要的复杂性的臭味。这不是一件好事。我们不希望设计背着许多不必要的抽象。通常,我们更愿意一直等到确实需要那些抽象时再把它放置进去。

1.只受一次愚弄

        有句古老的谚语说:“愚弄我一次,应感羞愧的是你。再次愚弄我,应感羞愧的是我。”这也是一种有效的对待软件设计的态度。为了防止软件背着不必要的复杂性,我们会允许自己被愚弄一次。这意味着在我们最初编写代码时,假设变化不会发生。当变化发生时,我们就创建抽象来隔离以后发生的同类变化。简而言之,我们愿意被第一颗子弹击中,然后我们会确保自己不再被同一只枪发射的其他任何子弹击中。

2.刺激变化

        如果我们决定接受第一颗子弹,那么子弹到来的越早、越快就对我们越有利。我们希望在开发工作展开不久就知道可能发生的变化。查明可能发生的变化所等待的时间越长,要创建正确的抽象就越困难。
        因此,我们需要去刺激变化。我们已在第2章中讲述的一些方法来完成这项工作。
        (1)我们首先编写测试。测试描绘了系统的一种使用方法。通过首先编写测试,我们迫使系统成为可测试的。在一个具有可测试性的系统中发生变化时,我们可以坦然对之。因为我们已经构建了使系统可测试的抽象。并且通常这些抽象中的许多都会隔离以后发生的其他种类的变化。
        (2)我们使用很短的迭代周期进行开发个周期为几天而不是几周。
        (3)我们在加入基础结构前就开发特性,并且经常性地把那些特性展示给涉众。
        (4)我们首先开发最重要的特性。
        (5)尽早地、经常性地发布软件。尽可能快地、尽可能频繁地把软件展示给客户和使用人员。

9.3.7 使用抽象获得显示封闭

        第一颗子弹已经击中我们,用户要求我们在绘制正方形之前先绘制所有的圆。现在我们希望可以隔离以后所有的同类变化。
        怎样才能使得DrawAllShapes函数对于绘制顺序的变化是封闭的呢?请记住封闭是建立在抽象的基础之上的。因此,为了让DrawAllShapes对于绘制顺序的变化是封闭的,我们需要一种“顺序抽象体”。这个抽象体定义了一个抽象接口,通过这个抽象接口可以表示任何可能的排序策略。
        一个排序策略意味着,给定两个对象可以推导出应该先绘制哪一个。我们可以定义一个Shpe类的抽象方法叫作Precedes.。这个方法以另外一个Shape作为参数,并返回-一个bool型结果。如果接收消息的Shape对象应该先于作为参数传入的Shape对象绘制,那么函数返回true。
        在C++中,这个函数可以通过重载operator<来表示。程序9.3中展示了添加了排序方法后的Shape类。
        既然我们已经有了决定两个Shape对象的绘制顺序的方法,我们就可以对列表中的shape对象进行排序后依序绘制。程序9.4展示了C++的实现代码。

图9.3.7.1

         这给我们提供了一种对Shape对象排序的方法,也使得可以按照一定的顺序来绘制它们。但是我们仍然没有一个好的用来排序的抽象体。按照目前的设计,Shape对象应该覆写Precedes方法来指定顺序。这究竟是如何工作的呢?我们应该在Circle:Precedes成员函数中编写一些什么代码,来保证圆一定会被先于正方形绘制呢?请看程序9.5。

 图9.3.7.2

        显然这个函数以及所有Shape类的派生类中的Precedes函数都不符合OCP。没有办法使得这些函数对于Shape类的新派生类做到封闭。每次创建一个新的Shape类的派生类时,所有的Precedest)函数都需要改动。

        当然,如果从来不需创建新的Shape类的派生类,就没有关系了。另一方面,如果需要频繁的创建新的Sape类的派生类,这个设计就会遭到沉重的打击。我们再次被第一颗子弹击中。

9.3.8 使用“数据驱动”的方法获取封闭性

        如果我们要使Shape类的各个派生类间互不知晓,可以使用表格驱动的方法。程序9.6展示了一·种可能的实现。

         通过这种方法,我们成功地做到了一般情况下DrawAllShapes函数对于顺序问题的封闭,也使得每个Shape派生类对于新的Shape派生类的创建或者基于类型的Shape对象排序规则的改变是封闭的。(比如,改变顺序为正方形必须最先绘制。)

        对于不同的Shapes的绘制顺序的变化不封闭的惟-一部分就是表本身。可以把表放置在一个单独的模块中,和所有其他模块隔离,因此对于表的改动不会影响到其他任何模块。事实上,在C++中,我们可以在链接时选择要使用的表。

9.4 结论

        在许多方面,OCP都是面向对象设计的核心所在。遵循这个原则可以带来面向对象技术所声称的巨大好处(也就是,灵活性、可重用性以及可维护性)。然而,并不是说只要使用一种面向对象语言就是遵循了这个原则。对于应用程序中的每个部分都肆意地进行抽象同样不是一个好主意。正确的做法是,开发人员应该仅仅对程序中呈现出频繁变化的那些部分做出抽象。拒绝不成熟的抽象和抽象本身一样重要。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/363879.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

2024年华东杯B题数学建模论文:基于车辆运动学转弯模型的自动驾驶规划问题

摘要 随着自动驾驶技术的发展&#xff0c;车辆转弯问题成为关键挑战。本文针对自动驾驶车辆在转弯过程中的数学建模、路径规划及避障策略进行了深入研究&#xff0c;旨在提升自动驾驶车辆的行驶安全性与效率。 针对问题1&#xff0c;对于四轮前轮驱动车辆的转弯问题&#xff0c…

MCU解决800V电动汽车牵引逆变器的常见设计挑战的3种方式

电动汽车 (EV) 牵引逆变器是电动汽车的。它将高压电池的直流电转换为多相&#xff08;通常为三相&#xff09;交流电以驱动牵引电机&#xff0c;并控制制动产生的能量再生。电动汽车电子产品正在从 400V 转向 800V 架构&#xff0c;这有望实现&#xff1a; 快速充电 – 在相同…

嵌入式Linux系统编程 — 4.5 strcmp、strchr 等函数实现字符串比较与查找

目录 1 字符串比较 1.1 strcmp() 函数 1.2 strncmp() 函数 1.3 示例程序 2 字符串查找 2.1 strchr() 函数 2.2 strrchr() 函数 2.3 strstr() 函数 2.4 strpbrk() 函数 2.5 示例程序 1 字符串比较 strcmp() 和 strncmp() 函数是C语言标准库中用于比较两个字符串的函…

【STM32修改串口波特率】

STM32微控制器中的串口波特率调整通常涉及到USART&#xff08;通用同步接收器/发送器&#xff09;模块的配置。USART模块提供了多个寄存器来设置波特率&#xff0c;其中关键的寄存器包括BRR&#xff08;波特率寄存器&#xff09;和USART_CR1&#xff08;控制寄存器1&#xff09…

JVM专题十:JVM中的垃圾回收机制

在JVM专题九&#xff1a;JVM分代知识点梳理中&#xff0c;我们主要介绍了JVM为什么采用分代算法&#xff0c;以及相关的概念&#xff0c;本篇我们将详细拆分各个算法。 垃圾回收的概念 垃圾回收&#xff08;Garbage Collection&#xff0c;GC&#xff09;确实是计算机编程中的…

python-斐波那契数列

[题目描述] 斐波那契数列是指这样的数列&#xff1a;数列的第一个和第二个数都为 1&#xff0c;接下来每个数都等于前面 2个数之和。 给出一个正整数 a&#xff0c;要求斐波那契数列中第 a 个数是多少。输入&#xff1a; 第 1 行是测试数据的组数 n&#xff0c;后面跟着 n 行输…

Dahlia Hart: Stylized Casual Character(休闲角色模型)

此包包含两个发型和两个服装&#xff0c;每个都有多种颜色选择。每个发型都适合与物理资源一起使用&#xff0c;并包含各种表情和音素混合形状。 下载&#xff1a;​​Unity资源商店链接资源下载链接 效果图&#xff1a;

算法设计与分析--近似算法内容整理

文章目录 P、NP、NP-hard 和 NPC多项式时间概念区分NP-hard 的证明例题 1 证明 T S P TSP TSP 问题是 N P − h a r d NP-hard NP−hard 问题 。例题 2 证明最大加权独立集问题是 N P − h a r d NP-hard NP−hard 问题。 扩展 NP-hard 问题3-SAT 问题TSP 旅行商问题 Load B…

探索高效开发神器:Blackbox AI(免费编程助手)

人不走空 &#x1f308;个人主页&#xff1a;人不走空 &#x1f496;系列专栏&#xff1a;算法专题 ⏰诗词歌赋&#xff1a;斯是陋室&#xff0c;惟吾德馨 &#x1f916; 想要代码生成&#xff1f;&#x1f44c; &#x1f4ac; 需要和AI聊天解决难题&#xff1f;&#…

ORBSLAM3_ROS_Ubuntu18_04环境搭建安装

orbslam3安装 ORB-SLAM3配置及安装教程&#xff08;2023.3&#xff09;_orbslam3安装-CSDN博客 换源&#xff0c;换成国内的 搜索software 安装工具 sudo apt install git sudo apt update sudo apt install gcc g cmake安装 cmake安装新版本 ubuntu20.04安装cmake详细…

python--基础篇--正则表达式--py脚本--题目解答

文章目录 验证输入用户名和QQ号是否有效并给出对应的提示信息从一段文字中提取出国内手机号码替换字符串中的不良内容拆分长字符串 验证输入用户名和QQ号是否有效并给出对应的提示信息 """ 验证输入用户名和QQ号是否有效并给出对应的提示信息要求&#xff1a;用…

Nvidia Jetson/RK3588+AI双目立体相机,适合各种割草机器人、扫地机器人、AGV等应用

双目立体视觉是基于视差原理&#xff0c;依据成像设备从不同位置获取的被测物体的图像&#xff0c;匹配对应点的位置偏移&#xff0c;得到视差数据&#xff0c;进而计算物体的空间三维信息。为您带来高图像质量的双目立体相机&#xff0c;具有高分辨率、低功耗、远距离等优点&a…

石家庄高校大学智能制造实验室数字孪生可视化系统平台项目验收

智能制造作为未来制造业的发展方向&#xff0c;已成为各国竞相发展的重点领域。石家庄高校大学智能制造实验室积极响应国家发展战略&#xff0c;结合自身优势&#xff0c;决定引进数字孪生技术&#xff0c;构建一个集教学、科研、生产于一体的可视化系统平台。 数字孪生可视化…

STM32小项目———感应垃圾桶

文章目录 前言一、超声波测距1.超声波简介2.超声波测距原理2.超声波测距步骤 二、舵机的控制三、硬件搭建及功能展示总结 前言 一个学习STM32的小白~ 有问题请评论区或私信指出 提示&#xff1a;以下是本篇文章正文内容&#xff0c;下面案例可供参考 一、超声波测距 1.超声波…

rclone 上传资料到 onedrive 遇到限速问题解决

原因分析 可能和脚本参数设置有关系,我的参数是: rclone copy "F:\阿里云盘\6666\局域网" "od:影视" --ignore-existing -u -v -P --transfers20 --ignore-errors --buffer-size128M --check-first --checkers10 --drive-acknowledge-abuse差不多8G大小的…

LLaMA:挑战大模型Scaling Law的性能突破

实际问题 在大模型的研发中,通常会有下面一些需求: 计划训练一个10B的模型,想知道至少需要多大的数据?收集到了1T的数据,想知道能训练一个多大的模型?老板准备1个月后开发布会,给的资源是100张A100,应该用多少数据训多大的模型效果最好?老板对现在10B的模型不满意,想…

第三节:如何理解Spring的两个特性IOC和AOP(自学Spring boot 3.x第一天)

大家好&#xff0c;我是网创有方&#xff0c;接下来教大家如何理解Spring的两个特性IOC和AOP。本节有点难&#xff0c;大家多理解。 IOC&#xff08;控制反转&#xff09; 定义与核心思想&#xff1a; IOC&#xff0c;全称Inversion of Control&#xff0c;即控制反转。 其核…

nginx 1024 worker_connections are not enough while connecting to upstream

现象 请求api响应慢&#xff0c;甚至出现504 gateway timeout&#xff0c;重启后端服务不能恢复&#xff0c;但重启nginx可以恢复。 解决方案 worker_connections使用了默认值 1024&#xff0c;当流量增长时&#xff0c;导致连接不够 在nginx.conf中修改连接数就可以了&…

正版软件 | R-Studio Corporate:企业级数据恢复的终极解决方案

数据是企业的生命线&#xff0c;而数据丢失可能随时威胁到企业的正常运营。R-Studio Corporate 是一款专为企业环境设计的多功能数据恢复软件&#xff0c;确保您在面临数据危机时&#xff0c;能够迅速、高效地恢复宝贵数据。 跨平台操作&#xff0c;灵活恢复 R-Studio Corporat…

【文档智能】DLAFormer:端到端的解决版式分析、阅读顺序方法

前言 前面文章介绍到&#xff0c;文档智能中版式分析(DLA)&#xff08;《【文档智能 & RAG】RAG增强之路&#xff1a;增强PDF解析并结构化技术路线方案及思路》&#xff09;、阅读顺序&#xff08;《【文档智能】符合人类阅读顺序的文档模型-LayoutReader及非官方权重开源…