徐昊 · TDD 项目实战 70 讲
徐昊
Thoughtworks 中国区 CTO
18159 人已学习
新⼈⾸单¥98
登录后,你可以任选4讲全文学习
课程目录
已完结/共 88 讲
实战项目二|RESTful开发框架:依赖注入容器 (24讲)
实战项目三|RESTful Web Services (44讲)
徐昊 · TDD 项目实战 70 讲
15
15
1.0x
00:00/00:00
登录|注册

32|DI Container(20):如何对代码和测试进行重构?

你好,我是徐昊。今天我们继续使用 TDD 的方式实现注入依赖容器。

回顾代码与任务列表

到目前为止,我们的代码是这样的:
Component.java
package geektime.tdd.di;
import java.lang.annotation.Annotation;
public record Component(Class<?> type, Annotation qualifiers) {
}
ComponentRef.java
package geektime.tdd.di;
import java.lang.annotation.Annotation;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.Objects;
public class ComponentRef<ComponentType> {
public static <ComponentType> ComponentRef<ComponentType> of(Class<ComponentType> component) {
return new ComponentRef(component, null);
}
public static <ComponentType> ComponentRef<ComponentType> of(Class<ComponentType> component, Annotation qualifier) {
return new ComponentRef(component, qualifier);
}
public static ComponentRef of(Type type) {
return new ComponentRef(type, null);
}
public static ComponentRef of(Type type, Annotation qualifier) {
return new ComponentRef(type, qualifier);
}
private Type container;
private Component component;
ComponentRef(Type type, Annotation qualifier) {
init(type, qualifier);
}
protected ComponentRef() {
Type type = ((ParameterizedType) getClass().getGenericSuperclass()).getActualTypeArguments()[0];
init(type, null);
}
private void init(Type type, Annotation qualifier) {
if (type instanceof ParameterizedType container) {
this.container = container.getRawType();
this.component = new Component((Class<ComponentType>) container.getActualTypeArguments()[0], qualifier);
} else
this.component = new Component((Class<ComponentType>) type, qualifier);
}
public Type getContainer() {
return container;
}
public Class<?> getComponentType() {
return component.type();
}
public boolean isContainer() {
return container != null;
}
public Component component() {
return component;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
ComponentRef<?> that = (ComponentRef<?>) o;
return Objects.equals(container, that.container) && component.equals(that.component);
}
@Override
public int hashCode() {
return Objects.hash(container, component);
}
}
Context.java
package geektime.tdd.di;
import java.util.Optional;
public interface Context {
<ComponentType> Optional<ComponentType> get(ComponentRef<ComponentType> ref);
}
ContextConfig.java
package geektime.tdd.di;
import jakarta.inject.Provider;
import jakarta.inject.Qualifier;
import java.lang.annotation.Annotation;
import java.util.*;
public class ContextConfig {
private Map<Component, ComponentProvider<?>> components = new HashMap<>();
public <Type> void bind(Class<Type> type, Type instance) {
components.put(new Component(type, null), (ComponentProvider<Type>) context -> instance);
}
public <Type> void bind(Class<Type> type, Type instance, Annotation... qualifiers) {
if (Arrays.stream(qualifiers).anyMatch(q -> !q.annotationType().isAnnotationPresent(Qualifier.class)))
throw new IllegalComponentException();
for (Annotation qualifier : qualifiers)
components.put(new Component(type, qualifier), context -> instance);
}
public <Type, Implementation extends Type>
void bind(Class<Type> type, Class<Implementation> implementation) {
components.put(new Component(type, null), new InjectionProvider<>(implementation));
}
public <Type, Implementation extends Type>
void bind(Class<Type> type, Class<Implementation> implementation, Annotation... qualifiers) {
if (Arrays.stream(qualifiers).anyMatch(q -> !q.annotationType().isAnnotationPresent(Qualifier.class)))
throw new IllegalComponentException();
for (Annotation qualifier : qualifiers)
components.put(new Component(type, qualifier), new InjectionProvider<>(implementation));
}
public Context getContext() {
components.keySet().forEach(component -> checkDependencies(component, new Stack<>()));
return new Context() {
@Override
public <ComponentType> Optional<ComponentType> get(ComponentRef<ComponentType> ref) {
if (ref.isContainer()) {
if (ref.getContainer() != Provider.class) return Optional.empty();
return (Optional<ComponentType>) Optional.ofNullable(getProvider(ref))
.map(provider -> (Provider<Object>) () -> provider.get(this));
}
return Optional.ofNullable(getProvider(ref)).map(provider -> (ComponentType) provider.get(this));
}
};
}
private <ComponentType> ComponentProvider<?> getProvider(ComponentRef<ComponentType> ref) {
return components.get(ref.component());
}
private void checkDependencies(Component component, Stack<Component> visiting) {
for (ComponentRef dependency : components.get(component).getDependencies()) {
if (!components.containsKey(dependency.component()))
throw new DependencyNotFoundException(component, dependency.component());
if (!dependency.isContainer()) {
if (visiting.contains(dependency.component())) throw new CyclicDependenciesFoundException(visiting);
visiting.push(dependency.component());
checkDependencies(dependency.component(), visiting);
visiting.pop();
}
}
}
interface ComponentProvider<T> {
T get(Context context);
default List<ComponentRef> getDependencies() {
return List.of();
}
}
}
InjectionProvider.java
package geektime.tdd.di;
import jakarta.inject.Inject;
import jakarta.inject.Qualifier;
import java.lang.annotation.Annotation;
import java.lang.reflect.*;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.function.BiFunction;
import java.util.stream.Stream;
import static java.util.Arrays.stream;
import static java.util.stream.Stream.concat;
class InjectionProvider<T> implements ContextConfig.ComponentProvider<T> {
private Constructor<T> injectConstructor;
private List<Field> injectFields;
private List<Method> injectMethods;
private List<ComponentRef> dependencies;
public InjectionProvider(Class<T> component) {
if (Modifier.isAbstract(component.getModifiers())) throw new IllegalComponentException();
this.injectConstructor = getInjectConstructor(component);
this.injectFields = getInjectFields(component);
this.injectMethods = getInjectMethods(component);
if (injectFields.stream().anyMatch(f -> Modifier.isFinal(f.getModifiers())))
throw new IllegalComponentException();
if (injectMethods.stream().anyMatch(m -> m.getTypeParameters().length != 0))
throw new IllegalComponentException();
this.dependencies = getDependencies();
}
@Override
public T get(Context context) {
try {
T instance = injectConstructor.newInstance(toDependencies(context, injectConstructor));
for (Field field : injectFields) field.set(instance, toDependency(context, field));
for (Method method : injectMethods) method.invoke(instance, toDependencies(context, method));
return instance;
} catch (InvocationTargetException | InstantiationException | IllegalAccessException e) {
throw new RuntimeException(e);
}
}
@Override
public List<ComponentRef> getDependencies() {
return concat(concat(stream(injectConstructor.getParameters()).map(this::toComponentRef),
injectFields.stream().map(f -> toComponentRef(f))),
injectMethods.stream().flatMap(m -> stream(m.getParameters()).map(this::toComponentRef)))
.toList();
}
private ComponentRef toComponentRef(Field field) {
return ComponentRef.of(field.getGenericType(), getQualifier(field));
}
private ComponentRef<?> toComponentRef(Parameter parameter) {
return ComponentRef.of(parameter.getParameterizedType(), getQualifier(parameter));
}
private static Annotation getQualifier(AnnotatedElement element) {
List<Annotation> qualifiers = stream(element.getAnnotations())
.filter(a -> a.annotationType().isAnnotationPresent(Qualifier.class)).toList();
if (qualifiers.size() > 1) throw new IllegalComponentException();
return qualifiers.stream().findFirst().orElse(null);
}
private static <T> List<Method> getInjectMethods(Class<T> component) {
List<Method> injectMethods = traverse(component, (methods, current) -> injectable(current.getDeclaredMethods())
.filter(m -> isOverrideByInjectMethod(methods, m))
.filter(m -> isOverrideByNoInjectMethod(component, m)).toList());
Collections.reverse(injectMethods);
return injectMethods;
}
private static <T> List<Field> getInjectFields(Class<T> component) {
return traverse(component, (fields, current) -> injectable(current.getDeclaredFields()).toList());
}
private static <Type> Constructor<Type> getInjectConstructor(Class<Type> implementation) {
List<Constructor<?>> injectConstructors = injectable(implementation.getConstructors()).toList();
if (injectConstructors.size() > 1) throw new IllegalComponentException();
return (Constructor<Type>) injectConstructors.stream().findFirst().orElseGet(() -> defaultConstructor(implementation));
}
private static <Type> Constructor<Type> defaultConstructor(Class<Type> implementation) {
try {
return implementation.getDeclaredConstructor();
} catch (NoSuchMethodException e) {
throw new IllegalComponentException();
}
}
private static <T> List<T> traverse(Class<?> component, BiFunction<List<T>, Class<?>, List<T>> finder) {
List<T> members = new ArrayList<>();
Class<?> current = component;
while (current != Object.class) {
members.addAll(finder.apply(members, current));
current = current.getSuperclass();
}
return members;
}
private static <T extends AnnotatedElement> Stream<T> injectable(T[] declaredFields) {
return stream(declaredFields).filter(f -> f.isAnnotationPresent(Inject.class));
}
private static boolean isOverride(Method m, Method o) {
return o.getName().equals(m.getName()) && Arrays.equals(o.getParameterTypes(), m.getParameterTypes());
}
private static <T> boolean isOverrideByNoInjectMethod(Class<T> component, Method m) {
return stream(component.getDeclaredMethods()).filter(m1 -> !m1.isAnnotationPresent(Inject.class)).noneMatch(o -> isOverride(m, o));
}
private static boolean isOverrideByInjectMethod(List<Method> injectMethods, Method m) {
return injectMethods.stream().noneMatch(o -> isOverride(m, o));
}
private static Object[] toDependencies(Context context, Executable executable) {
return stream(executable.getParameters()).map(p -> toDependency(context, p.getParameterizedType(), getQualifier(p))).toArray(Object[]::new);
}
private static Object toDependency(Context context, Field field) {
return toDependency(context, field.getGenericType(), getQualifier(field));
}
private static Object toDependency(Context context, Type type, Annotation qualifier) {
return context.get(ComponentRef.of(type, qualifier)).get();
}
}
任务列表的状态为:
无需构造的组件——组件实例
如果注册的组件不可实例化,则抛出异常
抽象类
接口
构造函数注入
无依赖的组件应该通过默认构造函数生成组件实例
有依赖的组件,通过 Inject 标注的构造函数生成组件实例
如果所依赖的组件也存在依赖,那么需要对所依赖的组件也完成依赖注入
如果组件有多于一个 Inject 标注的构造函数,则抛出异常
如果组件没有 Inject 标注的构造函数,也没有默认构造函数(新增任务)
如果组件需要的依赖不存在,则抛出异常
如果组件间存在循环依赖,则抛出异常
字段注入
通过 Inject 标注将字段声明为依赖组件
如果字段为 final 则抛出异常
依赖中应包含 Inject Field 声明的依赖
方法注入
通过 Inject 标注的方法,其参数为依赖组件
通过 Inject 标注的无参数方法,会被调用
按照子类中的规则,覆盖父类中的 Inject 方法
如果方法定义类型参数,则抛出异常
依赖中应包含 Inject Method 声明的依赖
对 Provider 类型的依赖
从容器中取得组件的 Provider(新增任务)
注入构造函数中可以声明对于 Provider 的依赖
注入字段中可以声明对于 Provider 的依赖
注入方法中可声明对于 Provider 的依赖
将构造函数中的 Provider 加入依赖(新增任务)
将字段中的 Provider 加入依赖(新增任务)
将方法中的 Provider 加入依赖(新增任务)
自定义 Qualifier 的依赖
注册组件时,可额外指定 Qualifier
针对 instance 指定一个 Qualifieri(新增任务)
针对组件指定一个 Qualiifer(新增任务)
针对 instance 指定多个 Qualifieri(新增任务)
针对组件指定多个 Qualiifer(新增任务)
注册组件时,如果不是合法的 Qualifier,则不接受组件注册(新增任务)
寻找依赖时,需同时满足类型与自定义 Qualifier 标注
在检查依赖时使用 Qualifier(新增任务)
在检查循环依赖时使用 Qualifier(新增任务)
构造函数注入可以使用 Qualifier 声明依赖(新增任务)
依赖中包含 Qualifier(新增任务)
如果不是合法的 Qualifier,则组件非法
字段注入可以使用 Qualifier 声明依赖(新增任务)
依赖中包含 Qualifier(新增任务)
如果不是合法的 Qualifier,则组件非法
函数注入可以使用 Qualifier 声明依赖(新增任务)
依赖中包含 Qualifier(新增任务)
如果不是合法的 Qualifier,则组件非法
支持默认 Qualifier——Named(不需要)
注册组件时,可从类对象上提取 Qualifier(不需要)
Singleton 生命周期
注册组件时,可额外指定是否为 Singleton
注册组件时,可从类对象上提取 Singleton 标注
对于包含 Singleton 标注的组件,在容器范围内提供唯一实例
容器组件默认不是 Single 生命周期
自定义 Scope 标注
可向容器注册自定义 Scope 标注的回调
确认放弃笔记?
放弃后所记笔记将不保留。
新功能上线,你的历史笔记已初始化为私密笔记,是否一键批量公开?
批量公开的笔记不会为你同步至部落
公开
同步至部落
取消
完成
0/2000
荧光笔
直线
曲线
笔记
复制
AI
  • 深入了解
  • 翻译
    • 英语
    • 中文简体
    • 中文繁体
    • 法语
    • 德语
    • 日语
    • 韩语
    • 俄语
    • 西班牙语
    • 阿拉伯语
  • 解释
  • 总结

