设计模式概述及七大原则

news2024/11/17 19:39:27

一、设计模式的目的

编写软件过程中,我们会面临着来自耦合性,内聚性以及可维护性,可扩展性,重用性,灵活性等多方面的挑战,设计模式是为了让程序(软件),具有更好:

  1. 代码重用性 (即:相同功能的代码,不用多次编写)
  2. 可读性 (即:编程规范性, 便于其他程序员的阅读和理解)
  3. 可扩展性 (即:当需要增加新的功能时,非常的方便,称为可维护)
  4. 可靠性 (即:当我们增加新的功能后,对原来的功能没有影响)
  5. 使程序呈现高内聚,低耦合的特性

二、设计模式的七大原则

设计模式之七大基本原则 - 知乎 (zhihu.com)

2.1 单一职责原则

2.1.1 基本介绍

对类来说,一个类应该只负责一项职责。如类A负责两个不同职责:职责1,职责2,当职责1需求变更而改变类A时,可能造成职责2执行错误,所以需要将类A的粒度分解为A1,A2。

2.1.2 应用实例

方案一:

/**
 *  交通工具类
 *  方式1
 *  1. 在方式1 的run方法中,违反了单一职责原则
 *  2. 解决的方案非常的简单,根据交通工具运行方法不同,分解成不同类即可
 */
public class SingleResponsibility1 {

    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 + " 在公路上运行....");
    }
}

方案二:

/**
 * 方案2的分析
 * 1. 遵守单一职责原则
 * 2. 但是这样做的改动很大,即将类分解,同时修改客户端
 * 3. 改进:直接修改Vehicle 类,改动的代码会比较少=>方案3
 */
public class SingleResponsibility2 {

    public static void main(String[] args) {

        RoadVehicle roadVehicle = new RoadVehicle();
        roadVehicle.run("摩托车");
        roadVehicle.run("汽车");

        AirVehicle airVehicle = new AirVehicle();

        airVehicle.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 + "水中运行");
    }
}

方案三:

/**
 * 方式3的分析
 * 1. 这种修改方法没有对原来的类做大的修改,只是增加方法
 * 2. 这里虽然没有在类这个级别上遵守单一职责原则,但是在方法级别上,仍然是遵守单一职责
 */
public class SingleResponsibility3 {

    public static void main(String[] args) {
        Vehicle2 vehicle2  = new Vehicle2();
        vehicle2.run("汽车");
        vehicle2.runWater("轮船");
        vehicle2.runAir("飞机");
    }
}

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 + " 在水中行....");
    }

    //方法2.
    //..
    //..

    //...
}

2.1.3 注意事项

  • 降低类的复杂度,一个类只负责一项职责;
  • 提高类的可读性、可维护性;
  • 降低变更引起的风险;
  • 通常情况下,我们应当遵守单一职责原则,只有逻辑足够简单,才可以在代码级违反单一职责原则:只有类中方法数量足够少,可以在方法级别保存单一职责原则。

2.2 接口隔离原则

2.2.1 基本介绍

客户端不应该依赖它不需要的接口,即一个类对另一个类的依赖应该建立在最小的接口上。

2.2.2 应用实例

问题引入

如图,类A通过接口Interface1依赖类B,类C通过接口Interface1依赖类D,如果接口Interface1对于类A和类C来说不是最小接口,那么类B和类D必须去实现他们不需要的方法。

image-20230305183302488

/**
 * 接口隔离
 * 如果有多个类通过接口进行相互依赖的时候,需要将接口进行拆分以便于产生多余的依赖方法
 * 此处 A和 B进行联系使用了接口的123方法,但是4和5没有进行使用;所以可以将接口拆分为若干个部分(所谓的接口隔离)
 *
 * 在这里将Interface split为 InterfaceSplit1 和InterfaceSplit2 粒度更小,没有无关使用方法
 */
public class Segregation1 {
    public static void main(String[] args) {

    }
}


//接口
interface Interface1 {
    void operation1();
    void operation2();
    void operation3();
    void operation4();
    void operation5();
}

class B implements Interface1 {
    public void operation1() {
        System.out.println("B 实现了 operation1");
    }

    public void operation2() {
        System.out.println("B 实现了 operation2");
    }
    public void operation3() {
        System.out.println("B 实现了 operation3");
    }
    public void operation4() {
        System.out.println("B 实现了 operation4");
    }
    public void operation5() {
        System.out.println("B 实现了 operation5");
    }
}

class D implements Interface1 {
    public void operation1() {
        System.out.println("D 实现了 operation1");
    }

