.NET依赖注入[29]

12.2.3 使用自动注册按惯例配置容器

  考虑到列表12.8的注册,在你的项目中拥有这几行代码可能是完全没问题的。然而,当一个项目增长时,设置DI容器所需的注册量也会增加。随着时间的推移,你可能会看到许多类似的注册出现。 它们通常会遵循一个共同的模式。下面的列表显示了这些注册是如何开始显得有些重复的。

清单12.9 将代码式配置使用时,注册中的重复情况

services.AddTransient < IProductRepository, SqlProductRepository > ();
services.AddTransient < ICustomerRepository, SqlCustomerRepository > ();
services.AddTransient < IOrderRepository, SqlOrderRepository > ();
services.AddTransient < IShipmentRepository, SqlShipmentRepository > ();
services.AddTransient < IImageRepository, SqlImageRepository > ();
services.AddTransient < IProductService, ProductService > ();
services.AddTransient < ICustomerService, CustomerService > ();
services.AddTransient < IOrderService, OrderService > ();
services.AddTransient < IShipmentService, ShipmentService > ();
services.AddTransient < IImageService, ImageService > ();

  反复写这样的注册代码违反了DRY原则。它也像是一块没有生产力的基础结构代码,对应用程序没有什么价值。如果你能自动注册组件,你可以节省时间并减少错误,前提是这些组件遵循某种惯例。许多DI容器提供了自动注册功能,让你可以引入你自己的约定,并在配置上应用约定。

定义 自动注册是通过扫描一个或多个程序集来自动注册容器中的组件的能力,基于一定的惯例来实现所需的摘要。自动注册有时被称为批量注册或Assembly扫描。

惯例高于配置
  一个越来越流行的架构模型是 "约定高于配置 "的概念。与其编写和维护大量的配置代码,你可以就影响代码基础的约定达成一致。ASP.NET Core MVC根据控制器名称寻找控制器的方式就是一个简单约定的好例子。

  • 一个名为Home的控制器的请求进来了。
  • 默认的控制器工厂在知名命名空间的列表中搜索一个名为HomeController的类。如果它找到了这样的一个类,那么它就是匹配的。
  • 默认的控制器工厂将类的类型转发给控制器激活器,后者构建了一个控制器的实例。

这里的惯例是,一个控制器必须被命名为[ControllerName]控制器。
 
惯例可以应用于ASP.NET Core MVC控制器以上。你添加的约定越多,你就越能将容器配置的各个部分自动化。

TIP 惯例重于配置有更多的优点,而不仅仅是支持DI配置。它使你的代码更加一致,因为只要你遵循你的约定,它就会自动工作。

  在现实中,你可能需要将自动注册与代码式配置或配置文件结合起来,因为你可能无法将每一个组件都纳入一个有意义的约定。 但是,你越是能将你的代码库推向惯例,它的可维护性就越强。
  Autofac支持自动注册,但我们认为使用另一个DI容器来配置示例的电子商务应用程序会更有趣。因为我们喜欢把例子限制在本书讨论的DI容器中,并且因为Microsoft.Extensions.DependencyInjection没有任何自动注册功能,我们将使用Simple Injector来说明这个概念。
  回顾清单12.9,你可能会同意,各种数据访问组件的注册是重复的。我们可以围绕它们表达某种约定吗?列表12.9中的五种具体的Repository类型都有一些共同的特点:

  • 它们都被定义在同一个assembly中。
  • 每个具体的类都有一个以Repository结尾的名字。
  • 每个都实现了一个单一的接口。

  似乎一个合适的约定会通过扫描相关的程序集并注册所有符合约定的类来表达这些相似性。即使Simple Injector支持自动注册,它的自动注册API也是围绕着注册共享相同接口的类型组。它的API本身并不允许你表达这个约定,因为没有一个单一的接口来描述这组集合。
  起初,这种遗漏可能显得相当尴尬,但在.NET的反射API之上定义一个自定义的LINQ查询通常很容易编写,提供更多的灵活性,并防止你不得不学习另一个API–假设你熟悉LINQ和.NET的反射API。下面的列表显示了这样一个使用LINQ查询的约定。

清单12.10 使用简单注入器扫描存储库的惯例

