技术标签: 设计模式
用抽象构建架构,用实现扩展细节。
- 单一职责原则:实现类要职责单一;
- 接口隔离原则:在设计接口的时候要精简单一;
- 依赖倒转原则:面向接口编程;
- 里氏替换原则:不要破坏继承关系;
- 开闭原则原则:对扩展开发,对修改关闭;
- 迪米特法则:最少知道原则;
- 合成复用原则:先考虑组合或聚合,后考虑继承;
降低代码复杂度、降低系统耦合度、提高可读性
对于一个类,只有一个引起该类变化的原因;该类的职责是唯一的,且这个职责是唯一引起其他类变化的原因。
将不同的职责封装到不同的类或者模块中,当有新的需求将现有的职责分为颗粒度更小的职责的时候,应该及时对现有代码进行重构。
(1)降低类的复杂度,一个类只负责一个职责。这样写出来的代码逻辑肯定要比负责多项职责简单得多。
(2)提高类的可读性,提高系统的可维护性。
(3)降低变更引起的风险。变更是必然的,如果单一职责原则遵守得好,当修改一个功能的时候可以显著降低对其他功能的影响。
(1)降低类的复杂度,一个类只负责一项职责;
(2)提高类的可读性,可维护性;
(3)降低变更引起的风险;
(4)通常情况下,我们应当遵守单一职责原则,只有逻辑足够简单、才可以在代码级违反单一职责原则,只有类中的方法足够少,才可以在类中保持方法级别的单一职责原则。
避免接口过于臃肿
客户端不应该依赖它不需要的接口,一个类对另一个类的依赖应该建立在最小的接口上。
适度细化接口,将臃肿的接口拆分为独立的几个接口。
(1)将臃肿庞大的接口分解为多个粒度小的接口,可以预防外来变更的扩散,提高系统的灵活性和可维护性。
(2)接口隔离提高了系统的内聚性,减少了对外交互,降低了系统的耦合性。
(3)使用多个专门的接口还能够体现对象的层次,因为可以通过接口的继承,实现对总接口的定义。
(4)能减少项目工程中的代码冗余。过大的大接口里面通常放置许多不用的方法,当实现这个接口的时候,被迫设计冗余的代码。
如果接口的粒度大小定义合理,能够保证系统的稳定性;但是,如果定义过小,则会造成接口数量过多,使设计复杂化;如果定义太大,灵活性降低,无法提供定制服务,给整体项目带来无法预料的风险。
避免需求变化导致过多的维护工作
每一个逻辑的实现都是由原子逻辑组成的,不可分割的原子逻辑就是低层模块(一般是接口,抽象类),原子逻辑的组装就是高层模块。在Java语言中,抽象就是指接口和或抽象类,两者都不能被直接实例化。细节就是实现类,实现接口或继承抽象类而产生的类就是细节,可以被直接实例化。
面向接口编程,使用接口或者抽象类制定好规范和契约,而不去设计任何具体的操作,把展现细节的任务交给他们的实现类去完成。
采用依赖倒置原则可以减少类间的耦合性,提高系统的稳定性,降低并行开发引起的风险,提高代码的可读性和可维护性。
(1)接口声明依赖对象;
(2)构造函数传递依赖对象;
在类中通过构造函数声明依赖对象(好比spring中的构造器注入),采用构造器注入。
(3)Setter方法传递依赖对象
在抽象中设置setter方法声明依赖对象(spring中的方法注入)
依赖倒转原则的本质就是通过抽象使各个类或模块实现彼此独立,不互相影响,实现模块间的松耦合。
在项目中使用这个规则需要以下原则:
(1)每个类尽量都要有接口或抽象类:依赖倒转的基本要求,有抽象才能依赖倒转;
(2)变量的表明类型尽量是接口或者抽象类;
(3)任何类都不应该从具体类派生;
(4)尽量不要重写基类已经写好的方法(里氏替换原则);
(5)结合里氏替换原则来使用:
接口负责定义public属性和方法,并且声明与其他对象的依赖关系;
抽象类负责公共构造部分的实现;
实现类准确的实现业务逻辑,同时在适当的时候对父类进行细化;
一句话,依赖倒转原则就是面向接口编程。
避免系统继承体系被破坏
所有引用基类的地方必须能透明地使用其子类的对象。
(1)子类可以实现父类的抽象方法,但是不能覆盖父类的非抽象方法;
(2)子类可以增加自己特有的方法;
(3)当子类覆盖或实现父类的抽象方法时,方法的前置条件(即方法的形参)要比父类方法的输入参数更宽松;方法的后置条件(即方法的返回值)要比父类更严格。
(4)如果子类不能完整地实现父类的方法,或者父类的一些方法在子类中已经发生畸形,则建议断开继承关系,采用依赖,聚合,组合等关系继承。
package designMode.advance.principle;
public class Liskov {
public static void main(String[] args) {
A a = new A();
System.out.println("11-3=" + a.func1(11, 3));
System.out.println("1-8=" + a.func1(1, 8));
System.out.println("-----------------------");
B b = new B();
//因为B类不再继承A类,因此调用者,不会再func1是求减法
//调用完成的功能就会很明确
System.out.println("11+3=" + b.func1(11, 3));//这里本意是求出11+3
System.out.println("1+8=" + b.func1(1, 8));// 1+8
System.out.println("11+3+9=" + b.func2(11, 3));
//使用组合仍然可以使用到A类相关方法
System.out.println("11-3=" + b.func3(11, 3));// 这里本意是求出11-3
}
}
//创建一个更加基础的基类
class Base {
//把更加基础的方法和成员写到Base类
}
// A类
class A extends Base {
// 返回两个数的差
public int func1(int num1, int num2) {
return num1 - num2;
}
}
// B类继承了A
// 增加了一个新功能:完成两个数相加,然后和9求和
class B extends Base {
//如果B需要使用A类的方法,使用组合关系
private A a = new A();
//这里,重写了A类的方法, 可能是无意识
public int func1(int a, int b) {
return a + b;
}
public int func2(int a, int b) {
return func1(a, b) + 9;
}
//我们仍然想使用A的方法
public int func3(int a, int b) {
return this.a.func1(a, b);
}
}
提高扩展性、便于维护
对扩展开放(对提供方),对修改关闭(对使用方)。
当软件需要变化时,尽量通过扩展软件实体的行为来实现变化,而不是通过修改已有的代码来实现。
开闭原则是面向对象设计中最基础的设计原则,它指导我们如何建立稳定灵活的系统,开闭原则只定义了对修改关闭,对扩展开放。
因为抽象灵活性好,适应性广,只要抽象的合理,可以基本保证架构的稳定。而软件中易变的细节,我们用从抽象派生的实现类来进行扩展,当软件需要发生变化时,我们只需要根据需求重新派生一个实现类来扩展就可以了,当然前提是抽象要合理,要对需求的变更有前瞻性和预见性。
降低类与类之间的耦合度
迪米特法则又叫最少知道原则,即一个类对自己依赖的类知道的越少越好,对于依赖的类不管有多复杂,都尽量将逻辑封装在类的内部,对外除了提供public方法,不泄漏任何信息。
更简单的说法:只与直接朋友通信。
直接朋友:每个对象都会与其它对象有耦合关系,耦合的方式有很多,依赖、关联、组合、聚合等。我们称出现在成员变量,方法参数,方法返回值中的类称为直接朋友,而出现在局部变量中的类不能称为直接朋友,也就是说,陌生的类不要以局部变量的形式出现在类的内部。
(1)在类的结构设计上,尽量降低类成员的访问权限;
(2)在类的设计上,优先考虑将一个类设计成不变类;
(3)在类的引用上,将引起其他类的次数降到最低;
(4)不暴露类的属性成员,而应该提供相应的访问器(getter、setter);
(5)谨慎使用序列化(serializable)功能;
过分的使用迪米特原则,会产生大量这样的中介和传递类,类之间需要通信就通过第三方转发的方式,就会造成系统的不同模块之间的通信效率降低、使系统的不同模块之间不容易协调等缺点,同时大大增加了系统的复杂度。所以在釆用迪米特法则时需要反复权衡,确保高内聚和低耦合的同时,保证系统的结构清晰。
下面代码违反了迪米特法则,我平时就这么写的。。。
package designMode.advance.principle;
import java.util.ArrayList;
import java.util.List;
public class Demeter {
public static void main(String[] args) {
//创建了一个 SchoolManager 对象
SchoolManager schoolManager = new SchoolManager();
//输出学院的员工id 和 学校总部的员工信息
schoolManager.printAllEmployee(new CollegeManager());
}
}
//学校总部员工类
class Employee {
private String id;
public void setId(String id) {
this.id = id;
}
public String getId() {
return id;
}
}
//学院的员工类
class CollegeEmployee {
private String id;
public void setId(String id) {
this.id = id;
}
public String getId() {
return id;
}
}
//管理学院员工的管理类
class CollegeManager {
//返回学院的所有员工
public List<CollegeEmployee> getAllEmployee() {
List<CollegeEmployee> list = new ArrayList<CollegeEmployee>();
for (int i = 0; i < 10; i++) { //这里我们增加了10个员工到 list
CollegeEmployee emp = new CollegeEmployee();
emp.setId("学院员工id= " + i);
list.add(emp);
}
return list;
}
}
//学校管理类
//分析 SchoolManager 类的直接朋友类有哪些 Employee、CollegeManager
//CollegeEmployee 不是 直接朋友 而是一个陌生类,这样违背了 迪米特法则
class SchoolManager {
//返回学校总部的员工
public List<Employee> getAllEmployee() {
List<Employee> list = new ArrayList<Employee>();
for (int i = 0; i < 5; i++) { //这里我们增加了5个员工到 list
Employee emp = new Employee();
emp.setId("学校总部员工id= " + i);
list.add(emp);
}
return list;
}
//该方法完成输出学校总部和学院员工信息(id)
void printAllEmployee(CollegeManager sub) {
//分析问题
//1. 这里的 CollegeEmployee 不是 SchoolManager的直接朋友
//2. CollegeEmployee 是以局部变量方式出现在 SchoolManager
//3. 违反了 迪米特法则
//获取到学院员工
List<CollegeEmployee> list1 = sub.getAllEmployee();
System.out.println("------------学院员工------------");
for (CollegeEmployee e : list1) {
System.out.println(e.getId());
}
//获取到学校总部员工
List<Employee> list2 = this.getAllEmployee();
System.out.println("------------学校总部员工------------");
for (Employee e : list2) {
System.out.println(e.getId());
}
}
}
使用迪米特法则改进:
//该方法完成输出学校总部和学院员工信息(id)
void printAllEmployee(CollegeManager sub) {
//分析问题
//1. 将输出学院的员工方法,封装到CollegeManager
sub.printEmployee();
//获取到学校总部员工
List<Employee> list2 = this.getAllEmployee();
System.out.println("------------学校总部员工------------");
for (Employee e : list2) {
System.out.println(e.getId());
}
}
//管理学院员工的管理类
class CollegeManager2 {
//返回学院的所有员工
public List<CollegeEmployee> getAllEmployee() {
List<CollegeEmployee> list = new ArrayList<CollegeEmployee>();
for (int i = 0; i < 10; i++) { //这里我们增加了10个员工到 list
CollegeEmployee emp = new CollegeEmployee();
emp.setId("学院员工id= " + i);
list.add(emp);
}
return list;
}
//输出学院员工的信息
public void printEmployee() {
//获取到学院员工
List<CollegeEmployee> list1 = getAllEmployee();
System.out.println("------------学院员工------------");
for (CollegeEmployee e : list1) {
System.out.println(e.getId());
}
}
}
简单来说,就是将获取学院员工的方法写到学校员工类中。
防止类的体系庞大
它要求在软件复用时,要尽量先使用组合或者聚合等关联关系来实现,其次才考虑使用继承关系来实现。
如果要使用继承关系,则必须严格遵循里氏替换原则。合成复用原则同里氏替换原则相辅相成的,两者都是开闭原则的具体实现规范。
(1)通常的复用分为继承复用和合成复用,继承复用虽然有简单和易实现的优点,但它也有如下的缺点:
因为继承会将父类的实现细节暴露给子类,父类对子类是透明的,所以这种复用又称为“白箱”复用。
父类的改变会直接影响子类,不利于类的扩展和维护。
从父类继承而来的实现是静态的,在编译时已经定义,运行时无法发生变化。
(2)采用组合或聚合复用时,可以将已有对象纳入新对象中,使之成为新对象的一部分,新对象可以调用已有对象的功能,它有以下优点。
因为成分对象的内部细节是新对象看不见的,所以这种复用又称为“黑箱”复用。
这种复用所需的依赖较少,新对象存取成分对象的唯一方法是通过成分对象的接口。
这种复用可以在运行时动态进行,新对象可以动态地引用与成员对象类型相同的对象。
文章浏览阅读965次,点赞18次,收藏21次。现在我们有了 2021 年和 2023 年的 NDVI 数据帧,我们需要从 2021 年的值中减去 2023 年的值以捕获 NDVI 的差异。该数据集包括像素级别的植被值,我们将编写一个自定义函数来根据红色和绿色波段的表面反射率计算 NDVI。在我的上一篇文章中,我演示了如何将单个多边形分割/镶嵌为一组大小均匀的六边形。现在我们有了植被损失数据,让我们使用 Kepler.gl 可视化每个六边形的植被损失。将地图保存为 HTML 文件,在浏览器中打开 HTML 以获得更好的视图。现在我们将调用该函数并使用、
文章浏览阅读3.3k次,点赞6次,收藏5次。正态分布,又称高斯分布或钟形曲线,是统计学中最为重要和常用的分布之一。_echarts正态分布图
文章浏览阅读217次。首先要在Mainfest.xml中加入所需要的权限:[html] view plain copyprint?uses-permission android:name="android.permission.SEND_SMS"/> uses-permission android:name="android.permission.READ_SMS"/> _android bundle.get("pdus");
文章浏览阅读2.6k次。0、说明最近在学习 Data Assimilation Research Testbed (DART) 相关内容,其软件是在 Unix/Linux 操作系统下编译和运行的 ,由于我的电脑是 Windows 10 的,DART 推荐可以使用 Windows Subsystem For Linux (WSL) 来创建一个 Windows 下的 Linux 子系统。以下的内容主要介绍如何安装 WSL2,以及 WSL2 的联网。1、如何在 Windows 10 下安装WSL具体的安装流程可以在 microso_wsl2 联网
文章浏览阅读1k次。DB_LINK 介绍在本机数据库orcl上创建了一个prod_link的publicdblink(使用远程主机的scott用户连接),则用sqlplus连接到本机数据库,执行select * from scott.emp@prod_link即可以将远程数据库上的scott用户下的emp表中的数据获取到。也可以在本地建一个同义词来指向scott.emp@prod_link,这样取值就方便多了..._添加 database link重复的数据库链接命
文章浏览阅读3.1k次。ylbtech-云-腾讯云-实时音视频:实时音视频(TRTC)支持跨终端、全平台之间互通,从零开始快速搭建实时音视频通信平台1.返回顶部 1、腾讯实时音视频(Tencent Real-Time Communication,TRTC)拥有QQ十几年来在音视频技术上的积累,致力于帮助企业快速搭建低成本、高品质音视频通讯能力的完整解决方案。..._腾讯实时音视频 分享链接
文章浏览阅读534次,点赞10次,收藏8次。编写一个完整的日历表需要处理许多细节,包括公历和农历之间的转换、节气、闰年等。运行程序后,会输出指定年份的日历表。注意,这个程序只是一个简单的示例,还有很多可以改进和扩展的地方,例如添加节气、节日等。_农历库c语言
文章浏览阅读1w次,点赞28次,收藏27次。FL Studio21.1.1.3750中文破解版是最优秀、最繁荣的数字音频工作站 (DAW) 之一,日新月异。它是一款录音机和编辑器,可让您不惜一切代价制作精美的音乐作品并保存精彩的活动画廊。为方便用户,FL Studio 21提供三种不同的版本——Fruity 版、Producer 版和签名版。所有这些版本都是独一无二的,同样具有竞争力。用户可以根据自己的需要选择其中任何一种。FL Studio21.1.1.3750中文版可以说是一站式综合音乐制作单位,可以让您录制、作曲、混音和编辑音乐。_fl studio 21 注册机
文章浏览阅读1.3k次。冯诺依曼计算机工作原理冯 诺依曼计算机工作原理的核心是 和 程序控制世界上不同型号的计算机,就其工作原理而言,一般都是认为冯 诺依曼提出了什么原理冯 诺依曼原理中,计算机硬件系统由那五大部分组成的 急急急急急急急急急急急急急急急急急急急急急急冯诺依曼结构计算机工作原理的核心冯诺依曼结构和现代计算机结构模型 转载重学计算机组成原理 一 冯 诺依曼体系结构从冯.诺依曼的存储程序工作原理及计算机的组成来..._简述冯诺依曼计算机结构及工作原理
文章浏览阅读559次。这次在随机乱下的基础上加上了一些简单的处理,如进营、炸棋、吃子等功能,在和敌方棋子产生碰撞之后会获取敌方棋子大小的一些信息,目前采用的是事件驱动模型,当下完一步棋界面返回结果后会判断是否触发了相关事件,有事件发生则处理相关事件,没有事件发生则仍然是随机下棋。1.事件驱动模型首先定义一个各种事件的枚举变量,目前的事件有工兵吃子,摸暗棋,进营,明确吃子,炸棋。定义如下:enum MoveE..._军棋引擎
文章浏览阅读85次。1, 模板观念与函数模板简单模板: template< typename T > T Function( T a, T b) {… }类模板: template struct Object{……….}; 函数模板 template< class T> inline T Function( T a, T b){……} 不可以使用不同型别的..._geekband 讲义
文章浏览阅读158次。"^\d+$" //非负整数(正整数 + 0)"^[0-9]*[1-9][0-9]*$" //正整数"^((-\d+)|(0+))$" //非正整数(负整数 + 0)"^-[0-9]*[1-9][0-9]*$" //负整数"^-?\d+$" //整数"^\d+(\.\d+)?$" //非负浮点数(正浮点数 + 0)"^(([0-9]+\.[0-9]*[1-9][0-9]*)|([0..._vb.net 正则表达式 取html中的herf