Как избежать нарушения LSP

Я хочу отделить данные от источника данных. Один класс для взаимодействия с базой данных и класс для манипулирования данными. Но мой подход нарушает 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. Как этого избежать?

10

Решение

Если ваш язык будет поддерживать дженерики, то проблему будет довольно просто решить:

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 и не изучал синтаксис, но вы должны это выяснить.

6

Другие решения

В любом случае ваша иерархия наследования нарушает принцип 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)
{
//...
}
}
4

По вопросам рекламы [email protected]