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

news2024/11/23 16:48:18

目录

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.coloradmin.cn/o/1877674.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Spring专题一:源码编译

下载源码 因为公司使用的是Spring5.2.x所以就下载了这个版本&#xff0c;github源码地址如下&#xff1a; GitHub - spring-projects/spring-framework at v5.2.6.RELEASE&#xff1a; 如果网络不稳定可以使用下载压缩版即可&#xff0c;网络稳定的话还是建议使用git clone …

C语言的数据结构:树与二叉树(哈夫曼树篇)

前言 上篇讲完了二叉树&#xff0c;二叉树的查找性能要比树好很多&#xff0c;如平衡二叉树保证左右两边节点层级相差不会大于1&#xff0c;其查找的时间复杂度仅为 l o g 2 n log_2n log2​n&#xff0c;在两边层级相同时&#xff0c;其查找速度接近于二分查找。1w条数据&am…

今日AI提示词|新媒体运营场景-小红薯笔记通用指令

指令写作的技巧与步骤 明确定义需求 提供足够的上下文 使用简单直白的语言 举例说明 添加限制条件 多次迭代优化&#xff0c;给出详细的要求 小红薯笔记通用指令 从现在开始&#xff0c;你担任我的小红书创作者。你的任务是根据我提供给你的主体&#xff0c;撰写一篇小…

运维锅总详解HAProxy

本文尝试从HAProxy简介、HAProxy工作流程及其与Nginx的对比对其进行详细分析&#xff1b;在本文最后&#xff0c;给出了为什么Nginx比HAProxy更受欢迎的原因。希望对您有所帮助&#xff01; HAProxy简介 HAProxy&#xff08;High Availability Proxy&#xff09;是一款广泛使…

开源项目-商城管理系统

哈喽,大家好,今天主要给大家带来一个开源项目-商城管理系统 商城管理系统分前后端两部分。前端主要有商品展示,我的订单,个人中心等内容;后端的主要功能包括产品管理,门店管理,会员管理,订单管理等模块 移动端页面

汽车电子工程师入门系列——AUTOSAR通信服务框架(上)

我是穿拖鞋的汉子,魔都中坚持长期主义的汽车电子工程师。 老规矩,分享一段喜欢的文字,避免自己成为高知识低文化的工程师: 屏蔽力是信息过载时代一个人的特殊竞争力,任何消耗你的人和事,多看一眼都是你的不对。非必要不费力证明自己,无利益不试图说服别人,是精神上的节…

uniapp+vue3开发微信小程序踩坑集

本文主要记录使用uniappvue3开发微信小程序遇见的各种常见问题及注意点。&#xff08;持续更新&#xff09; 问题&#xff1a; 自定义组件为什么有些样式加不上去 给自定义组件增加class的时候&#xff0c;有时候不生效有时候生效&#xff0c;一度让我怀疑自己记忆错乱。后来…

实验 1 图像基本操作

1. 实验目的 ①熟悉 Photoshop 基本操作&#xff1b; ②掌握 Matlab 、PythonOpenCV 中处理图像的基本方法&#xff1b; ③掌握图像的半调转换和抖动转换。 2. 实验内容 ①Photoshop 基本操作&#xff1a;打开图像文件&#xff0c;选择区域&#xff0c;旋转、裁剪图像、图层…

PHP电商系统开发指南数据库管理

回答&#xff1a;数据库管理是电商系统开发的关键&#xff0c;涉及数据的存储、管理和检索。选择合适的数据库引擎&#xff0c;如mysql或 postgresql。创建数据库架构&#xff0c;定义数据的组织方式&#xff08;如产品表、订单表&#xff09;。进行数据建模&#xff0c;考虑实…

华润万家超市卡怎么用?

华润的礼品卡不仅能线下门店使用&#xff0c;还能直接叫送货上门 我最近用积分兑了几张华润卡&#xff0c;但是又没有购物需求&#xff0c;送朋友吧面值又不大&#xff0c;朋友也说用不上 最后朋友建议我在收卡云上把卡出掉&#xff0c;我试了下92折出掉了&#xff0c;价格还…

MySQL高级-MVCC-基本概念(当前读、快照读)

