三层架构

我的理解

三层架构的定义:三层架构就是为了符合“高内聚,低耦合”思想,把各个功能模块划分为表示层(UI) 、业务逻辑层(BLL)和数据访问层(DAL)三层架构。额外的还有一层实体类(Model) ,作为数据传递的载体,在各层之间传递数据。

USL:User Show Layer表示层(也可以叫UI,即User Interface用户界面)

BLL:Business Logic Layer业务逻辑层

DAL:Data Access Layer数据访问层

内聚就是一个模块内各个元素彼此结合的紧密程度,高内聚就是一个模块内各个元素彼此结合的紧密程度高(只负责单一功能)。

耦合:一个完整的系统,模块与模块之间,尽可能的使其独立存在,也就是说,让每个模块,尽可能的独立完成某个特定的子功能.模块与模块之间的接口,尽量的少而简单.如果某两个模块间的关系比较复杂的话,最好首先考虑进一步的模块划分.这样有利于修改和组合.

简单点说就是:高内聚是指单一功能的要高内聚,都内聚在一起。两个模块之间的联系就是耦合,我们要尽量减少两个模块之间的联系,这就叫低耦合,但是耦合是避免不了的,低耦合的好处是:你一个模块要修改,对其他模块的影响会很低。并且,以后我们要是做大的项目,一个项目会进行分工,我们要是采用高内聚低耦合的做法,可以让我们每一个人都只要去关注自己写的就行了,有利于分工合作,不然要是耦合性很高的话,你要改一点东西,添加一些东西都得和别人商量协商,这个协商就很复杂,因为耦合性高,所以你们的代码都是紧密联系的,你们将会很难改动,一改动就得考虑多方面的影响。

表示层又称表现层UI,位于三层构架的最上层,与用户直接接触,主要是BS信息系统中的Web浏览页面。
业务逻辑层BLL的功能是对具体问题进行逻辑判断与执行操作,接收到表现层UI的用户指令后,会连接数据访问层DAL,访问层在三层构架中位于表示层与数据层中间位置,同时也是表示层与数据层的桥梁,实现三层之间的数据连接和指令传达
数据访问层 DAL是数据库的主要操控系统,实现数据的增加、删除、修改、查询等操作,并将操作结果反馈到业务逻辑层BBL。
实体类库是数据库表的映射对象,在信息系统软件实际开发的过程中,要建立对象实例,将关系数据库表采用对象实体化的方式表现出来,辅助软件开发中对各个系统功能的控制与操作执行。

UI主要涉及的就是页面的展示。DAL主要涉及的是数据库的增删改查,BLL用于处理表示层传过来的请求,要是这个请求没有必要访问数据库就直接处理了给返回给表示层,要是得访问数据库就会去连接DAL层。实体类库就是用来接收数据库返回的信息的,他把数据库返回的信息封装到实体类里面,然后返回。

三层架构的设计的缺点:维护成本增加,因为需要各自维护每一个层了,而且代码的冗余度增加了。

一般使用三层架构的情况:系统的功能多,庞大,业务需求还在不断地增加,需要不断的维护时我们使用三层架构比较好,因为你改变其中一个层对其他的层的代码的影响会小一点呀。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pFV2hR6f-1666010089320)(第十九次任务-三层架构/image-20221014093810638.png)]

正规笔记

一、三层架构是什么?

首先我们要明白三层架构的内容有哪三层?答:UI表现层、BLL业务逻辑层、DAL数据访问层。关于三层架构的定义,官方的解释是:三层架构就是为了符合“高内聚,低耦合”思想,把各个功能模块划分为表示层(UI)、业务逻辑层(BLL)和数据访问层(DAL)三层架构,各层之间采用接口相互访问,并通过对象模型的实体类(Model)作为数据传递的载体,不同的对象模型的实体类一般对应于数据库的不同表,实体类的属性与数据库表的字段名一致。

