PHP/Laravel -贷款计算逻辑设计理念

kiz8lqtg  于 2023-10-22  发布在  PHP
关注(0)|答案(1)|浏览(94)

背景

我正在开发一个应用程序模块,根据当前配置的国家/地区,该模块具有用于贷款计算的不同域类实现。
我正在努力解决一个类,它具有所有国家共享的主逻辑(一种父类)和一些派生类,它们在行为和计算逻辑上有一些细微的差异。

  • 主课程 *
<?php

namespace We\Domain\Loans;

use Carbon\Carbon;
use Illuminate\Support\Collection;

use We\Domain\Constants\CreditStructureConstants;
use We\Domain\Constants\RoundingPrecisionConstants;

class LoanOptionPlan
{
    use LoanCalculator;

    protected $integerAmountPrecision = RoundingPrecisionConstants::INTEGER_AMOUNT;
    protected $ratePrecision = RoundingPrecisionConstants::RATE;
    protected $principalAmount;
    protected $negativePrincipalWithFeesAmount;
    protected $repaymentAmount;
    protected $lastDueDate;
    protected $termTotalDays;
    protected $grossInterestRate;
    protected $netInterestRate;
    protected $annualAmortizationRate;
    protected $termAmortizationRate;
    protected $totalFinanceCharge;
    protected $annualPercentageRate;
    protected $installmentsQuantity;
    protected $installmentAmount;
    protected $grossInterestAmount;
    protected $interestTaxesAmount;
    protected $insuranceAmount;
    protected $cashOutFeeAmount;
    protected $cashInFeeAmount;
    protected $feeTotalAmount;

    protected $installments;
    protected $id;

    public function __construct(
        LoanOption $option, int $installmentsQuantity, ?int $planId = NULL
    )
    {
        $firstDueDate = $option->getFirstDueDate();
        $principalAmount = $option->getPrincipalAmount();
        $grossInterestRate = $option->getGrossInterestRate();
        $taxesCoefficient = $option->getTaxesCoefficient();
        $termTotalDays = $this->calculateTermTotalDays(
            $firstDueDate, $option->getRequestDate(), $installmentsQuantity
        );
        $cashOutFeeAmount = $this->calculateCashoutFeeAmount(
            $option->getCashOutMaxRate(), $option->getCashOutMinRate(), $termTotalDays, $installmentsQuantity,
            $principalAmount
        );
        $netInterestRate = $this->calculateNetInterestRate($grossInterestRate, $taxesCoefficient);
        $annualAmortizationRate = $this->calculateAnnualAmortizationRate(
            $netInterestRate, $option->getAnnualNetInsuranceRate()
        );
        $installmentTermDurationAverage = $this->calculateInstallmentTermDurationAverage(
            $termTotalDays, $installmentsQuantity
        );
        $termAmortizationRate = $this->calculateTermAmortizationRate(
            $annualAmortizationRate, $installmentTermDurationAverage
        );
        $principalWithFeesAmount = $this->calculatePrincipalWithFeesAmount(
            $principalAmount, $cashOutFeeAmount
        );
        $negativePrincipalWithFeesAmount = $this->calculateNegativePrincipalWithFeesAmount(
            $principalWithFeesAmount
        );
        $installmentAmount = $this->calculateInstallmentAmount(
            $termAmortizationRate, $installmentsQuantity, $negativePrincipalWithFeesAmount
        );
        $cashFlowValues = $this->calculateCashFlowValues($negativePrincipalWithFeesAmount);

        $this->principalAmount = $principalAmount;
        $this->negativePrincipalWithFeesAmount = $negativePrincipalWithFeesAmount;
        $this->repaymentAmount = $this->calculateRepaymentAmount($installmentAmount, $installmentsQuantity);
        $this->lastDueDate = $firstDueDate->copy()->addMonthsNoOverflow($installmentsQuantity);
        $this->termTotalDays = $termTotalDays;
        $this->grossInterestRate = $grossInterestRate;
        $this->netInterestRate = $netInterestRate;
        $this->annualAmortizationRate = $annualAmortizationRate;
        $this->termAmortizationRate = $termAmortizationRate;
        $this->totalFinanceCharge = $this->calculateTotalFinanceCharge(
            $cashFlowValues, $termTotalDays, $installmentsQuantity
        );
        $this->annualPercentageRate = $this->calculateAnnualPercentageRate($cashFlowValues);
        $this->installmentsQuantity = $installmentsQuantity;
        $this->installmentAmount = $installmentAmount;
        $this->cashOutFeeAmount = $cashOutFeeAmount;
        $this->feeTotalAmount = $cashOutFeeAmount;
        $this->addInstallments($option, $installmentsQuantity);
        $this->id = $planId;
    }

