我目前面临着一个非常有趣的架构和实施困境。
我有一个名为ServiceInterface
的接口,它有一个名为execute()
然后我对这个接口有两个不同的实现:Service1
和Service2
,它们正确地实现了执行方法。
我有一个名为MainController
的控制器,这个控制器有一个"类型提示"对于ServiceInterface
(依赖注入),这意味着可以将Service1
和Service2
都称为该依赖注入的解析。
现在有趣的部分:
我不知道要使用哪些实现(Service1
或Service2
),因为我只知道我是否可以使用其中一个或其他来自用户输入上一步。
这意味着用户选择了一项服务,根据该值,我知道是否可以使用Service1
或Service2
。
我目前正在使用会话值解决依赖注入,所以根据值我返回一个实例或其他,但我真的认为这不是一个好方法。
请告诉我,如果你遇到类似的问题,你如何解决,或者我能做些什么才能以正确的方式实现这一目标。
提前致谢。如果需要进一步的信息,请告诉我。
答案 0 :(得分:14)
经过几天研究和思考很多关于这方面的最佳方法,使用Laravel我终于解决了。
我必须说这在Laravel 5.2
中特别困难,因为在这个版本中, Session 中间件只在路由中使用的控制器中执行,这意味着如果对于某些我使用控制器(没有链接死记硬背)并尝试访问会话的原因是不可能的。
所以,因为我不能使用会话我决定使用URL参数,这里有解决方法,我希望你们中的一些人发现它很有用。
所以,你有一个界面:
interface Service
{
public function execute();
}
然后是接口的几个实现:
服务一:
class ServiceOne implements Service
{
public function execute()
{
.......
}
}
服务二。
class ServiceTwo implements Service
{
public function execute()
{
.......
}
}
现在有趣的部分:有一个控制器,其功能与服务接口具有依赖关系,但我需要根据使用输入将其解析为ServiceOne或ServiceTwo。所以:
控制器
class MyController extends Controller
{
public function index(Service $service, ServiceRequest $request)
{
$service->execute();
.......
}
}
请注意ServiceRequest,验证请求已经具有解析依赖关系所需的参数(称之为'service_name'
)
现在,在AppServiceProvider中,我们可以通过以下方式解决依赖关系:
class AppServiceProvider extends ServiceProvider
{
public function boot()
{
}
public function register()
{
//This specific dependency is going to be resolved only if
//the request has the service_name field stablished
if(Request::has('service_name'))
{
//Obtaining the name of the service to be used (class name)
$className = $this->resolveClassName(Request::get('service_name')));
$this->app->bind('Including\The\Namespace\For\Service', $className);
}
}
protected function resolveClassName($className)
{
$resolver = new Resolver($className);
$className = $resolver->resolveDependencyName();
return $className;
}
}
所以现在所有的责任都是Resolver类,这个类基本上使用传递给contructor的参数来返回将用作Service接口实现的类的fullname(带命名空间): / p>
class Resolver
{
protected $name;
public function __construct($className)
{
$this->name = $className;
}
public function resolveDependencyName()
{
//This is just an example, you can use whatever as 'service_one'
if($this->name === 'service_one')
{
return Full\Namespace\For\Class\Implementation\ServiceOne::class;
}
if($this->name === 'service_two')
{
return Full\Namespace\For\Class\Implementation\ServiceTwo::class;
}
//If none, so whrow an exception because the dependency can not be resolved
throw new ResolverException;
}
}
嗯,我真的希望对你们中的一些人有所帮助。
祝福!
----------编辑-----------
我只是意识到,直接使用请求数据并不是一个好主意,在Laravel的容器内部,从长远来看它确实会带来一些麻烦。
最好的方法是直接注册支持的所有可能实例(serviceone和servicetwo),然后直接从控制器或中间件解析其中一个,那么控制器“谁决定”使用什么服务(来自所有可用的)基于请求的输入。
最后它的工作原理相同,但它会让你以更自然的方式工作。
我要感谢rizqi。来自Laravel闲聊的问题频道的用户。
他亲自为此创造了一个金色的article。请阅读它,因为完全以正确的方式解决了这个问题。
答案 1 :(得分:3)
您定义控制器与ServiceInterface
一起使用的事实是正确的
如果你必须根据前一步骤选择服务的具体实现(正如我所理解的那样,在先前的请求中发生),将值存储在会话或数据库中也是正确的,因为你没有替代方案:要选择实现,您必须知道输入的值
重要的一点是在一个地方“隔离”具体实现的解决方案与输入值:例如,创建一个方法,将此值作为参数,并从值返回服务的具体实现: / p>
public function getServiceImplementation($input_val)
{
switch($input_val)
{
case 1 : return new Service1();
case 2 : return new Service2();
}
}
并在您的控制器中:
public function controllerMethod()
{
//create and assign the service implementation
$this->service = ( new ServiceChooser() )->getServiceImplementation( Session::get('input_val') );
}
在此示例中,我使用了另一个类来存储方法,但您可以将方法放在控制器中或使用简单工厂模式,具体取决于应用程序应在应用程序中解析的位置
答案 2 :(得分:2)
这是一个有趣的问题。我目前正在使用Laravel 5.5并且一直在考虑它。我还希望我的服务提供者根据用户输入返回一个特定的类(实现一个接口)。我认为最好手动传递来自控制器的输入,这样就可以更容易地看到发生了什么。我还会在配置中存储类名的可能值。 因此,基于您在上面定义的服务类和接口,我提出了这个:
/config/services.php
return [
'classes': [
'service1' => 'Service1',
'service2' => 'Service2',
]
]
/app/Http/Controllers/MainController.php
public function index(ServiceRequest $request)
{
$service = app()->makeWith(ServiceInterface::class, ['service'=>$request->get('service)]);
// ... do something with your service
}
/app/Http/Requests/ServiceRequest.php
public function rules(): array
$availableServices = array_keys(config('services.classes'));
return [
'service' => [
'required',
Rule::in($availableServices)
]
];
}
/app/Providers/CustomServiceProvider.php
class CustomServiceProvider extends ServiceProvider
{
public function boot() {}
public function register()
{
// Parameters are passed from the controller action
$this->app->bind(
ServiceInterface::class,
function($app, $parameters) {
$serviceConfigKey = $parameters['service'];
$className = '\\App\\Services\\' . config('services.classes.' . $serviceConfigKey);
return new $className;
}
);
}
}
这样我们可以验证输入以确保传递有效服务,然后控制器处理将Request对象的输入传递给ServiceProvider。我只是想到在维护这段代码时,将会清楚发生了什么,而不是直接在ServiceProvider中使用请求对象。 PS请记住注册CustomServiceProvider!
答案 3 :(得分:1)
我发现处理此问题的最佳方法是使用工厂模式。您可以创建一个类ServiceFactory
并且它有一个方法create()
它可以接受一个参数,该参数用于动态选择要实例化的具体类。
它有一个基于参数的case语句。
它将使用App::make(ServiceOne::class)
或App::make(ServiceTwo::class)
。具体取决于哪一个是必需的。
然后您可以将此注入您的控制器(或依赖于工厂的服务)。
然后,您可以在服务单元测试中模拟它。