官方给的解释其实也并不难理解,我们在开发一个完整的功能模块的时候,肯定会有用户界面,根据用户的选择就应该去执行对应逻辑代码,进行对应的逻辑处理,而逻辑处理往往大部分都是在和数据库打交道,需要对数据进行增删改查。所以我们把这个模块分为这几层还是比较容易理解的。由于这样的分层可以让代码的实现变得更加的有条理、有逻辑,所以我们把用户界面、逻辑处理、与数据库的交互分开实现,至于为什么各层之间采用接口相互访问,下面说“三层架构怎么用?”的时候着重说明。

二、为什么要用三层架构

在真实的业务开发中,往往是需要团队合作开发的,毕竟一个完整的实际项目,它的开发周期会很长,这就意味着里面会有非常多的功能模块,比如一个简单的图书管理系统,就有管理员对图书的增删改查、对用户的增删查改,用户对图书的增删查改等等。这样的一个简单项目由一个团队来开发只需要一到两天即可,而我们却用了整整两周不止。

那么既然需要团队协同开发,自然离不开功能模块的划分了,这时候就需要使用三层架构的思想了,在三层架构中,各层互相独立,完成自己该完成的任务,项目可以多人同时开发,开发人员可以只关注整个结构中的其中某一层(自己负责的那一层即可)。举一个简单的例子,我们要在数据库查找一个人,那么首先需要一个方法去数据库查找,需要查找的条件,比如id,那么我们就初步建立了一个方法 findById(int id)。然后要是这个人找到了,找到之后我们需要打印这个人的信息,那方法就应该进一步完善,需要添加返回值,所以方法就变为了这样的:User findById(int id)。这个方法是用来在数据库进行查询的,那万一没有这个人呢,或者找到这个人我需要把这个人进行一些列的包装处理呢?这一些列的逻辑操作我们就可以把它放在逻辑处理层来,在逻辑处理层创建一个方法,处理完了之后,返回给用户界面。

从另一个方面来说,三层架构有利于各层逻辑的复用,比如上面说到的图书管理系统中管理员可以对图书进行增删查改,我们将这些方法放在一个类中,再出现对用户的增删查改时,我们就可以复用这些代码,修改参数和返回值类型即可。

这样分层处理,也更有利于代码的移植、维护,比如数据库SQLServer 转 Oracle数据库时,我们只需要修改一个层即可,因为与数据库的交互都在数据访问层中。

综上所述,使用三层架构会有三个好处:1.有利于项目的分工合作 2.有利于逻辑层的代码的复用 3.利于代码后期的维护(即修改)且移植性也很好。

三、三层架构怎么用?