文章目录 1、MVCC基本概念1.1、当前读1.1.1、创建表 stu1.1.2、测试 1.2、快照读 1、MVCC基本概念 全称Multi-Version Concurrency Control&#xff0c;多版本并发控制。指维护一个数据的多个版本&#xff0c;使得读写操作没有冲突&#xff0c;快照读为MySQL实现MVCC提供了一个…

Python之父推荐!Star 60k!这本 CPython 书把内部实现全讲透了!

都说 Python 是人工智能的“天选”语言&#xff0c;为什么呢&#xff1f; 可能很多读者都知道&#xff0c;Python 的解释器是用 C 语言写的&#xff0c;所以其实我们在谈论 “Python” 的时候&#xff0c;99.9% 的情况说的就是 “CPython”&#xff01; CPython 是目前最流行的…

《昇思25天学习打卡营第11天 | 昇思MindSpore基于 MindSpore 实现 BERT 对话情绪识别》

11天本节学习到BERT全称是来自变换器的双向编码器表征量&#xff0c;它是Google于2018年末开发并发布的一种新型语言模型。BERT模型的主要创新点都在pre-train方法上&#xff0c;即用了Masked Language Model和Next Sentence Prediction两种方法分别捕捉词语和句子级别的repres…

数学学习与研究杂志社《数学学习与研究》杂志社2024年第6期目录

课改前沿 基于核心素养的高中数学课堂教学研究——以“直线与圆、圆与圆的位置关系”为例 张亚红; 2-4 核心素养视角下初中生数学阅读能力的培养策略探究 贾象虎; 5-7 初中数学大单元教学实践策略探索 耿忠义; 8-10《数学学习与研究》投稿&#xff1a;cn7kantougao…

x3daudio1_7.dll在哪个文件夹?打开游戏提示找不到x3daudio1_7.dll怎么解决?

电脑打开游戏软件时候&#xff0c;我相信大部分人都会遇到提示找不到x3daudio1_7.dll文件。或错误代码126&#xff1a;加载x3daudio1_7.dll失败等等问题&#xff0c;那这个是怎么回事呢&#xff1f;需要怎么解决这个问题&#xff1f;下面我给大家一一解答。 一、x3daudio1_7.dl…

C#测试调用DotnetSpider爬取网页内容

微信公众号“DotNet”的文章《.NET快速实现网页数据抓取》介绍了调用开源网页爬取模块DotnetSpider爬取cnblog网站文章的基本方式。之前学习过使用HtmlAgilityPack抓取并分析网页内容&#xff0c;DotnetSpider也依赖HtmlAgilityPack模块&#xff0c;不过前者属于轻量、高效的爬…

昇思MindSpore学习笔记5--数据变换Transforms

摘要&#xff1a; 昇思MindSpore的数据变换&#xff0c;包括通用变换Common Transforms、图像变换Vision Transforms、标准化Normalize、文本变换Text Transforms、匿名函数变换Lambda Transforms。 一、数据变换Transforms概念 原始数据需预处理后才能送入神经网络进行训练…

docker网络功能介绍

一、 网络启动过程二、 修改容器dns和主机名① 临时处理&#xff08;容器终止或重启后不会保存&#xff09;② 通过参数指定 三、 容器内访问控制① 容器访问外部网络② 容器间互相访问&#xff08;1&#xff09;访问所有端口&#xff08;2&#xff09;访问指定端口 四、 docke…

.[emcrypts@tutanota.de].mkp勒索病毒新变种该如何应对?

引言 在数字化时代&#xff0c;随着信息技术的迅猛发展&#xff0c;网络安全问题日益凸显。其中&#xff0c;勒索病毒作为一种极具破坏力的恶意软件&#xff0c;给个人和企业带来了巨大的经济损失和数据安全风险。近期&#xff0c;一种名为“.mkp勒索病毒”的新型威胁开始在网络…

2000-2021年县域金融机构存贷款数据

2000-2021年县域金融机构存贷款数据 1、时间&#xff1a;2000-2021年 2、指标&#xff1a;统计年度、地区编码ID、县域代码、县域名称、所属地级市、所属省份、年末金融机构贷款余额/亿元、年末金融机构存款余额/亿元、年末城乡居民储蓄存款余额/亿元 3、来源&#xff1a;县…