//选择一个Assembly来做惯例
var assembly = typeof (SqlProductRepository).Assembly;
//定义一个LINQ查询,用来定位程序集中符合具体标准的所有类型,并以Repository结尾。
var repositoryTypes = from type in assembly.GetTypes() where!type.Abstract where type.Name.EndsWith("Repository") select type;
//遍历LINQ查询以注册每个类型
foreach(Type type in repositoryTypes) {
    container.Register(type.GetInterfaces().Single(), type);
}

  在迭代过程中,每一个通过where过滤器的类都应该针对其接口进行注册。例如,因为SqlProductRepository的接口是IProductRepository,所以它最终将成为IProductRepository到SqlProductRepository的映射。
  这个特别的约定扫描了包含数据访问组件的程序集。你可以通过多种方式获得对该程序集的引用,但最简单的方法是选择一个有代表性的类型,如SqlProductRepository,并从中获得程序集,如清单12.10所示。你也可以选择一个不同的类,或者通过名字找到这个程序集。

注意 通过 Microsoft.Extensions.DependencyInjection,清单 12.10 中的约定代码几乎是相同的。只有foreach循环的主体是不同的,因为那是唯一调用DI容器API的地方。

  将这个约定与清单12.9中的四个注册进行比较,你可能认为这个约定的好处看起来可以忽略不计。 的确,因为在当前的例子中只有四个数据访问组件,所以代码语句的数量随着约定而增加。但是这个约定的扩展性要好得多。一旦你写了它,它就能处理数以百计的组件而不需要任何额外的努力。
  你也可以用约定来解决列表12.6和12.8中的其他映射,但这样做并没有什么价值。作为一个例子,你可以用这个约定来注册所有的服务:

ar assembly = typeof (ProductService).Assembly;
var serviceTypes = from type in assembly.GetTypes() where!type.Abstract where type.Name.EndsWith("Service") select type;
foreach(Type type in serviceTypes) {
    container.Register(type.GetInterfaces().Single(), type);
}

  这个约定扫描了所有名称以Service结尾的具体类的识别程序集,并将每个类型注册到它所实现的接口上。这就有效地将ProductService注册到了IProductService接口上,但是因为你目前没有任何其他匹配的约定,所以没有什么收获。只有当更多的服务被添加进来时,如清单12.9所示,制定一个约定才开始有意义。
  使用LINQ手工定义约定,对于所有从自己的接口派生出来的类型来说可能是有意义的,正如你之前在资源库中看到的那样。但是当你开始注册基于通用接口的类型时,正如我们在第10.3.3节中广泛讨论的那样,这种策略很快就开始崩溃了–通过反射查询通用类型通常不是一件令人愉快的事情。
  这就是为什么Simple Injector的自动注册API是围绕着基于通用抽象的类型注册而建立的,比如列表10.12中的ICommandService接口。 Simple Injector允许所有ICommandService实现的注册只需一行代码就可以完成。

Assembly assembly = typeof (AdjustInventoryService).Assembly;
container.Register(typeof (ICommandService < > ), assembly);

注意 ICommandService<>是指定开放通用版本的C#语法,通过省略TCommand通用类型参数完成。

  通过向它的一个Register重载提供一个程序集列表,Simple Injector在这些程序集中迭代找到任何实现ICommandService的非通用的具体类型,同时通过其特定的ICommandService接口注册每个类型。这有通用的类型参数TCommand被填入一个实际的类型。

定义 一个已经填入其泛型参数的泛型(例如,ICommandService),被称为封闭式泛型。同样地,当你只有泛型定义本身时(例如,ICommandService),这样的类型被称为开放泛型。

  在一个有四个ICommandService实现的应用程序中,前面的API调用将相当于下面的配置代码列表。

代码异味 清单 12.12 使用代码式配制注册实现方案

container.Register(typeof (ICommandService < AdjustInventory > ), typeof (AdjustInventoryService));
container.Register(typeof (ICommandService < UpdateProductReviewTotals > ), typeof (UpdateProductReviewTotalsService));
container.Register(typeof (ICommandService < UpdateHasDiscountsApplied > ), typeof (UpdateHasDiscountsAppliedService));
container.Register(typeof (ICommandService < UpdateHasTierPricesProperty > ), typeof (UpdateHasTierPricesPropertyService));

  然而,遍历程序集列表以找到合适的类型,并不是你能用Simple Injector的自动注册API实现的唯一事情。另一个强大的功能是注册通用的装饰器,比如你在列表10.15、10.16和10.19中看到的那些。Simple Injector允许使用它的RegisterDecorator方法重载来应用装饰器,而不是像清单10.21中那样手动组成装饰器的层次结构。

