Sorry for the very late answer, but this is a scenario which is valid to PHP to this day (v5.6/7.1), and is confounding to a lot of programmers. I've done a lot of research on the topic of LSP in regards to contractual obligations and violations, as I am developing a framework that is fully SOLID compliant, and I can say there are a lot of varied opinions on the matter.
Given that PHP does not currently have DbC (Design by Contract), and at best has invariant type hinting for method parameters and return types, we have to consider the underlying sentiment of the Liskov Substitution Principle. While it is possible to enforce parameter and return types in both interfaces and abstract functions, this is just one tool to ensure that Liskov is not violated, but it does not cover DbC. As Robert Martin states in his article The Liskov Substitution Principle (bold/italics added by me):
The LSP makes clear that in OOD the ISA relationship pertains to
behavior. Not intrinsic private behavior, but extrinsic public
behavior; behavior that clients depend upon
Under DbC, the validity of a DataType
in relation to a consuming class is something that is conceptually identical across all concrete DataTypeHandlers
- if you pass a DataType
to a DataTypeHandler
, it is either valid or invalid. We'll revisit this later.
Now, to the human mind it seems logical to implement that rule as a typed parameter in the method, like so:
public function getWhereConditions(DataValue $dataValue) {...}
// Override in a child class...
public function getWhereConditions(NumberDataValue $dataValue) {...}
But this violates the LSP as it involves a covariance of method parameters (i.e. NumberDataValue is a more refined type of DataValue), and changes the extrinsic public behaviour.
If, instead of applying this rule as a (public) method parameter type, we instead abstract it out to a (private) validity check against the passed argument, then we are shifting the behaviour from "public" to "private". As stated by Robert Martin, this is not a violation of LSP. (Read the last few pages at the end of his paper to see an example using PersistentSets that don't have an ISA relationship to Sets, which backs up this claim).
Here's an example in PHP which demonstrates what I mean. Note that the abstract methods have invariant parameters (i.e. DataType), but allow for variance in private behaviour. Also, note that the getWhereConditions()
method is inherited by all concrete implementations, ensuring the public behaviour remains consistent (throw a DataTypeException
, or returns a WhereConditionResultInterface
):
abstract class DataHandler()
{
public function getWhereConditions(DataValue $dataValue)
{
if(!$this->isDataValid($dataValid) {
throw new DataTypeException('Invalid data given');
}
$this->processGetWhereConditions($dataValue);
}
abstract protected function isDataValid(DataValue $dataValue);
/** @return WhereConditionResultInterface */
abstract protected function processGetWhereConditions(DataValue $dataValue);
}
final class NumberDataHandler extends DataHandler
{
protected function isDataValid(DataValue $dataValue)
{
return is_a($dataValue, NumberDataValueInterface::class);
}
protected function processGetWhereConditions(DataValue $dataValue)
{
/** @var NumberDataValueInterface $dataValue - Type hinting for modern IDEs */
// Do stuff specific to NumberDataValueInterface here
}
}
final class GeoCoordinateDataHandler extends DataHandler
{
protected function isDataValid(DataValue $dataValue)
{
return is_a($dataValue, GeoCoordinateDataValueInterface::class);
}
protected function processGetWhereConditions(DataValue $dataValue)
{
/** @var GeoCoordinateDataValueInterface $dataValue - Type hinting for modern IDEs */
// Do stuff specific to GeoCoordinateDataValueInterface here
}
}
TypeA
andTypeB
of interfaceI
. For every call in the interfaceI
,TypeA
andTypeB
behave in accordance to LSP. Someone then creates a free function which takes anI
, tests whether its type isTypeA
, and if it is throws an exception. That doesn't effect LSP with respect toTypeA
orTypeB
- LSP applies to the types which vary, not the behaviour of other code. If instead of a free function, it is a method of another object which throws the exception, this does not alter the LSP status ofTypeA
orTypeB
...