    public void operation2() {
        System.out.println("D 实现了 operation2");
    }
    public void operation3() {
        System.out.println("D 实现了 operation3");
    }
    public void operation4() {
        System.out.println("D 实现了 operation4");
    }
    public void operation5() {
        System.out.println("D 实现了 operation5");
    }
}

// A 类通过接口Interface1 依赖(使用) B类,但是只会用到1,2,3方法
class A {
    public void depend1(Interface1 i) {
        i.operation1();
    }
    public void depend2(Interface1 i) {
        i.operation2();
    }
    public void depend3(Interface1 i) {
        i.operation3();
    }
}

// C 类通过接口Interface1 依赖(使用) D类,但是只会用到1,4,5方法
class C {
    public void depend1(Interface1 i) {
        i.operation1();
    }
    public void depend4(Interface1 i) {
        i.operation4();
    }
    public void depend5(Interface1 i) {
        i.operation5();
    }
}

解决

  1. 类A通过接口Interface1依赖类B,类C通过接口Interface1依赖类D,如果接口Interface1对于类A和类C来说不是最小接口,那么类B和类D必须去实现他们不需要的方法

  2. 将接口Interface1拆分为独立的几个接口,类A和类C分别与他们需要的接口建立依赖关系。也就是采用接口隔离原则

  3. 接口Interface1中出现的方法,根据实际情况拆分为三个接口

image-20230305184249415

public class Segregation {

    public static void main(String[] args) {
        // TODO Auto-generated method stub
        // 使用一把
        A a = new A();
        a.depend1(new B()); // A类通过接口去依赖B类
        a.depend2(new B());
        a.depend3(new B());

        C c = new C();

        c.depend1(new D()); // C类通过接口去依赖(使用)D类
        c.depend4(new D());
        c.depend5(new D());}

}

// 接口1
interface Interface1 {
    void operation1();

}

// 接口2
interface Interface2 {
    void operation2();

    void operation3();
}

// 接口3
interface Interface3 {
    void operation4();

    void operation5();
}

class B implements Interface1, Interface2 {
    public void operation1() {
        System.out.println("B 实现了 operation1");
    }

    public void operation2() {
        System.out.println("B 实现了 operation2");
    }

    public void operation3() {
        System.out.println("B 实现了 operation3");
    }

}

class D implements Interface1, Interface3 {
    public void operation1() {
        System.out.println("D 实现了 operation1");
    }

    public void operation4() {
        System.out.println("D 实现了 operation4");
    }

    public void operation5() {
        System.out.println("D 实现了 operation5");
    }
}

class A { // A 类通过接口Interface1,Interface2 依赖(使用) B类,但是只会用到1,2,3方法
    public void depend1(Interface1 i) {
        i.operation1();
    }

    public void depend2(Interface2 i) {
        i.operation2();
    }

    public void depend3(Interface2 i) {
        i.operation3();
    }
}

class C { // C 类通过接口Interface1,Interface3 依赖(使用) D类,但是只会用到1,4,5方法
    public void depend1(Interface1 i) {
        i.operation1();
    }

    public void depend4(Interface3 i) {
        i.operation4();
    }

    public void depend5(Interface3 i) {
        i.operation5();
    }
}

2.3 依赖倒转原则

2.3.1 基本介绍

  • 高层模块不应该依赖低层模块,二者都应该依赖其抽象

  • 抽象不应该依赖细节,细节应该依赖抽象

  • 依赖倒转(倒置)的中心思想是面向接口编程

  • 依赖倒转原则是基于这样的设计理念:相对于细节的多变性,抽象的东西要稳定的多。以抽象为基础搭建的架构比以细节为基础的架构要稳定的多。在java中,抽象指的是接口或抽象类,细节就是具体的实现类

  • 使用接口或抽象类的目的是制定好规范,而不涉及任何具体的操作,把展现细节的任务交给他们的实现类去完成

2.3.2 应用实例