徐八叉的文章深入探讨了利用TDD的方式实现注入依赖容器的重要性,强调了重构代码和测试的必要性。通过介绍构造函数注入、字段注入、方法注入、对Provider类型的依赖、自定义Qualifier的依赖以及Singleton生命周期等内容,读者可以学习到如何利用TDD的方式来实现注入依赖容器,并对代码和测试进行重构。文章突出了实际操作的步骤,为想要深入了解DI Container的读者提供了有益的指导和参考。同时,作者还介绍了器组件默认不是Single生命周期、自定义Scope标注以及向容器注册自定义Scope标注的回调等内容,为读者提供了全面的技术指导。整体而言,本文内容丰富,涵盖了DI Container的重要技术特点,对于想要深入学习该领域的读者来说,是一篇非常有价值的技术文章。

仅可试看部分内容,如需阅读全部内容,请付费购买文章所属专栏
《徐昊 · TDD 项目实战 70 讲》
新⼈⾸单¥98
立即购买
登录 后留言

全部留言(3)

  • 最新
  • 精选
  • 张铁林
    https://github.com/vfbiby/tdd-di-container/tree/master 104个测试的数字对上了,就是不知道是不是一致
    2022-06-03
  • aoe
    建模上的缺陷 - 模型冗余:相同的计算逻辑同时出现在构造方法、实现方法中 - 模型不能真正反应需求上要解决的问题 - 对概念上的封装存在缺失 - 在工作中很难发现,缓慢的让代码进入到一个滑坡状态、越来越复杂
    2022-05-24
  • 人间四月天
    厉害,重构真厉害
    2022-05-24
收起评论
显示
设置
留言
3
收藏
沉浸
阅读
分享
手机端
快捷键
回顶部