Reputation: 15464
I want to separate data from source of the data. One class for database interaction and class for data manipulation. But my approach violates LSP: preconditions cannot be strengthened in a subtype
and raises strict error: Declaration of DataRepositoryItem::save() should be compatible with DataRepositoryAbstract::save(DataAbstract $data)
class DataAbstract {
}
class DataItem extends DataAbstract {
}
class DataObject extends DataAbstract {
}
abstract class DataRepositoryAbstract {
/** @return DataAbstract */
public function loadOne(){}
/** @return DataAbstract[] */
public function loadAll(){}
public function save(DataAbstract $data){}
}
class DataRepositoryItem extends DataRepositoryAbstract {
/** @return DataItem */
public function loadOne(){}
/** @return DataItem[] */
public function loadAll(){}
public function save(DataItem $data) {} // <--- violates LSP, how to avoid it?
}
class DataRepositoryObject extends DataRepositoryAbstract {
/** @return DataObject */
public function loadOne(){}
/** @return DataObject[] */
public function loadAll(){}
public function save(DataObject $data) {} // <--- violates LSP, how to avoid it?
}
How to recombine the code to fit LSP?
Update: Ok, I could rewrite methods.
class DataRepositoryItem extends DataRepositoryAbstract {
/** @return DataItem */
public function loadOne(){}
/** @return DataItem[] */
public function loadAll(){}
public function save(DataAbstract $data) {
assert($date instanceof DataItem);
//...
}
}
Works in PHP, but still violates LSP. How to avoid it?
Upvotes: 9
Views: 504
Reputation: 43728
If your language would support generics then the problem would be quite simple to solve:
interface Repository<T> {
public function save(T $data): void;
}
class DataItemRepository implements Repository<DataItem> {...}
If you don't have generics then you could simply avoid trying to have a generic repository in the first place, which does more harm than good. Is there really any client code that should depend on DataRepositoryAbstract
rather than a concrete repository class? If not then why forcing a useless abstraction in the design?
interface DataItemRepository {
public function loadOne(): DataItem;
public function loadAll(): array;
public function save(DataItem $dataItem): void;
}
class SqlDataItemRepository implements DataItemRepository {
...
}
interface OtherRepository {
public function loadOne(): Other;
public function loadAll(): array;
public function save(Other $other): void;
}
Now, if somehow all save
operations can be handled in a generic way you could still implement a RepositoryBase
class which is extended by all repositories without violating the LSP.
abstract class RepositoryBase {
protected function genericSave(DataAbstract $data): void { ... }
}
class SqlDataItemRepository extends RepositoryBase implements DataItemRepository {
public function save(DataItem $item): void {
$this->genericSave($item);
}
}
However at that point you should probably use composition over inheritance by having your repositories collaborate with a GenericRepository
instance:
class GenericRepository implements DataItemRepository {
public function save(DataAbstract $data): void {...}
}
class SqlDataItemRepository {
private GenericRepository $repository;
public function save(DataItem $item): void {
$this->repository->save(item);
}
}
Upvotes: 7
Reputation: 6456
In any event, your inheritance hierarchy violates LSP principle, because save method and its use depends on a concrete class from an incoming object. Even if you remove a type assertion in the save method then you will not be able to use the child class DataRepositoryItem instead of parent class DataRepositoryAbstract because saving a DataItem entity differs from saving a DataAbstact entity. Let's imagine the following case of using DataRepositoryItem instead of DataRepositoryAbstract:
$repository = new DataRepositoryItem();
$entity = new DataAbstract()
// It causes incorrect behavior in DataRepositoryItem
$repository->save($entity);
We can conclude: There is no point in declaring a save method in DataRepositoryAbstract. Save method should be declared only in the concrete repository class.
abstract class DataRepositoryAbstract
{
/**
* @return DataAbstract
*/
public function loadOne(){}
/**
* @return DataAbstract[]
*/
public function loadAll(){}
}
class DataRepositoryItem extends DataRepositoryAbstract
{
/**
* @return DataItem
*/
public function loadOne(){}
/**
* @return DataItem[]
*/
public function loadAll(){}
/**
* @param DataItem
*/
public function save(DataItem $data) {}
}
class DataRepositoryObject extends DataRepositoryAbstract
{
/**
* @return DataObject
*/
public function loadOne(){}
/**
* @return DataObject[]
*/
public function loadAll(){}
/**
* @param DataObject
*/
public function save(DataObject $data) {}
}
This inheritance hierarchy provides an ability to read data from DataRepositoryObject and DataRepositoryItem the same as from DataRepositoryAbstract.
But let me ask: Where and how do you use DataRepositoryAbstract class? I sure you use it to ensure contact between concrete repository class and another code. It means that your DataRepositoryAbstract class doesn't implement any functionality, that isn't used functionally and it is a pure interface. If my assumption is valid then you should use an interface instead of an abstract class
Interfaces:
interface BaseDataRepositoryInterface
{
/**
* @return DataAbstract
*/
public function loadOne();
/**
* @return DataAbstract[]
*/
public function loadAll();
}
interface DataRepositoryItemInterface extends BaseDataRepositoryInterface
{
/**
* @return DataItem
*/
public function loadOne();
/**
* @return DataItem[]
*/
public function loadAll();
/**
* @param DataItem $data
*/
public function save(DataItem $data);
}
interface DataRepositoryObjectInterface extends BaseDataRepositoryInterface
{
/**
* @return DataObject
*/
public function loadOne();
/**
* @return DataObject[]
*/
public function loadAll();
/**
* @param DataObject $data
*/
public function save(DataObject $data);
}
Concrete implementation:
class DataRepositoryItem implements DataRepositoryItemInterface
{
public function loadOne()
{
//...
}
public function loadAll()
{
//...
}
public function save(DataItem $data)
{
//...
}
}
Upvotes: 3