共计 9201 个字符,预计需要花费 24 分钟才能阅读完成。
大家好,我是小菜,一个渴望在互联网行业做到蔡不菜的小菜。可柔可刚,点赞则柔,白嫖则刚!死鬼~ 看完记得给我来个三连哦!
本文次要介绍
软件的设计准则
如有须要,能够参考
如有帮忙,不忘 点赞 ❥
微信公众号已开启,小菜良记,没关注的同学们记得关注哦!
忆往昔,学习软件设计准则的时候还是在大学,那时候编程入坑不深,只感觉可学可不学,毕竟课程有些无聊,看今朝,是本人年老了!
一、UML 图
不要感觉奇怪为什么不讲软件设计准则而说到了 UML 图 ,因为软件设计准则和软件设计模式中你讲到最多的便是利用类图来示意 类与类之间的关系 ,因而咱们须要 先会看,再会理,最初再来写!
1. 什么是 UML
对立建模语言(Unified Modeling Language,UML),是用来设计软件的可视化建模语言,它的特点是简略、对立、图形化、能表白软件设计中的动静与动态信息。
UML 从指标零碎的不同角度登程,定义了用例图、类图、对象图、状态图、流动图、时序图、合作图、构件图、部署图等 9 种图。而咱们来重点理解一下 类图。
2. 类图
1)类图的概念
类图(Class disgram)是显示了模型的动态构造,特地是模型中存在的类、类的内部结构以及它们与其余类的关系等。类图不显示暂时性的信息,类图是面向对象建模的次要组成部分。
2)类图的作用
类图是一种动态的结构图,形容了零碎中类的汇合、类的属性和类之间的关系,能够简化对系统的了解。
3)类图的示意
类图的示意是应用蕴含类名(className)、属性(field)和办法(method)且带有分割线的矩形来示意,格局如下:
示例如下:
咱们能够看到 属性 / 办法 后面存在 + / –,他们示意了这个属性或办法的可见性,示意可见性的符号有三种,如下:
- +: 示意 public
- –: 示意 private
- #: 示意 protected
从下面的示例中咱们能够总结出:
- 属性的残缺示意形式是:可见性 名称 : 类型 [= 缺省值]
- 办法的残缺示意形式是:可见性 名称(参数列表) [: 返回类型]
中括号里的内容是可选的
也能够将类型放在变量名后面,返回值类型放在办法名后面
4)类图的关系
1. 关联关系
关联关系是对象之间的一种援用关系,用于示意一个类对象与另一个类对象之间的分割。比方老师和学生,学生与课程,校长与学校。关联关系是类与类之间最 罕用 的一种关系,分为 个别关联关系 , 聚合关系 和组合关系
个别关联关系又分为: 单向关联
, 双向关联
, 自关联
- 单向关联
从图中咱们能够很直观的看出以下几点:
- 有两个类,别离是 Company(公司类) 和 Address(地址类)
- 每个 Company(公司类) 中都有一个 Address(地址类)
单向关联是用一个带箭头的实线示意
简而言之:每个公司都有一个地址,通过让 Company(公司类) 持有一个类型为 Address(地址类) 的成员变量来实现。
- 双向关联
双向关联艰深的意思就是:你中有我,我中有你
。从上图中咱们能够看出:单方类中各自持有对方类型的成员变量。在 Company(公司类) 中持有一个 Employee(员工类) 的汇合,示意一个公司有能够有多个员工,而在 Employee(员工类) 中持有一个 Company(公司类) 的成员变量,示意这个员工所属于哪个公司。
须要留神的是,双向关联的连线与单向关联有所不同。双向关联是用一个 不带箭头的直线 示意。
- 自关联
自关联又是一种比拟非凡的关联关系,艰深来说就是 我中有我
,是应用一个 带有箭头且指向本身的线 示意,这种关联关系咱们个别在迭代器模式中比拟常见。
2. 聚合关系
聚合关系其实也是关联关系的一种,只不过这种关联关系属于 强关联关系 ,是 整体和局部之前的关系。
聚合关系也是通过 成员变量
来实现的,其中成员对象是整体对象的一部分,然而成员对象能够脱离整体对象而独立存在。比方一个公司和员工的关系,公司中存在员工,然而如果公司开张了,这些员工仍然存在,他们能够去别的公司。
注: 聚合关系是应用 带空心菱形的实线 示意的,特地须要留神菱形的方向, 菱形是指向整体的
3. 组合关系
既然聚合关系是一种 "好聚好散"
的关系,那有没有那种 "鱼死网破"
的关系呢?必定是有的,那就是聚合关系的加强版—组合关系。这是一种更加强烈的聚合关系。
在组合关系中,整体对象能够管制局部对象的生命周期,一旦整体对象不存在,局部对象也将不存在,局部对象不能脱离整体对象而存在,有点 "一荣俱荣,一损俱损"
的内味了。这就如同咱们头和嘴的关系,如果头不存在了,那么嘴也就不存在了。
注: 组合关系是应用 带实心菱形的实线 示意的,同样咱们须要留神菱形的方向, 菱形是指向整体的
4. 依赖关系
依赖关系是一种应用关系,它是对象之间耦合度最弱的一种关联形式,是临时性的关联。在代码中,咱们通常是某个类的办法通过局部变量、办法参数或者对静态方法的调用来拜访另一个类(被依赖类)中的某些办法来实现一些职责。艰深的了解就是:我须要你的时候,咱们就存在了临时性的关系,我不须要你的时候,咱们之间就毫无关系。说着说着,越来越感觉像事实中 女神和舔狗 的关系。就比方以下图示:
注: 依赖关系是应用 带箭头的虚线 来示意,箭头从应用类指向被依赖的类。
5. 继承关系
继承关系是对象之间耦合度最大的一种关系,示意个别与非凡的关系,是父类与子类之间的关系,是一种继承关系。比方 Student 类 和 Teacher 类 都是 Person 类 的子类。
注: 继承关系是应用 空心三角箭头的实线 来示意,箭头从子类指向父类。
6. 实现关系
实现关系是接口与实现类之间的关系,在这种关系中,类实现了接口,类中的操作实现了接口中所申明的所有的形象操作。
注: 实现关系是应用 带空心三角箭头的虚线 来示意,箭头从实现类指向接口。
微信搜:小菜良记
更多干货值得关注
二、软件设计准则
下面铺垫了那么多常识,配角终于能够上场了。咱们也不卖关子了,间接来看下咱们将要理解到的软件设计准则:
从导图中咱们理解到须要的设计准则总共有 6 种,不用感到胆怯,接下来小菜带你一个一个去理解!
1. 开闭准则
开闭准则属于比拟根底的设计准则,了解起来也比较简单,就一句话:
对扩大凋谢,对批改敞开
,然而往往就是这么简略的一句话,做起来却分外的艰难。在程序须要进行扩大的时候,不能去批改原有的代码,实现一个热插拔的成果。
咱们先理一下思路,既然是对批改敞开,对扩大凋谢,那必定是不能对原有类进行批改了,不能对原有类进行批改,又想要达到这种成果,那咱们就得应用 接口和抽象类
了。
因为形象进去的货色灵活性较好,适用性较广,只有形象的正当,就能够根本放弃软件架构的稳固。而软件中易变的细节能够从抽象类中派生出新的实现类来进行扩大,当需要发生变化时候,咱们只须要依据需要从新派生一个实现类来扩大就能够了。
示例: 咱们玩游戏都有一个英雄角色,这个时候为了氪金,就有了皮肤的需要,然而不同皮肤咱们就不能对原有的英雄类进行批改,这是不合理的,所以咱们就得从原有的英雄类中派生出新的英雄进去。
2. 里氏代换准则
里氏代换准则:任何基类能够呈现的中央,子类肯定能够呈现。简略来说就是子类能够扩大父类的性能,但不能扭转父类原有的性能,也就是子类继承父类时,除了增加新的办法实现新增性能外,尽量不要重写父类的办法。
里氏代换准则 也是面向对象设计中最根本的准则之一,艰深意思就是 父类总能被子类代替。如果咱们通过重写父类的办法来实现新的性能,这样写起来尽管比较简单,然而整个继承体系的可复用性会比拟差,特地是使用多态比拟频繁时,程序运行出错的概率会十分大。
示例:正方形是一个非凡的长方形,只不过是正方形的长宽都一样,那咱们就用反证法来试一下是否合乎里氏代换准则
咱们先理一下目前的所看到的货色,有三个类:Rectangle(长方形类)、Square(正方形类)、Client(客户端类)
,其中 正方形类 继承自 长方形类 ,因而这两个类属于继承关系。 客户端类 其中有个办法须要依赖 长方形类 ,因而这两个类属于 依赖关系 。一般来说长方形的长会大于长方形的宽,因而 客户端类 中的办法实现的性能就是当长方形的严惩于长时,长的值就会加1,直到长的值大于宽的值。
接下来咱们用代码来实现一下:
Rectangle
:
Square
:
Client
:
咱们能够看到有个长为 10,宽为 15 的长方形,咱们也胜利实现了性能,扩容后的长方形长为 16,宽为 15。依据里氏代换准则,咱们如果传入的是 正方形类 也是能够实现这个性能的,咱们持续来试一下:
public static void main(String[] args) {Square square = new Square();
square.setWidth(10);
System.out.println(square);
resize(square);
}
咱们在控制台等了许久,发现并没有输入后果,直到栈溢出。这是长始终与宽相等,导致退出不了循环。那么这个时候咱们得出结论,这种设计是不合乎 里氏代换准则
的,因而这种设计是谬误的。所以咱们平时在设计零碎的时候,就须要思考咱们设计的 父子类 是否合乎 里氏代换准则
,那么依据以上例子咱们能够作出改良,既然 长方形类 不适宜做 正方形类 的子类,那咱们是否应该思考形象出一个 四边形类 进去,来作为两个类的父类。
其中咱们形象出了 Quadrangle(四边形类) 其中定义了获取长和宽的两个办法,Square(正方形类) 和 Rectangle(长方形类)别离实现 四边形类,接下来咱们用代码来实现:
Quadrangle
:
public interface Quadrangle {
double getWidth();
double getLength();}
Square
:
Rectangle
:
Client:
看完代码咱们扭转的只有减少了 四边形类 和批改了 正方形类 ,这样子咱们应用扩容办法的时候须要传的是 长方形类 ,而 正方形类 不继承与 长方形类 ,扩容这个办法不实用,因而满足了 里氏代换准则。
3. 依赖倒转准则
依赖倒转高层模块不应该依赖底层模块,两者都应该依赖其形象;形象不应该依赖细节,细节应该依赖形象。
简略来说依赖倒转的外围就是对形象进行编程,不要对实现进行编程,这样就会升高客户与实现模块之间的耦合。
示例:咱们如果要组装一台电脑,组装电脑须要用到的配件有硬盘,内存和 CPU,每个配件都有不同的品牌以供选择,咱们这里抉择希捷的硬盘,英特尔的 CPU,金士顿的内存,图示如下:
代码实现如下:
XIJieHardDisk
:
@Data
public class XIJieHardDisk {private String capacity;}
KingstonMemory
:
@Data
public class KingstonMemory {private String capacity;}
IntelCpu
:
public class IntelCpu {public void run() {System.out.println("英特尔处理器开始运行");
}
}
Computer
:
@Data
public class Computer {
private XIJieHardDisk hardDisk;
private IntelCpu cpu;
private KingstonMemory memory;
public void run() {System.out.println("计算机开始运行,参数如下:");
System.out.println("硬盘容量为 :" + hardDisk.getCapacity());
System.out.println("内存容量为 :" + memory.getCapacity());
cpu.run();}
public static void main(String[] args) {Computer computer = new Computer();
XIJieHardDisk hardDisk = new XIJieHardDisk();
hardDisk.setCapacity("1T");
IntelCpu cpu = new IntelCpu();
KingstonMemory memory = new KingstonMemory();
memory.setCapacity("16G");
computer.setHardDisk(hardDisk);
computer.setCpu(cpu);
computer.setMemory(memory);
computer.run();}
}
/** OUTPUT:计算机开始运行,参数如下:
硬盘容量为 : 1T
内存容量为 : 16G
英特尔处理器开始运行
**/
依据运行后果咱们计算机也顺利组装胜利,然而目前看起来如同是没问题,如果咱们想要换个品牌的 CPU 或者 内存条 ,咱们除了减少一个 对应品牌的类 之外咱们是不是还要批改 Computer 类 ,刚看完上局部的小伙伴必定马上意识到这不就违反了 开闭准则
吗,真是瞎胡闹。
那既然这种设计是谬误的,咱们就依照 依赖倒装准则 来改良一下: 高层模块不应该依赖底层模块,两者都应该依赖其形象
,咱们须要批改 Computer 类,让Computer 类 依赖形象(各个配件的接口),而不是依赖于各个组件具体的实现类。图示如下:
代码中咱们只须要批改 Computer 类 和减少三个配件的主接口,让子品牌别离实现对应的接口即可:
HardDisk:
public interface HardDisk {
void setCapacity(String data);
String getCapacity();}
Memory:
public interface Memory {
void setCapacity(String data);
String getCapacity();
}
Cpu:
public interface Cpu {void run();
}
Computer:
@Data
public class Computer {
private HardDisk hardDisk;
private Cpu cpu;
private Memory memory;
public void run() {System.out.println("计算机开始运行,参数如下:");
System.out.println("硬盘容量为 :" + hardDisk.getCapacity());
System.out.println("内存容量为 :" + memory.getCapacity());
cpu.run();}
public static void main(String[] args) {Computer computer = new Computer();
XIJieHardDisk hardDisk = new XIJieHardDisk();
hardDisk.setCapacity("1T");
IntelCpu cpu = new IntelCpu();
KingstonMemory memory = new KingstonMemory();
memory.setCapacity("16G");
computer.setHardDisk(hardDisk);
computer.setCpu(cpu);
computer.setMemory(memory);
computer.run();}
}
/** OUTPUT:计算机开始运行,参数如下:
硬盘容量为 : 1T
内存容量为 : 16G
英特尔处理器开始运行
**/
这样子让高层模块依赖形象模块,就能够实现解耦了,更加不便扩大。
4. 接口隔离准则
客户端不应该被迫依赖于它不实用的办法,一个类对另一个类的依赖应该建设在最小的接口上。
简略来说就是 强扭的瓜不甜
,不适宜本人的就不要强行加成。比如说一台手机能够打电话,发短信,上网,然而上网这个性能对于老人机就不实用了,咱们就不应该把上网这个性能强行加给老人机。本来的设计应该是这样的:
这样子显著是不合理的,咱们依据 接口隔离准则
进行改良:一个类对另一个类的依赖应该建设在最小接口上
,那咱们应该把每个性能都抽取成各个接口,而后每种手机通过依赖的形式引入性能,图示如下:
代码实现如下:
InternetFun:
public interface InternetFun {void internet();
}
MessageFun:
public interface MessageFun {void message();
}
PhoneFun:
public interface PhoneFun {void phone();
}
NewPhone:
public class NewPhone implements InternetFun, MessageFun, PhoneFun {
@Override
public void internet() {System.out.println("上网性能已具备");
}
@Override
public void message() {System.out.println("发短信性能已具备");
}
@Override
public void phone() {System.out.println("打电话性能已具备");
}
}
OldPhone:
public class OldPhone implements PhoneFun, MessageFun{
@Override
public void message() {System.out.println("发短信性能已具备");
}
@Override
public void phone() {System.out.println("打电话性能已具备");
}
}
通过 接口隔离准则
,咱们能够须要什么性能,就实现什么接口,满足了最小依赖。
5. 迪米特法令
迪米特法令又成为起码常识准则。只和你的间接敌人交谈,不跟 “ 陌生人谈话 ”。
简略来说就是如果两个软件实体无需间接通信,那么就不该当产生间接的互相调用,能够通过第三方转发该调用,目标就是为了升高类之间的耦合度,进步模块间的绝对独立性。
示例: 对于明星来说,他不用和经纪公司还有粉丝间接打交道,这些事件只须要交给经纪人来解决就好了,经纪人就相当于是第三方,粉丝见面会和签约这种事都是通过经纪人在两头解决,而不必明星本人打交道。图示如下:
代码实现如下:
Star:
@Data
public class Star {
private String name;
public Star(String name) {this.name = name;}
}
Company:
@Data
public class Company {
private String name;
public Company(String name) {this.name = name;}
}
Fans:
@Data
public class Fans {
private String name;
public Fans(String name) {this.name = name;}
}
Agent:
@Data
public class Agent {
private Star star;
private Company company;
private Fans fans;
public Agent(Star star, Company company, Fans fans) {
this.star = star;
this.company = company;
this.fans = fans;
}
public void meeting() {System.out.println("经纪人安顿" + star.getName() + "与粉丝:" + fans.getName() + "见面了");
}
public void bussiness() {System.out.println("经纪人安顿" + star.getName() + "与娱乐公司:" + company.getName() + "签约了");
}
public static void main(String[] args) {Star star = new Star("小菜");
Company company = new Company("Cbuc 娱团");
Fans fans = new Fans("小菜菜");
Agent agent = new Agent(star, company, fans);
agent.meeting();
agent.bussiness();}
}
/** OUTPUT:
经纪人安顿小菜与粉丝: 小菜菜 见面了
经纪人安顿小菜与娱乐公司:Cbuc 娱团 签约了
**/
迪米特法令
就是通过中间人来实现单方及多方的交互,这样每方之间的关系就不会很芜杂。
6. 合成复用准则
合成复用准则是指尽量先应用组合或者聚合等关联关系来实现,其次才思考应用继承关系来实现
通常类的复用分为 继承复用 和 合成复用 两种
继承复用 相对来说会比较简单易实现,然而也存在以下毛病:
- 继承复用毁坏了类的封装性。因为继承会将父类的实现细节裸露给子类。父类对子类是通明的,所以这种复用又称为
"白箱" 复用
- 子类与父类的耦合度高。父类的任何扭转都会导致子类实现发生变化,这不利于类的扩大与保护
- 它限度了复用的灵活性。从父类继承而来的实现是动态的,在编译时曾经定义,所以在运行时不可能发生变化
采纳组合或聚合复用时,能够将已有对象纳入新对象中,使之成为新对象的一部分,新对象能够调用已有对象的性能,它有以下长处:
- 它保护了类的封装性,因为成分对象的外部细节是新对象看不见的,所以这种复用又称为
"黑箱" 复用
- 对象间的耦合度低,能够在类的成员地位申明形象
- 复用的灵活性高,这种复用能够在运行时动静进行,新对象能够动静地援用于成分对象类型雷同的对象
咱们来看下不同复用的图示:
继承复用:
首先有个汽车的抽象类,这个时候如果依照 ” 动力源 ” 划分的话,咱们又能够扩大为 “ 汽油汽车 ” 和 “ 能源汽车 ”,接着咱们又能够针对色彩来划分,分为彩色和红色乃至更多其余色彩。这样子有个很显著的问题就是,通过继承复用就会产生很多子类。那咱们接下来就用 聚合复用 来实现一下:
咱们将色彩独自抽取了进去,以聚合的形式来实现,能够看到整个设计也比较简单,这样就达到了实现 合成复用准则
END
软件设计准则就曾经讲完啦,往往感觉不以为然的货色才更加重要,这就是咱们的根底,得更加牢固才行。放个小彩蛋,据说小菜会接着出软件设计模式解说,连忙关注起来吧!微信搜:小菜良记
。
路漫漫,小菜与你一起求索!
明天的你多致力一点,今天的你就能少说一句求人的话!
我是小菜,一个和你一起学习的男人。
????
微信公众号已开启,小菜良记,没关注的同学们记得关注哦!