使用三层架构时,我们首先要先创建好不同的包,每一个包对应三层架构的一个层。下面我通过一个学生登录功能的案例给大家讲解三层架构的用法。

  1. 首先我们在数据库中创建了 student表,结构如下:

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Up8KoPVK-1666010089322)(第十九次任务-三层架构/image-20210422102615511.png)]

  2. 创建数据传递的载体实体类,一般我们将实体类放在domain/pojo/entity 这三者其中一个的包下,实体类的属性需要与数据库中的表一一对应。

    我在 src 目录下创建了存放实体类的包:entity,然后在包下创建了Student类,它的属性和数据表student的属性一一对应,然后在实体类中给各个属性创建了对应的get/set方法,创建了有参/无参构造方法以及toString()方法。

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BQdU0IhN-1666010089323)(第十九次任务-三层架构/image-20210422103234603.png)]

  3. 创建工具包util,用于存放一些常用且不变的方法,比如IO流的读写,反反复复都是那些代码,我们就将这些重复的代码抽取出来将它封装成一个工具类,在我们需要用到这个方法的时候,直接调用即可。在验证登录这个案例里面,我们将连接数据库需要的代码抽取出来,这是因为每一次对数据库表进行增删改查访问的时候都需要连接数据库,所以将它抽取成工具类方便使用。

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HWMAkNld-1666010089323)(第十九次任务-三层架构/image-20210422110700621.png)]

  4. 创建数据访问层dao,在数据访问层中将对数据库表进行操作的方法写在接口 StudentDao 中,然后通过实现类 StudentImpl 写具体操作数据库表的逻辑代码。

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qGRfRFF1-1666010089324)(第十九次任务-三层架构/image-20210422104819850.png)]

    先将需要用到的方法都封装好,不写方法的实现,用到某一个方法,写这个方法的具体实现。这里我们要做的是登录验证,所以我们封装了登录方法 login ,接下来我们分析:验证登录肯定需要学生先输入用户名和密码,有了用户名和密码之后去数据库表中查对应的数据,查到了就将这个学生的信息打印出来。经过分析我们知道方法的参数就是 name 和 password ,返回值就是这个学生 Student。

    方法的具体实现,就是通过sql语句对数据库表进行操作,都是固定的模板,通过 JDBCTemplate模板 调用实现的方法即可。

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8srzQSDg-1666010089325)(第十九次任务-三层架构/image-20210422105032099.png)]

  5. 创建业务逻辑层service,业务逻辑层主要是对具体问题进行逻辑判断与执行操作,接收到表现层 UI 的用户指令后(用户某一步操作),会连接数据访问层 DAL进行业务处理。访问层在三层构架中位于表示层与数据层中间位置,同时也是表示层与数据层的桥梁,实现三层之间的数据连接和指令传达,可以对接收数据进行逻辑处理,实现数据的修改、获取、删除等功能,并将处理结果反馈到表示层 UI 中,实现软件功能。

    在登录案例里面,我们通过学生输入的名字和密码在数据库表里进行查找,如果没有,我们就告诉学生输入的姓名或者密码错误;如果学生输入的信息为空,那么我们就根本不用访问数据访问层的方法,直接提示用户输入错误。这一系列的逻辑判断我们都写在逻辑层service里面。[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LvTN6y2q-1666010089326)(第十九次任务-三层架构/image-20210422112359515.png)]

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LKyr4jLy-1666010089327)(第十九次任务-三层架构/image-20210422112422703.png)]

  6. 创建用户表示层view,表示层又称表现层 UI,位于三层构架的最上层,与用户直接接触,主要是 B/S 信息系统中的 Web浏览页面,由于我们还没有使用Web页面,所以我们的用户表示层,就是现在大家写的主函数里用户输入的信息。

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1CKxvT1e-1666010089328)(第十九次任务-三层架构/image-20210422113025351.png)]

    可以看到,我们的用户界面只有一句核心代码,就是调用业务逻辑层的 loginStu() 方法,其他的处理全都通过三层架构的方式分开存放,代码的逻辑变得非常的清晰。

    注意:

    这里解释为什么我们要先写接口再写实现类:使用接口是为了调用与实现解耦,带来的好处是可以各干各的

    我们先写接口再去写实现类的这种做法有什么好处呢?比如我们的三层架构(从下到上的层次顺序是表示层,业务逻辑层,数据访问层。或者说是从下到上依次是view包下的类,service包下的类,dao包下的类。),然后我们要写view包下的具体类里面的方法时,肯定是会需要用到上层service包下的某个具体类的方法的,但是我们写view里面的类的方法是,很可能上面的service包下的具体类并没有写好,那我们将会比较难受,然后先去写那个service包下的具体类的话,你这个view包下的这个类的思路可能你就会丢失了,因为时间线比较久嘛。所以我们要是写view下类的方法时需要用到上层service的某个类的方法时,我们先写一个接口,然后在接口里面补充对应的抽象方法,先不写实现类里面的具体实现,但是在接口里面写一个注释表名那个抽象方法是干嘛的,且你建一个实现类,里面也先不写方法,然后你的view里面继续去写你的代码,这样你的view类写的思路不会被打扰了。对于你写service包下的类的方法要用到dao包下的方法时,也是一样处理就行了,你先在dao包下建一个接口,先不写具体的实现。然后后来你写完下层的类的时候,去写service类的方法时,只要参照接口的注释去写实现就行了,不用去关注其他的细节,你只要去关注你如何实现这个接口里的注释的要求就行了。还有一点就是:你上层写好的方法可以建一个单元测试类去专门测试某个实现类的方法,比如你在JDBC里面你就用过,专门测试CustomerDAOImpl实现类的单元测试类,可以看那个“idea的快捷键”的md笔记,你测试发现可以成功运行,这样后面实际被其他类用到的时候就不用担心那个类的出错了。其实对于dao层我们也可以不用这个自下向上的设计理念,我们dao包中的那个接口和实现类也可以在view包下的类之前写,因为一个程序哪些地方需要访问数据库的还是比较容易预测的,所以,我们也可以先写dao包下的接口和实现类。然后去设计view包下的类,然后我们view包下的类遇到需要逻辑处理的,先到service中建一个接口,然后写抽象方法,然后把这个view类写好了,回来把service包下的那个实现类写好来,这里写service包下的实现类我们可以从两边来对应地写了,因为view类和dao类的相关部分已经写好了。建议写好一个类的一个方法,然后就去service中对应的实体类里把那个方法补充好,然后和dao包的类也匹配好。

    还有就是要是你service里面有两个类的部分功能相同,建议也可以先写一个接口,然后让两个类去实现这个接口,但是这个接口的名字要取好一点,符合实际一点,一般某种功能我们可以定义为一个接口,比如:你一个有报警功能的门和一个有报警功能的电动车,你可以为这两个实体类设计一个接口,叫报警接口。你可以让两个类都来实现这个报警接口。比如我们实际使用的时候,不同业务模块之间的共用,不一定是共用某段代码(共用某个一样的代码段的话就用工具类来解决),也可能是共用某段逻辑框架,这时候就需要抽象一个接口层出来,再通过不同的注入逻辑实现(即用多态的性质,具体执行到实际生成的对象里面去执行,这样程序就能去正确的类的代码段去执行了)。比如一个service下的两个模块,模块1是登记学生信息,模块2是新闻发布,看上去风马牛不相及。但分析下来如果两个模块都有共同点,顺序都是1、验证是否有权限 2、验证输入参数是否合法 3、将输入参数转化为业务数据 4、数据库存取 5、写log,那就可以在service写一个接口,接口里面里面有上述5个函数,再分别写两个实现类去实现这个接口。具体执行的时候,通过因为实际生成的对象不同,去执行不同的代码就行了。

    这样做的好处是:

    1. 这样的做法可以很好地解决并行开发中的团队协作问题
    2. 这样的做法使得,系统的可扩展性进⼀步增强,当增加新的功能点时,你先添加接口里面的方法,然后实现层可以轻松的同步修改(因为会有提示嘛),并且不会影响别的类的工作。
    3. 适合于项⽬较⼤和开发⼈员较多时采⽤。

    缺点:

    1. 增加框架设计难度和开发的⼯作量。
    2. 项⽬较⼩时不宜采⽤。

    建议的步骤(或原则)

    1. 先看看项目的内容和要求是什么,然后就会有一个大概的思路或冲动。
    2. 根据这个大概的思路和冲动去写初代dao类里面的方法(即数据访问层),你要想这个项目大概需要什么数据库访问的方法,这个是可以大概预估的。但是注意,现在写的dao类还不是最终的dao类,之后根据实际使用的需求可能得进行一些微调(比如你初步写好dao之后,写到service调用dao类的方法时,你发现这个service类的方法写起来需要一个dao类那个方法有一个返回值,但是当时没有设计dao类的那个方法的返回值,那么现在就可以微调了。),并且这个dao类的有一些方法应该是刚开始没有想到的,所以那个dao类里面没有写你这个方法,所以得之后来把这个方法补充添加进去到dao类里面里面。注意:你写dao类的时候,先写一个dao接口,然后再写那个接口的实现类,接口的名字要想好不要随便取名字,要符合习惯,你可以用一个功能设计一个接口的原则来设计,可以把几个dao类的通用功能的那个部分设计为一个接口(这个随机应变的,看具体情况,你要是突然想到把哪些方法合为一个接口设计起来结构比较完美,你就那么设计就行了)。
    3. 写好初步dao类之后,就去写view类(表现层),基本一个呈现页面一个view,写view类的时候要是用到比较复杂的逻辑判断的时候,比如你view里面需要一个登入的逻辑处理的功能,你可以在service中先写一个接口,然后写接口的方法(在那个接口的方法上写一个注释,注释提供什么功能,需要什么参数,返回什么东西,比如那个登入的方法,我就在那个接口的方法上注释一个“用户登录功能(输入用户名和密码,返回是否能登入成功)”),然后写好了之后,你知道那个接口的那个方法是提供了一个什么功能,你先不写那个service接口的实现类(可以先提供实现类的类名和方法声明,具体的语句先不写),然后回到view里面继续来写,然后你用多态的形式来调用那个接口的方法,你知道那个方法的功能是什么就行了,就是你先假设那个实现类的那个方法具体的语句已经写好了,直接放到view类里面来用,然后继续view这个方法下面的语句的设计,直到view类的这个方法写好了。
    4. 等我们把view的那个方法写好了,然后去补充service层(即业务逻辑层)刚才没有写的实现类的方法里面的具体语句。这个时候,因为是view类里面的方法已经写好了,且dao类的那个方法也写好了,所以相当于从两面夹攻来写这个service实现类了,这样会简单一点,但是要考虑这个写的类的方法和dao对应的方法配合的同时,也要去考虑这个写的方法和下面的view类的方法匹配(或者说考虑现在写的这个类的方法要能达到接口里那个注释的功能设计)。
    5. 然后这个service层的实现类的方法写好后,就开始写这个view类的下一个方法了,然后如此重复。
    6. 注意:你每次再写一个方法的时候,都要去看看之前的代码的结构和语句,考虑他们有没有优化的空间,能不能再改进一点。就像是我们用视频学习时,那个老师优化代码的思路一样,哪些方法可以提出来然后组成一个工具类什么的,设计一个接口、父类什么的。记住,我们的优化是一步一步来的,我们项目的进度前进了一些之后,你才能看出这个进度前进后,我们原来代码有哪些优化空间,所以是项目每添加了一些功能后就去想一下有没有优化的空间。

    注意事项:

    1. 成员变量:比如你要用到上一层的对象,你就可以把这个对象,放在成员变量的位置。你看view类里面是会多次用到了service对象的,所以我们习惯于把service对象放在那个view类的成员方法的位置。还有那个Scanner对象,你一般也放在view类的成员变量的位置,因为那个Scanner对象一般会在这个view里面被多次用到,你经常一个view里面的多个方法都是需要输入语句的,然后你要是在每一个view的方法里面都写一个局部变量,那么有点浪费,应该把这个Scanner对象提到view类的成员变量的位置。所以就一句话,就是一个类里面的多个方法里面要是可能重复用到某一个对象,那么这个对象可以放在view类的成员方法位置,可以减少代码的复用。

    2. 还有就是utils工具类,我们可以在你写项目刚开始的时候去提供一部分,因为有一部分工具类的是可以想到的,但是有一些工具类的中的方法得在开发的过程中去提取。比如我们要是想用我们自己写的工具类去访问数据库,我们就可以把那个通用查询、通用修改的工具类先写好,放在这个项目的utils包里面。然后就是那些某些代码经常重复用到的,你可以放工具类里面,且工具类里面一般放静态方法,这些重复的代码得你设计的过程中去渐渐地构造,并且可能需要在开发的过程中去优化里面的代码。

    3. view里面建议就放简单的sout代码或者输入的代码,不用有过多的逻辑判断,你放简单的if、while、for可以,因为你要是把那种简单的for、if、while语句都放在service里面,你service的那个方法名叫什么呢?提供的功能是什么呢?都是很难描述的(不能很轻松地看出那个方法是提供了一个什么样的功能的方法,功能不是很清楚)。

    4. dao类里面应该尽量只放增删改查的方法

    5. 至于实体类,你有想法的时候就去设计,比如你刚开始设计dao类的时候,你看看那个表可能会有冲动去写实体类,那就去写。还有就是你中途创作的时候需要返回东西,或传递东西,你可能会有冲动写实体类,有冲动就写。总之就是项目一开始,你先写dao类,然后有些实体类是很容易联想到的,那个时候就去构造那个实体类,但是这个时候构造出来的实体类还不是很完美,需要以后开发的过程中不断地微调。还有一些实体类是你开发的过程中才发现你需要那么一个实体类,然后就去写那个实体类。