请编程完成persion接收消息的功能

  • 方案一:

    public class DependecyInversion {
        public static void main(String[] args) {
            Persion persion = new Persion();
            persion.receive(new Email());
        }
    }
    
    class Email {
        public String getInfo() {
            return "电子邮件:Hello World!!!";
        }
    }
    
    class Persion {
        public void receive(Email e) {
            System.out.println(e.getInfo());
        }
    }
    
  • 方案二:

    /**
     * 如果我们获取的对象是微信、短信等等,则新增类,同时Persion也要增加相应的接收方法
     * 解决思路:引入一个抽象的接口IReceiver,表示接收者,这样Persion类与接口IReceiver发生依赖
     * 因为Email、WeChat等等属于接收的范围,它们各自实现IReceiver接口就可以,这样我们就符合依赖倒转原则
     */
    public class DependecyInversion2 {
        public static void main(String[] args) {
            Persion2 persion2 = new Persion2();
            persion2.receive(new Email2());
            persion2.receive(new WeChat());
        }
    }
    
    interface IReceiver {
        public String getInfo();
    }
    
    class Email2 implements IReceiver {
        @Override
        public String getInfo() {
            return "电子邮件:Hello World!";
        }
    }
    
    class WeChat implements IReceiver {
        @Override
        public String getInfo() {
            return "微信消息:Hello weixin";
        }
    }
    
    
    class Persion2 {
        public void receive(IReceiver i) {
            System.out.println(i.getInfo());
        }
    }
    

2.3.3 依赖传递的三种方式

通过接口传递

public class DependencyPass {
    public static void main(String[] args) {

        ChangHong changHong = new ChangHong();
		OpenAndClose openAndClose = new OpenAndClose();
		openAndClose.open(changHong);
    }
}

// 方式1: 通过接口传递实现依赖
// 开关的接口
interface IOpenAndClose {
    public void open(ITV tv); //抽象方法,接收接口
}

interface ITV { //ITV接口0-
    public void play();
}

// 实现接口
class OpenAndClose implements IOpenAndClose {
    public void open(ITV tv) {
        tv.play();
    }
}

class ChangHong implements ITV {

    @Override
    public void play() {
        System.out.println("长虹电视机,打开");
    }

}

构造方法传递

public class DependencyPass {
    public static void main(String[] args) {
        ChangHong changHong = new ChangHong();

		// 通过构造器进行依赖传递
        OpenAndClose openAndClose = new OpenAndClose(changHong);
		openAndClose.open();
    }
}

// 方式2: 通过构造方法依赖传递
interface IOpenAndClose {
    public void open(); //抽象方法
}

interface ITV { //ITV接口
    public void play();
}

class OpenAndClose implements IOpenAndClose {

    private ITV tv; // 成员
    
    public OpenAndClose(ITV tv){ //构造器
        this.tv = tv;
    }
    
    public void open() {
        this.tv.play();
    }
}

class ChangHong implements ITV {

    @Override
    public void play() {
        System.out.println("长虹电视机,打开");
    }

}

setter方式传递

public class DependencyPass {
    public static void main(String[] args) {
        ChangHong changHong = new ChangHong();
        //通过setter方法进行依赖传递
        OpenAndClose openAndClose = new OpenAndClose();
        openAndClose.setTv(changHong);
        openAndClose.open();
    }
}


// 方式3 通过setter方法传递
interface IOpenAndClose {
    public void open(); // 抽象方法

    public void setTv(ITV tv);
}

interface ITV { // ITV接口
    public void play();
}

class OpenAndClose implements IOpenAndClose {
    private ITV tv;

    public void setTv(ITV tv) {
        this.tv = tv;
    }

    public void open() {
        this.tv.play();
    }
}

class ChangHong implements ITV {

    @Override
    public void play() {
        System.out.println("长虹电视机,打开");
    }

}

2.3.4 注意事项

  • 低层模块尽量都要有抽象类或接口,或者两者都有,程序稳定性更好

  • 变量的声明类型尽量是抽象类或接口,这样我们的变量引用和实际对象间,就存在一个缓冲层,利于程序扩展和优化

  • 继承时遵循里氏替换原则

2.4 里氏替换原则

OO(面向对象) 中继承性的思考和说明

  • 继承包含这样一层含义:父类中凡是已经实现好的方法,实际上是在设定规范和契约,虽然它不强制要求所有的子类必须遵循这些契约,但是如果子类对这些已经实现的方法任意修改,就会对整个继承体系造成破坏

  • **继承在给程序设计带来便利的同时,也带来了弊端。比如使用继承会给程序带来侵入性,程序的可移植性降低,增加对象间的耦合性。**如果一个类被其他的类所继承,则当这个类需要修改时,必须考虑到所有的子类,并且父类修改后,所有涉及到子类的功能都有可能产生故障

  • 问题提出:在编程中,如何正确使用继承?=> 里氏替换原则

2.4.1 基本介绍

  • 里氏替换原则在1988年,由麻省理工学院的以为姓里的女士提出的

  • 如果对每个类型为 T1 的对象 o1,都有类型为 T2 的对象 o2,使得以 T1 定义的所有程序 P 在所有的对象 o1 都代换成 o2 时,程序 P 的行为没有发生变化,那么类型 T2 是类型 T1 的子类型。换句话说,所有引用基类的地方必须能透明地使用其子类的对象

  • 在使用继承时,遵循里氏替换原则,在子类中尽量不要重写父类的方法。即:子类在继承父类时,除添加新的方法完成新增功能外,尽量不要重写父类的方法

  • 里氏替换原则告诉我们,继承实际上让两个类耦合性增强了,在适当的情况下,可以通过聚合、组合、依赖来解决问题

