Я хочу отделить данные от источника данных. Один класс для взаимодействия с базой данных и класс для манипулирования данными. Но мой подход нарушает LSP: preconditions cannot be strengthened in a subtype
и поднимает строгую ошибку: 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?
}
Как рекомбинировать код для соответствия LSP?
Обновить: Хорошо, я мог бы переписать методы.
class DataRepositoryItem extends DataRepositoryAbstract {
/** @return DataItem */
public function loadOne(){}
/** @return DataItem[] */
public function loadAll(){}
public function save(DataAbstract $data) {
assert($date instanceof DataItem);
//...
}
}
Работает на PHP, но все еще нарушает LSP. Как этого избежать?
Если ваш язык будет поддерживать дженерики, то проблему будет довольно просто решить:
public interface Repository<T> {
public void save(T data);
}
public class DataItemRepository implements Repository<DataItem> {...}
Если у вас нет дженериков, то вы можете просто не пытаться иметь общий репозиторий, который приносит больше вреда, чем пользы. Есть ли действительно какой-либо клиентский код, который должен зависеть от DataRepositoryAbstract
а не конкретный класс хранилища? Если нет, то зачем навязывать бесполезную абстракцию в дизайне?
public interface DataItemRepository {
public DataItem loadOne();
public DataItem[] loadAll();
public void save(DataItem dataItem);
}
public class SqlDataItemRepository implements DataItemRepository {
...
}
public interface OtherRepository {
public Other loadOne();
public Other[] loadAll();
public void save(Other other);
}
Теперь если как-то все save
операции могут быть обработаны общим способом, вы все еще могли бы реализовать RepositoryBase
класс, который распространяется на все репозитории без нарушения LSP.
public abstract class RepositoryBase {
protected genericSave(DataAbstract data) { ... }
}
public class SqlDataItemRepository extends RepositoryBase implements DataItemRepository {
public void save(DataItem item) {
genericSave(item);
}
}
Однако в этот момент вам, вероятно, следует использовать композицию вместо наследования, если ваши репозитории будут работать совместно с GenericRepository
пример:
public void save(DataItem item) {
genericRepository.save(item);
}
PS: Обратите внимание, что ни один из кодов не является реальным кодом PHP. Я не программист PHP и не изучал синтаксис, но вы должны это выяснить.
В любом случае ваша иерархия наследования нарушает принцип LSP, потому что метод save и его использование зависят от конкретного класса из входящего объекта. Даже если вы удалите утверждение типа в методе сохранения, вы не сможете использовать дочерний класс DataRepositoryItem вместо родительского класса DataRepositoryAbstract, поскольку сохранение сущности DataItem отличается от сохранения сущности DataAbstact. Давайте представим следующий случай использования DataRepositoryItem вместо DataRepositoryAbstract:
$repository = new DataRepositoryItem();
$entity = new DataAbstract()
// It causes incorrect behavior in DataRepositoryItem
$repository->save($entity);
Мы можем заключить: нет смысла объявлять метод сохранения в DataRepositoryAbstract. Метод save должен быть объявлен только в конкретном классе репозитория.
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) {}
}
Эта иерархия наследования обеспечивает возможность чтения данных из DataRepositoryObject и DataRepositoryItem так же, как из DataRepositoryAbstract.
Но позвольте мне спросить: где и как вы используете класс DataRepositoryAbstract? Я уверен, что вы используете его для обеспечения связи между конкретным классом хранилища и другим кодом. Это означает, что ваш класс DataRepositoryAbstract не реализует никакой функциональности, которая не используется функционально и является чистым интерфейсом. Если мое предположение верно, то вы должны использовать интерфейс вместо абстрактного класса
Интерфейсы:
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);
}
Конкретная реализация:
class DataRepositoryItem implements DataRepositoryItemInterface
{
public function loadOne()
{
//...
}
public function loadAll()
{
//...
}
public function save(DataItem $data)
{
//...
}
}