laraveldesign-patternsdependency-injectiondesign-principlesdependency-inversion

How should I use Factory method design pattern that follows Dependency Inversion Principle


I've created a command like this, but I would like to refactor it to follow the DI principle:

public function handle()
{
    $productChoice = $this->argument('product');

    if ($productChoice == 1) {
        $product = new ProductA();
    } elseif ($productChoice == 2) {
        $product = new ProductB();
    } else {
        $this->error('Invalid product choice. Use 1 for ProductA or 2 for ProductB.');
        return;
    }

    if ($product instanceof ProductI) {
        $this->info($product->details());
    } else {
        $this->error('The selected product does not implement the ProductI interface.');
    }
}

As you can see, the command is dependent on productA and productB. And my question is how to refactor it.

Here are some thoughts but I'm not sure which one is better or if there is a better way:

1- Create factory class like the following and inject it to the command class

class ProductFactory
{
    public function createProduct($productChoice)
    {
        switch ($productChoice) {
            case 1:
                return new ProductA();
            case 2:
                return new ProductB();
            default:
                return null;
        }
    }
}

2- Creating a Service provider like the following and use it in the command like $product = $this->app->make("Product{$productChoice}")

public function register()
{
    $this->app->bind('Product1', function () {
        return new ProductA();
    });

    $this->app->bind('Product2', function () {
        return new ProductB();
    });
}

Solution

  • What you need is a mapping from an integer to a class.

    In that case, I would prefer the factory you suggested to keep it clean and simple, but with some small modifications to the creation method:

    public static function createFromId(int $productId): ProductI 
    {
        return match ($productId) {
            1 => new ProductA(),
            2 => new ProductB(),
            default => throw new InvalidArgumentException('No product with this id'),
        };
    }
    
    • Static method, so you can call ProductFactory::createFromId($productChoice).
    • Renamed choice to id (since choice is no property of the product itself).
    • Typed hint with int.
    • Declared return type ProductI.
    • match statement instead of switch.
    • Do not allow null but throw an exception instead.

    You can also decide to return an EmptyProduct or DefaultProduct that implements ProductI when the id is invalid.

    As suggested by @matiaslauriti, you can also create a createFromProductEnum method in the factory when you are using PHP 8.1 or higher, like this:

    Enum ProductEnum: int 
    {
        case A = 1;
        case B = 2;
    }
    
    public static function createFromProductEnum(ProductEnum $product): ProductI 
    {
        return match ($product) {
            ProductEnum::A => new ProductA(),
            ProductEnum::B => new ProductB(),
        };
    }
    

    Then you can create the object with ProductFactory::createFromProductEnum(ProductEnum::from($productChoice)).