2.4.2 应用实例

一个程序引发的问题和思考

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();
        System.out.println("11-3=" + b.func1(11, 3));// 14,这里本意是求出11-3
        System.out.println("1-8=" + b.func1(1, 8));// 9,这里本意是求出1-8
        System.out.println("11+3+9=" + b.func2(11, 3));
    }
}

// A类
class A {
    // 返回两个数的差
    public int func1(int num1, int num2) {
        return num1 - num2;
    }
}

// B类继承了A
// 增加了一个新功能:完成两个数相加,然后和9求和
class B extends A {
    //这里,重写了A类的方法, 可能是无意识
    public int func1(int a, int b) {
        return a + b;
    }

    public int func2(int a, int b) {
        return func1(a, b) + 9;
    }
}

解决方法:

  • 我们发现原来运行正常的相减功能发生了错误,原因就是类B无意中重写了父类的方法,造成原有功能出现错误。在实际编程中,我们常常会通过重写父类的方法完成新的功能,这样写起来虽然简单,但整个继承体系的复用性会比较差,特别是运行多态比较频繁的时候。

  • 通用的做法是:原来的父类和子类都继承一个更通俗的基类,原有的继承关系去掉,采用依赖、聚合、组合等关系替代。

  • image-20230610115617305

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));// 14
		System.out.println("1+8=" + b.func1(1, 8));// 9
		System.out.println("11+3+9=" + b.func2(11, 3));// 23

		//使用组合仍然可以使用到A类相关方法
		System.out.println("11-3=" + b.func3(11, 3));// 8,这里本意是求出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);
	}
}

2.5 开闭原则

2.5.1 基本介绍

  • 开闭原则(Open Closed Principle)是编程中最基础、最重要的设计原则
  • 一个软件实体如类,模块和函数应该对扩展开放(对提供方),对修改关闭(对使用方)。用抽象构建框架,用实现扩展细节。
  • 当软件需要变化时,尽量通过扩展软件实体的行为来实现变化,而不是通过修改已有的代码来实现变化。

2.5.2 应用实例

一个画图形的功能

问题引入

一个画图形的功能,类图设计如下:

image-20230610115716676

public class Ocp {
	public static void main(String[] args) {
		//使用看看存在的问题
		GraphicEditor graphicEditor = new GraphicEditor();
		graphicEditor.drawShape(new Rectangle());
		graphicEditor.drawShape(new Circle());
		graphicEditor.drawShape(new Triangle());
	}
}

//这是一个用于绘图的类 [使用方]
class GraphicEditor {
	//接收Shape对象,然后根据type,来绘制不同的图形
	public void drawShape(Shape s) {
		if (s.m_type == 1)
			drawRectangle(s);
		else if (s.m_type == 2)
			drawCircle(s);
		else if (s.m_type == 3)
			drawTriangle(s);
	}

	//绘制矩形
	public void drawRectangle(Shape r) {
		System.out.println(" 绘制矩形 ");
	}

	//绘制圆形
	public void drawCircle(Shape r) {
		System.out.println(" 绘制圆形 ");
	}
	
	//绘制三角形
	public void drawTriangle(Shape r) {
		System.out.println(" 绘制三角形 ");
	}
}

//Shape类,基类
class Shape {
	int m_type;
}

class Rectangle extends Shape {
	Rectangle() {
		super.m_type = 1;
	}
}

class Circle extends Shape {
	Circle() {
		super.m_type = 2;
	}
}

//新增画三角形
class Triangle extends Shape {
	Triangle() {
		super.m_type = 3;
	}
}

优缺点:

  • 优点是比较好理解,简单易操作

  • 缺点是违反了设计模式的 OCP 原则,即对扩展开放(提供方),对修改关闭(使用方)。即当我们给类增加新功能的时喉,尽量不修改代码,或者尽可能少修改代码

  • 比如我们这时要新增加一个图形种类,我们需要做如下修改,修改的地方较多

改进思路:

  • 把创建 Shape 类做成抽象类,并提供一个抽象的 draw 方法,让子类去实现即可

  • 这样我们有新的图形种类时,只需要让新的图形类继承 Shape,并实现 draw 方法即可

  • 使用方的代码就不需要修改,满足了开闭原则

