DesignPatternsPHP / DesignPatternsPHP

Sample code for several design patterns in PHP 8.x

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

AbstractFactory pattern breaks Liskov substitution principle

kopecny-shockworks opened this issue · comments

Both CsvParser and JsonParser implement Parser interface, but they are not substituable one for the other.
Imagine a client class:

class ParserClient
{
  private $parser;
  public function __construct(Parser $parser)
  {
    $this->parser = $parser;
    $this->fileName = __DIR__ . '/test.json'; //just imagine this file exists and contains valid json data
  }

  public function execute()
  {
    $contents = \file_get_contents($this->fileName);
    return $this->parser->parse($contents);
  }
}

Now the client should work no matter what parser it is passed, but the opposite is true:

$factory = new ParserFactory();
$c1 = new ParserClient($factory->createJsonParser());
$c2 = new ParserClient($factory->createCsvParser());
$c1->execute(); //ok
$c2->execute(); //error

That said, both CsvParser and JsonParser have a method with the same name and signature, but they really don't implement the same interface.
This is a common misconception.
If they really were to implement the same interface, then you could say that any class with a method accepting one argument of type string and returning an array, should name the method "parse" a implement the "Parser" interface. But here it should be even more obvious that it is wrong.

Better example would be to provide JsonParser interface, and have two implementations, one that wraps native PHP functions, and one that does the parsing fully alone without using php json_* functions.
Then they both actualy share an interface - the JsonParser interface, since they both can parse a valid json string without causing an error and cause an error in any other case.

Or maybe if the Parser interface exposed a method that would return an array of accepted formats, then both CsvParser and JsonParser could implement it and not break LSP, because the client would be expected to call such method first to check if their parser can handle their format.
The client the would look like this:


$format = $this->detectFormat($this->fileName); //maybe from file extension
if(in_array($format, $this->parser->getAllowedFormats())) {
     $contents = \file_get_contents($this->fileName);
     return $this->parser->parse($contents);
} else {
     return ['Could not handle file format ' . $format];
}

Now the client was well aware of the fact that the parser may not be able to parse data in the clients format and the client contains code to handle such situation. Substituing one parser for another now causes no unexpected behaviour.

But let me provide an example of AbstractFactory which IMHO is much more explanatory about the importance of AbstractFactory design pattern:

interface Shape
{
  public function getArea(): float
}

class Square implements Shape
{
  private $size;

  public function __construct(float $size)
  {
    $this->size = $size;
  }

  public function getArea(): float
  {
     return $this->size * $this->size;
  }
}

class Circle implements Shape
{
    private $radius;

    public function __construct(float $radius)
    {
      $this->radius = $radius;
    }

    public function getArea(): float
    {
        return $this->radius * $this->radius * pi();
    }
}

class UnitCircle implements Shape
{
  public function getArea(): float
  {
    return pi();
  }
}

class ShapeFactory
{
  public function createSquare(float $size): Square
  {
    return new Square($size);
  }

  public function createCircle(float $radius): Shape
  {
    if ($radius == 1) {
      return $this->createUnitCircle();
    }
    return new Circle($radius); 
  }

  public function createUnitCircle(): UnitCircle
  {
    return new UnitCircle(); 
  }
}

class ShapeClient
{
  private $shape;

  public function __construct(Shape $shape)
  {
     $this->shape = $shape;
  }

  public function execute()
  {
     echo "<div>" . $shape->getArea() . "</div>";
  }
}


$c1 = new ShapeClient($factory->createSquare(1.0));
$c2 = new ShapeClient($factory->createCircle(1.0)); //note that this may return Circle or UnitCircle, but the client won't care
$c1->execute(); //ok
$c2->execute(); //ok

The ShapeClient doesnt care what shape it is given, it will work for any Shape implementation, it just print's the shape's area in a div.

commented

Also, the boolean parameter -skipHeaderLine- should be removed from the createCsvParser method. It is not recommended.

https://martinfowler.com/bliki/FlagArgument.html

Thanks for all your in put. I changed the pattern to a more simple approach.