编写软件过程中,程序员面临着来自耦合性,内聚性以及可维护性,可扩展性,重用性,灵活性等多方面的挑战,设计模式是为了让程序(软件),具有更好:
- 代码重用性 (即:相同功能的代码,不用多次编写)
- 可读性 (即:编程规范性, 便于其他程序员的阅读和理解)
- 可扩展性 (即:当需要增加新的功能时,非常的方便,称为可维护)
- 可靠性 (即:当我们增加新的功能后,对原来的功能没有影响)
- 使程序呈现高内聚,低耦合的特性
一、七大原则
设计模式原则,其实就是程序员在编程时,应当遵守的原则,也是各种设计模式的基础
1.1 单一职责原则
概述
一个类应该只负责一项职责。如果一个类负责两个职责,那么修改其中一个职责的功能时,可能会影响另一个职责。
示例
方案一:
/**
* 示例单一原则
* @author cVzhanshi
* @create 2023-01-29 20:23
*/
public class single {
public static void main(String[] args) {
Vehicle vehicle = new Vehicle();
vehicle.run("摩托车");
vehicle.run("轮船");
vehicle.run("飞机");
}
}
class Vehicle{
public void run (String vehicle){
System.out.println(vehicle + "在公路上走...");
}
}
// 运行结果
// 摩托车在公路上走...
// 轮船在公路上走...
// 飞机在公路上走...
分析:
- 在示例一的 run 方法中,违反了单一职责原则
- 解决的方案非常的简单,根据交通工具运行方法不同,分解成不同类即可
方案二:
class RoadVehicle {
public void run(String vehicle) {
System.out.println(vehicle + "公路运行");
}
}
class AirVehicle {
public void run(String vehicle) {
System.out.println(vehicle + "天空运行");
}
}
class WaterVehicle {
public void run(String vehicle) {
System.out.println(vehicle + "水中运行");
}
}
分析:
- 遵守单一职责原则
- 改动很大,即将类分解,同时修改客户端
- 改进:直接修改 Vehicle 类,改动的代码会比较少
方案三:
class Vehicle2 {
public void run(String vehicle) {
//处理
System.out.println(vehicle + " 在公路上运行....");
}
public void runAir(String vehicle) {
System.out.println(vehicle + " 在天空上运行....");
}
public void runWater(String vehicle) {
System.out.println(vehicle + " 在水中行....");
}
}
分析:
- 这种修改方法没有对原来的类做大的修改,只是增加方法
- 这里虽然没有在类这个级别上遵守单一职责原则,但是在方法级别上,仍然是遵守单一职责
单一职责原则注意事项和细节
- 降低类的复杂度,一个类只负责一项职责。
- 提高类的可读性,可维护性
- 降低变更引起的风险
- 通常情况下,我们应当遵守单一职责原则,只有逻辑足够简单,才可以在代码级违反单一职责原则;只有类中方法数量足够少,可以在方法级别保持单一职责原则(如上述的方案三,如果方案三还有其他的方法,那么就违反了单一职责原则)
1.2 接口隔离原则
概述
客户端不应该依赖它不需要的接口,即一个类对另一个类的依赖应该建立在最小的接口上
示例
说明:
- 接口A中有五个方法
- 类D依赖类B使用到了接口中的1,2,3方法
- 类E依赖类C使用到了接口中的1,4,5方法
/**接口隔离原则示例
* @author cVzhanshi
* @create 2023-01-30 15:44
*/
public class demo01 {
public static void main(String[] args) {
B b = new B();
D d = new D();
d.depend1(b);
d.depend2(b);
d.depend3(b);
C c = new C();
E e = new E();
e.depend1(c);
e.depend4(c);
e.depend5(c);
}
}
class D{
public void depend1(A a){
a.function1();
}
public void depend2(A a){
a.function2();
}
public void depend3(A a){
a.function3();
}
}
class E{
public void depend1(A a){
a.function1();
}
public void depend4(A a){
a.function4();
}
public void depend5(A a){
a.function5();
}
}
class C implements A{
@Override
public void function1() {
System.out.println("C 实现了 function1");
}
@Override
public void function2() {
System.out.println("C 实现了 function2");
}
@Override
public void function3() {
System.out.println("C 实现了 function3");
}
@Override
public void function4() {
System.out.println("C 实现了 function4");
}
@Override
public void function5() {
System.out.println("C 实现了 function5");
}
}
class B implements A{
@Override
public void function1() {
System.out.println("B 实现了 function1");
}
@Override
public void function2() {
System.out.println("B 实现了 function2");
}
@Override
public void function3() {
System.out.println("B 实现了 function3");
}
@Override
public void function4() {
System.out.println("B 实现了 function4");
}
@Override
public void function5() {
System.out.println("B 实现了 function5");
}
}
interface A{
void function1();
void function2();
void function3();
void function4();
void function5();
}
分析:
-
如果接口A对于类D和类E来说不是最小接口,那么类 B 和类 C 必须去实现他们不需要的方法。所以违反了接口隔离原则
-
改进:将接口A拆分为独立的几个接口,类 D 和类E 分别与他们需要的接口建立依赖关系。采用接口隔离原则
-
拆分详情:
- 接口A存在方法1,因为类D,E都使用了
- 接口B存在方法2,3,因为类D单独使用了
- 接口C存在方法4,5,因为类E单独使用了
这样的话类D、E依赖的类就不需要实现他们不需要的方法了
示例代码
/**
* 接口隔离原则示例
* @author cVzhanshi
* @create 2023-01-30 15:57
*/
public class demo02 {
public static void main(String[] args) {
D d = new D();
E e = new E();
F f = new F();
G g = new G();
d.depend1(f);
d.depend2(f);
d.depend3(f);
e.depend1(g);
e.depend4(g);
e.depend5(g);
}
}
class D{
public void depend1(F f){
f.function1();
}
public void depend2(F f){
f.function2();
}
public void depend3(F f){
f.function3();
}
}
class E{
public void depend1(G g){
g.function1();
}
public void depend4(G g){
g.function4();
}
public void depend5(G g){
g.function5();
}
}
// 依赖类
class F implements A, B{
@Override
public void function1() {
System.out.println("F 实现了 function1");
}
@Override
public void function2() {
System.out.println("F 实现了 function2");
}
@Override
public void function3() {
System.out.println("F 实现了 function3");
}
}
// 依赖类
class G implements A, C{
@Override
public void function1() {
System.out.println("G 实现了 function1");
}
@Override
public void function4() {
System.out.println("G 实现了 function4");
}
@Override
public void function5() {
System.out.println("G 实现了 function5");
}
}
interface A{
void function1();
}
interface B{
void function2();
void function3();
}
interface C{
void function4();
void function5();
}
1.3 依赖倒转原则
概述
- 高层模块不应该依赖低层模块,二者都应该依赖其抽象
- 抽象不应该依赖细节,细节应该依赖抽象
- 依赖倒转(倒置)的中心思想是面向接口编程
- 依赖倒转原则是基于这样的设计理念:相对于细节的多变性,抽象的东西要稳定的多。以抽象为基础搭建的架构比以细节为基础的架构要稳定的多。在 java 中,抽象指的是接口或抽象类,细节就是具体的实现类
- 使用接口或抽象类的目的是制定好规范,而不涉及任何具体的操作,把展现细节的任务交给实现类去完成
示例
示例一:
/**
* 依赖倒转原则示例
* @author cVzhanshi
* @create 2023-01-30 20:24
*/
public class demo01 {
public static void main(String[] args) {
Person person = new Person();
person.receive(new Email());
}
}
class Email{
public String getInfo(){
return "电子邮箱信息:hello";
}
}
class Person{
public void receive(Email email){
System.out.println(email.getInfo());
}
}
分析:
- 如果我们要获取微信,短信等等的信息,则需要新增类,同时 Perons 也要增加相应的接收方法
- 改进:引入一个抽象的接口 IReceiver, 表示接收者, 这样 Person 类与接口 IReceiver 发生依赖。Email, WeiXin 等等属于接收的范围,他们各自实现 IReceiver 接口就可以了,这样客户端不需要改变。这样我们就符合依赖倒转原则
示例二:
/**
* @author cVzhanshi
* @create 2023-01-30 20:31
*/
public class demo02 {
public static void main(String[] args) {
Person person = new Person();
person.receive(new WeChat());
person.receive(new Email());
}
}
interface IReceive{
String getInfo();
}
class Email implements IReceive{
@Override
public String getInfo() {
return "电子邮箱信息:hello";
}
}
class WeChat implements IReceive{
@Override
public String getInfo() {
return "微信信息:weChat";
}
}
class Person{
public void receive(IReceive receive){
System.out.println(receive.getInfo());
}
}
依赖关系传递的三种方式
接口传递
/**
* @author cVzhanshi
* @create 2023-01-30 20:31
*/
public class demo02 {
public static void main(String[] args) {
Person person = new Person();
person.receive(new WeChat());
person.receive(new Email());
}
}
interface IReceive{
String getInfo();
}
class Email implements IReceive{
@Override
public String getInfo() {
return "电子邮箱信息:hello";
}
}
class WeChat implements IReceive{
@Override
public String getInfo() {
return "微信信息:weChat";
}
}
class Person{
public void receive(IReceive receive){
System.out.println(receive.getInfo());
}
}
构造方法传递
/**
* @author cVzhanshi
* @create 2023-01-30 20:31
*/
public class demo02 {
public static void main(String[] args) {
EmailClass emailClass = new EmailClass();
Person person = new Person(emailClass);
person.getInfo();
}
}
interface IReceive{
void getInfo();
}
interface Email{
public void getMessage();
}
class EmailClass implements Email{
@Override
public void getMessage() {
System.out.println("EmailClass messages");
}
}
class Person implements IReceive{
private Email email;
public Person(Email email) {
this.email = email;
}
@Override
public void getInfo() {
email.getMessage();
}
}
通过setter方法传递
/**
* @author cVzhanshi
* @create 2023-01-30 20:31
*/
public class demo02 {
public static void main(String[] args) {
EmailClass emailClass = new EmailClass();
Person person = new Person();
person.setEmail(emailClass);
person.getInfo();
}
}
interface IReceive{
void getInfo();
}
interface Email{
public void getMessage();
}
class EmailClass implements Email{
@Override
public void getMessage() {
System.out.println("EmailClass messages");
}
}
class Person implements IReceive{
private Email email;
public void setEmail(Email email) {
this.email = email;
}
@Override
public void getInfo() {
email.getMessage();
}
}
依赖倒转原则的注意事项和细节
- 低层模块尽量都要有抽象类或接口,或者两者都有,程序稳定性更好
- 变量的声明类型尽量是抽象类或接口, 这样我们的变量引用和实际对象间,就存在一个缓冲层,利于程序扩展和优化
- 继承时遵循里氏替换原则
1.4 里氏替换原则
概述
继承在给程序设计带来便利的同时,也带来了弊端。比如使用继承会给程序带来侵入性,程序的可移植性降低,增加对象间的耦合性,如果一个类被其他的类所继承, 则当这个类需要修改时,必须考虑到所有的子类,并且父类修改后,所有涉及到子 类的功能都有可能产生故障。
里氏替换原则
-
所有引用基类的地方必须能透明地使用其子类的对象
-
在使用继承时,遵循里氏替换原则,在子类中尽量不要重写父类的方法
-
继承实际上让两个类耦合性增强了,在适当的情况下,可以通过聚合,组合,依赖来解决问题。
就是说,使用继承不要去破坏原有的东西,不要给程序带来侵入性。
示例
示例一:
public class demo01 {
public static void main(String[] args) {
A a = new A();
System.out.println("11-3=" + a.func1(11, 3)); // 11-3=8
System.out.println("1-8=" + a.func1(1, 8)); // 1-8=-7
System.out.println("-----------------------");
B b = new B();
System.out.println("11-3=" + b.func1(11, 3)); // 11-3=14
System.out.println("1-8=" + b.func1(1, 8)); // 1-8=9
System.out.println("11+3+9=" + b.func2(11, 3)); // 11+3+9=23
}
}
class A {
public int func1(int num1, int num2) {
return num1 - num2;
}
}
class B extends A {
public int func1(int a, int b) {
return a + b;
}
public int func2(int a, int b) {
return func1(a, b) + 9;
}
}
说明:因为类B不知情的情况下重写了类A的方法,把类A 的减法变成加法。导致了main函数类B想使用减法是出错。这就是继承带来的侵入性。
改造:原来的父类和子类都继承一个更通俗的基类,原有的继承关系去掉, 采用依赖,聚合,组合等关系代替.
public class demo02 {
public static void main(String[] args) {
A a = new A();
System.out.println("11-3=" + a.func1(11, 3)); // 11-3=8
System.out.println("1-8=" + a.func1(1, 8)); // 1-8=-7
System.out.println("-----------------------");
B b = new B();
System.out.println("11-3=" + b.func3(11, 3)); // 11-3=8
System.out.println("1-8=" + b.func3(1, 8)); // 1-8=-7
System.out.println("11+3+9=" + b.func2(11, 3)); // 11+3+9=23
}
}
class Base {
}
class A extends Base {
public int func1(int num1, int num2) {
return num1 - num2;
}
}
class B extends Base {
private A a = new A();
public int func1(int a, int b) {
return a + b;
}
public int func2(int a, int b) {
return func1(a, b) + 9;
}
public int func3(int a, int b) {
return this.a.func1(a, b);
}
}
1.5 开闭原则
概述
- 模块和函数应该对扩展开放(对提供方),对修改关闭(对使用方)。用抽象构建框架,用实现扩展细节。
- 当软件需要变化时,尽量通过扩展软件实体的行为来实现变化,而不是通过修改已 有的代码来实现变化。
示例
代码示例一:
public class demo01 {
public static void main(String[] args) {
GraphicEditor graphicEditor = new GraphicEditor();
graphicEditor.drawShape(new Rectangle());
graphicEditor.drawShape(new Circle());
}
}
// 使用方 这是一个用于绘图的类
class GraphicEditor {
public void drawShape(Shape s) {
if (s.m_type == 1)
drawRectangle(s);
else if (s.m_type == 2)
drawCircle(s);
}
public void drawRectangle(Shape r) {
System.out.println("矩形");
}
public void drawCircle(Shape r) {
System.out.println("圆形");
}
}
class Shape {
int m_type;
}
class Rectangle extends Shape {
Rectangle() {
super.m_type = 1;
}
}
class Circle extends Shape {
Circle() {
super.m_type = 2;
}
}
说明:
- 优点是比较好理解,简单易操作。
- 缺点是违反了设计模式的ocp原则,即对扩展开放(提供方),对修改关闭(使用方)。 即当我们给类增加新功能的时候,尽量不修改代码,或者尽可能少修改代码.
- 比如我们这时要新增加一个图形种类 三角形,我们需要做如下修改,修改的地方 较多
修改:思路:把创建Shape类做成抽象类,并提供一个抽象的draw方法,让子类去实现即可, 这样我们有新的图形种类时,只需要让新的图形类继承Shape,并实现draw方法即可, 使用方的代码就不需要修 -> 满足了开闭原则
/**
* @author cVzhanshi
* @create 2023-03-14 10:12
*/
public class demo02 {
public static void main(String[] args) {
Editor editor = new Editor();
editor.drawShape(new A());
editor.drawShape(new B());
}
}
class Editor{
public void drawShape(Shape s){
s.draw();
}
}
abstract class Shape{
public abstract void draw();
}
class A extends Shape {
@Override
public void draw() {
System.out.println("画A");
}
}
class B extends Shape {
@Override
public void draw() {
System.out.println("画B");
}
}
1.6 迪米特法则
概述
- 一个对象应该对其他对象保持最少的了解
- 类与类关系越密切,耦合度越大
- 迪米特法则(Demeter Principle)又叫最少知道原则,即一个类对自己依赖的类知道的越少越好。也就是说,对于被依赖的类不管多么复杂,都尽量将逻辑封装在类的内部。对外除了提供的public 方法,不对外泄露任何信息。
- 迪米特法则还有个更简单的定义:只与直接的朋友通信
直接的朋友:每个对象都会与其他对象有耦合关系,只要两个对象之间有耦合关系, 我们就说这两个对象之间是朋友关系。耦合的方式很多,依赖,关联,组合,聚合等。其中,我们称出现成员变量,方法参数,方法返回值中的类为直接的朋友,而出现在局部变量中的类不是直接的朋友。也就是说,陌生的类最好不要以局部变量的形式出现在类的内部。
示例
错误示例,违背了迪米特法则
class Test {
public void test() {
System.out.println("测试");
}
}
class Apple {
public void test() {
// 陌生的类最好不要以局部变量的形式出现在类的内部
Test test = new Test();
test.test();
}
}
改正
class Test {
public void test() {
System.out.println("测试");
}
}
class Apple {
public void test(Test test) {
test.test();
}
}
迪米特法则注意事项和细节
- 迪米特法则的核心是降低类之间的耦合。
- 但是注意:由于每个类都减少了不必要的依赖,因此迪米特法则只是要求降低类间(对象间)耦合关系, 并不是要求完全没有依赖关系。
1.7 合成复用原则
尽量使用合成或者聚合 的方式,而不是使用聚合
1.8 设计原则核心思想
- 找出应用中可能需要变化之处,把它们独立出来,不要和那些不需要变化的代码混在一起。
- 针对接口编程,而不是针对实现编程。
- 为了交互对象之间的松耦合设计而努力。
二、UML类图
用于描述系统中的类(对象)本身的组成和类(对象)之间的各种静态关系
类之间的关系:依赖、泛化(继承)、实现、关联、聚合与组合
-
类:类图分三层,第一层是类名,如果是抽象类就用斜体表示;第二层代表属性或字段;第三层是类的操作即方法,方法前面会用符号表示权限修饰符,‘+’表示public,‘-’表示private,‘#’表示protected。
-
接口:接口和类图的区别是顶端有
<<interface>>
显示,第二行表示的是接口的方法;接口还有第二种表示方法(棒棒糖表示法),圆圈边上表示接口名称,接口的方法再实现类里面。 -
继承关系:空心三角形 + 实线
-
实现关系:空心三角形 + 虚线
-
关联关系:实线 + 箭头
-
聚合关系:空心的菱形 + 实现箭头;聚合表示一种弱的”拥有“关系,体现的是A对象可以包含B对象,但B对象不是A对象一部分
空心菱形 + 实线箭头表示
-
组合(合成)关系:组成是一种强的“拥有”关系,体现了严格的部分和整体的关系,部分和整体的生命周期一样。组合关系(关联关系和聚合关系也可以有基数)还具备基数的,表示对应关系。
实心的菱形 + 实线箭头
-
依赖关系:虚线 + 箭头
如:类中用到了对方、成员属性、方法的返回类型、方法接收的参数类型、方法中使用到
三、设计模式简介
- 设计模式是程序员在面对同类软件工程设计问题所总结出来的有用的经验, 模式不是代码,而是某类问题的通用解决方案,设计模式(Design pattern) 代表了最佳的实践。
- 设计模式的本质提高软件的维护性,通用性和扩展性,并降低软件的复杂度。
设计模式分为三种类型,共23种
- 创建型模式:单例模式、抽象工厂模式、原型模式、建造者模式、工厂模式。
- 结构型模式:适配器模式、桥接模式、装饰模式、组合模式、外观模式、享元模式、代理模式。
- 行为型模式:模版方法模式、命令模式、访问者模式、迭代器模式、观察者模式、中介者模式、备忘录模式、解释器模式(Interpreter模式)、状态模 式、策略模式、职责链模式(责任链模式)。