改进代码

public class Ocp {

	public static void main(String[] args) {
		GraphicEditor graphicEditor = new GraphicEditor();
		graphicEditor.drawShape(new Rectangle());
		graphicEditor.drawShape(new Circle());
		graphicEditor.drawShape(new Triangle());
		graphicEditor.drawShape(new OtherGraphic());
	}

}

//这是一个用于绘图的类 [使用方]
class GraphicEditor {
	//接收Shape对象,调用draw方法
	public void drawShape(Shape s) {
		s.draw();
	}
}

//Shape类,基类
abstract class Shape {
	public abstract void draw();//抽象方法
}

class Rectangle extends Shape {
	@Override
	public void draw() {
		// TODO Auto-generated method stub
		System.out.println(" 绘制矩形 ");
	}
}

class Circle extends Shape {
	@Override
	public void draw() {
		// TODO Auto-generated method stub
		System.out.println(" 绘制圆形 ");
	}
}

//新增画三角形
class Triangle extends Shape {
	@Override
	public void draw() {
		// TODO Auto-generated method stub
		System.out.println(" 绘制三角形 ");
	}
}

//新增一个图形
class OtherGraphic extends Shape {
	@Override
	public void draw() {
		// TODO Auto-generated method stub
		System.out.println(" 绘制其它图形 ");
	}
}

2.6 迪米特法则

  • 一个对象应该对其他对象保持最少的了解

  • 类与类关系越密切,耦合度越大

2.6.1 基本介绍

  • 一个对象应该对其他对象保持最少的了解

  • 类与类关系越密切,耦合度越大

  • 迪米特法则又叫最少知道原则,即一个类对自己依赖的类知道的越少越好。也就是说,对于被依赖的类不管多么复杂,都尽量将逻辑封装在类的内部。对外除了提供的 public 方法,不对外泄露任何信息

  • 迪米特法则还有个更简单的定义:只与直接的朋友通信

  • 直接的朋友:每个对象都会与其他对象有耦合关系,只要两个对象之间有耦合关系,我们就说这两个对象之间是朋友关系。耦合的方式很多:依赖、关联、组合、聚合等。其中,**我们称出现成员变量,方法参数,方法返回值中的类为直接的朋友,而出现在局部变量中的类不是直接的朋友。**也就是说,陌生的类最好不要以局部变量的形式出现在类的内部

2.6.2 应用实例

问题引入

编程实现以下功能:有一个学校,下属有各个学院和总部,现要求打印出学校总部员工ID和学院员工的id。

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());
		}
	}
}

问题分析:

  • 前面设计的问题在于 SchoolManager 中,CollegeEmployee 类并不是 SchoolManager 类的直接朋友(分析)

  • 按照迪米特法则,应该避免类中出现这样非直接朋友关系的耦合

改进代码

public class Demeter {
	public static void main(String[] args) {
		System.out.println("~~~使用迪米特法则的改进~~~");
		//创建了一个 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;
	}
	
	//输出学院员工的信息
	public void printEmployee() {
		//获取到学院员工
		List<CollegeEmployee> list1 = getAllEmployee();
		System.out.println("------------学院员工------------");
		for (CollegeEmployee e : list1) {
			System.out.println(e.getId());
		}
	}
}

//学校管理类

//分析 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. 将输出学院的员工方法,封装到CollegeManager
		sub.printEmployee();
	
		//获取到学校总部员工
		List<Employee> list2 = this.getAllEmployee();
		System.out.println("------------学校总部员工------------");
		for (Employee e : list2) {
			System.out.println(e.getId());
		}
	}
}

2.6.3 注意事项

  • 迪米特法则的核心是降低类之间的耦合性。

  • 但是注意:由于每个类都减少了不必要的依赖,因此迪米特法则只是要求降低类间(对象间)耦合关系,并不是要求完全没有依赖关系。

2.7 合成复用原则

合成复用原则就是尽量使用合成/聚合的方式,而不是使用继承。

image-20230610124109621

三、设计原则核心思想

  • 1)找出应用中可能需要变化之处,把它们独立出来,不要和那些不需要变化的代码混在一起

  • 2)针对接口编程,而不是针对实现编程

  • 3)为了交互对象之间的松耦合设计而努力

四、UML类图

UML:类图关系总结 - 知乎 (zhihu.com)

4.1 类的表示方式

在UML类图中,类使用包含类名、属性(field) 和方法(method) 且带有分割线的矩形来表示,比如下图表示一个Employee类,它包含name,age和address这3个属性,以及work()方法。

image-20230715145631626

