深入理解IOC设计模式(上)

187 阅读6分钟

深入理解IOC设计原则(上)

前置知识

面向对象设计模式(OOD)有五大准则(即SOLID原则)

单一功能原则(Single responsibility principle):规定每个类都应该有一个单一的功能,并且该功能应该由这个类完全封装起来。所有它的(这个类的)服务都应该严密的和该功能平行(功能平行,意味着没有依赖)。

开闭原则 (The Open/Closed Principle, OCP) 在面向对象编程领域中,开闭原则 (The Open/Closed Principle, OCP) 规定“软件中的对象(类,模块,函数等等)应该对于扩展是开放的,但是对于修改是封闭的”[1],这意味着一个实体是允许在不改变它的源代码的前提下变更它的行为。该特性在产品化的环境中是特别有价值的,在这种环境中,改变源代码需要代码审查,单元测试以及诸如此类的用以确保产品使用品质的过程。遵循这种原则的代码在扩展时并不发生改变,因此无需上述的过程。

里氏替换原则(Liskov Substitution principle)是对[子类型]的特别定义。它由[芭芭拉·利斯科夫]在1987年在一次会议上名为“数据的抽象与层次”的演说中首先提出。里氏替换原则的内容可以描述为: “派生类(子类)对象可以在程序中代替其基类(超类)对象。”

接口隔离原则(英语:interface-segregation principles, 缩写:ISP)指明客户(client)不应被迫使用对其而言无用的方法或功能。 接口隔离原则(ISP)拆分非常庞大臃肿的接口成为更小的和更具体的接口,这样客户将会只需要知道他们感兴趣的方法。这种缩小的接口也被称为角色接口(role interfaces)。 接口隔离原则(ISP)的目的是系统解开耦合,从而容易重构,更改和重新部署。接口隔离原则是在SOLID中五个面向对象设计(OOD)的原则之一,类似于在GRASP中的高内聚性。

依赖反转原则(Dependency inversion principle,DIP)是指一种特定的解耦(传统的依赖关系创建在高层次上,而具体的策略设置则应用在低层次的模块上)形式,使得高层次的模块不依赖于低层次的模块的实现细节,依赖关系被颠倒(反转),从而使得低层次模块依赖于高层次模块的需求抽象。

依赖反转原则

img

该原则规定

高层次的模块不应该依赖于低层次的模块,两者都应该依赖于抽象接口。
抽象接口不应该依赖于具体实现。而具体实现则应该依赖于抽象接口。

问题

1.何为高层次模块,何为低层次模块?

高层次模块指的是那些实现较为抽象、功能较为宏观的模块,比如业务逻辑层或应用层;低层次模块指的是实现较为具体、功能较为细粒度的模块,比如数据访问层或服务层。

举一个例子:

现在有一个需求要求在已有项目中增加一个功能,绘制一个正方形。

通常我们拿到需求后会这样写:

class ChildDrawer extend SuperDrawer{
   //....
  public static drawSquare(startPos:Cartesian3,endPos:Cartesian3){
   //pocess props to get new elements   
   this.drawShap(elemen1,element2,....)//from super
  }
   //....
}

这里的SuperDrawer作为父级对象,功能较为抽象,层级较为宏观,因此就是高层次模块,ChildDrawer实现的功能离业务较近,功能粒度较细,因此就是低层次模块

2.为什么高层级模块和低层级模块都应该依赖于抽象接口

还是刚刚那个例子:

//顶层接口
//形状,仅包括一个绘制的方法
interface Shape{
    draw:(...Cartesian3[])=>void;
}

class Shp implements Shape{
    public draw(...Cartesian3[]){
        //....
    }
}

此时有高层次模块

class Drawer{
    constructor(shp:Shp){
        this.shp=shp
    }
    public drawShape(){
        this.shp.draw();
    }
}

此时有具体业务模块

class Square implements Shape{

    public draw(start:Cartesian3,end:Cartesian3){
        const pos1=start;
        const pos2=new Cartesian3(end.x,start.y);
        const pos3=new Cartesian3(start.x,end.y);
        const pos4=end;
        Tools.draw(pos1,pos2,pos3,pos4);
    }
}

例子

//绘制四边形
const square=new Square();
const drawService=new Drawer(Square);
drawService.drawShape();
//绘制三角形
const triangle=new Triangle();
const drawService=new Drawer(Square);

再看一个例子

假设我们正在开发一个简单的应用程序,其中有一个模块用于获取用户信息。我们可以先定义一个抽象接口 UserRepository,表示用户数据的存储和获取方式:

interface UserRepository {
  getUser(id: number): Promise<User>;
}

然后我们再定义一个高层次的模块 UserService,它依赖于抽象接口 UserRepository

class UserService {
  private userRepository: UserRepository;

  constructor(userRepository: UserRepository) {
    this.userRepository = userRepository;
  }

  async getUser(id: number): Promise<User> {
    return this.userRepository.getUser(id);
  }
}

最后,我们再定义一个具体的类 InMemoryUserRepository,它实现了抽象接口 UserRepository,用于从内存中获取用户信息:

class InMemoryUserRepository implements UserRepository {
  private users: User[];

  constructor(users: User[]) {
    this.users = users;
  }

  async getUser(id: number): Promise<User> {
    const user = this.users.find((u) => u.id === id);
    if (!user) {
      throw new Error(`User with id ${id} not found`);
    }
    return user;
  }
}

现在我们可以使用这个程序来获取用户信息,而且可以方便地修改数据源,比如改为从数据库中获取用户信息,只需要定义一个新的类实现抽象接口 UserRepository,然后在定义 UserService 对象时传入即可:

// 从内存中获取用户信息
const userRepository = new InMemoryUserRepository([
  { id: 1, name: "Alice" },
  { id: 2, name: "Bob" },
]);
const userService = new UserService(userRepository);

// 从数据库中获取用户信息
const userRepository = new DatabaseUserRepository();
const userService = new UserService(userRepository);

这个例子中,我们使用依赖倒置原则来设计程序,将低层次的模块 InMemoryUserRepository 通过抽象接口 UserRepository 间接地依赖于高层次的模块 UserService,降低了模块之间的耦合度(此时模块之间没有继承关系),使得程序更加灵活、可扩展和可维护。

(如果还有问题,可以参考go语言接口的定义,语法简单的,go语言完全没有继承,全是这种组合方式)

3.如何理 “解抽象接口不应该依赖于具体实现。而具体实现则应该依赖于抽象接口。”

抽象接口是一个高层次的概念,它定义了系统的抽象行为。而具体实现则是一个低层次的概念,它实现了抽象接口定义的具体行为。

这个原则的意义在于,通过将具体实现依赖于抽象接口,可以将系统解耦,使得系统更加灵活、可扩展和可维护。具体实现可以通过实现抽象接口来实现系统的具体行为,这样就可以方便地替换具体实现,而不需要修改抽象接口的定义。同时,抽象接口也可以被多个具体实现所共享,这样可以提高代码的复用性和可维护性。

image-20230628010010409

4.这样设计的好处

  • 系统更柔韧:可以修改一部分代码而不影响其他模块。

  • 系统更健壮:可以修改一部分代码而不会让系统崩溃。

  • 系统更高效:组件松耦合,且可复用,提高开发效率。