依赖注入学习

本文介绍了依赖的概念,深入探讨了SOLID设计原则中的依赖倒置原则,解释了如何通过接口隔离、迪米特原则等减少耦合。此外,还讲解了控制反转(IOC)和依赖注入(DI)的核心思想,阐述了它们如何解耦代码,以及在Java中依赖注入的JSR-330标准和`javax.inject`包的使用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

什么是依赖?

依赖是类与类之间的联系,依赖关系表示一个类依赖于另一个类的定义,通俗来讲,就是一种需要;例如一个人(person)可以买车(car)和房子(house),Person类依赖于Car类和House类。

public static void main(String[] args){
	Person person = new Person();
	person.buy(new House());
	person.buy(new Car());
}
static class Person{
	//表示依赖House
	public void buy(House house){ }
	//表示依赖car
	public void buy(Car car){ }
}
static class House{

}
static class Car{

}

依赖倒置(Dependency inversion principle)

依赖倒置是面向对象设计领域的一种软件设计原则

软件设计有六大设计原则,合称SOLID

1.单一职责原则(Single Responsibility Principle,简称SRP)

  • 核心思想:应该有且仅有一个原因引起的类的变更
  • 问题描述:假如有类Class完成职责T1,T2,当职责T1或T2有变更需要修改时,有可能影响到该类的另外一个职责正常工作。
  • 好处:类的复杂度降低、可读性提高、可维护性提高、扩展性提高、降低了变更引起的风险
  • 需注意:单一职责原则提出了一个编写程序的标准,用“职责”或“变化原因”来衡量接口或类设计得是否优良,但是“职责”和“变化原因”都是不可以度量的,因项目和环境而异

2.里氏替换原则(Liskov Substitution Principle,简称LSP)

  • 核心思想:在使用基类的地方可以任意使用其子类,能保证子类完美替换基类
  • 通俗来讲:只要父类能出现的地方子类就能出现,反之,父类则未必能胜任
  • 好处:增强程序的健壮性,即使增加了子类,原有的子类还可以继续运行
  • 需注意:如果子类不能完整的实现父类的方法,或者父类的某些方法在子类中已经发生“畸变”,则建议断开父子继承关系,采用依赖、聚合、组合等关系代替继承。

3.依赖倒置原则(Dependence Inversion Principle,简称DIP)

  • 核心思想:高层模块不应该依赖底层模块,二者都应该依赖其抽象;抽象不应该依赖细节;细节应该依赖抽象
  • 说明:高层模块就是调用端,底层模块就是具体实现类。抽象就是指接口或者抽象类。细节就是实现类。
  • 通俗来讲:依赖倒置原则本质上就是通过抽象(接口或者抽象类)使各个类或者模块的实现彼此独立,互补影响,实现模块间的松耦合。
  • 问题描述:类A直接依赖B,假如要将类A改为依赖C,则必须通过修改类A的代码来达成。这种场景下,类A一般是高层模块,负责复杂的业务逻辑;类B和类C是低层模块,负责基本的原子操作;假如修改类A,会给程序带来不必要的风险
  • 解决方案:将类A修改为依赖接口interface,类B和类C各自实现接口interface,类A通过接口interface间接与类B或者类C发生联系,则会大大降低修改类A的几率。
  • 好处:依赖倒置的好处在小型项目中很难体现出来。但在大中型项目中可以减少需求变化引起的工作量。使并行开发更友好。

4.接口隔离原则(Interface Segregation Principle,简称ISP)

  • 核心思想:类间的依赖关系应该建立在最小的接口上
  • 通俗来说:建立单一接口,不要建立庞大臃肿的接口,尽量细化接口,接口中的方法尽量少。也就是说,我们要为各个类建立专用的接口,而不是要试图去建立一个很庞大的接口供所有的依赖它的类去调用。
  • 问题描述:类A通过接口interface依赖B,类C通过接口interface依赖类D,如果接口interface对于类A和类B来说不是最小接口,则B和D必须去实现他们不需要的方法
  • 需注意
  • 接口尽量小,但是要有限度。对接口进行细化可以提高程序设计灵活性,但是接口过于小,则会造成接口数量过多,是设计复杂化。所以一定要适度
  • 提高内聚,减少对外交互。是接口用最少的方法去完成最多的事情
  • 为依赖接口的类定制服务。只暴露给调用的类它需要的方法,它不需要的方法则隐藏起来。只有专注地位一个模块提供定制服务,才能建立最小的依赖关系。

