【问题标题】:Dynamic Service Use动态服务使用
【发布时间】:2019-01-07 16:27:09
【问题描述】:

我想使用服务处理表中的数据,并在记录中指定要使用的正确服务。所以假设我有一个包含以下数据的表

| id | value1  | value2  | service        | result |
|----|---------|---------|----------------|--------|
| 1  | string1 | string2 | string_version |        |
| 2  | int1    | int2    | int_version    |        |
| 3  | string3 | string3 | string_version |        |

我想处理我的父服务calculator中的每一行,它循环每条记录并根据service的值获取服务,服务使用value1value2计算结果然后存储结果在result

目前我已经创建了 calculator 服务,其中一个参数是服务容器 - 然后我可以使用 get 来获得我需要的实际服务:

class Calculator {
  private $container;

  public function __construct(ContainerInterface $container) {
     $this->container = $container;
  }

  public function calcResult($record) {
    $service = $this->container->get($record->getService());
    $result = $service->process($record->getValue1(),$result->getValue2());
    $record->setResult($result);
  }

}

但是......这不是非常可测试的 - 因为我在整个服务容器中传递。将来,我还希望能够添加新服务 - 因此第 3 方可以添加具有特定服务名称的捆绑包并使用它来处理表中的记录

这是我尝试做的一个非常简化的版本 - 但能够从另一个服务动态传递/获取服务是我想做的。

如何修改此代码,以便我可以根据数据库中的动态值获取服务并允许第 3 方添加“处理”服务 - 当然会实现接口 - 所以确保存在正确的方法

【问题讨论】:

  • A Service Locator 基本上是一个容器,其实现给定接口的服务数量有限。
  • @Cerad 谢谢 - 这正是我想要的
  • 就像上面提到的@Cerad 一样,您可以使用服务定位器。但是,有更好的版本是标记服务。请参阅下面的详细答案。

标签: symfony


【解决方案1】:

你有两个明显的候选人:

我个人(将始终)在这种情况下使用标记服务(策略模式),但仍会为每个示例提供示例,因此由您决定。

注意:如果您使用如下所示的服务定位器,您的服务中会出现重复和一些难看的代码。

服务定位器

interface ServiceLocatorInterface
{
    public function locate(string $id);
}

-

use Psr\Container\ContainerExceptionInterface;
use Psr\Container\ContainerInterface;
use Symfony\Component\DependencyInjection\ServiceSubscriberInterface;

class ServiceLocator implements ServiceLocatorInterface, ServiceSubscriberInterface
{
    private $locator;

    public function __construct(ContainerInterface $locator)
    {
        $this->locator = $locator;
    }

    public static function getSubscribedServices()
    {
        return [
            'string_version' => StringCalculator::class,
            'int_version' => IntCalculator::class,
        ];
    }

    public function locate(string $id)
    {
        if (!$this->locator->has($id)) {
            throw new ServiceLocatorException('Service was not found.');
        }

        try {
            return $this->locator->get($id);
        } catch (ContainerExceptionInterface $e) {
            throw new ServiceLocatorException('Failed to fetch service.');
        }
    }
}

-

class StringCalculator
{
    public function calculate($value1, $value2)
    {
        return $value1.' - '.$value2;
    }
}

-

class IntCalculator
{
    public function calculate($value1, $value2)
    {
        return $value1 + $value2;
    }
}

用法:

class YourService
{
    private $serviceLocator;

    public function __construct(\App\ServiceLocatorInterface $serviceLocator)
    { 
        $this->serviceLocator = $serviceLocator;
    }

    public function yourMethod()
    {
        /** @var StringCalculator $calculator */
        $calculator = $this->serviceLocator->locate('string_version');
        $result = $calculator->calculate('1', '2'); // result: 1 - 2

        /** @var IntCalculator $calculator */
        $calculator = $this->serviceLocator->locate('int_version');
        $result = $calculator->calculate(1, 2); // result: 3
    }
}

标记服务

service:
    App\Strategy\Calculator:
        arguments: [!tagged calculator]

    App\Strategy\StringCalculatorStrategy:
        tags:
            - { name: calculator }

    App\Strategy\IntCalculatorStrategy:
        tags:
            - { name: calculator }

-

use Traversable;

class Calculator
{
    private $calculators;

    public function __construct(Traversable $calculators)
    {
        $this->calculators = $calculators;
    }

    public function calculate(string $serviceName, $value1, $value2)
    {
        /** @var CalculatorStrategyInterface $calculator */
        foreach ($this->calculators as $calculator) {
            if ($calculator->canProcess($serviceName)) {
                return $calculator->process($value1, $value2);
            }
        }
    }
}

-

interface CalculatorStrategyInterface
{
    public function canProcess(string $serviceName): bool;

    public function process($value1, $value2);
}

-

class StringCalculatorStrategy implements CalculatorStrategyInterface
{
    public function canProcess(string $serviceName): bool
    {
        return $serviceName === 'string_version';
    }

    public function process($value1, $value2)
    {
        return $value1.' '.$value2;
    }
}

-

class IntCalculatorStrategy implements CalculatorStrategyInterface
{
    public function canProcess(string $serviceName): bool
    {
        return $serviceName === 'int_version';
    }

    public function process($value1, $value2)
    {
        return $value1 + $value2;
    }
}

用法:

class YourService
{
    private $calculator;

    public function __construct(\App\Strategy\Calculator $calculator)
    { 
        $this->calculator = $calculator;
    }

    public function yourMethod()
    {
        // result: 1 - 2
        $result = $this->calculator->calculate('string_version', 1, 2);
        // result: 3
        $result = $this->calculator->calculate('int_version', 1, 2);
    }
}

【讨论】:

  • 您是正确的,有几种方法可以解决这个常见问题,但是您的服务定位器示例过于复杂。当您提前确切知道您需要哪些服务时,请使用订阅者。通过调整订阅者代码来添加其他服务有点困难。当你需要更动态的东西时,扩展 Symfony ServiceLocator 类。不幸的是,!tagged 表示法目前不适用于服务定位器。
  • 但最大的区别在于,策略方法实际上将实例化所有可用的服务,而定位器模式仅在您实际访问给定服务时才实例化。它有真正的不同吗?也许/也许不是,但作为一项规则,如果不需要,不创建对象是一件好事。您可能还想通读我在链接答案中的辩论。
  • 我同意,服务定位器示例不是一个完全成熟的示例,而更像是一个初始知识共享示例。 !tagged 版本并不适用于服务定位器。它纯粹用于标记服务示例。
  • 我宁愿尽可能地将我的代码与我正在使用的框架分离。我也尽我所能避免反模式的东西,并坚持正确的模式,比如策略模式。策略模式是开放/封闭原则的一个很好的例子。仅仅因为服务定位器选项仅在需要时实例化对象,我不觉得我应该忽略 OOP 原则和最佳实践。如果我不在乎,那么是的,服务定位器将是理想的。我倾向于遵循 OOP 的最佳实践而不是便利。 PS - unnecessarliy complicated - Symfony 文档就是这样做的,而不是我 :)
猜你喜欢
  • 2018-05-31
  • 1970-01-01
  • 2017-05-03
  • 1970-01-01
  • 2012-09-17
  • 2012-09-20
  • 2011-01-30
  • 2018-12-15
相关资源
最近更新 更多