Принцип подстановки Лискоу

stalxed

Новичок
Не могу понять детали этого принципа.
Вкратце его описание таково: если клиент работает с интерфейсом базового класса, и ему "подсунуть" дочерний, он должен работать и с ним. Вольная трактовка конечно.

Но вот простой теоретический пример:
PHP:
class Record_Abstract
{}
class Record_One extends Record_Abstract
{}

class Collection_Abstract
{
    /**
     * Add record to collection.
     *
     * @param Record_Abstract $record
     * @return Record_Abstract
     */
    public function add(Record_Abstract $record)
    {
         //some code...

         return $record;
    }
}

class Collection_One extends Collection_Abstract
{
    /**
     * Add record to collection.
     *
     * @param Record_One $record
     * @return Record_Abstract
     * @throws Exception
     */
    public function add(Record_Abstract $record)
    {
        if ( ! ($record instanceof Record_One))
            throw new Exception();

         return parent::add($record);
    }
}
Будет ли это нарушением принципа подстановки Лискоу?
Ведь базовый класс коллекции:
1) не выдаёт эксепшенов, а дочерний выдаёт.
2) базовый класс добавляет в коллекцию любую запись, а дочерний только 1 типа.
 

stalxed

Новичок
В книге agile Роберта Мартина описывается немного похожий случай.
Лишь с одним отличием - записи коллекции совершенно разные типы, не имеющие общих предков.
Автор приходит к решению, что в базовом классе метод add лучше удалить, т.е. у базового класса в интерфейсе не будет метода add. Т.е. он опустил метод add вниз по иерархии наследования.
И дальше про принцип Лискоу сказал, что в методах дочерних классов не должно быть эксепшенов, которые не ожидает клиент базового класса.

Т.е. код выше - не верный или я запутался?
 

Adelf

Administrator
Команда форума
st@l][ED
Ну вообщето твой метод следует переписать так:
PHP:
    public function add(Record_One $record)
    {
         return parent::add($record);
    }
И как ты понимаешь, теперь функция add - совсем другая. Поэтому тут есть нарушение. Правда в динамических языках.. этот принцип чуть теряет в важности. Имхо.
 

stalxed

Новичок
Adelf я почему-то думал, что такой код вообще не будет работать, т.к. среда PhpUnit выдаёт:
<br />
<b>Notice</b>: Undefined variable: trace in <b>C:\zend\wamp\projects\public_html\libs\Data\CollectionWrite\Info.class.php</b> on line <b>29</b><br />
<br />
<b>Fatal error</b>: Uncaught exception 'PHPUnit_Framework_Error_Notice' with message 'Declaration of Data_CollectionWrite_Info::add() should be compatible with that of Data_CollectionWrite_Abstract::add()' in C:\zend\wamp\projects\public_html\libs\Data\CollectionWrite\Info.class.php:29
Stack trace:
Это на интерпретаторе php среды ZendStudio.
На моём локальном пхп - нет такой ошибки.
Видать от версии php зависит, врятли PhpUnit сам выполняет проверку...

Так что кодом выше:
PHP:
    public function add(Record_One $record)
    {
         return parent::add($record);
    }
Лучше вообще не пользоваться...

Получается лучше, как и описал в книге Роберт Мартин, перенести add вверх по иерархии?
Например так:
PHP:
<?php

class Record_Abstract
{}
class Record_One extends Record_Abstract
{}

abstract class Collection_Abstract
{
    /**
     * Add record to collection.
     *
     * @param Record_Abstract $record
     * @return Record_Abstract
     */
    protected function doAdd(Record_Abstract $record)
    {
         //some code...

         return $record;
    }
}

class Collection_One extends Collection_Abstract
{
    /**
     * Add record to collection.
     *
     * @param Record_One $record
     * @return Record_Abstract
     * @throws Exception
     */
    public function add(Record_One $record)
    {
         return $this->doAdd($record);
    }
}
?>
Но тогда базовый класс в своём интерфейсе лишён способа добавления записи в коллекцию.
Т.е. клиент когда выбирает коллекцию - смотрит на базовый класс - нет способа добавления вообще.
Смотрит дочерние, а там бац - вроде как одинаковые методы add, но с разными аргументами...

Хотя в чём то есть верность данного способа. Клиент видит сразу, что в разные коллекции можно добавлять только определённые записи.
А оперировать коллекциями можно одинаковыми способами(описанными в интерфейсе базового класса).

Да! Точно! Так что принцип, имхо, не теряет актуальности и в динамических языках.

Adelf спасибо за пищу к размышлению! У кого есть ещё какие мысли по данному вопросу - просьба описать, т.к. в своих мыслях у меня нет 100% уверенности...
 

AmdY

Пью пиво
Команда форума
st@l][ED
у вас несовместимость на уровне интерфейсов.
add(Record_Abstract $record)
add(Record_One $record) - здесь должен оставать Record_Abstract
на самом деле должна быть лишь проверка этого самого интерфейса или абстрактного класса, а не конкретной реализации
 

stalxed

Новичок
Не получится. Если о моей задачи.
У меня есть базовая коллекция Collection_Abstract. Она может делать всё, кроме индексирования. Оно доступно только конкретным коллекциям. Индексирование зависит от деталей самих записей.
Я пока пришёл к решению в последнем посте.

Т.е. вроде такого:
PHP:
abstract class Collection_Abstract
{
    /**
     * Add record to collection.
     *
     * @param Record_Abstract $record
     * @return Record_Abstract
     */
    protected function doAdd(Record_Abstract $record)
    {
         //some code...

         return $record;
    }
}

class Collection_OneWithIndex extends Collection_Abstract
{
    /**
     * Add record to collection.
     *
     * @param Record_One $record
     * @return Record_One
     * @throws Exception
     */
    public function add(Record_One $record)
    {
         return $this->doAdd($record);
    }
}

class Collection_WithoutIndex extends Collection_Abstract
{
    /**
     * Add record to collection.
     *
     * @param Record_Abstract $record
     * @return Record_Abstract
     * @throws Exception
     */
    public function add(Record_Abstract $record)
    {
         return $this->doAdd($record);
    }
}
Т.е. коллекция Collection_WithoutIndex может работать с любыми записями, но в ней нет функции индексирования.
Но, как можно заметить, add нет в интерфейсе базового класса.
Так как, я пришёл к выводу, что add лучше сместить по иерархии вниз.
Весь вопрос в том - правильно ли это. Или моё решение всё же имеет изъяны. Хотя вроде сейчас нет нарушения принципа Лискоу.

Andy я заметил ваше начальное сообщение) Вы сделали неравенство в виде
Record_Abstract != Record_Interface
Грустно от профи этого форума видеть такое...
 

