Composition vs Inheritance

fixxxer

К.О.
Партнер клуба
Угу. То есть имеем классический пример из классических аргументов в пользу composition over inheritance. Ничего интересного. Надо бы почистить тред.
 

WMix

герр M:)ller
Партнер клуба
в чем разница между
есть прикольная библиотека, там все функции которые нужны, еще 5 ненужных и одной не хватает, взял унаследовал, нехватающую дописал, лишнее не учел в интерфейсе,
PHP:
// прикольная библиотека
class A{
  public function b(){} //  (одна) ненужных
// все функции которые нужны
  public function c(){}
  public function d(){}
}

interface D{
  public function c();
  public function d();
  public function x();// и одной не хватает,
}

// решение наследованием
class B extends A implements D{
  public function x(){
     return $this->a($this->b()) +$this->c();
  }
}


// решение композицией
class B implements D{
// вода или шум, хорошо что без комментов, иначе строк 70 ненужного кода
  private $a;

  public function __construct(A $a){
    $this->a = $a;
  }
  public function c(){
     return $this->a->c();
  }
  public function d(){
     return $this->a->d();
  }

// код
  public function x(){
     return $this->a($this->b()) +$this->c();
  }
}
 

WMix

герр M:)ller
Партнер клуба
@fixxxer, ответь теперь ты, в чем разница, если работаешь все одно с типом interface
 

fixxxer

К.О.
Партнер клуба
Если я отнаследовался, то возникает соблазн использовать код, поддерживающий контракт A. Например, стороннюю библиотеку (в примере с тем же PDO такой соблазн вполне реален). LSP говорит нам, что так сделать можно.

PHP:
class ThirdPartyLibrary {
    public function foo(A $a) {..};
}

class MyLibrary {
    public function bar(D $d) {..}
}

$d = new B(); // необязательно напрямую, пусть внутри DI, это ничего не меняет
$thirdPartyLibrary->foo($d);
$myLibrary->bar($d);
класс B можно легко заменить
И вот уже не так-то легко.

Допустим, ты лично никогда не совершишь такую ошибку, но ты можешь сказать за всех, кто работает с кодом?

В PHP так сделать вообще элементарно, он даже не пикнет. В других языках появится instanceof или кастинг, будет чуть заметнее, но тем не менее, возможно. И это может быть не так явно, как в моем примере, а в цепочке из 20 вложенных вызовов.
 
Последнее редактирование:

WMix

герр M:)ller
Партнер клуба
@fixxxer, и в твоем случае могут instanceof ThirdPartyLibrary создать и унаследовать и и и, с дури можно и Ъ поломать..
но мы же review делаем
 

fixxxer

К.О.
Партнер клуба
Запрещать пользоваться LSP - не особо логично, но, допустим.

Ок, еще варианты:
1) тебя не устраивает контракт метода c() и ты хочешь его поменять
2) ты обновил библиотеку и в новой версии появился метод x()
3) ...

Есть миллион аргументов против, и только один "за" - лень написать несколько forwarding methods. Причем, это решается фичей "Generate delegation methods" в IDE. IDEA для Java так делать умеет, в PhpStorm... ну, надо сделать фичреквест.
 

флоппик

promotor fidei
Команда форума
Партнер клуба
Наследование ок, когда наследуешься от абстрактного класса, при этом ничего не добавляешь к фактическому публичному интерфейсу класса, а только реализуешь абстрактные методы.
Во всех остальных случаях так или иначе возникнет проблема с LSP и/или DIP.
Даже говоря про ПХП, когда это было удобно для какого-то общего бойлерплейт кода, но сейчас есть хотя бы трейты.
 

fixxxer

К.О.
Партнер клуба
Даже говоря про ПХП, когда это было удобно для какого-то общего бойлерплейт кода, но сейчас есть хотя бы трейты.
Я, кстати, считаю трейты (в таком виде, в каком они реализованы в PHP) ошибкой дизайна. Было бы намного лучше, если бы реализовали Java 8 default methods в интерфейсах (кажется, все разумные случаи использования трейтов сводятся к некоей дефолтной реализации определенного интерфейса).

При этом вариант с трейтами более проблемный, чем с default methods. Возьмем вот этот пример Java-кода из документации по ссылке выше:

PHP:
import java.time.*;

public interface TimeClient {
    void setTime(int hour, int minute, int second);
    void setDate(int day, int month, int year);
    void setDateAndTime(int day, int month, int year,
                               int hour, int minute, int second);
    LocalDateTime getLocalDateTime();
    
    static ZoneId getZoneId (String zoneString) {
        try {
            return ZoneId.of(zoneString);
        } catch (DateTimeException e) {
            System.err.println("Invalid time zone: " + zoneString +
                "; using default time zone instead.");
            return ZoneId.systemDefault();
        }
    }
        
    default ZonedDateTime getZonedDateTime(String zoneString) {
        return ZonedDateTime.of(getLocalDateTime(), getZoneId(zoneString));
    }
}
Если "портировать" это код на PHP с выносом дефолтных методов в trait, получается какая-то фигня: трейт потребует наличия метода getLocalDateTime из интерфейса TimeClient, и нет никакой возможности указать, что трейт можно использовать только в контексте класса, реализующего TimeClient. Да, можно добавить в trait абстрактный метод, чтобы "не было красненького", но никакой информации о том, что трейт предназначен для реализации интерфейса TimeClient, нет, и в случае рефакторинга трейты придется приводить в соответствующее измененному интерфейсу состояние вручную.
 

fixxxer

К.О.
Партнер клуба
@Adelf, проще, конечно. Пример с TimeClient-ом вообще неудачный в этом смысле.

И default methods, и трейты предназначены, по сути, для реализации множественного наследования.

Если исходить из позиции, что множественное наследование никогда не нужно, то нафиг не нужны ни дефолтные методы, ни трейты.
А если считать, что иногда оно все же нужно, тогда, внезапно, достаточно интерфейсов с default methods, и не нужны ни абстрактные классы, ни наследование. :)
 

Adelf

Administrator
Команда форума
Мне кажется будет крайне сложно найти такой пример где нужен будет именно интерфейс с дефолт методами(вместо абстрактного класса). Эта всеобщая любовь к интерфейсам(относительно абстрактных классов) немного туманит рассуждения.
 

fixxxer

К.О.
Партнер клуба
Мне кажется будет крайне сложно найти такой пример где нужен будет именно интерфейс с дефолт методами(вместо абстрактного класса). Эта всеобщая любовь к интерфейсам(относительно абстрактных классов) немного туманит рассуждения.
Легко, если рассматривать какие-нибудь базово-инфраструктурные штуки типа Collection и их "расширения" типа Countable/Iterable, которые хочется уметь комбинировать произвольно.

У меня и трейты только с такой целью получались.

Еще есть как бы вариант с трейтами типа Illuminate\Notifications\Notifiable, но тут сразу возникает вопрос, откуда трейт возьмет свои зависимости. Откуда зависимости берутся внутри Laravel-овского трейта, я знаю, даже не заглядывая в код, и потому сразу нафиг. :) В тех редких случаях, когда никаких зависимостей нет (скажем, трейт для сбора и отдачи наружу списка domain events) - ну, да, ок, но и в этом случае интерфейс вида -able тоже явно уместен, и с default methods как-то аккуратнее.
 
Последнее редактирование:
Сверху