属性/方法名称前加的加号和减号表示了这个属性/方法的可见性,UML类图中表示可见性的符号有三种:

  • +:表示public
  • -:表示private
  • #:表示protected

属性的完整表示方式是: 可见性 名称 :类型 [ = 缺省值]

方法的完整表示方式是: 可见性 名称(参数列表) [ : 返回类型]

注意:

1,中括号中的内容表示是可选的

2,也有将类型放在变量名前面,返回值类型放在方法名前面

举个栗子:

image-20230715145706479

上图Demo类定义了三个方法:

  • method()方法:修饰符为public,没有参数,没有返回值。
  • method1()方法:修饰符为private,没有参数,返回值类型为String。
  • method2()方法:修饰符为protected,接收两个参数,第一个参数类型为int,第二个参数类型为String,返回值类型是int。

4.2 类与类之间关系的表示方式

在UML类图中,常见的有以下几种关系: 泛化(Generalization), 实现(Realization),关联(Association),聚合(Aggregation),组合(Composition),依赖(Dependency)

各种关系的强弱顺序:继承 = 实现 > 组合 > 聚合 > 关联 > 依赖

4.2.1 继承关系

继承关系也叫泛化关系,是对象之间耦合度最大的一种关系,表示一般与特殊的关系,是父类与子类之间的关系,是一种继承关系。

**表示:**泛化关系用带空心三角箭头的实线来表示,箭头从子类指向父类。在代码实现时,使用面向对象的继承机制来实现泛化关系。例如,Student 类和 Teacher 类都是 Person 类的子类,其类图如下图所示:

image-20230715145957011

4.2.2 实现关系

实现关系是接口与实现类之间的关系。在这种关系中,类实现了接口,类中的操作实现了接口中所声明的所有的抽象操作。

在 UML 类图中,实现关系使用带空心三角箭头的虚线来表示,箭头从实现类指向接口。例如,汽车和船实现了交通工具。

image-20230715150008122

4.2.3 聚合关系

聚合关系:是整体与部分的关系,且部分可以离开整体而单独存在。如车和轮胎是整体和部分的关系,轮胎离开车仍然可以存在。

聚合关系是关联关系的一种,是强的关联关系;关联和聚合在语法上无法区分,必须考察具体的逻辑关系。

**表示:**在 UML 类图中,聚合关系可以用带空心菱形的实线来表示,菱形指向整体

image-20230715150826452

4.2.4 组合关系

组合关系:是整体与部分的关系,但部分不能离开整体而单独存在。如公司和部门是整体和部分的关系,没有公司就不存在部门。

组合关系是关联关系的一种,是比聚合关系还要强的关系,它要求普通的聚合关系中代表整体的对象负责代表部分的对象的生命周期。

**表示:**在 UML 类图中,组合关系用带实心菱形的实线来表示,菱形指向整体。

image-20230715150832256

聚合和组合的区别

聚合关系(Aggregation)表示的是整体和部分的关系,整体与部分可以分开。聚合关系是关联关系的特例,所以他具有关联的导航性与多重性。
如:一台电脑由键盘(keyboard)、显示器(monitor),鼠标等组成;组成电脑的各个配件是可以从电脑上分离出来的,使用带空心菱形的实线来表示。

组合关系:也是整体与部分的关系,但是整体与部分不可以分开。
再看一个案例:在程序中我们定义实体:Person与IDCard、Head, 那么 Head 和Person 就是 组合,IDCard 和 Person 就是聚合。但是如果在程序中Person实体中定义了对IDCard进行级联删除,即删除Person时,连同IDCard一起删除,那么IDCard 和 Person 就是组合了.

package com.yxj.uml.composition;

public class Head {

}

package com.yxj.uml.composition;

public class IDCard {

}


package com.yxj.uml.composition;

public class Person {
    private IDCard card; //聚合关系
    private Head head = new Head(); //组合关系

}


4.2.5 关联关系

关联关系是对象之间的一种引用关系,用于表示一类对象与另一类对象之间的联系,如老师和学生、师傅和徒弟、丈夫和妻子等。关联关系是类与类之间最常用的一种关系,分为一般关联关系、聚合关系和组合关系。我们先介绍一般关联。

关联又可以分为单向关联,双向关联,自关联。

1. 单向关联

image-20230715150920865

在UML类图中单向关联用一个带箭头的实线表示。上图表示每个顾客都有一个地址,这通过让Customer类持有一个类型为Address的成员变量类实现。

2. 双向关联

image-20230715150932000

从上图中我们很容易看出,所谓的双向关联就是双方各自持有对方类型的成员变量。

