目录
前置知识
一、什么是设计模式
设计模式就是解决软件开发某些特定问题而提出的一些解决方案也可以理解成解决问题的思路。
二、设计模式的目的
设计模式的目的是为了提高代码重用性、可读性、可扩展性、可靠性,使得程序呈现出高内聚、低耦合的
特性。
- 代码重用性:相同功能的代码无需多次重复编写
- 可读性:编程按照一定规范,便于其他程序员的阅读和理解
- 可扩展性:当我们可以非常方便简单地增加新功能
- 可靠性:我们增加或删除部分功能时,对原有系统其他功能没有影响
- 高内聚、低耦合:
高内聚就是指一个软件模块是由相关性很强的代码组成,只负责一项任务,也就是常说的单一责任原则
低耦合就是降低一个元素与其他元素之间的联系、感知和依赖
七大原则
设计模式原则,其实就是程序员编程时应当遵守的原则,也是各种设计模式的基础
(即为什么要这么设计的依据)。
- 单一职责原则
- 接口隔离原则
- 依赖倒转原则
- 里氏替换原则
- 开闭原则
- 迪米特法则
- 合成复用原则
原则一:单一职责原则
一、案例一:交通工具问题
1. 问题分析
对于类来说,就是一个类应该只负责一项职责,如果类A负责了职责1和职责2,当职责1的需求发生改变时,可能对职责2的执行造成影
响,因此需要将类A分解为类A1和类A2。
例如下面举一个交通工具类的例子,当程序运行时会出现飞机在公路上运行的尴尬,就是因为这一个交通工具类既负责空中的,也负责陆
地的,才导致问题。
public class Main1 {
public static void main(String[] args) {
Vehicle vehicle = new Vehicle();
vehicle.run("汽车");
vehicle.run("飞机");
}
}
//交通工具
class Vehicle{
public void run(String vehicle){
System.out.println(vehicle+"在公路上运行");
}
}
2. 解决思路
2.1 类级别单一职责
将交通工具类分解为陆地交通工具和空中交通工具,使得一个类只负责一件事,如此虽然遵守了单一职责原则,但改动太大,为了一个方
法增加两个类,因此我们还能再简化。
public class Main1 {
public static void main(String[] args) {
RoadVehicle vehicle1 = new RoadVehicle();
vehicle1.runRoad("汽车");
AirVehicle vehicle2 = new AirVehicle();
vehicle2.runAir("飞机");
}
}
//交通工具
class RoadVehicle{
public void runRoad(String vehicle){
System.out.println(vehicle+"在陆地上运行");
}
}
class AirVehicle{
public void runAir(String vehicle){
System.out.println(vehicle+"在天空上运行");
}
}
2.2 方法级别单一职责
上面是在类级别上遵守单一职责,而下面这种是在方法级别上遵守单一职责。
class Vehicle{
public void runAir(String vehicle){
System.out.println(vehicle+"在天空上运行");
}
public void runRoad(String vehicle){
System.out.println(vehicle+"在陆地上运行");
}
}
3. 知识小结
- 为了降低类的复杂度,做到一个类只负责一项职责;
- 可以提高类的可读性和可维护性,降低变更引起的风险;
- 通常情况下,我们应当逻辑单一职责原则,只有逻辑足够简单才能在代码上违反单一职责原则,只有类中方法数量很少,才可以只在方法级别上遵守单一职责原则。
二、案例二:待更新
原则二:接口隔离原则
一、案例一:大接口拆分小接口
1. 问题分析
客户端不应该依赖它不需要的接口,即一个类对另一个类的依赖应该建立在最小的接口上。
为什么会有接口隔离?考虑这么一种情况,现有一个接口包含了三个方法,有类A和类B实现了该接口,而类C和
类D依赖于该接口。
(这里依赖的意思,类C和D中有方法的参数是接口,而我们可以将实现了接口的类A和B作为C和D方法的参
数)。下面用代码实现。
interface Interface1{
void method1();
void method2();
void method3();
}
class A implements Interface1{
@Override
public void method1() {}
@Override
public void method2() {}
@Override
public void method3() {}
}
class B implements Interface1{
@Override
public void method1() {}
@Override
public void method2() {}
@Override
public void method3() {}
}
class C{
public void c_method1(Interface1 interface1){
interface1.method1();
}
public void c_method2(Interface1 interface1){
interface1.method2();
}
}
class D{
public void d_method3(Interface1 interface1){
interface1.method3();
}
}
public class Main2 {
public static void main(String[] args) {
A a = new A();
B b = new B();
C c = new C();
c.c_method1(a);
D d = new D();
d.d_method3(b);
}
}
2. 解决思路
这样的设计明显是有问题的,因为我们的类C只用到接口的方法一和方法二,而类D只用到了接口的方法三,因此
我们采用接口隔离,将接口分开。
把刚刚的接口Interface1拆分为接口Interface1和Interface2,使得类C和类D分别依赖两个不同接口。
interface Interface1{
void method1();
void method2();
}
interface Interface2{
void method3();
}
class A implements Interface1{
@Override
public void method1() {}
@Override
public void method2() {}
}
class B implements Interface2{
@Override
public void method3() {}
}
class C{
public void c_method1(Interface1 interface1){
interface1.method1();
}
public void c_method2(Interface1 interface1){
interface1.method2();
}
}
class D{
public void d_method3(Interface2 interface2){
interface2.method3();
}
}
原则三:依赖倒转原则
一、案例一:面向接口编程
1. 问题分析
- 高层模块不应该依赖底层模块,二者都应该依赖于抽象
- 抽象不应该依赖于细节,细节要依赖于抽象
- 抽象在Java中就是指接口或抽象类,细节就是具体的实现类
- 依赖倒转的中心思想就是面向接口编程
- 使用接口或抽象类的目的是为了制定好规范,而不涉及任何具体的作用,把展现细节的任务交给它们的实现类。
下面三种方式实现依赖传递:
2. 依赖传递方式
2.1 通过接口实现依赖传递
interface OpenInterface{
public void open(TV tv);
}
interface TV{
public void play();
}
class Open implements OpenInterface{
@Override
public void open(TV tv) {
tv.play();
}
}
class ChangHong implements TV{
@Override
public void play() {
System.out.println("长虹电视打开了!");
}
}
public class DependencyInversion {
public static void main(String[] args) {
ChangHong changHong = new ChangHong();
OpenInterface openInterface = new Open();
openInterface.open(changHong);
}
}
2.2 通过构造方法实现依赖传递
interface OpenInterface{
public void open();
}
interface TV{
public void play();
}
class Open implements OpenInterface{
public TV tv;
Open(TV tv){
this.tv = tv;
}
@Override
public void open() {
this.tv.play();
}
}
class ChangHong implements TV{
@Override
public void play() {
System.out.println("长虹电视打开了!");
}
}
public class DependencyInversion {
public static void main(String[] args) {
ChangHong changHong = new ChangHong();
OpenInterface openInterface = new Open(changHong);
}
}
2.3 通过setter实现依赖传递
interface OpenInterface{
public void open();
public void setTV(TV tv);
}
interface TV{
public void play();
}
class Open implements OpenInterface{
public TV tv;
@Override
public void open() {
this.tv.play();
}
@Override
public void setTV(TV tv) {
this.tv = tv;
}
}
class ChangHong implements TV{
@Override
public void play() {
System.out.println("长虹电视打开了!");
}
}
public class DependencyInversion {
public static void main(String[] args) {
ChangHong changHong = new ChangHong();
OpenInterface openInterface = new Open();
openInterface.setTV(changHong);
openInterface.open();
}
}
3. 总结
- 底层模块尽量都要有抽象类或接口,这样程序稳定性更好!
- 变量的声明类型尽量都是抽象类或接口,这样我们的变量引用和实际对象间就有一个缓冲,利于程序扩展和优化。
- 继承时遵循里氏替换。
原则四:里氏替换原则
一、案例一:基类问题
- 所有引用基类的地方,必须能透明的使用其子类。
- 因此,在继承时,子类尽量不要重写父类的方法。
- 继承会让两个类的耦合性增强,因此适当情况下,可以通过聚合、组合、依赖来解决问题。(假如A继承于B,我们可以抽取其公共部分为Base类,然后使用组合,在A类中创建B的对象)
public class Liskov {
public static void main(String[] args) {
// TODO Auto-generated method stub
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);
}
}
原则五:开闭原则
一、案例一:图形编辑问题
1. 问题分析
- 开闭原则(Open Closed Principle)是编程中最基础、最重要的设计原则
- 一个软件实体,例如类、模块、函数应该对扩展开发(针对开发方),对修改关闭(针对使用方)。用抽象构建框架,用实现扩展细节。
- 当软件需要变化时,尽量通过扩展软件实体的行为来实现变化,而不是通过修改已有代码来实现变化。
- 编程中遵循的其他原则,以及使用设计模式的目的就是遵守开闭原则。
下面举个例子:这种方式就没有遵循开闭原则,无论是扩展还是修改。假如要新增一个绘制三角形,就要改动很多。
public class Ocp {
public static void main(String[] args) {
GraphicEditor graphicEditor = new GraphicEditor();
graphicEditor.drawRectangle(new Rectangle());
graphicEditor.drawCircle(new Circle());
}
}
//这是一个用于绘图的类【使用方】
class GraphicEditor{
//根据不同类型来绘制不同图形
public void drawShape(Shape shape){
if (shape.type == 1)
drawRectangle(shape);
else if(shape.type == 2)
drawCircle(shape);
}
public void drawRectangle(Shape r){
System.out.println("矩形");
}
public void drawCircle(Shape c){
System.out.println("圆形");
}
}
//图形基类
class Shape{
int type;
}
//矩形
class Rectangle extends Shape{
Rectangle(){
super.type = 1;
}
}
//圆形
class Circle extends Shape{
Circle(){
super.type = 2;
}
}
2. 解决思路
按照开闭原则方式优化:如果要绘制三角形,只需新增三角形类来继承Shape,既满足对扩展开发,也满足对修改关闭!
package design_principle.ocp.improve;
public class Ocp {
public static void main(String[] args) {
GraphicEditor graphicEditor = new GraphicEditor();
graphicEditor.drawShape(new Rectangle());
graphicEditor.drawShape(new Circle());
}
}
//这是一个用于绘图的类【使用方】
class GraphicEditor{
//根据不同类型来绘制不同图形
public void drawShape(Shape shape){
shape.draw();
}
}
//图形基类
abstract class Shape{
int type;
public abstract void draw();
}
//矩形
class Rectangle extends Shape {
Rectangle(){
super.type = 1;
}
@Override
public void draw() {
System.out.println("矩形");
}
}
//圆形
class Circle extends Shape {
Circle(){
super.type = 2;
}
@Override
public void draw() {
System.out.println("圆形");
}
}
原则六:迪米特法则
一、案例一:直接朋友问题
- 一个对象应该对其他对象保持最少的了解
- 类与类关系越密切,耦合度越大
- 迪米特法则也叫最少知道原则,即一个类对自己依赖的类知道的越少越好。对于被依赖的类不管多么复杂,都尽量将逻辑封装在类的内部。对外除了提供public方法外,不要泄露任何信息。
- 迪米特法则还有个更简单的定义:只与直接朋友通信。
- 直接朋友:只要两个对象间有耦合关系,那么它们就是朋友。耦合包括依赖、关联、组合、聚合等等。其中,我们将成员变量、方法参数、方法返回值中的类称为直接朋友,而出现在局部变量中的类(例如class A的一个方法里有class B对象)不是直接朋友。换句话说,陌生的类最好不要以局部变量的形式出现在类的内部。
注意:迪米特法则的核心只要求降低类之间的耦合,并不是完全没有依赖。
//客户端
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());
}
}
}
原则七:合成复用原则
一、案例一:继承聚合问题
尽量使用合成/聚合的方式,而不是使用继承。下面四种方式都可以让B使用A的方法,但继承的方式耦合度太高。