依赖注入技术问询:嵌套逻辑层对象构建位置选择及DI容器动态参数实例化解决方案
场景背景
假设存在如下依赖关系:
- Controller 依赖 Service
- Service 依赖 History、Source 和 HttpClient
- Source 依赖 SourceRepository 与动态参数
id,且只有从 SourceRepository 获取信息后,Source 才具备使用价值 - History 依赖 HistoryRepository 与 Source
以下是简化后的PHP伪代码示例:
public class Source { ... public function __construct(InterfaceRepository $repository, int $id) { $this->data = $this->repository->findById($id); } } public class History { ... public function __construct(InterfaceRepository $repository, InterfaceSource $source) { ... } public function hasHistory(): bool { return $this->repository->exists($this->source->data); } public function duplicateHistory() { ... } } public class Service { ... public function __construct(InterfaceHistory $history, InterfaceSource $source, InterfaceHttpClient $httpClient) { ... } public function send() { if ($this->history->hasHistory()) { return $this->history->duplicateHistory(); } return $this->sendNewRequest(); } public function sendNewRequest() { $this->httpClient->postRequest($this->source->data); } } public class Controller { public function doSomething(int $id) { $sourceRepository = new SourceRepository(); $source = new Source($sourceRepository, $id); $historyRepository = new HistoryRepository(); $history = new History($historyRepository, $source); $httpClient = new Guzzle(); $service = new Service($history, $source, $httpClient); $service->send(); } }
问题1:是否所有对象的构建工作都必须放在最高层级(如本例中的Controller)?还是存在部分对象构建可放在中间层级的情况?
绝对不是所有对象构建都要堆在最高层级!核心原则是只在「组合根」(Composition Root)里处理对象的依赖组装,而组合根不一定只是Controller——它是应用中负责组装所有依赖的单一(或少数)入口点,比如框架的启动类、服务提供者类,或者像你例子里的Controller方法(但Controller本身属于上层,适合当组合根的一部分)。
那什么时候可以在中间层级处理构建?只有当某个对象的依赖是该层级专属、且不需要上层干预的情况,但要注意:这部分构建不能破坏依赖注入的核心——即依赖的反转控制(IoC)。举个例子:
- 如果Source类需要的
id是由Service内部生成的(而不是从Controller传入),那Service可以在内部构建Source,但前提是Source的其他依赖(比如SourceRepository)是通过Service的构造函数注入进来的,而不是在Service里new SourceRepository()。 - 但如果像你例子里
id是由Controller从外部获取的(比如请求参数),那Source的构建就必须放在能拿到id的层级(也就是Controller),或者通过工厂模式把构建逻辑封装,但工厂本身要注入到组合根里。
总结一下:
- 禁止在业务类(比如Service、History)里直接
new依赖(你例子里Controller直接new SourceRepository()其实也不太规范,最好是注入Repository而不是硬编码实例化) - 组合根是唯一应该处理依赖组装的地方,中间层级可以处理部分构建,但必须基于注入的依赖来做,不能硬编码实例化。
问题2:若使用Dependency Injection Container(DI容器),由于Source类的实例化需要动态传入id参数,DI容器无法直接完成其实例化,该如何解决这一问题?
这是DI容器使用中非常常见的场景,有几个成熟的解决方案:
方案1:使用工厂模式
创建一个SourceFactory类,它依赖SourceRepository(由DI容器注入),然后提供一个方法来接收id并创建Source实例:
class SourceFactory { private $repository; public function __construct(InterfaceRepository $repository) { $this->repository = $repository; } public function create(int $id): Source { return new Source($this->repository, $id); } }
然后在DI容器里注册SourceFactory,当你需要Source时,从容器获取工厂,调用create($id)即可——这样既保留了DI的优势,又能处理动态参数。
方案2:使用容器的「工厂方法」注册
大多数DI容器(比如Symfony DI、Laravel Container)都支持注册工厂方法来创建对象。比如在Laravel里:
$this->app->bind(Source::class, function ($app, $parameters) { $repository = $app->make(InterfaceRepository::class); return new Source($repository, $parameters['id']); });
然后当你需要Source时,通过容器的make方法传入参数:
$source = app()->make(Source::class, ['id' => $request->id]);
方案3:延迟初始化(谨慎使用)
你可以修改Source类,把id的传入和数据加载分开:
public class Source { private $repository; private $data; public function __construct(InterfaceRepository $repository) { $this->repository = $repository; } public function load(int $id) { $this->data = $this->repository->findById($id); return $this; } }
这样DI容器可以直接实例化Source,然后在需要的时候调用load($id)。但这个方案的缺点是Source在调用load前处于无效状态,可能导致运行时错误,所以除非万不得已,优先选择工厂模式。
方案4:使用参数解析器(部分容器支持)
有些容器允许你在注册时指定动态参数的来源,比如从请求中获取。比如Symfony可以通过RequestStack来注入请求中的id参数,但这种方式耦合了HTTP请求,只适合Web场景。
最后,不管用哪种方案,核心都是把静态依赖(比如Repository)交给容器注入,动态参数(比如id)在需要的时候传入,避免容器和动态业务参数绑定。
内容的提问来源于stack exchange,提问作者Edson Horacio Junior