    protected function convertToArray() : array
    {
        foreach ($this->installments as $installment) {
            $installments[] = $installment->convertToArray();
        }

        return [
            'plan_id' => $this->id,
            'installmentsQuantity' => $this->installmentsQuantity,
            'original_amount' => $this->principalAmount,
            'amount' => $this->principalAmount,
            'final_amount' => $this->repaymentAmount,
            'days' => $this->termTotalDays,
            'payment' => $this->installmentAmount,
            'interestRate' => $this->grossInterestRate,
            'cft' => $this->totalFinanceCharge,
            'apr' => $this->annualPercentageRate,
            'cashout_fee' => $this->cashOutFeeAmount,
            'cashin_fee' => $this->cashInFeeAmount,
            'administrative_fee' => $this->feeTotalAmount,
            'interest_amount' => $this->grossInterestAmount,
            'tax_amount' => $this->interestTaxesAmount,
            'insurance_amount' => $this->insuranceAmount,
            'installmentsOptions' => !empty($installments) ? $installments : []
        ];
    }

    public function getPrincipalAmount(): float
    {
        return $this->principalAmount;
    }

    public function getNegativePrincipalWithFeesAmount() : float
    {
        return $this->negativePrincipalWithFeesAmount;
    }

    public function getRepaymentAmount(): float
    {
        return $this->repaymentAmount;
    }

    public function getLastDueDate() : Carbon
    {
        return $this->lastDueDate;
    }

    public function getTermTotalDays() : int
    {
        return $this->termTotalDays;
    }

    public function getGrossInterestRate() : float
    {
        return round($this->grossInterestRate, $this->ratePrecision);
    }

    public function getNetInterestRate() : float
    {
        return round($this->netInterestRate, $this->ratePrecision);
    }

    public function getAnnualAmortizationRate() : float
    {
        return $this->annualAmortizationRate;
    }

    public function getTermAmortizationRate() : float
    {
        return $this->termAmortizationRate;
    }

    public function getTotalFinanceCharge() : float
    {
        return round($this->totalFinanceCharge, $this->ratePrecision);
    }

    public function getAnnualPercentageRate() : float
    {
        return round($this->annualPercentageRate, $this->ratePrecision);
    }

    public function getInstallmentsQuantity() : int
    {
        return $this->installmentsQuantity;
    }

    public function getInstallmentAmount() : float
    {
        return $this->installmentAmount;
    }

    public function getGrossInterestAmount() : float
    {
        return $this->grossInterestAmount;
    }

    public function getInterestTaxesAmount() : float
    {
        return $this->interestTaxesAmount;
    }

    public function getInsuranceAmount() : float
    {
        return $this->insuranceAmount;
    }

    public function getCashOutFeeAmount() : float
    {
        return $this->cashOutFeeAmount;
    }

    public function getCashInFeeAmount() : float
    {
        return $this->cashInFeeAmount;
    }

    public function getFeeTotalAmount() : float
    {
        return round($this->feeTotalAmount, $this->integerAmountPrecision);
    }

    public function getInstallments() : Collection
    {
        return $this->installments;
    }

    public function getId() : ?int
    {
        return $this->id;
    }

    protected function createInstallment(LoanOption $option, int $installmentNumber) : void
    {
        $this->installments->push(new LoanOptionPlanInstallment($option, $this, $installmentNumber));
    }

    private function addInstallments(LoanOption $option, int $installmentsQuantity)
    {
        for (
            $installmentNumber = CreditStructureConstants::FIRST_INSTALLMENT_NUMBER;
            $installmentNumber <= $installmentsQuantity; $installmentNumber++
        ) {
            $this->createInstallment($option, $installmentNumber);
            $this->sumInstallmentComponents($this->installments->last());
        }
    }

    private function sumInstallmentComponents(ArLoanOptionPlanInstallment $installment)
    {
        $cashInFeeAmount = $installment->getCashInFeeAmount();

        $this->grossInterestAmount += $installment->getGrossInterestAmount();
        $this->interestTaxesAmount += $installment->getInterestTaxesAmount();
        $this->insuranceAmount += $installment->getInsuranceAmount();
        $this->cashInFeeAmount += $cashInFeeAmount;
        $this->feeTotalAmount += $cashInFeeAmount;
    }
}
  • 派生类 *
<?php

namespace We\Domain\Loans;

use Carbon\Carbon;
use Illuminate\Support\Collection;

use We\Domain\Constants\CreditStructureConstants;
use We\Domain\Constants\RoundingPrecisionConstants;

class UyLoanOptionPlan extends LoanOptionPlan
{
    private $indexedUnitExchangeRate;