在UML类图中,双向关联用一个不带箭头的直线表示。上图中在Customer类中维护一个List,表示一个顾客可以购买多个商品;在Product类中维护一个Customer类型的成员变量表示这个产品被哪个顾客所购买。

3. 自关联

image-20230715150943494

自关联在UML类图中用一个带有箭头且指向自身的线表示。上图的意思就是Node类包含类型为Node的成员变量,也就是“自己包含自己”。

4.2.6 依赖关系

依赖关系是一种使用关系,它是对象之间耦合度最弱的一种关联方式,是临时性的关联。在代码中,某个类的方法通过局部变量、方法的参数或者对静态方法的调用来访问另一个类(被依赖类)中的某些方法来完成一些职责。

在 UML 类图中,依赖关系使用带箭头的虚线来表示,箭头从使用类指向被依赖的类。下图所示是司机和汽车的关系图,司机驾驶汽车:

image-20230715151000417

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/756838.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

微信小程序下拉刷新获取数据和触底事件刷新实现

一、下拉刷新 1.json文件 说明&#xff1a;开启下拉刷新&#xff0c;然后设置窗口的背景色&#xff0c;方便观看。 "enablePullDownRefresh": true,"backgroundColor":"#FFC0CB" 2. js文件 说明&#xff1a;重新发起请求&#xff0c;并显示加…

c++11 标准模板(STL)(std::basic_istream)(八)

定义于头文件 <istream> template< class CharT, class Traits std::char_traits<CharT> > class basic_istream : virtual public std::basic_ios<CharT, Traits> 类模板 basic_istream 提供字符流上的高层输入支持。受支持操作包含带格式的…

【AT89C52单片机项目】音乐播放器的设计

实验目的 用51系列单片机设计一个可以切歌的音乐播放器。 实验仪器 AT89C52单片机。 音乐播放模块。 设计要求 采用重装定时器计数方式1的初值来实现发出不同频率的声音&#xff0c;通过控制延时长度来实现不同的节拍&#xff0c;之后将音乐数据表填入&#xff0c;即可实…

【python】python汽车保险数据分析(数据+代码+报告)【独一无二】

&#x1f449;博__主&#x1f448;&#xff1a;米码收割机 &#x1f449;技__能&#x1f448;&#xff1a;C/Python语言 &#x1f449;公众号&#x1f448;&#xff1a;测试开发自动化 &#x1f449;荣__誉&#x1f448;&#xff1a;阿里云博客专家博主、51CTO技术博主 &#x…

【C++/嵌入式笔试面试八股】三、01.ARM体系架构 | 中断与异常

ARM体系架构 ARM 01.ARM产品线、 02.ARM体系结构 03.ARM硬件系统组成和运行原理🍊 直接在flash上执行较慢(NOR flash可以直接运行,NAND flash不行,按块访问),搬到内存运行,叫重定位 控制器负责取值译码 ALU负责运算 寄存器负责暂存数据 一条机器指令的执行过程,包含

SpringBoot中整合Sharding Sphere实现数据加解密/数据脱敏/数据库密文,查询明文

场景 为防止数据泄露&#xff0c;需要在插入等操作时将某表的字段在数据库中加密存储&#xff0c;在需要查询使用时明文显示。 Sharding Sphere ShardingSphere是一套开源的分布式数据库中间件解决方案组成的生态圈&#xff0c; 它由Sharding-JDBC、Sharding-Proxy和Shardi…

如何采集来赞达Lazada虾皮shopee各区域商品详情页面数据

以虾皮shopee根据ID取商品详情 API 返回值说明为例 shopee.item_get 公共参数 名称类型必须描述keyString是调用key&#xff08;必须以GET方式拼接在URL中&#xff09;注册Key和secret接入secretString是调用密钥api_nameString是API接口名称&#xff08;包括在请求地址中&am…

C++学习之static关键字小结

static在C编程中随处可见&#xff0c;但是其真实的使用方式没有总结过&#xff0c;因此在使用中经常出一些问题&#xff0c;下面就C编程中static关键字在的几种用法进行相关总结说明如下。 1、在函数内部定义带有static关键字的变量时&#xff0c;则变量是保存在静态存储区的&…

[QT编程系列-13]:QT快速学习 - 1- 初识

目录 第1章 QT的介绍 1.1 QT VS MFC 1.2 QT历史 1.3 QT的应用 1.4 QT学习方法 1.5 QT对象树 1.6 2-8定律 1.7 QT优势&#xff1a; 1.8 QT支持的平台 第2章 QT UI是各种控件对象的堆积 第3章 QT UI是各种控件的堆积 第4章 控件窗口的控制 第1章 QT的介绍 1.1 QT V…