清单12.13 使用自动注册来注册通用装饰器

//RegisterDecorator提供了开放通用的ICommandService<TCommand>服务类型和装饰器的开放通用实现。
//使用这些信息,Simple Injector将它解析的每一个ICommandService<TCommand>都用适当的装饰器来包装。
container.RegisterDecorator(typeof (ICommandService < > ), typeof (AuditingCommandServiceDecorator < > ));
container.RegisterDecorator(typeof (ICommandService < > ), typeof (TransactionCommandServiceDecorator < > ));
container.RegisterDecorator(typeof (ICommandService < > ), typeof (SecureCommandServiceDecorator < > ));

  Simple Injector按照注册顺序应用装饰器,这意味着,就清单12.13而言,审计装饰器用交易装饰器包装,交易装饰器用安全装饰器包装,结果是一个与清单10.21所示相同的对象图。
  开放式通用类型的注册可以被看作是一种自动注册的形式,因为对RegisterDecorator的单一方法调用可以导致一个装饰器被应用于许多注册。如果没有这种形式的通用装饰器类的自动注册,你将*为每个封闭的ICommandService实现单独注册每个封闭版本的装饰器,如下表所示。

错误示范 清单12.14 使用代码式配置为注册通用装饰器

container.RegisterDecorator(typeof (ICommandService < AdjustInventory > ), typeof (AuditingCommandServiceDecorator < AdjustInventory > ));
container.RegisterDecorator(typeof (ICommandService < AdjustInventory > ), typeof (TransactionCommandServiceDecorator < AdjustInventory > ));
container.RegisterDecorator(typeof (ICommandService < AdjustInventory > ), typeof (SecureCommandServiceDecorator < AdjustInventory > ));
container.RegisterDecorator(typeof (ICommandService < UpdateProductReviewTotals > ), typeof (AuditingCommandServiceDecorator < UpdateProductReviewTotals > ));
container.RegisterDecorator(typeof (ICommandService < UpdateProductReviewTotals > ), typeof (TransactionCommandServiceDecorator < UpdateProductReviewTotals > ));
container.RegisterDecorator(typeof (ICommandService < UpdateProductReviewTotals > ), typeof (SecureCommandServiceDecorator < UpdateProductReviewTotals > ));
container.RegisterDecorator(typeof (ICommandService < UpdateHasDiscountsApplied > ), typeof (AuditingCommandServiceDecorator < UpdateHasDiscountsApplied > ));
container.RegisterDecorator(typeof (ICommandService < UpdateHasDiscountsApplied > ), typeof (TransactionCommandServiceDecorator < UpdateHasDiscountsApplied > ));
container.RegisterDecorator(typeof (ICommandService < UpdateHasDiscountsApplied > ), typeof (SecureCommandServiceDecorator < UpdateHasDiscountsApplied > ));
//为简洁起见,省略了其他注册。
...

  这个列表中的代码很麻烦,而且容易出错。此外,它还会导致组合根的指数级增长。

提示 自动注册最突出的缺点是你会失去一些控制。必须有可能对每个被自动注册设施选中的组件进行自动布线。当有一个特定的组件需要手工接线时,它应该被排除在自动注册之外以防止错误。

  在一个遵守SOLID原则的系统中,你会创建许多小而集中的类,但现有的类却不容易改变,增加了可维护性。 自动注册可以防止构成根部不断被更新。这是一个强大的技术,有可能使DI容器不可见。一旦适当的约定到位,你可能只需要在极少数情况下修改容器配置。

12.2.4 混合和匹配配置方法

  到目前为止,你已经看到了配置DI容器的三种不同方法:

  • 配置文件 Configuration files
  • 代码式配制 Configuration as Code
  • 自动注册 Auto-Registration

  这些都不是相互排斥的。你可以选择混合自动注册和抽象到具体类型的特定映射,甚至混合这三种方法,有一些自动注册,一些代码式配制,以及一些配置文件,用于后期绑定。
  作为一个经验法则,你应该选择自动注册作为一个起点,代码式配制来处理更多的特殊情况。 你应该把配置文件保留给那些你需要在不重新编译应用程序的情况下改变实现的情况–这比你想象的要少。
  现在我们已经介绍了如何配置DI容器以及如何用一个容器来解决对象图,你应该对如何使用它们有一个很好的想法。使用DI容器是一回事,但了解何时使用是另一回事。

上一篇:C#程序集相关的概念


下一篇:javascript 数据类型1