为什么使用服务容器而不是新的 class

Why use service container instead of new class

什么场景下我们应该使用服务容器而不是直接调用new class? 我从Laravel官方文档上看到它主要用于管理class依赖和执行依赖注入,但是我不确定我是否理解正确。

例如薪资申请,我们将有薪资 class、员工 Class、休假 Class、时间表 Class。 因此,要处理每月工资单,工资单 class 将取决于

下面是我平时写的(没有任何设计模式)

Class PayrollProcessController 
{
    public function index(){
         $payrollObj = new Payroll();
         $payroll->setPayrollMonth('202106');
         $payroll->process();
    }
}

Class Payroll 
{
    private $payrollMonth;

    public function process()
    {
        // loop employee from database
        foreach ($employees as $employee_id){
             $salary = $this->calculateNetSalary($employee_id); 
             // save into db
        }
    }

    public function calculateNetSalary($employee_id)
    {
         $employee = Employee::find($employee_id);
         $basicSalary = $employee->getBasicSalary();
         $basicSalaryPerDay = $basicSalary / date('t');         
         $basicSalaryPerHour = $basicSalaryPerDay / 8;
         $leaveTakenDay = Leave::getUnpaidLeaveCount( $employee->getId(), $this->payrollMonth);
         $leaveDeductionAmount = $basicSalaryPerDay * $leaveTakenDay;
         $timesheetLateHours = Timesheet::getLateHoursCount($employee->getId(), $this->payrollMonth);
         $lateDeductionAmount = $basicSalaryPerHour * $timesheetLateHours;
         $netSalary = $basicSalary - $leaveDeductionAmount - $lateDeductionAmount;
         return $netSalary;
    }
}

如果应用服务容器概念,

Class PayrollProcessController 
{
    public function index(Payroll $payroll){  
          $payroll->setPayrollMonth('202106');        
          $payroll->process();
    }
}

Class Payroll 
{
    public function process()
    {
        // loop employee from database
        foreach ($employees as $employee_id){

            $employee = app(Employee::class);
            $employee = $employee->find($employee_id);
            $leave = app(Leave::class);
            $timesheet = app(Timesheet::class);

            $salary = $this->calculateNetSalary($employee, $leave, $timesheet); 
             // save into db
        }
    }

    public function calculateNetSalary(Employee $employee, Leave $leave, Timesheet $timesheet)
    {
         $basicSalary = $employee->getBasicSalary();
         $basicSalaryPerDay = $basicSalary / date('t');         
         $basicSalaryPerHour = $basicSalaryPerDay / 8;
         $leaveTakenDay = $leave->getUnpaidLeaveCount( $employee->getId(), $this->payrollMonth);
         $leaveDeductionAmount = $basicSalaryPerDay * $leaveTakenDay;
         $timesheetLateHours = $timesheet->getLateHoursCount($employee->getId(), $this->payrollMonth);
         $lateDeductionAmount = $basicSalaryPerHour * $timesheetLateHours;
         $netSalary = $basicSalary - $leaveDeductionAmount - $lateDeductionAmount;
         return $netSalary;
    }
}

问题:

  1. 以上概念是否正确?我可以看到服务容器允许我将工资单、员工、时间表、休假 class 移动到 laravel 包中,这样我就可以安装在另一个 laravel 应用程序中(主要好处是我可以重新使用代码,如果需要,还可以通过在服务提供商中指定 class 路径来覆盖 class)

  2. 我再次查看了服务容器文档 https://laravel.com/docs/8.x/container#when-to-use-the-container,写成:

    First, if you write a class that implements an interface and you wish to type-hint that interface on a route or class constructor, you must tell the container how to resolve that interface. Secondly, if you are writing a Laravel package that you plan to share with other Laravel developers, you may need to bind your package's services into the container.

    所以我可以说除非我们将工资单模块发布为 laravel 包供其他人使用或编写单元测试,否则使用服务容器没有意义吗?

  3. 如果我们看opencart的registry class,是不是和Laravel的服务容器很像? https://github.com/opencart/opencart/blob/e22ddfb060752cd8134abbfb104202172ab45c86/upload/system/framework.php#L9

  4. 以opencart为例,Languageclass是单例设计模式,registry是容器设计模式?

    $language = new \Opencart\System\Library\Language($config->get('language_code'));
    $language->addPath(DIR_LANGUAGE);
    $language->load($config->get('language_code'));
    $registry->set('language', $language);
    

通过 Laravels IoC 容器 (service container) 实现的依赖注入的目的是分离对象的创建和使用。所以我认为 app(Employee::class); 并不比 new EmployeeEmployee::create() 好。 IoC 不仅仅是将对象注册到容器中,并在需要时将其拉出。

一个实际的例子可能是你有一个 class 传输 SMS 消息。

class Sms
{
    private $smsProvider;

    public function send()
    {
        $this->smsProvider->send();
    }
}

因此您的 SmsSender class 需要 smsProvider 才能运行。与其在 Sms class 中创建 SMS 提供程序的 new 实例,不如将提供程序注入 class (通常是此类示例中的构造函数)并以这种方式使用它。如果你想让它更加灵活,你可以为 SMS 提供者定义一个接口,然后允许服务容器从你的 IoC 映射中注入正确的具体 class。

class Sms
{
    private $smsProvider;

    public __construct(ISmsProvider $smsProvider)
    {
        $this->smsProvider = $smsProvider;
    }

    public function send()
    {
        $this->smsProvider->send();
    }
}

class SmsTwillio implements ISmsProvider
{
}

class SmsNexmo implements ISmsProvider
{
}

然后您将在服务容器中定义映射以将对 ISmsProvider 的引用绑定到具体实现,例如 SmsTwillioSmsNexmo.

使用您的示例,您可以定义 PayrollService:

class PayrollService
{
    public function process($employees, $start, $end = null)
    {
        foreach ($employees as $employee) {
            $salary = $this->calculateNetSalary($employee);
        }
    }

    private function calculateNetSalary(Employee $employee)
    {
    }
}

class ProcessPayrollController extends ProcessPayrollController
{
    private $payrollService;

    public __constructor(PayrollService $PayrollService)
    {
        $this->payrollService = $payrollService;
    }

    public function __invoke()
    {
        $result = $this->payrollService->process(Employee::all(), '2021-06-01');
    }
}

我认为您的 Payroll class 不需要 LeaveTimesheet。我会在你的 Employee class 上定义 functionsscopes,其中 return 是数据:

$employee->daysUnpaidLeave('2021-06-01');

$employee->hoursLate('2021-06-01');

日期是从 start 日期计算到今天的日期,或者提供一个可选的 end 日期作为第二个参数。

您可以更深入地研究兔子洞并查看其他 creational design patterns 在运行时创建对象,但是,设计模式可能会很快被滥用并增加而不是减少复杂性,因此请保持简单。

OpenCart中的Registry也是类似的原理。在该示例中,$registry->get('language'); 将始终 return Language 的相同实例,所以是的,它将是一个单例。

您的 Payroll 可能是单例,因为您不需要它的多个实例,因为它只是处理您提供给它的数据。