【广州华锐互动】VR石油化工作业实训平台

VR石油化工作业实训平台是一种基于虚拟现实技术的教学、科研辅助工具&#xff0c;可以提供交互式的实验环境&#xff0c;让学生和研究人员在虚拟环境中进行实验。该系统可以帮助学生更好地理解石油炼制过程中的各个环节&#xff0c;包括原油加工、分离、精制等。 通过这种方式…

Python 数据管理

数据管理是收集、收集原始数据并将其转换为另一种格式的过程&#xff0c;以便在更短的时间内更好地理解、决策、访问和分析。 数据管理的重要性 数据管理是数据科学项目中非常重要的一步。下面的例子将解释它的重要性&#xff1a; 图书销售网站希望显示不同领域的畅销书&#…

推荐一款IDEA神级插件【Bito】而且免费!

什么是Bito&#xff1f; Bito是一款在IntelliJ IDEA编辑器中的插件&#xff0c;Bito插件是由ChatGPT团队开发的&#xff0c;它是ChatGPT团队为了提高开发效率而开发的一款工具。ChatGPT团队是一支专注于自然语言处理技术的团队&#xff0c;他们开发了一款基于GPT的自然语言处理…

轮播图,用vue来写一个简单的轮播图

轮播图&#xff0c;用vue来写一个简单的轮播图 写的很简单&#xff0c;就是一个小练习&#xff0c;哈哈哈&#xff0c;下面的几张图分别是轮播图的第一张&#xff0c;中间图&#xff0c;最后一张的效果图。 使用了vue 中的属性绑定 v-bind ,v-show 以及 事件监听 v-on 指令。 思…

NZ13:VBA给数据加密处理

【分享成果&#xff0c;随喜正能量】爱出者爱返&#xff0c;福往者福来。怀揣一颗善良的心&#xff0c;本本分分&#xff0c;坦坦荡荡&#xff0c;多去帮助需要的人&#xff0c;坚持善良做人&#xff0c;才能不负于人&#xff0c;不负自己。。 我的教程一共九套及VBA汉英手册一…

黄皮书-线接触热弹流润滑 Fortran+Matlab转译代码

原Fortran代码有错误&#xff0c;进行了修改&#xff0c;数值上差别不大。根据Fortran代码转的Matlab&#xff0c;可以完美运行&#xff0c;但是因为精度问题有差异&#xff0c;只能说趋势是一致的。 需要私我-资源里只是Fortran运行结果

Spark(25):Spark部署模式对比

目录 0. 相关文章链接 1. Spark支持的集群管理器 2. YARN 模式运行机制 2.1. YARN Cluster 模式 2.2. YARN Client 模式 3. Standalone 模式运行机制 3.1. Standalone Cluster 模式 3.2. Standalone Client 模式 0. 相关文章链接 Spark文章汇总 1. Spark支持的集群管…

Python爬虫——urllib_微博cookie登陆

cookie登陆适用场景&#xff1a; 适用场景&#xff1a;数据采集的时候&#xff0c;需要绕过登陆&#xff0c;然后进入到某个页面 # 适用场景&#xff1a;数据采集的时候&#xff0c;需要绕过登陆&#xff0c;然后进入到某个页面 import urllib.requesturl https://weibo.cn/7…

不用显示器,不用鼠标和键盘,让我们用主机远程访问OK3588的桌面

不用显示器&#xff0c;不用鼠标和键盘&#xff0c;让我们用主机远程访问OK3588的桌面 MobaXterm软件介绍串口终端运行命令MobaXterm访问开发板 MobaXterm软件介绍 MobaXterm是一款增强型终端软件&#xff0c;对于Windows平台上的程序员、网络管理员和开发者是一款极其优秀的工…

数据库 --- mysql(03)-- mysql字符集、表操作(01)

MYSQL 1、mysql字符集 &#xff08;1&#xff09;简介&#xff1a; MySQL字符集包括字符集&#xff08;CHARACTER&#xff09;和校对规则&#xff08;COLLATION&#xff09;两个概念&#xff1a; 字符集&#xff08;CHARACTER&#xff09;是一套编码校对规则&#xff08;CO…

Spark(24):Spark内核概述

目录 0. 相关文章链接 1. Spark核心组件 1.1. Driver 1.2. Executor 2. Spark通用运行流程概述 0. 相关文章链接 Spark文章汇总 1. Spark核心组件 1.1. Driver Spark 驱动器节点&#xff0c;用于执行 Spark 任务中的 main 方法&#xff0c;负责实际代码的执行工作。Dr…