避免违反LSP

sec*_*tus 9 php oop inheritance liskov-substitution-principle solid-principles

我想将数据与数据源分开.一个用于数据库交互的类和用于数据操作的类.但是我的方法违反了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?
}
Run Code Online (Sandbox Code Playgroud)

如何重新组合代码以适应LSP?

更新:好的,我可以重写方法.

class DataRepositoryItem extends DataRepositoryAbstract {
    /** @return DataItem */
    public function loadOne(){}
    /** @return DataItem[] */
    public function loadAll(){}
    public function save(DataAbstract $data) {
        assert($date instanceof DataItem);
        //...
    }               
}
Run Code Online (Sandbox Code Playgroud)

在PHP中工作,但仍违反LSP.怎么避免呢?

pla*_*alx 6

如果您的语言支持泛型,则问题解决起来非常简单:

public interface Repository<T> {
    public void save(T data);
}

public class DataItemRepository implements Repository<DataItem> {...}
Run Code Online (Sandbox Code Playgroud)

如果你没有泛型,那么你可以简单地避免尝试拥有一个通用的存储库,这弊大于利.是否真的有任何客户端代码应该依赖于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);
}
Run Code Online (Sandbox Code Playgroud)

现在,如果以某种方式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);
    }
}
Run Code Online (Sandbox Code Playgroud)

但是,此时您应该通过让存储库与GenericRepository实例协作来使用组合而不是继承:

public void save(DataItem item) {
    genericRepository.save(item);
}
Run Code Online (Sandbox Code Playgroud)

PS:请注意,这些代码都不是真正的PHP代码.我不是PHP程序员,也没有查找语法,但你应该弄明白.

  • "真的有任何客户端代码应该依赖于DataRepositoryAbstract而不是具体的存储库类吗?如果没有,那么为什么在设计中强制使用无用的抽象?" 这是关键点,对此赞不绝口!我想动机是类似的代码应该有一个共同的基类.抵制诱惑! (3认同)

Max*_*rov 3

无论如何,您的\xe2\x80\x82继承层次结构违反了LSP原则,因为save方法及其使用取决于传入对象的具体类。即使您在 save 方法中删除类型断言,您也将无法使用子类 DataRepositoryItem 而不是父类 DataRepositoryAbstract,因为保存 DataItem 实体与保存 DataAbstact 实体不同。让我们想象一下使用 DataRepositoryItem 而不是 DataRepositoryAbstract 的以下情况:

\n\n
$repository = new DataRepositoryItem();\n$entity = new DataAbstract()\n// It causes incorrect behavior in DataRepositoryItem\n$repository->save($entity);\n
Run Code Online (Sandbox Code Playgroud)\n\n

我们可以得出结论:在 DataRepositoryAbstract 中声明 save 方法是没有意义的。Save 方法只能在具体的存储库类中声明。

\n\n
abstract class DataRepositoryAbstract \n{\n    /** \n     * @return DataAbstract \n     */\n    public function loadOne(){}\n\n    /** \n     * @return DataAbstract[] \n     */\n    public function loadAll(){}                              \n}\n\nclass DataRepositoryItem extends DataRepositoryAbstract \n{\n    /** \n     * @return DataItem \n     */\n    public function loadOne(){}\n\n    /** \n     * @return DataItem[] \n     */\n    public function loadAll(){}\n\n    /** \n     * @param DataItem\n     */\n    public function save(DataItem $data) {}\n}\n\nclass DataRepositoryObject extends DataRepositoryAbstract \n{\n    /** \n     * @return DataObject \n     */\n    public function loadOne(){}\n\n    /** \n     * @return DataObject[] \n     */\n    public function loadAll(){}\n\n    /** \n     * @param DataObject\n     */\n    public function save(DataObject $data) {}\n}\n
Run Code Online (Sandbox Code Playgroud)\n\n

此继承层次结构提供了从 DataRepositoryObject 和 DataRepositoryItem 读取数据的能力,就像从 DataRepositoryAbstract 读取数据一样。

\n\n

但让我问:在哪里以及如何使用 DataRepositoryAbstract 类?我确信您使用它来确保具体存储库类和另一个代码之间的联系。这意味着您的 DataRepositoryAbstract 类没有实现任何功能,没有在功能上使用,它是一个纯接口。如果我的假设是正确的,那么你应该使用接口而不是抽象类

\n\n

接口:

\n\n
interface BaseDataRepositoryInterface\n{        \n    /** \n     * @return DataAbstract \n     */\n    public function loadOne();\n\n    /** \n     * @return DataAbstract[] \n     */\n    public function loadAll();      \n}\n\ninterface DataRepositoryItemInterface extends BaseDataRepositoryInterface\n{\n    /** \n     * @return DataItem \n     */\n    public function loadOne();\n\n    /** \n     * @return DataItem[] \n     */\n    public function loadAll(); \n\n    /** \n     * @param DataItem $data \n     */\n    public function save(DataItem $data);\n}\n\ninterface DataRepositoryObjectInterface extends BaseDataRepositoryInterface\n{\n    /** \n     * @return DataObject \n     */\n    public function loadOne();\n\n    /** \n     * @return DataObject[] \n     */\n    public function loadAll(); \n\n    /** \n     * @param DataObject $data \n     */\n    public function save(DataObject $data);\n}\n
Run Code Online (Sandbox Code Playgroud)\n\n

具体实现:

\n\n
class DataRepositoryItem implements DataRepositoryItemInterface \n{       \n    public function loadOne()\n    {\n    //...       \n    }\n\n    public function loadAll()\n    {\n    //...\n    }\n\n    public function save(DataItem $data)\n    {\n    //...\n    }\n}\n
Run Code Online (Sandbox Code Playgroud)\n