AmdY

Пью пиво
Команда форума
это проблема полифоризма php, нельзя у нас сделать два метода, которые бы вызывались в зависимости от переданных параметров.

сделал вроде хорошо. только @return у тебя нехороший получился, передаёшь Record_One а возвращается Record_Abstract, поправь.

честно не помню, что писал, но как запостил понял, что фигня, и поправил срузу же, пока никто не ответил. но вот претензии по поводу Record_Abstract != Record_Interface не понял. ясное дело что они не равны. тем более у интерфейсов есть возможность множественной имплементации, которая в вашем случае бы не помешала.
 

stalxed

Новичок
это проблема полифоризма php, нельзя у нас сделать два метода, которые бы вызывались в зависимости от переданных параметров.
В данном случае речь идёт не об этом. Ибо даже если бы это было можно и было бы 2 метода:
-Collection_OneWithIndex::Add(Record_Abstract $record).
-Collection_OneWithIndex::Add(Record_One $record)
то метод Collection_OneWithIndex::Add(Record_Abstract $record) рушил бы инкапсуляцию дочернего класса Collection_OneWithIndex.
Сохраняем принцип подстановки Лискоу и нарушаем принцип инкапсуляции(так как через метод Collection_OneWithIndex::Add(Record_Abstract $record) мы могли бы добавлять записи, которые при индексации приведут к ошибкам) . Это тоже не дело.

сделал вроде хорошо. только @return у тебя нехороший получился, передаёшь Record_One а возвращается Record_Abstract, поправь.
Спасибо, на это внимания не обратил. Просто я этот пример(похожий на мою задачу) писал только для форума, без интерпретатора...

поводу Record_Abstract != Record_Interface не понял. ясное дело что они не равны..
Мы говорим про аргументы методов. В них указываются ТИПЫ. И в данном контексте class == abstract class == interface. Ведь их даже не отличить то никак(без соглашений в именование), если смотреть на объявления(прототипы) методов.

тем более у интерфейсов есть возможность множественной имплементации, которая в вашем случае бы не помешала.
Откуда такой вывод без знания контекста задачи?
 

AmdY

Пью пиво
Команда форума
у тебя получилась коллекция которой нельзя пользоваться не реализовав метод add, а его реализация не является обязательной.
логичнее смотрелся бы полностью рабочий абстрактный класс Collection_Abstract с реализованным методом add(Record_Abstract $record), а для индексируемой коллекции этот класc бы переопределялся и делалась проверка if ($record instanceof Record_Indexed)
 

stalxed

Новичок
у тебя получилась коллекция которой нельзя пользоваться не реализовав метод add, а его реализация не является обязательной.
Мне это тоже не нравится, но с другой стороны - я не вижу явно никаких нарушений законов ООП.

логичнее смотрелся бы полностью рабочий абстрактный класс Collection_Abstract с реализованным методом add(Record_Abstract $record), а для индексируемой коллекции этот класc бы переопределялся и делалась проверка if ($record instanceof Record_Indexed)
Я в начале так и сделал. Но мне это не понравилось. Собственно этому посвящён мой первый пост.
Насчёт логичнее не понятно. Чем логичнее? Принципом полиморфизма? Неа! Ведь хотя и будет единый интерфейс(у метода add), но он по сути разный интерфейс. Можно сказать у них будет разный интерфейс, только реализованный костылём вида:
PHP:
if ( ! ($record instanceof Record_One))
    throw new Exception();
 

AmdY