    public function __construct(
        LoanOption $option, int $installmentsQuantity, float $indexedUnitExchangeRate, ?int $planId = NULL
    )
    {
        $this->indexedUnitExchangeRate = $indexedUnitExchangeRate;
        parent::__construct($option, $installmentsQuantity, $planId);
    }

    protected function createInstallment(LoanOption $option, int $installmentNumber) : void
    {
        $this->installments->push(new UyLoanOptionPlanInstallment($option, $this, $installmentNumber));
    }
}

我想知道是否有一种方法可以重新实现或更改主类中调用的“calculateCashoutFeeAmount”和“calculateInstallmentAmount”函数引用,并使用相应的“子”实现(将具有不同的签名),而无需复制构造函数逻辑。计算函数是从trait中使用的,所以派生类将在内部使用不同的trait。
我不知道我的设计是否可以接受,我已经想了一整天了,但我还没有得出一个结论。一个来自“父”类的抽象方法可能是一种可能性,但是我不能从那个类创建示例。我面临的另一个问题是,许多计算相互依赖(Matroska风格),因此可能会发生鸡生蛋还是蛋生蛋的情况。
我应该改变设计,还是可以在不重复代码的情况下解决?
任何建议或意见将不胜感激。

qnakjoqk

qnakjoqk1#

你可以通过使用接口或抽象类,或者使用依赖注入来解决这个问题。在这个场景中,LoanOptionPlan类和UyLoanOptionPlan类之间似乎存在关系。但是,当像calculateCashoutFeeAmountcalculateInstallmentAmount这样的方法的签名发生变化时,这种关系就会破裂。
此类包含太多属性,无法理解。我认为这是一个更大的问题。要管理类中过多的属性,您可以使用DTO对数据进行分组,将相关属性模块化为子类,并通过使用Facade设计模式隐藏不必要的属性来降低复杂性。
我可以根据你的评论给予一个例子,但你应该根据你的工作来更广泛地思考。这个例子可能给予不了你一个解决方案,我只是根据你的代码思考。首先,您可以创建一个接口来封装计算函数。这个接口将作为实现它的类必须遵守的契约。举例来说:

interface LoanCalculatorInterface
{
    public function calculateCashoutFeeAmount(LoanOption $option, int $termTotalDays, int $installmentsQuantity, float $principalAmount): float;
    public function calculateInstallmentAmount(float $termAmortizationRate, int $installmentsQuantity, float $negativePrincipalWithFeesAmount): float;
    // Other calculation methods...
}

接下来,创建一个名为LoanCalculator的类来实现这个接口:

class LoanCalculator implements LoanCalculatorInterface
{
    public function calculateCashoutFeeAmount(LoanOption $option, int $termTotalDays, int $installmentsQuantity, float $principalAmount): float
    {
        // Cashout fee calculation logic
        // ...
    }

    public function calculateInstallmentAmount(float $termAmortizationRate, int $installmentsQuantity, float $negativePrincipalWithFeesAmount): float
    {
        // Installment amount calculation logic
        // ...
    }
}

现在,在LoanOptionPlan类中,不再使用这些计算函数作为属性,而是将它们作为服务注入。这可以通过依赖注入来实现。举例来说:

class LoanOptionPlan
{
    private $loanCalculator;

    public function __construct(
        LoanOption $option, 
        int $installmentsQuantity, 
        LoanCalculatorInterface $loanCalculator, 
        ?int $planId = NULL
    ) {
        $cashOutFeeAmount = $loanCalculator->calculateCashoutFeeAmount($option, $this->termTotalDays, $installmentsQuantity, $principalAmount);
        $installmentAmount = $loanCalculator->calculateInstallmentAmount($this->termAmortizationRate, $installmentsQuantity, $negativePrincipalWithFeesAmount);
    }
}

UyLoanOptionPlan中,适当地实现calculateCashoutFeeAmountcalculateInstallmentAmount方法:

class UyLoanCalculator implements LoanCalculatorInterface
{
    public function calculateCashoutFeeAmount(LoanOption $option, int $termTotalDays, int $installmentsQuantity, float $principalAmount): float
    {
        // Specific cashout fee calculation logic for UyLoanPlan
        // ...
    }

    public function calculateInstallmentAmount(float $termAmortizationRate, int $installmentsQuantity, float $negativePrincipalWithFeesAmount): float
    {
        // Specific installment amount calculation logic for UyLoanPlan
        // ...
    }
}

这样,当你想改变计算函数的逻辑时,你只需要创建一个实现LoanCalculatorInterface的新类。这种方法使您的代码更加灵活、可读和易于维护。

相关问题