5.迪米特原则(Law of Demeter ,简称LoD)

  • 核心思想:类间解耦
  • 通俗来说:一个类对自己依赖的类知道的越少越好。自从我们接触编程开始,就知道了软件编程的总的原则:高内聚,低耦合。无论面向过程还是面向对象编程,只有使各个模块之间的耦合尽量的低,才能调高代码的复用率。低耦合的优点不言而喻,但是怎么样编程才能做到低耦合呢?那正是迪米特法则要去完成的。

6.开放封闭原则(Open Close Principle,简称:OCP)

  • 核心思想:尽量通过扩展软件实体来解决需求变化,而不是通过修改已有代码来完成变化
  • 通俗来说:一个软件产品在生命周期内,都会发生变化,既然变化是一个既定的事实,我们就应该在设计的时候尽量适应这些变化,以提高项目的灵活性和稳定性。

依赖倒置的原则定义如下:

1.上层模块不应该依赖底层模块,他们都应该依赖于抽象
2. 抽象不应该依赖于细节,细节应该依赖于抽象

什么是上层模块和下层模块?

不管你承不承认,“有人的地方就有江湖”,我们说人人平等,但是对于任何一个组织机构而言,它有一定的架构的设计有职能的划分。
高层模块就是调用端,底层模块就是具体实现类。抽象就是指接口或者抽象类。细节就是实现类。
业务层属于上层模块,逻辑层和数据层自然就归类为底层

什么是抽象和细节?

抽象如其名字一样,是一件很抽象的事物。抽象往往是相对于具体而言的,具体也可以被称为细节,当然也被称为具象。

控制反转(IOC)

控制反转IOC是Inversion Of Control的缩写,意思就是对于控制权的反转。

Person自己掌握着内部mDriveable的实例化.
现在,我们可以更改一种方式。将mDriveable的实例化转移到Person的外面

public class Person{
	private Driveable mDriveable;
	public Person(Driveable driveable){
		this.mDriveable = driveable;
	}
	public void goOut(){
		System.out.print("出门啦");
		mDriveable.drive();
	}
	public static void main(String[] args){
		Person person = new Person(new Car());
		person.goOut();
	}
}

就这样,无论出行方式怎么变,Person这个类都不需要更改代码了。

在上面的代码中,Person把内部依赖的创建权力移交给类中的main()方法。也就是说Person只关心依赖提供的功能,但并不关心依赖的创建。

这种思维其实就是IOC,IOC是一种新的设计模式,它对上层模块和底层模块进行了更进一步的解耦。控制反转的意思就是反转了上层模块对于底层模块的依赖控制。

依赖注入(Dependency injection)

依赖注入,也经常被简称为DI。

为了不因为依赖的变动而去修改Person,也就是说可能再Driveable实现类的改变下而不改动Person这个类的代码,尽可能减少两者之间的耦合。我们需要用IOC模式来进行该写代码。

这个需要我们移交出对于依赖实例化的控制权,那么依赖怎么办?Person无法实例化依赖了,它就需要在外部(IOC容器)赋值给它,这个赋值动作有个专门的术语叫注入(injection),需要注意的是在IoC概念中,这个注入依赖的地方被称为IOC容器,但是在依赖注入概念中,一般被称为注射器(injector)

表达通俗一点的就是:我不想自己实例化依赖,你(injector)创建它们,然后在合适的时候注入给我

实现依赖注入的有3种方式:
1. 构造函数注入
2. setter方式注入
3. 接口注入

/**
* 接口方式注入,接口的存在,表明了一种依赖配置的能力
*/
public interface DependencySetter{
	void set(Driveable driveable);
}
public class Person implements DependcySetter{
	//接口方式注入
	@override
	public void set(Driveable driveable){
		this.mDriveable = mDriveable;
	}
	private Driveable mDriveable;
	
	//构造函数注入
	public Person(Driveable driveable){
		this.mDriveable = driveable;
	}

	//setter方式注入
	public void setDriveable(Driveable mDriveable){
		this.mDriveable = mDriveable;
	}
}

java依赖注入标准

JSR-330是Java的依赖注入标准。定义了如下的术语描述依赖注入:

  • A类型依赖B类型(或者说B类型被A类型依赖),则A类型成为“依赖(物)dependency”
  • 运行时查找依赖的过程,成为“解析resolving”依赖
  • 如果找不到依赖的实例,称该依赖是“不能满足的unsatisfied”
  • 在依赖注入dependency injection 机制中,提供依赖的工具成为依赖注入器dependency injector

