软件设计与体系结构

软件设计与体系结构
李阳期末考试78分完美上岸!
后序有时间再慢慢更新嘿嘿
软件设计与体系结构
逆水行舟用力撑,一篙松劲退千寻
——-董必武《题赠送中学生》
这里暂时就不更新了,等我如果挂科了我在更新!
第一章 软件设计模式相关内容介绍
1.设计模式概述
1.1产生背景
1990年软件工程界开始研讨设计模式的话题,后来召开了多次关于设计模式的研讨会。直到1995 年,艾瑞克·伽马(EichGamma).理查德·海尔姆(Richard Helm)、拉尔夫·约翰森(Ralph Johnson)、约翰·威利斯迪斯(John Vlissides)等4位作者合作出版了《设计模式:可复用面向对象软件的基础》一书,在此书中收录了23个设计模式,这是设计模式领域里程碑的事件,导致了软件设计模式的突破。这4位作者在软件开发领域里也以他们的”四人组”(Gang of Four,GoF)著称。
1.2软件设计模式概念
软件设计模式(Software Design Pattern),又称设计模式,是一套被反复使用、多数人知晓的、经过分类编目的、代码设计经验的总结。它描述了在软件设计过程中的一些不断重复发生的问题,以及该问题的解决方案。也就是说,它是解决特定问题的一系列套路,是前辈们的代码设计经验的总结,具有一定的普遍性,可以反复使用。
1.3学习软件设计的必要性
设计模式的本质是面向对象设计原则的实际运用,是对类的封装性、继承性和多态性以及类的关联关系和组合关系的充分理解。
1.4正确使用设计模式具有以下优点。
- ·可以提高程序员的思维能力、编程能力和设计能力。
- ·使程序设计更加标准化、代码编制更加工程化,使软件开发效率大大提高,从而缩短软件的开发周期。·
- 使设计的代码可重用性高、可读性强、可靠性高、灵活性好、可维护性强。
1.5设计模式分类
创建型模式
用于描述”怎样创建对象”,它的主要特点是”将对象的创建与使用分离。GoF(四人组)书中提供了单例、原型、工厂方法、抽象工厂、建造者等5种创建型模式。
结构型模式
用于描述如何将类或对象按某种布局组成更大的结构,GoF(四人组)书中提供了代理、适配器、桥接、装饰、外观、享元、组合等7种结构型模式。
行为型模式
用于描述类或对象之间怎样相互协作共同完成单个对象无法单独完成的任务,以及怎样分配职责。GoF(四人组)书中提供了模板方法、策略、命令、职责链、状态、观察者、中介者、迭代器、访问者、备忘录、解释器等 11种行为型模式。
2.UML
统一建模语言(Unified Modeling Language,UML)是用来设计软件的可视化建模语言。它的特点是简单、统一、图形化、能表达软件设计中的动态与静态信息。
UML从目标系统的不同角度出发,定义了用例图、类图、对象图、状态图、活动图、时序图、协作图、构件图、部署图等9种图。
- 2)静态结构图类图、对象图、包图、组件图、部署图
- 3)动态行为图:交互图(时序图与协作图)、状态图、活动图
2.1类图概述
类图(Class diagram)是显示了模型的静态结构,特别是模型中存在的类、类的内部结构以及它们与其他类的关系等。类图不显示暂时性的信息。类图是面向对象建模的主要组成部分。
2.2类图作用
- ·在软件工程中,类图是一种静态的结构图,描述了系统的类的集合,类的属性和类之间的关系,可以简化了人们对系统的理解;
- ·类图是系统分析和设计阶段的重要产物,是系统编码和测试的重要模型。
2.3类图表示法
2.3.1类图表示方式
属性/方法名称前加的加号和减号表示了这个属性/方法的见性,UML类图中表示可见性的符号有三种:。
- +:表示public
- -:表示private。
- #:表示protected
属性的完整表示方式是:可见性 名称∶类型 [=缺省值]
方法的完整表示方式是:可见性:名称(参数列表)[ ︰返回类型]
2.3.2类与类之间的表示方式
2.3.2.1关联联系
关联关系实际上就是类与类之间的联系,他是依赖关系的特例
关联具有导航性:即双向关系或单向姜系
关系具有多重性:如“1”(表示有且仅有一个),“0…”(表示0个或者多个),“0,1”(表示0个或者一个),“n..m”(表示n到m个都可以),,”m.….*””(表示至少m个)。
关联关系是对象之间的一种引用关系,用于表示一类对象与另一类对象之间的联系,如老师和学生、师傅和徒弟、丈夫和妻子等。关联关系是类与类之间最常用的一种关系,分为一般关联关系、聚合关系和组合关系。
2.3.2.1.1单向关联
在UML类图中单向关联用一个带箭头的实线表示。上图表示每个顾客都有一个地址,这通过让customer类持有一个类型为Address的成员变量类实现。(指向的是Address类型的)
小德莫😍
package com.sgg.UML; |
我们的Person1里面有IDCard1,但是我们的IDCard1没有Person1
2.3.2.1.2双向关联
双方各持有对方类型的成员变量
(这里一个顾客可购买多个商品,一个商品也可以指定被哪个顾客购买)
在UML类图中,双向关联用一个不带箭头的直线表示。上图中在customer类中维护一个List\
小德莫
package com.sgg.UML; |
你中有我 我中有你
你我如同并蒂莲,共沐风雨心相连
2.3.2.1.3自关联(链表)
自关联在UML类图中用一个带有箭头且指向自身的线表示。上图的意思就是Node类包含类型为Node的成员变量,也就是”自己包含自己”。
2.3.2.2聚合关系
聚合关系(Aggregation)表示的是整体和部分的关系,整体与部分可以分开。
聚合关系是关联关系的特例,所以他具有关联的导航性与多重性。
如:一台电脑由键盘(keyboard)、显示器(monitor),’鼠标等组成;组成电脑的各个配件是可以从电脑上分离出来的,使用带空心菱形的实线来表示:
如果他们的关系可以分开就是聚合关系,不可分开就是组合关系
聚合关系是关联关系的一种,是强关联关系,是整体和部分之间的关系。
聚合关系也是通过成员对象来实现的,其中成员对象是整体对象的一部分,但是成员对象可以脱离整体对象而独立存在。例如,学校与老师的关系,学校包含老师,但如果学校停办了,老师依然存在。(公司倒闭了,但是程序员还可以去其他公司找工作)
在UML类图中,聚合关系可以用带空心菱形的实线来表示,菱形指向整体。下图所示是大学和教师的关系图:
大学是整体,老师是部分(成员对象是整体对象的一部分,但是成员变量可以脱离整体存在)
这里没有小德莫 我懒得写了
算辽算辽 以防我自己后面看不懂我还是写吧
李阳!你可不能做一个懒惰的人!
package com.sgg.UML; |
over!
2.3.2.3组合关系
组合表示类之间的整体与部分的关系,但它是一种更强烈的聚合关系。
在组合关系中,整体对象可以控制部分对象的生命周期,一旦整体对象不存在,部分对象也将不存在,部分对象不能脱离整体对象而存在。例如,身体和头的关系,身体没了,头也不存在了。(皮之不存毛将附焉)
在UML类图中,组合关系用带实心菱形的实线来表示,菱形指向整体。下图所示是头和嘴的关系图:
小德莫
package com.sgg.UML; |
当我们创建computer对象的时候,mouse和monitor都会自动new对象,他们是共生的
我泥中有尔,尔泥中有我【我侬词】
2.3.2.4依赖关系
(耦合最小的)
只要是在类中用到了对方,那他们就存在依赖关系
- 1)类中用到了对方
- 2)如果是类的成员属性
- 3)如果是方法的返回类型
- 4)是方法接收的参数类型
- 5)方法中使用到
依赖关系是一种使用关系,它是对象之间耦合度最弱的一种关联方式,是临时性的关联。在代码中,某个类的方法通过局部变量、方法的参数或者对静态方法的调用来访问另一个类(被依赖类)中的某些方法来完成一些职责。
在UML类图中,依赖关系使用带箭头的虚线来表示,箭头从使用类指向被依赖的类。下图所示是司机和汽车的关系图,司机驾驶汽车:
在一个方法中创建了另外一个对象,通过那个对象来调用方法
(这里的Driver创建了一个Car类型的形参,通过形参调用Car里面的方法)
我们来用非常非常简单的代码来演示吧!( •̀ ω •́ )✧
package com.sgg.UML; |
好啦!接下来聪明的你猜一猜哪里是有依赖的呢?
答案是全都有哈哈,这几个都有用到哦
2.3.2.5泛化/继承关系
(耦合最大的)-泛化关系
泛化就是继承
继承关系是对象之间耦合度最大的一种关系,表示一般与特殊的关系,是父类与子类之间的关系工是一种继承关系。
在UML类图中,泛化关系用带空心三角箭头的实线来表示,箭头从子类指向父类。在代码实现时,使用面向对象的继承机制来实现泛化关系。例如,Student类和Teacher类都是Person类的子类,其类图如下图所示:
我们来看一个非常非常简单的小demo
package com.sgg.UML; |
哇,这就是继承,不过是箭头表示,虽然很简单但是不要忘记哦
2.3.2.6实现关系
实际上就是A类实现B,他是依赖关系的特例(比如接口被类实现了 )
实现关系是接口与实现类之间的关系。在这种关系中,类实现了接口,类中的操作实现了接口中所声明的所有的抽象操作。
在UML类图中,实现关系使用带空心三角箭头的虚线来表示,箭头从实现类指向接口。例如,汽车和船实现了交通工具,
实现了接口
小小的德莫
package com.sgg.UML; |
很简单吧嘻嘻
3.软件设计原则
在软件开发中,为了提高软件系统的可维护性和可复用性,增加软件的可扩展性和灵活性,程序员要尽量根据6条原则采开发程序,从而提高软件开发效率、节约软件开发成本和维护成本。
3.1设计模式目的
编写软件过程中,程序员面临着来(耦合性,内聚性以及可维护性,可扩展性,重用性,灵活性等多方面的挑战,设计模式是为了让程序(软件),具有更好
1)代码重用性(即相同功能的代码不用多次编写)
2)可读性
3)可扩展性
4)可靠性
5)使程序呈现高内聚,低耦合的特性
3.2七大原则
单一职责原则 SRP
接口隔离原则 ISP
依赖倒转原则 DIP
里氏替换原则 LSP
开闭原则 OCP
迪米特法则 (最少知道原则) LoD
合成复用原则CARP
3.2.1单一职责 SRP—一个类一个方法
一个类一个方法
对类来说的,即一个类应该只负责一项职责。如类A负责两个不同职责:职责1,职责2。当职责1需求变更而改变A时,可能造成职责2执行错误,所以需要将类A的粒度分解为A1,A2
eg:
package com.sgg.class1; |
在这个方法中违反了单一设计原则,这个类既要负责公路上的车辆还要负责天上飞的(飞机)
我们可以根据交通工具运行的方法不同,分解为不同的类即可
解决办法:
package com.sgg.class1; |
遵守了单一原则
但是这样做改动太大了,即要将代码修改,还需要修改客户端
改进:
package com.sgg.class1; |
这里虽然没有在类上遵循单一设计原则,但是在方法级别上任然是遵守单一设计原则
(这不就是加方法嘛,小小单一设计原则拿下拿下)
单一设计原则注意事项和细节
- 1)降低类的复杂度,一个类只负责一项职责。
- 2)提高类的可读性,可维护性
- 3)降低变更引起的风险
- 4)通常情况下,我们应当遵守单一职责原则,只有逻辑足够简单,才可以在代码级违反单一职责原则;只有类中方法数量足够少,可以在方法级别保持单一职责原则
3.2.2接口隔离原则ISP—最小接口
最小接口
客户端不应该依赖它不需要的接口,即一个类对另一个类的依赖应该建立在最小的接口上
A通过接口会使用到B,但是他只会使用到123的方法,但是b实现了接口,所以他必须要实现接口的所有的功能,照成结果方法使用不了导致浪费(我瞎编的,不过大概可能就是这个意思)
我们来看代码吧,可能会好理解一点!
一个接口里面5个方法
package com.sgg.class2; |
B实现了接口里的5个功能
package com.sgg.class2; |
D也实现了接口的5个功能
package com.sgg.class2; |
现在我们的A依赖B但是只需要1 2 3个方法
package com.sgg.class2; |
那劳资的B里面的剩下俩方法不就白写了,rnm退钱
现在我们的C依赖D但是只需要14 5方法
package com.sgg.class2; |
啊你玩我呢,那我D还写这么多干嘛
没错,这样我们就很好的解释了为什么需要接口隔离原则,你明白了吗(●’◡’●)
(这里还有一个小小的疑问,我们的A和C是怎么依赖B和D的呢?Interface1 i 这里的i可以经行替换为我们的B和D)
类A通过接lnterface1依赖类B,类c通过接口Interface1依赖类D,如果接口lnterface1对于类A和类c来说不是最小接口那么类B和类D必须去实现他们不需要的方法。
按隔离原则应当这样处理:
将接口Interface1拆分为独立的几个接口,类A和类c分别衔他们需要的接口建立依赖关系。也就是采用接口隔离原则
所以这些接口要以最小接口原则经行拆分
现在我们要经行改进,变得爸爸妈妈都不认识你
将接口lnterface1拆分为独立的几个接口,类A和类c分别与他们需要的接口建立依赖关系。也就是采用接口隔离原则
接口Interface1中出现的方法,根据实际情况拆分为三个接口
代码实现
这就是接口隔离(感觉代码量变好高)
我们来看看代码
首先是3个接口
package com.sgg.class2; |
package com.sgg.class2; |
package com.sgg.class2; |
B和D两个实现类
package com.sgg.class2; |
package com.sgg.class2; |
A和C两个依赖类
package com.sgg.class2; |
package com.sgg.class2; |
嘿嘿嘿嘿嘿… 这样我们就避免了资源浪费,不过感觉程序会很庞大
测试类
package com.sgg.class2; |
安全下车 咕噜拜
3.2.3依赖倒转原则 DIP—依赖接口和抽象
依赖接口和抽象
1)高层模块不应该依赖低层模块,二者都应该依赖其抽象
2)抽象不应该依赖细节,细节应该依颗抽象
3)依赖倒转(倒置)的中心思想是面向接口编程
4)依赖倒转原则是基于这样的设计理念:相对于细节的多变性,抽象的东西要稳定的多。以抽象为基础搭建的架构比以细节为基础的架构要稳定的多。在java中,抽象指的是接口或抽象类,细节就是具体的实现类
5)使用接口或抽象类的目的是制定好规范,而不涉及任何具体的操作,把展现细节的任务交给他们的实现类去完成
…你有没有一点点蒙 反正我先蒙了
那么我们一起来看一个小案例吧(●’◡’●)
package com.sgg.class3; |
这种比较简单且容易实现
但是如果我们需要获取其他途径的信息,比如微信…等,则新增加类Perons也要增加相应的接收方法
哎你别说 你还真别说 好像是感觉不太好,那我们一起来看看怎么修改的吧
解决思路:
- 引用抽象的接口IReceive,表示接收者,这样Person类与接口IRceive发生依赖
- 因为Email,Weixing等等都属于接收范围,他们各自实现IReceive接口就ok了,这样我们就符合了依赖倒转原则了
废话不多说我们直接上代码!干就完了
首先定义一个接口
package com.sgg.class3; |
再来一个Email
package com.sgg.class3; |
一个人员去实现方法
package com.sgg.class3; |
最后再来一个测试类
package com.sgg.class3; |
神奇的事情发生了
结果居然和之前是一样的
太奇妙了神奇
我们在仔细的看一下人员类的方法,因为就这个改变最大
public void receive(IReceiver receiver) { |
首先我们创建了一个Person的对象,然后传入了一个Email的对象 然后调用
这样,如果我们在加一个微信类,只需要在主方法里面new就可以了
好像明白了依赖倒置原则了,之前需要在Persion里面重新写实例化方法,现在都不需要变更了
依赖关系传递的三种方式(看懂即可)
这里微微有点绕(我又迷糊了)
- 接口传递
- 构造方法传递
- setter传递
方式一 接口传递
package com.sgg.class3.DemoTest; |
看不懂看不懂完全看不懂-这种耦合度还是有点高的,下面两种方式更好
哦哦我看明白了,先别着急放弃,我给你慢慢讲:
- 首先呢我们创建了两个接口一个电视接口ITV,一个关闭某个电视的接口IOpenAndClose(关键字某个)我们要关闭哪个电视就往里面传入哪个接口就可以了,
- 然后呢我们定义了一个海尔电视去实现这个接口,
- 重点来啦OpenAndClose类去实现上面的IOpenAndClose接口 当我们创建这个对象的时候,我们只需要往里面传入我们的接口类型的,我们就能调用哪个play方法。ITV是一个接口,所以我们传入的是接口
- 然后我们实例化HarErTV harErTV = new HarErTV();重点是后面的 OpenAndClose openAndClose = new OpenAndClose();openAndClose.open(harErTV);
- 这里我们实例化之后,openAndClose里面有一个open方法,open方法里面是一个接口类型,因为我们传入的是harErTV,所以我们会调用HarErTV里面的play方法
- 我懵懵懂懂不知道什么是爱,也不知道该该如何表达,不过大概是这个意思,不太明白的可以补一下面向对象的知识,相信会有更大收获
方式二 构造方法传递
package com.sgg.class3.DemoTest; |
我又来啦,我们一起来看下这段代码吧,别放弃别放弃别放弃求求你了!
- 首先还是两个接口(先不要看main方法)
- 然后呢我们的OpenAndClose2 实现了IOpenAndClose2那么我们是不是要实现接口里面的方法,定义了一个接口类型变量,然后传入一个带参数的构造器,将我们传入的变量给了这个临时变量,然后定义一个方法open通过临时变量来获取这个play方法(很巧妙,但是我写不出来)
- 然后下面的changhong实现接口里面有一个play方法
- 最后我们经行调用:我们的是一个带参数的构造器,然后这个传入的类里面有一个play方法,这时候我们调用open方法就会自动执行this.itv2.play();也就是相当于调用changhong.play();
- 不过大致是这个意思
妙妙妙🐱🐱
方法三 setter传递
这个比较常用 我见过这个代码
package com.sgg.class3.DemoTest; |
enn…我该怎么编给你听,不对是讲给你听
我看明白了,这个就和上面方法二差不多,就多了一个设置方法
- 首先接口的定义IOpenAndClose3 open():表示开启某个设备(如电视)。setTv(ITV3 tv):这个方法用于依赖注入,将一个实现了 ITV3 接口的对象(即电视)传递给当前类。接口 ITV3就不重复了
类 OpenAndClose3实现了 IOpenAndClose3 接口它有一个字段 tv,类型是 ITV3,用于存储一个具体的电视实例。setTv(ITV3 tv) 方法是一个 setter 方法,通过它可以将一个实现了 ITV3 接口的对象(例如 Geli 类的实例)注入到 OpenAndClose3 类中。open() 方法调用了 tv.play(),启动电视播放内容。
主程序逻辑 :使用 openAndClose3.setTv(geli) 方法将 Geli 对象(电视)注入到 OpenAndClose3 类中。也就是说,OpenAndClose3 类现在持有了 Geli 类的引用。最后,调用 openAndClose3.open(),它会调用 Geli 的 play() 方法,输出 “格力电视我没见过”。
- 总结:依赖注入(Dependency Injection):OpenAndClose3 类通过 setTv() 方法依赖注入了一个 ITV3 类型的对象(这里是 Geli),实现了类之间的松耦合。这使得 OpenAndClose3 类不需要关心具体的电视类型,只要是实现了 ITV3 接口的类,它都能正常工作。
接口的作用:IOpenAndClose3 和 ITV3 接口分别定义了开关操作和电视播放的行为。通过接口,程序的各个部分可以独立开发和扩展,比如你可以轻松添加新的电视品牌(例如 Sony),只需要让新的品牌类实现 ITV3 接口,而不需要修改 OpenAndClose3 类。
总结
- 低层模块尽量都要有抽象类或接口,或者两者都有,程序稳定性更好.
- 变量的声明类型尽量是抽象类或接口,这样我们的变量引用和实际对象间,就存在一个缓冲层,利于程序扩展和优化
- 继承时遵循里氏替换原则
有点类似需要第三方的感觉,比如说我是小学生我喜欢玩王者荣耀,但是我每天只能玩一个小时的游戏,我想上最强王者,那么我可以联系代练,让代练去操控我的号,解决防沉迷顺便帮我上最强王者比我自己玩要好的多,我损失了钱(大量类)但是我成功上了王者(方便了功能操作)总体来说利大于弊。因此需要一个缓冲
3.2.4里氏替换原则LSP—继承
继承
oo中的基础性的思考和说明
- 1)继承包含这样一层含义:父类中凡是已经实现好的方法,实际上是在设定规范和契约,虽然它不强制要求所有的子类必须遵循这些契约,但是如果子类对这些已经实现的方法任意修改,就会对整个继承体系造成破坏。
- 2)继承在给程序设计带来便利的同时,也带来了弊端。比如使用继承会给程序带来侵入性,程序的可移植性降低,增加对象间的耦合性,如果一个类被其他的类所继承,则当这个类需要修改时,必须考虑到所有的子类,并且父类修改后,所有涉及到子类的功能都有可能产生故障
- 3)问题提出:在编程中,如何正确的使用继承?=>里氏替换原则
基本介绍
- 1)里氏替换原则(Liskov Substitution Principle)在1988年,由麻省理工学院的以为姓里的女士提出的。
- 2)如果对每个类型为T1的对象o1,都有类型为T2的对象o2,使得以T1定义的所有程序P在所有的对象o1都代换成o2时,程序P的行为没有发生变化,那么类型T2是类型T1的子类型。换句话说,所有引用基类的地方必须能透明地使用其子类的对象。
- 3)在使用继承时,遵循里氏替换原则,在子类中尽量不要重写父类的方法(迫不得已也不可以,那你可以用聚合,组合,依赖来解决问题。)
- 4)里氏替换原则告诉我们,继承实际上让两个类耦合性增强了,在适当的情况下,可以通过聚合,组合,依赖来解决问题。.
突突突说一堆,不过说白了就是子类替换父类对之前的父类的地方没有影响,子类可以扩展父类功能但不能改变父类原有功能
代码案例
这里例子不太好—建议参考一下正方形不是长方形,以及鸵鸟不是鸟等案例
我决定要做一个违背祖宗的决定!
package com.sgg.class4; |
这里因为重写了父类的方法,所以结果会不一样,子类重写了父类,但是造成了影响—-重写违反了里氏替换原则
所以到底是故意的还是不小心的
1)我们发现原来运行正常的相减功能发生了错误。原因就是类B无意中重写了父类的方法,造成原有功能出现错误。在实际编程中,我们常常会通过重写父类的方法完成新的功能,这样写起来虽然简单,但整个继承体系的复用性会比较差。特别是运行多态比较频繁的时候
2)通用的做法是:原来的父类和子类都继承一个更通俗的基类,原有的继承关系去掉,采用依赖,聚合,组合等关系代替.
改写案例
package com.sgg.class4; |
3.2.5 开闭原则OCP(特别重要)
不能修改源代码,只能进行功能增加
1)开闭原则(Open Closed Principle)是编程中最基础、最重要的设计原则
2)一个软件实体如类,模块和函数应该对扩展开放,对修改关闭。(对提供方扩展开放,对使用方修改关闭)用抽象构建框架,用实现扩展细节。
3)当软件需要变化时,尽量通过扩展软件实体的行为来实现变化,而不是通过修改已有的代码来实现变化。
4))编程中遵循其它原则,以及使用设计模式的目的就是遵循开闭原则。
就是比方我们在玩王者荣耀的时候,我们可以打开王者荣耀快乐的玩游戏,腾讯公司(提供商)可以对王者荣耀里面的功能扩充,但是呢我们玩家(使用方)不能经行修改(比如开挂等等)
案例
我决定…
package com.sgg.principle.OCP; |
- 1)优点是比较好理解,简单易操作。
- 2)缺点是违反了设计模式的ocp原则,即对扩展开放()。对修改关闭。即当我们给类增加新功能的时候,尽量不修改代码,或者尽可能少修改代码.
- 3)比如我们这时要新增加一个图形种类,我们需要做如下修改,修改的地方较多
- 比如新加三角形,需要创建类,然后修改GraphicEditor(使用方)代码添加三角形的功能
好像有点道理,改动地方确实比较多
改进
思路:把创建Shape类做成抽象类,并提供一个抽象的draw方法,让子类去实现即可,这样我们有新的图形种类时,只需要让新的图形类继承Shape,并实现draw方法即可
使用方代码就不需要修->满足了开闭原则
package com.sgg.principle.OCP; |
啊他是怎么运行的啊
好像有点点头绪了,这是抽象的知识
定义一个抽象方法,然后实现,再用一个类传一个方法类型的方法
emmm… 假设我们再新建一个类,只需要创建类,不需要修改实现类GraphicEditor里面的东西
妙妙喵🐱🐱
3.2.6迪米特法则LoD
米莱迪法则 (•_•)
陌生的类不要以局部变量的形式出现在类的内部
基本介绍
- 1)一个对象应该对其他对象保持最少的了解
- 2)类与类关系越密切,耦合度越大
- 3)迪米特法则(Demeter Principle)又叫最少知道原则,即一个类对自己依赖的类知道的越少越好。也就是说,对于被依赖的类不管多么复杂,都尽量将逻辑封装在类的内部。对外除了提供的public方法,不对外泄露任何信息
- 4)迪米特法则还有个更简单的定义:只与直接的朋友通信
- 5)直接的朋友:每个对象都会与其他对象由耦合关系,只要两个对象之间有耦合关系,我们就说这两个对象之间是朋友关系。耦合的方式很多,依赖,关联,组合,聚合等。其中,我们称出现成员变量,方法参数,方法返回值中的类为直接的朋友,而出现在局部变量中的类不是直接的朋友。也就是说,陌生的类最好不要以局部变量的形式出现在类的内部。
案例
旧版
package com.sgg.principle.LoD; |
我有点累了有点不太想说话了
不许累!!!!!!!!!!!!!
哎学吧学吧 不学干嘛呢
我们来分析一下直接朋友间接朋友吧!
以SchoolManager为例
class SChoolManner{ |
- 首先Employee是直接朋友(出现在成员变量,方法参数,方法返回值中的类为直接朋友)这里是方法参数
- CollegeManager也是直接朋友,他是方法参数
- CollegeEmployee 对应for循环的,他不是成员变量,方法参数,方法返回值中的类,所以是间接朋友,违背了迪米特法则,以局部变量的形式出现的
错误原因
比如说你天天用你室友的水卡洗澡(在学校总部类中打印学院人数),虽然洗干净澡的目的都达到了(结果正确输出了),不过水卡毕竟是人家的(类是学校总部类),人情比钱更加难以偿还,为了避免这种情况,我们要使用自己的水卡(在学院类中打印输出)我是天才🐱
这里我们打印学院人员的代码出现在了管理学校总部人员的类之中了,因此需要改进
改版
- 1)前面设计的问题在于SchoolManager中,CollegeEmployee类并不是SchoolManager类的直接朋友(分析)
- 2)按照迪米特法则,应该避免类中出现这样非直接朋友关系的耦合
package com.sgg.principle.LoD; |
卧槽,好厉害,原来这就是软件设计
注意
- 1)迪米特法则的核心是降低类之间的耦合
- 2)但是注意:由于每个类都减少了不必要的依赖,因此迪米特法则只是要求降低类间(对象间)耦合关系,并不是要求完全没有依赖天系
3.2.7 合成复用原则 CARP
基本介绍
原则是尽量使用合成/聚合的方式,而不是继承
案例引用
我们假设有一个什么呢我想想…假设我们村里有一个乒乓球室(一个类A),我想去里面打乒乓球(一个类B想要实现A里面的方法),然后我就直接去那里天天打乒乓球(最简单的方法就是我们直接继承),可是有一天乒乓球馆扩建了(A类添加了很多B类不需要的功能,所以耦合性就变高了很多),里面建了篮球场,羽毛球,排球…,突然间体育馆人变多了很多,可我只想要打乒乓球,我还是喜欢安静一点的地方
那么我们该如何解决呢?(⊙_⊙)?
对啦聪明的你肯定想到了,我们前面有讲依赖传递的三种方式,这里无论用那种都可以将A与B的关系分开来
3.3设计原则的核心思想
- 1)找出应用中可能需要变化之处,把它们独立出来,不要和那些不需要变化的代码混在一起。
- 2)针对接口编程,而不是针对实现编程。
- 3)为了交互对象之间的松耦合设计而努力
我们的七大设计原则就到这里啦,要及时复习哦还是很容易混的,加油加油加油你一定可以的!( •̀ ω •́ )✧
4. 23种设计模式
woc!
简介
- 1)设计模式是程序员在面对同类软件工程设计问题所总结出来的有用的经验,模式不是代码,而是某类问题的通用解决方案,设计模式(Design pattern)代表了最佳的实践。这些解决方案是众多软件开发人员经过相当长的一段时间的试验和错误总结出来的。
- 2)设计模式的本质提高软件的维护性,通用性和扩展性,并降低软件的复杂
分类
- 创建型模式:单例摸式、抽象工厂模式、原型模式、建造者模式、工厂模式。
- 结构型模式:适配器模式、桥接模式、装饰模式、组合模式、外观模式、享元模式、代理模式。
- 行为型模式:模版方法模式、命令模式、访问者模式、迭代器模式、观察者模式、中介者模式、备忘录模式、解释器模式(Interpreter模式)、状态模式、策略模式、职责链模式(责任链模式)。
反正我是先爆炸了
创建型模式
1.单例模式
所谓类的单例设计模式,就是采取一定的方法保证在整个的软件系统中,对某个类只能存在一个对象实例,并且该类只提供一个取得其对象实例的方法。
单例模式的八种方式
- 1)饿汉式(静态常量)
- 2)饿汉式(静态代码块)
- 3)懒汉式(线程不安全)
- 4)懒汉式(线程安全,同步方法)
- 5)懒汉式(线程安全,同步代码块)
- 6)双重检查
- 7)静态内部类
- 8枚举
woc太多了不学了
1.饿汉式(静态常量)
- 构造器私有化(防止通过new创建实例)
- 类的内部创建对象
- 向外暴露一个静态的公共方法。getInstance(返回实例)
- 代码实现
小测试
package com.sgg.design.singleton; |
嗨嗨嗨,准备好要听李师傅瞎编了没呢,我们来一起解释一下代码吧
首先我们先来看Singleton类,这里将构造器私有化,也就是我们不能在外部写:
```java
Singleton singleton = new Singleton();
- 然后呢我们在本类中创建一个实例, private final static 私有,不可更改,且唯一的实例化(static)
- 欸,我们现在将这个实例给私有了,那别人怎么用呢,我们再给他一个静态方法,让外部只能调用这个方法才能有返回
- 同时呢,我们的单例模式的宗旨就是一个类只能存在一个对象实例,所以我们要使用静态来修饰
###### 优缺点
优缺点说明:
- 1)优点:这种写法比较简单,就是在类装载的时候就完成实例化。避免了线程同步问题。
- 2)缺点:在类装载的时候就完成实例化,没有达到Lazy Loading(满加载)的效果。**如果从始至终从未使用过这个实例,则会造成内存的浪费**
- 3)这种方式基于classloder(类装载)机制避免了多线程的同步问题,不过,instance在类装载时就实例化,在单例模式中大多数都是调用getInstance方法,但是导致类装载的原因有很多种,因此不能确定有其他的方式(或者其他的静态方法)导致类装载,这时候初始化instance就没有达到lazy loading的效果
- 4)结论:这种单例模式可用,**可能造成内存浪费**
---
##### 2.饿汉式(静态代码块)
我们来看代码吧☀️
```java
package com.sgg.design.singleton;
/**
* Created with IntelliJ IDEA.
*
* @Author: 李阳
* @Date: 2024/12/20/13:47
* @Description: 饿汉式(静态代码块)
*/
public class type2 {
public static void main(String[] args) {
Singleton2 instance3 = Singleton2.getInstance();
Singleton2 instance4 = Singleton2.getInstance();
// 检查两个实例是否相等,结果应为true
System.out.println(instance3 == instance4); // true
// 检查两个实例的 hashCode 是否相等,结果应为true
System.out.println(instance3.hashCode() == instance4.hashCode()); // true
}
}
class Singleton2 {
// 构造器私有化,外部不能直接 new
private Singleton2() {
}
// 本类内部创建单例对象
private static Singleton2 INSTANCE;
// 在静态代码块中创建单例对象
static {
INSTANCE = new Singleton2();
}
// 提供一个公有的静态方法,返回实例对象
public static Singleton2 getInstance() {
return INSTANCE;
}
}
准备好了吗,我又要开始瞎编啦
- 还是一样的我们私有化构造器 (单例类的构造器被私有化,意味着外部无法直接通过
new
关键字创建该类的实例。这是为了防止在外部代码中实例化多个Singleton2
对象,从而违反单例模式的原则。) INSTANCE
是一个private static
类型的变量。它用来存储单例对象。static
表示这个变量属于类本身,而不是某个实例,也就是说,类加载时这个变量就会被分配内存空间。static
静态代码块用于在类加载时初始化INSTANCE
对象。静态代码块在类被加载到 JVM 中时只执行一次,因此这里创建的INSTANCE
对象也只会存在一个实例。这样就实现了单例模式的特性,保证了Singleton2
只有一个实例。- 由于
INSTANCE
是static
的,所以即使Singleton2
类没有被实例化,INSTANCE
仍然可以通过类名直接访问。
什么?你还是听不懂?听不懂就对了,这是ai写的不是我说的,我也还没看懂
emm 不过这是一个因为我们先创建了一个私有变量instace
会在内存中开辟一段空间,这个空间是在类加载中就加载,我们传统的是实例化的时候加载,所以我们用的还是同一个实例化的对象满足了单例模式。
那么为什么我们要写成静态代码块的样子呢就很好解释了,就是想要在类加载的时候就加载这个对象,这种与上面那种其实是一样的只是写法不太一样。第二种相对比较动态
优缺点
- 1)这种方式和上面的方式其实类似,只不过将类实例化的过程放在了静态代码块中,也是在类装载的时候,就执行静态代码块中的代码,初始化类的实例。优缺点和上面是一样的。
- 2)结论:这种单例模式可用,但是可能造成内存浪费
3.懒汉式(线程不安全)
package com.sgg.design.singleton.Type03; |
嘿嘿嘿,有没有突然明白懒汉与饿汉的区别呢
懒汉很懒,饿的时候再吃饭
饿汉饿怕了,先把饭做好,还不饿我就已经准备吃饭了
优缺点
- 1)起到了Lazy Loading的效果,但是只能在单线程下使用。
- 2)如果在多线程下,一个线程进入_了if (singleton ==null)判断语句块,还未来得及往下执行,另一个线程也通过了这个判断语句,这时便会产生多个实例。所以在多线程环境下不可使用这种方式
- 3)结论:在实际开发中,不要使用这种方式.
4.懒汉式(线程安全,同步方法)
synchronized
package com.sgg.design.singleton.Type04; |
优缺点
- 1)解决了线程不安全问题
- 2)效率太低了,每个线程在想获得类的实例时候,执行getInstance()方法都要进行同步。而其实这个方法只执行一次实例化代码就够了,后面的想获得该类实例,直接return就行了。方法进行同步效率太低
- 3)结论:在实际开发中,不推荐使用这种方式
5.懒汉式(线程安全,同步代码块)
、
优缺点
- 1)这种方式,本意是想对第四种实现方式的改进,因为前面同步方法效率太低,改为同步产生实例化的的代码块
- 2)但是这种同步并不能起到线程同步的作用。跟第3种实现方式遇到的情形一致,假如一个线程进入了if (singleton+== null)判断语句块,还未来得及往下执行,另一个线程也通过了这个判断语句,这时便会产生多个实例
- 3)结论:在实际开发中,不能使用这种方式
6.双重检查(推荐)
保证一个线程在执行
我好累有点不太想说话了
凑合着看代码吧
package com.sgg.design.singleton.Type06; |
我们来看一下代码吧
首先我们先解释一下两个关键字
- synchronized可以保证方法或者代码块在运行时,同一时刻只有一个方法可以进入到临界区,同时它还可以保证共享变量的内存可见性,Java中每一个对象都可以作为锁,这是synchronized实现同步的基础。
- volatile 理解为把实例化的对象共享即可
- 假设我们有A,B两个线程,A进入了第一个if语句,这时候(synchronized)B就只能再门口等着,A执行第二个if语句实例化了对象就退出了(volatile),B进入第二个循环语句被告知instance不为空了,所以就不用执行那一个实例化语句了
- 往后面及时有再多的线程也进不去第二个循环了
优缺点
- Double-Check概念是多线程开发中常使用到的,如代码中所示,我们进行了两次if (singleton ==null)检查.这样就可以保证线程安全了。
- 这样,实例化代码只用执行一次,后面再次访问时,判断if (singleton == null),直接return实例化对象,也避免的反复进行方法同步.
- 线程安全;延迟加载;效率较高
- 结论:在实际开发中,推荐使用这种单例设计模式
7.静态内部类(推荐)
当我们的Singleton被装载时,我们的静态内部类不会被装载
当我们去调用Singleton 的get方法的时候,用到了这个静态变量的时候,我们的静态静态内会被装载,当我们类被装载的时候线程是安全的
package com.sgg.design.singleton.Type7; |
这代码感觉好有质量
我来编一下代码逻辑
这个静态内部类他在我们Singleton装载的时候并不会马上执行(保障了我们的单例和懒加载都是可以满足的)
当我们调用getInstance的时候,他会取这个静态内部类SingletonInstance里的instance属性,这个时候我们的SingletonInstance就可以加载了,因为我们的静态内在装载的过程中是线程安全的
优缺点
- 1)这种方式采用了类装载的机制来保证初始化实例时只有一个线程。
- 2)静态内部类方式在Singleton类被装载时并不会立即实例化,而是在需要实例化时,调用getInstance方法,牙会装载SingletonInstance类,从而完成singleton的实例化。
- 3)类的静态属性只会在第一次加载类的时候初始化,所以在这里,JVM帮助我们保证了线程的安全性,在类进行初始化时,别的线程是无法进入的。
- 4)优点:避免了线程不安全,利用静态内部类特点实现延迟加载,效率高
- 5)结论:推荐使用.
8.枚举(最完美)
package com.sgg.design.singleton.Type8; |
优缺点
- 1)这借助JDK1.5中添加的枚举来实现单例模式。不仅能避免多线程同步问题,而且还能防止反序列化重新创建新的对象
- 2)这种方式是tffective Java作者Josh Bloch提倡的方式
- 3)结论(推荐使用
单例模式的注意事项和细节说明
- 1))单例模式保证了系统内存中该类只存在一个对象,节省了系统资源,对于一些需要频繁创建销毁的对象,使用单例模式可以提高系绞性能
- 2)当想实例化一个单例类的时候,必须要记住使用相应的获取对象的方法,而不是使用new
- 3)单例模式使用的场景:需要频繁的进行创建和销毁的对象创建对象时耗时过多或耗费资源过多(即:重量级对象),但又经常用到的对象、工具类对象、频繁访问数据库或文件的对象(比如数据源、session工厂等)
又是一年冬至
2.简单工厂模式
案例应用
第一步:建一个抽象方法
(什么?你问我为什么建一个抽象方法)
抽象方法相当于标准,我们很多披萨只要实现这个方法就好了,避免大量操作
第二步:其他披萨去继承这个披萨抽象类就可以了,然后再子类中重写就可以啦
传统代码
没有使用工厂方法的
注意:超多方法来袭
pizza抽象类
package com.sgg.design.Factory.simplefactory.pizzastore.Pizza; |
希腊披萨
package com.sgg.design.Factory.simplefactory.pizzastore.Pizza; |
奶酪披萨
package com.sgg.design.Factory.simplefactory.pizzastore.Pizza; |
订购披萨的功能
package com.sgg.design.Factory.simplefactory.pizzastore.order; |
实现类
package com.sgg.design.Factory.simplefactory.pizzastore.order; |
假设我们现在增加一个新品种—胡椒披萨
呼叫劈杀
package com.sgg.design.Factory.simplefactory.pizzastore.Pizza; |
需要修改OrderPizza里面的内容
if (orderType .equals("greek")) { |
哎哎哎等等,新增功能会修改源代码怎么这么熟悉,你能联想到什么原则呢
对对对,就是那个那个那个那个嘛,我知道的👀
《开闭原则》
对啦,太聪明啦,我就知道你会想起来的
传统方式优缺点
- 优点是比较好理解,简单易操作。
- 缺点是违反了设计模式的ocp原则,即对扩展开放,对修改关闭。即当我们给类增加新功能的时候,尽量不修改代码,或者尽可能少修改代码.
- 比如我们这时要新增加一个Pizza的种类(Pepper披萨),我们需要做源代码修改.
改进思路
分析:修改代码可以接受,但是如果我们在其它的地方也有创建Pizza的代码,就意味着,也需要修改,而创建Pizza的代码,往往有多处。
思路:把创建Pizza对象封装到一个类中,这样我们有新的Pizza种类时,只需要修改该类就可,其它有创建到Pizza对象的代码就不需要修改了-—>简单工厂模式
简单工厂模式
- 1)简单工厂模式是属于创建型模式,是工厂模式的一种。简单工厂模式是由一个工厂对象决定创建出哪一种产品类的实例。简单工厂模式是工厂模式家族中最简单实用的模式
- 2)简单工厂模式:定义了一个创建对象的类,由这个类来封装实例化对象的行为(代码)
- 3)在软件开发中,当我们会用到大量的创建某种、某类或者某批对象时,就会使用到工厂模式.
我们现在定义了一个工厂(类似黑中介,代购)想买披萨是吧?先来找我,我帮你去买