Пью пиво
Команда форума
у..... в предыдущем посте я таки хрень написал.
суть в том, что коллекция не должна замарачиваться о типах объектов, которые в неё кладут, она должна быть максимально автономной и глупой.
а саму проверку на тип должен делать тот, кто этот объект использует.
фактически у нас есть два набора интерфейсов - первый для коллекции, второй для работы с индексируемыми объектами. Первый о втором знать не должен.
PHP:
abstract Collection_Abstract implements Collection_Interface {}
class Collection_OneWithIndex extends Collection_Abstract  {
    public function doIndex(Collection_Interface $collection) { 
         foreach($collection AS $record) {
               if ($record instanceof Record_Indexed) {
                    ....
               }    
         }
    }
}
 

stalxed

Новичок
Хм, способ индексации получше, чем у меня сейчас. Занимаюсь рефакторингом, спасибо!

Но вот тупой коллекцию сделать не могу.
Дело в том, что у меня задача сложнее, чем я её описал здесь. Я опустил детали.
Мне нужно в текущем проекте использовать только файлы, без различных БД. И это неплохо, так как все действия простые, и сложных выборок не требуется.

Есть 3 параллельных иерархии классов, CollectionRead, CollectionWrite и Record.
CollectionWrite просто получает ряд записей(через метод add) и записывает их в файлы.
CollectionRead может считывать конкретные записи, по номеру, названию и т.д.

Но, к примеру, CollectionRead_One ожидает, что в файлах находятся только записи определённого вида, если будут записи другого вида - полетят эксепшены.

Поэтому CollectionWrite_OneWithIndex не должна быть "глупой", и добавлять в неё "вражеские" записи нельзя. А можно добавлять только записи определённого типа.
Поэтому ограничить добавление только определённых записей в CollectionWrite_OneWithIndex как-то нужно...
 

stalxed

Новичок
Ура! Я, как мне кажется, нашёл идеальное решение для этой проблемы.
Конечно, я идиот, убил добрую половину дня на раздумия, два часа не мог уснуть - настолько сильно хотелось решить эту задачу, и вдруг бац пришло в голову решение и следом эйфария, ей богу - если счастье не в решение подобных задач, то в чём же ещё?

Ошибка была на стыке бизнес логики и ООП принципов.
А именно, в условиях задачи ясно, что коллекции могут работать лишь с определёнными записями.
И решение - с кем работать - принимают конкретные реализации коллекций.
И если задачу перевести с рельс ООП(а она сводилась к поискам правильных интерфейсов) на рельсы бизнес логики, то задача проста.
В абстрактный класс добавляем абстрактный метод, который решает, можно ли добавлять запись в коллекцию. А конкретная реализация всего лишь реализует подобную проверку.
В коде всё становится ещё проще:
PHP:
class Record_Abstract
{}
class Record_One extends Record_Abstract
{}

abstract class Collection_Abstract
{
    /**
     * Add record to collection.
     *
     * @param Record_Abstract $record
     * @return Record_Abstract
     * @throws Exception
     */
    public function add(Record_Abstract $record)
    {
         if ( ! $this->isWorkWithRecord($record))
             throw new Exception();

         //some code...

         return $record;
    }

    protected abstract function isWorkWithRecord(RecordAbstract $record);
}

class Collection_One extends Collection_Abstract
{
     protected abstract function isWorkWithRecord(RecordAbstract $record);
    {
        return ($record instanceof Record_One);
    }
}
AmdY огромное спасибо за помощь и поддержу! :)
 

AmdY

Пью пиво
Команда форума
вот это правильно.

а, я нашёл, этот метод называется accept http://www.php.net/manual/en/filteriterator.accept.php
припоминаю что видел где-то реализацию с filterSet и filterGet где устанавливались фильтры, но найти не могу, возможно их поддержка есть уже в spl

вот эксепшины в таких ситуациях мне не нравятся, лучше просто не добавлять их в коллекцию и продолжать работать записав нотис в лог. как вариант установка уровня setErrorReporting( $level ), где левел константа класса ERROR_NONE, ERROR_NOTICE, ERROR_EXCEPTION
 

manyrus

Новичок
st@l][ED, вау, красивое решение, мне нравится :). У меня когда-то была такая же проблема, я решил её как Вы :).
 

manyrus

Новичок
если клиент работает с интерфейсом базового класса, и ему "подсунуть" дочерний, он должен работать и с ним.
- это жу вроде одна из концепций ооп - полиморфизм :)
Цитата:
Полиморфизм — это свойство системы использовать объекты с одинаковым интерфейсом без информации о типе и внутренней структуре объекта.
 

varan

Б̈́̈̽ͮͣ̈Л̩̲̮̻̤̹͓ДͦЖ̯̙̭̥̑͆А͇̠̱͓͇̾ͨД͙͈̰̳͈͛ͅ
городить лишний метод только потому что php не поддерживает оператор throws как в java?
 
Интересно, не нарушает ли преобразование кода из первого сообщения к актуальной версии принцип открытости / закрытости? На первый взгляд - нет. Мнения? :)
 

stalxed

Новичок
2NetFly по мне оба листинга не нарушают принцип открытия-закрытия.
Я ошибаюсь?
 
Сверху