javax.inject

包Javax.inject 指定了获取对象的一种方法,该方法与构造器、工厂以及服务定位器(如JNDI)这些传统方法相比可以获得更好的可重用性、可测试性以及可维护性。此方法的处理过程就是大家熟知的依赖注入,它对大多数应用是非常有价值的

@Inject

注解@Inject 标识了可以注入的构造器、方法或者、字段。可以用于静态或实例成员。一个可注入的成员可以被任何访问修饰符(private、package-private、protected、public)修饰。注入顺序为构造器,字段,最后是方法。超类的字段、方法将优先于子类的字段、方法被注入。对于同一个类的字段是不区分注入顺序的,同一个类亦同。

Provider

接口Provider用于提供类型T的实例。Provider是一般情况是由注射器实现的。对于任何可注入的T而言,您也可以注入Provider.与直接注入T相比,注入Provider 使得:

  • 可以返回多个实例
  • 实例的返回可以延迟化或可选
  • 打破循环依赖
  • 可以在一个一直作用域的实例内查询一个更小作用于内的实例

@Qualifier

用于标识先顶起注解。任何人都可以定义新的限定器注解。一个限定器注解:

  • 是被@Qualifier@Retention(RUNTIME)标注的,通常也被@Documented标注
  • 可以拥有属性
  • 可能是公共API的一部分,就像依赖类型一样,而不是类型实现那样不作为公共API的一部分。
  • 如果标注了@Target可能会有一些用法限制。本规范只是指定了限定器注解可以被使用在字段和参数上,但一些注入器配置可能使用限定器注解在其他一些地方(例如方法和类)上

@Named

  • 基于String的【限定器】

@Scope

  • 用于标识作用域注解。一个作用域注解是被标识在包含一个可注入构造器的类上的,用于控制该类型的实例如何被注入器重用。缺省情况下,如果没有标识作用域注解,注入器将为每一次注入都创建(通过注入类型的构造器)新实例,并不重用已有实例,该实例实现应该是线程安全的。作用域实现由注入器完成

@Singleton

  • 标识了注入器只实例化一次的类型。该注解不能被继承。
### Spring 框架依赖注入的概念 依赖注入是一种设计模式,允许对象在其创建过程中由外部提供其依赖的对象。这有助于减少硬编码的依赖关系并提高模块间的松散耦合程度[^2]。 在Spring框架中,依赖注入是IoC容器的核心功能之一。通过这种方式,应用程序组件不需要负责查找或直接创建所依赖的服务或其他资源;相反,这些依赖会被容器自动提供给它们。这样做的好处是可以简化代码结构,增强灵活性以及便于单元测试[^3]。 ### 依赖注入的主要形式 #### Setter 注入 这是最简单的一种DI方法,在bean定义中指定了属性及其对应的值或者另一个bean的名字。当Bean被初始化时,Spring会调用相应的setter方法设置该属性的值。这种方法适用于可选依赖项,并且可以方便地修改配置而不必改变源码[^1]。 ```java public class TestController { private TestService testService; // setter 方法用于注入TestService型的依赖 public void setTestService(TestService service){ this.testService = service; } public void method1(){ testService.method1(); } } ``` #### 构造函数注入 构造器注入意味着所有的必需协作对象都作为构造参数传递给目标的新实例。对于那些不可变或者是强制性的依赖来说是非常理想的解决方案。一旦对象创建完成,则无法更改它的状态,因此更有利于保持一致性[^4]。 ```java public class MyClass { private final MyDependency dependency; // 使用构造函数来进行依赖注入 public MyClass(MyDependency dep) { this.dependency = Objects.requireNonNull(dep); } } ``` ### 处理循环依赖问题 在一个复杂的系统里可能会遇到两个或多个beans互相持有对方引用的情况——即所谓的“循环依赖”。为了应对这种情况,Spring支持一种特殊的机制:它可以在尚未完全初始化之前提前暴露一个bean的部分组装好的版本(通常是原始工厂代理)。然而需要注意的是并非所有情况下都能成功解决这个问题,特别是涉及到原型作用域或者其他特殊场景下可能仍然会出现`BeanCurrentlyInCreationException`错误。为了避免此异常的发生,推荐采用Setter方式代替构造器方式进行依赖注入,因为后者更容易引发上述提到的问题。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值