Как стать автором
Обновить

Комментарии 45

ну хоть кто-то расписал это на несколько страниц более менее прилично, с адекватными аргументами. Благодарю.

Очень длинная статья. Но в итоговом виде, порядок я бы поменял, так как в случае публичного api не имеет смысла вообще выпускать final классы, без интерфейсов. Поэтому, если хочешь написать final — будь добр одновременно с этим описать и контракт в виде интерфейса.

Очень часто встречаю код, коллег, где они видимо прочитали про принцип закрытости и низкого зацепления, и все свои классы всегда объявляют финальными (забывая выделять контракт). И получаем в коде вот такое:

final class Logger { /* какая-то реализация */ }


final class SomeService { 
    public function setLogger(Logger $logger);
}


Специально взял пример с логером у которого может быть множество реализаций, но контракт един (см. psr/logger)

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

Поэтому повторюсь, финальные классы без контрактов — это больше антипатерн, аля Singletone. Финальные классы нужно использовать всегда совместно с контрактом. Тогда код будет легко переиспользовать, он будет менее сцеплен, и более гибок.

Также стоит добавить и ложку дегтя про final. Не все библиотеки и фрейморки умеют корректно работать с таким классами, хорошим примером является symfony lazy сервис, symfony не сможет сделать сервис lazy если класс сервиса финальный.
Поэтому повторюсь, финальные классы без контрактов — это больше антипатерн, аля Singletone.

Если речь о логгере, то да. Впрочем, в качестве контракта может быть и абстрактный класс; более того, считаю pure abstract class более уместным, чем интерфейс, поскольку тут все же отношение is-a, а не can.


Но есть ещё огромный пласт domain modeling, где в рамках определенного bounded context совершенно четко и однозначно известно, что такое какой-нибудь User или Address, и пихать туда контракты — идиотизм высшей степени, поскольку контракт сущности единственен и определяется ей самой.

pure abstract class допустим, если язык поддерживает множественное наследование.
Иначе, один класс не сможет реализовать 2 интерфейса.

Мне разделение на классы и интерфейсы вообще видится скорее ошибочным. Появление default interface methods в Java тому свидетельство.


Хотя на практике ситуация, когда отношение is-a сразу к двум базовым классам не является архитектурной ошибкой, довольно редкая.

Тут стоит учитывать, что есть два типа зависимостей: стабильные (stable) и изменчивые (volatile) зависимости. И в зависимости от типа зависимости нужно решать — выделять интерфейс или нет.


Logger — классическая volatile зависимость. Он может иметь несколько конкретных реализаций. А значит для него необходимо выделить интерфейс и организовать зацепление через интерфейс. В соответствии с принципом инверсии зависимостей (dependency inversion principle, DIP).


И если SomeService зависит от конкретной реализации зависимости Logger, то тут явное нарушение DIP. Клиент должен зависеть от абстракции и не зависеть от конкретных деталей реализации.


В случае же стабильных (stable) зависимостей — объектов-данных (Data Object), сущностей предметной области (типа Post, Comment), — нет смысла выделять интерфейс и использовать DIP. Т.к. они не являются абстракциями, а объекты вполне конкретного типа. Использование интерфейсов для них только усложняет код.

Не соглашусь. Вы статью писали с прицелом на слабое зацепление, и упоминали, что слабое зацепление ведет к такому плюсу как пере использование кода. Если код при этом качественно разбит по принципам SOLID на классы — и все они финальны без контракта, то такой код не может быть качественно пере использован, так как хоть он и классный, но он полностью фиксирован реализацией.

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

Но даже DTO объекты в рамках какой-либо vendor библиотеки, не могут быть финальными без контракта, если хотя бы один публичный метод любого класса этой библиотеки принимает его в качестве аргумента. Принцип подстановки Барбары Лисков. Вы сами говорили о SOLID, но при этом этот принцип полностью вычеркиваете. Финальный класс без контракта требуемый в качестве аргумента, это запрещение использования данного принципа. Это запрещение разработчику который хочет воспользоваться вашим кодом, расширения его частей (даже через композицию), для его задач в рамках его программной инфрастуктуры.

Пример из жизни, это многострадальный usb разъем и то, что было до него. А был зоопарк разъемов. Этот зоопарк разъемов — это ваши финальные классы без контракта. Производитель выпускал оборудование с фиксированной реализацией иных аксессуаров к нему, только под своей маркой.

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

Принцип подстановки Барбары Лисков — это про ограничения при наследовании, а не призыв его использовать, дабы себя соблюсти.
"Не садись пьяным за руль" — это Барбара Лисков, "Хорошо бы вообще не пить" — это автор статьи.

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

Ну по такой аналогии, можно еще кучу статей написать и вычеркнуть оставшиеся.

А реализация интерфейса — это, по большому счёту, то же наследование, вид с боку. Только на немножко другом уровне абстракции.
Если вы посмотрите на принципы SOLID, то они все про ограничения, а ограничения вводятся не от хорошей жизни. И если используемый подход обеспечивает соблюдение этих ограничений "из коробки", то ничего плохого в этом уж точно нет.

А реализация интерфейса — это, по большому счёту, то же наследование, вид с боку. Только на немножко другом уровне абстракции.

Мне жаль если вы не понимаете разницу.


Если вы посмотрите на принципы SOLID, то они все про ограничения, а ограничения вводятся не от хорошей жизни. И если используемый подход обеспечивает соблюдение этих ограничений «из коробки», то ничего плохого в этом уж точно нет.

Предлагаемый подход не решает, SOLID из коробки, он его кастрирует, и вводит большинство читателей в заблуждение, приносит в код огромное количество костылей.


Вы читали мою мысль? Давайте на примере, библиотеки с http клиентом и набором классов (сервисов)


final class SomeHttpClient {
    // some implementation

   public function request(/* Some arguments */) {
       // some implementation
   }
}

final class SomeService {
   public function __construct(SomeHttpClient $httpClient) {
      // ...
   }

   public function getComment(int $authorId) {
   }

   // etc ...
}

Так, вот я говорю, что таким кодом нельзя пользоваться. Так как он полностью закрывает возможность его расширения, в том числе в рамках SOLID.


В коде есть метод который в аргументе принимает final класс. То есть закрыта даже композиция.


Допустим мы подключили эту библиотеку в проект. А через некоторое время приходит бизнес требование. К примеру: журналировать все внешние обращения, мерить время при внешних обращениях и т.д.


Самое простое и правильное решить это композицией над SomeHttpClient


public function ProfiledSomeHttpClient {
   private SomeHttpClient $httpClient;

   public function __constuct(SomeHttpClient $httpClient) {
       $this->httpClient = $httpClient;
   }

   public function  request(/* Some arguments */) {
       // Фиксируем время старта
       // ...

      $result = $this->httpClient->request(...$arguments);

      // Замеряем итоговое время и что то с ним делаем
       // ...
      return $result;
   }
}

И в коде где конфигурируется SomeService инжектим туда нашу обертку.


$profiledHttpClient = new ProfiledSomeHttpClient(
    new SomeHttpClient()
);

$service = new SomeService($profiledHttpClient);

Все задача решена в лучших традициях SOLID.


НО!!! Нет, у нас же требуется передать в SomeService именно финальный HttpClient. То есть жестко фиксированная реализация. И вместо подобного решения, все, что нам остается, это во всех местах в проекте, где вызывается методы из someService — там мерить время. Это полный ужас.


Именно поэтому я написал, да можно финалить классы, НО, если этот финальный класс где-то ожидается как аргумент метода иного класса, ВЫ обязаны закрыть его контрактом и ожидать именно контракт!


В итоге должно быть:


interface SomeHttpClientInterface {
    public function request(/* Some arguments */) 
}

final class SomeHttpClient implements SomeHttpClientInterface {
    // some implementation

   public function request(/* Some arguments */) {
       // some implementation
   }
}

final class SomeService {
   public function __construct(SomeHttpClientInterface $httpClient) {
      // ...
   }

   public function getComment(int $authorId) {
   }

   // etc ...
}

Только так. Иначе, то что описывает автор, приведет к ухудшению, а не улучшению кодовой базы.

Мне жаль если вы не понимаете разницу.

Мне жаль, если вы не видите общего за частностями


Предлагаемый подход не решает, SOLID из коробки, он его кастрирует

SOLID — это не парадигма программирования — это этакий забор вокруг ООП, помогающий не пуститься во все тяжкие. Если на месте одной из секций этого забора будет уже готовое естественное препятствие, то это не повод страдать и плакать, что забор остался не завершенным.


Собственно никто не спорит (и автор, кстати, тоже), что для использование публичного и библиотечного должно быть через контракты. Однако это не повод запихивать эти контракты вообще везде в потрохах, где нужно и где не нужно.
Модификатор final в первую очередь работает как дополнительная секция того самого забора — упираясь в него, разработчик должен чётко сформулировать для себя причины, почему его нужно убрать, а не наоборот.

Вы тут неправы.

Автор SomeService вполне мог завязать её на конкретную реализацию SomeHttpClient, чтобы декомпозировать функционал. Давать возможность подставлять (или, вообще, делать какие-то выводы об интерфейсе SomeHttpClient) другую реализацию в его планы не входило.

Вы же, в рамках решения задачи по логированию, накладываете на какую-то конкретную часть библиотеки дополнительное правило, что именно вот эта её часть ходит в сеть, и именно эту её часть надо декорировать. Т.е., смотрите на этот чёрный ящик (библиотеку), делаете выводы о его внутреннем устройстве, и завязываетесь на эти выводы.

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

И да, далеко не всегда попадаются внешние библиотеки, где интересный нам функционал, который нам надо модифицировать, вынесен под интерфейс. И мы либо модифицируем такую библиотеку, делая её частью проекта, либо декорируем всю библиотеку целиком. И в подменяем её, а «полным ужасом» во всех местах добавляем необходимый функционал.

И да, далеко не всегда попадаются внешние библиотеки, где интересный нам функционал, который нам надо модифицировать, вынесен под интерфейс.

Именно об этом я и говорю, ценность таких библиотек сильно низка, так как авторы не заботятся о контрактах, и не гарантируют его соблюдение в будущем. Отсюда или фокри или постоянные проблемы при обновлении версий.


И именно это и вызывает основную боль. А голословно утверждать что парни давайте все зафиналим, это плохо. У любого подхода есть плюсы и минусы, это надо четко понимать.

Вполне может быть, что автор чётко разделяет внешний контракт и реализацию, но из-за ограничений языка (нет видимости уровня модуля/неймспейса/...) технически не может разделить их. А пользователи библиотеки считают, что всё к чему они могут получить доступ без рефлекси и прочих хаков — публичный контракт библиотеки

Финальный класс без контракта требуемый в качестве аргумента, это запрещение использования данного принципа. Это запрещение разработчику который хочет воспользоваться вашим кодом, расширения его частей

Конечно, как и любая ограничивающая конструкция, final снижает гибкость и "отключает" некоторые возможности ООП.
Зато с помощью final Вы можете взять в свои руки управление гибкостью использования своих классов. И если Вы не предусмотрели интерфейс для финального класса, который передается в качестве параметра в некоторый метод, то значит данный метод и разрабатывался для работы с этим конкретным классом, stable зависимостью. Если вы просто уберете final и разрешите создавать наследников для некоторой зависимости, то обязаны, по сути, гарантировать, что метод сможет с ними взаимодействовать. При том что у Вас будет отсутствовать явный контракт для этих дочерних классов.
Если формально подходить к LSP, то его реализация и невозможна без контракта.


It is a semantic rather than merely syntactic relation, because it intends to guarantee semantic interoperability of types in a hierarchy.
In addition to the signature requirements, the subtype must meet a number of behavioural conditions.

Нет интерфейса и контракта — нет никаких гарантий того, что в результате наследования будет получен поведенческий подтип (behavioural subtyping) и поэтому нет LSP.

Давайте на конкретном примере.


Единственный разумный случай, когда в сущности может появится принимаемый в аргументах интерфейс — это double dispatch.


Все остальное, что мне приходит в голову — сплошные антипаттерны, смешение слоев domain и infrastructure.

Принцип подстановки Барбары Лисков.… Финальный класс без контракта требуемый в качестве аргумента, это запрещение использования данного принципа
Принцип подстановки просто не имеет семантической применимости при финализации. Ваши слова звучат как «финализация это запрещение наследования». Эээ, ну да, это семантика слова «final».
Пример из жизни, это многострадальный usb разъем и то, что было до него
USB — это конкретный протокол обмена информацией, принципиально ничем не отличающийся от проприетарных. На его стороне играл скорее социальный аспект — договорённости и т.п. Абстракцией является интерфейс обмена данными. В физическом мире, боюсь, найдётся мало аналогий для программных абстракций подобного порядка.

Двоякое впечатление. Пример с подсчетом ссылок явно должен решаться композицией, а не наследованием, чтобы не смешивать запрос данных с подсчётом ссылок и/или презентационной логикой и сохранить SRP. final тут никак не помогает, если не желает хуже даже.


Мне кажется использование final как защиту от дурака вообще не оправдано. Скорей всего это как раз знак проблем в архитектуре. Выше в комментариях правильно обозначили, что есть domain modelling, и там использование final — показатель того, что сущность полна(по сути финальна, отсюда и final), её не надо расширять. Думаю это единственно верное использование final с точки зрения проектирования.


Еще использование final может быть оправдано с точки зрения оптимизации кода, но не уверен, применимо ли это к php.

Пример с подсчетом ссылок явно должен решаться композицией

Ну это пример всё же. Подобрать для статьи такой пример, к которому никто не докопается — эта задача посложнее инвалидации кеша и придумывания имен :)


Думаю это единственно верное использование final с точки зрения проектирования.

final, как минимум, говорит нам, что у класса точно нет наследников и мы никому ничего не поломаем изменив внутреннюю логику (не нарушая контракта). Посмотрите, например, на Kotlin — там все классы изначально закрыты и требуют специального модификатора open для разрешения наследования. И это как минимум не мешает, а в целом заставляет каждый раз задуматься — не совершаю ли я ошибку, используя наследование в данном конкретном случае.

Kotlin — хороший пример. Я бы сказал, что final по умолчанию имеет право на жизнь и это лучше конвенции писать везде final. (Логичным вопросом будет "в чём разница?" Ответом будут затраты на анализ, ревью и поиск ошибок в случае, где final был не нужен или не был поставлен неправильно).


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


Если переходить к практическому применению


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

Это хороший бонус, но это нужно делать и без такой возможности. На самом деле всё очень сильно лежит в плоскости стадии проекта. В начальных стадиях очень многое ещё неизвестно и лишние запреты увеличивают затраты на поддержку и расширение. Когда проект большой, наоборот, нужно менять что либо как можно более явно, тут я бы сказал, что final по умолчанию — хороший помощник. Но это никак не отменяет самой идеи final — пометить класс как "окончательный". Это не ради защиты от дурака.


final, как минимум, говорит нам, что у класса точно нет наследников и мы никому ничего не поломаем изменив внутреннюю логику (не нарушая контракта).

Это следствие "окончательности". Спросите себя, почему в какой-то момент вы можете захотеть это сделать. Ну то есть та ситуация, где вы решили, что решением будет занаследоваться, при этом это нарушит L в SOLID. Либо класс был неправильно спроектирован (а тогда какое тут final?). Либо другие компоненты были неверно спроектированы (а тогда какой final у них?), вероятнее всего и то и то. Это ведь явно не похоже на ситуацию, где final решает проблему?


Ещё я за отсутствие final в библиотеках, никогда не знаешь, как твой код будут использовать и какие могут там быть баги. Дать возможность разработчикам исправить их — меньшее зло по сравнению с запретами неправильных наследований. И да, с точки зрения теории это как раз тот случай, где этого вроде бы делать не надо. Но наш мир реальный, а не идеальный, поэтому надо.


Подобрать для статьи такой пример, к которому никто не докопается — эта задача посложнее инвалидации кеша и придумывания имен :)

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

Ещё я за отсутствие final в библиотеках, никогда не знаешь, как твой код будут использовать и какие могут там быть баги. Дать возможность разработчикам исправить их — меньшее зло по сравнению с запретами неправильных наследований.
Как раз в библиотеках final нужен больше всего. Отсутствие final у классов это косвенный признак того, что наследование является точкой расширения данной библиотеки. Соответственно разработчик такой библиотеки (если он следует семантическому версионированию) обязан соблюдать обратную совместимость при изменении таких классов. На практике, это приводит к тому, что такие классы очень сложно поддаются рефакторингу. Нельзя просто так менять сигнатуры у protected методов и тем более удалить целиком класс. Фактически класс будет «заморожен» как минимум до следующего мажорного релиза.
Другой важный момент это то, что удалить final никогда не поздно. Если появится use case для того, чтобы открыть класс для наследования, то сделать это можно будет в любой момент, потому что это не ломает обратную совместимость.
Либо класс был неправильно спроектирован (а тогда какое тут final?). Либо другие компоненты были неверно спроектированы (а тогда какой final у них?), вероятнее всего и то и то. Это ведь явно не похоже на ситуацию, где final решает проблему?

Как раз наоборот. Если в один прекрасный момент final начинает мешать — это такой очень хороший маркер, что надо бы задуматься над причинами.


Еще раз попробую донести свою мысль.
final — это один из множества инструментов ограничения языка/разработчика. Ровно так же как модификаторы области видимости, строгость типизации, автоматические проверки code-style, следование SOLID и т.д. — artificial constraint. Сами по себе они не плохи и не хороши, рассуждать можно только в контексте задачи — что нам важнее, скорость написания или стоимость поддержки (ну и все остальные аргументы из вечного спора статическая/динамическая типизация, как основного поставщика аргументов).

Вот именно, что не знаешь как будут использовать и не хочется получать багрепорты BC is broken при вроде бы всего лишь патч релизе, потому что кто то использует твою библиотеку не для того, для чего ты её делал или не так как ты думал её использовать. Использование final это сигнал для других программистов "класс не спроектирован для использования в качестве родительского".Его отсутствие — можете наследоааться спокойно, если код не поддерживает версии пхп, где его не было ещё

Стоит сказать, что написание дополнительной строки кода – довольно невысокая цена за получаемые с агрегацией преимущества

Да ладно. Даже в вашем примере без док. блока, это не одна строка, а четыре.

public function getCommentKeys(): array
{
  return $this->commentBlock->getCommentKeys();
}

Если в новом классе нужно подменить всего один метод из десяти, то этот класс будет процентов на 90 состоять из таких вот суррогатных декораций. Вроде ничего страшного, но остаётся ощущение что с наследованием это выглядело бы намного красивее, по крайней мере внешне.
Поэтому форвардящий декоратор можно вынести в отдельный класс и наследоваться от него.

// Wrapper class - uses composition in place of inheritance
public class InstrumentedSet<E> extends ForwardingSet<E> {
 private int addCount = 0;
 public InstrumentedSet(Set<E> s) {
 super(s);
 }
 @Override public boolean add(E e) {
 addCount++;
 return super.add(e);
 }
 @Override public boolean addAll(Collection<? extends E> c) {
 addCount += c.size();
 return super.addAll(c);
 }
 public int getAddCount() {
 return addCount;
 }
}
// Reusable forwarding class
public class ForwardingSet<E> implements Set<E> {
 private final Set<E> s;
 public ForwardingSet(Set<E> s) { this.s = s; }
 public void clear() { s.clear(); }
 public boolean contains(Object o) { return s.contains(o); }
 public boolean isEmpty() { return s.isEmpty(); }
 public int size() { return s.size(); }
 public Iterator<E> iterator() { return s.iterator(); }
 public boolean add(E e) { return s.add(e); }
 public boolean remove(Object o) { return s.remove(o); }
 public boolean containsAll(Collection<?> c)
 { return s.containsAll(c); }
 public boolean addAll(Collection<? extends E> c)
 { return s.addAll(c); }
 public boolean removeAll(Collection<?> c)
 { return s.removeAll(c); }
 public boolean retainAll(Collection<?> c)
 { return s.retainAll(c); }
 public Object[] toArray() { return s.toArray(); }
 public <T> T[] toArray(T[] a) { return s.toArray(a); }
 @Override public boolean equals(Object o)
 { return s.equals(o); }
 @Override public int hashCode() { return s.hashCode(); }
 @Override public String toString() { return s.toString(); }
}


Статья очень большая, ее трудно осилить.
Автор не упомянул простое правило, которым можно руководствоваться, при создании иерархии типов: хочешь наследования? Напиши 3 тестовые реализации сам и дай 1 реализацию написать соседу. Лень? извольте в композицию

Конечно, наследование никто не отменяет. И поэтому в статье и не утверждается, что надо безоговорочно отказаться от наследования в пользу агрегации. Ведь это отдельные инструменты, и каждый может оказаться более удобным в конкретной задаче. И если задача как та, которую Вы описали, — один класс тесно связан с другим, будет изменяться вместе с ним и разделяет с ним большую часть реализации, — то берите наследование.


В статье лишь утверждается, что нужно "по умолчанию" ограничить возможность наследования с помощью final. А уж если Вы пошли по пути создания дочерних классов, то старайтесь уменьшить зацепление на детали реализации настолько, насколько это возможно. Например, используя описанные в статье техники — abstract классы и final методы. А уж если реализация может быть переопределена в дочернем классе, то задокументируйте в PHPDoc все существенные нюансы.

НЛО прилетело и опубликовало эту надпись здесь
Мне одному режет слух «слабое зацепление» вместо «слабой связанности» и «сокрытие» вместо «инкапсуляции»?

"Связность" и "зацепление" — несколько неустоявшиеся переводы английских cohesion и coupling. Ориентировался при использовании терминов на википедию. Хотя разные авторы и переводчики используют для coupling как зацепление, так и связность.
По поводу "инкапсуляции" и "сокрытия", тоже существуют разные мнения. В википедии написано, что они часто используются взаимозаменяемо:


The term encapsulation is often used interchangeably with information hiding.

Но если исходить из смыслового определения инкапсуляции, как сочетания данных с методами, и сокрытия, как ограничения доступа. То в статье речь идет как раз о сокрытии. Есть подробная статья по этому поводу.

На поддержке старых проектов, иногда возникает желание по-отрывать руки за private и final. Требования бизнеса постоянно меняются, и лучше иметь в арсенале гибкий класс, логику которого всегда можно изменить наследованием, чем кучу черных ящиков, в которых ничего уже не изменить. На теории все красиво, но в реальной жизни следование академическим принципам означает повышение стоимости поддержки такого кода.

А этот "гибкий класс" в итоге не превращается в "кучу серых ящиков, в которых уже ничего не изменить, потому что изменение в одном потребует менять половину оставшихся"?

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

На проектах «с историей» обычно такой треш, что думаешь не о каких-то там private и final, а о том, как своим мозгом вообще «понять и простить» всё то, что там понаписано. И как-то потом с этим работать.

И как-то потом с этим работать.

Руководствуясь правилом бойскаута

Это больше про умение понять где использовать наследование, а где сделать другой класс, даже если просто, интуитивно и быстрее наследоваться и получить функционал базового класса, но всё равно нужно создать другой класс потому что, это уже другая ось расширения.
Супер статья, многое понял. А вот комментарии в примерах кода почти нигде не нужны, как по мне. Ну что это такое,
/** массив комментариев */
private $comments = [];
 /** Кеш */
private $cache;

Если в переменной comments, инициализированной пустым массивом будет лежать что-то, кроме массива комментариев, надо повозить автора такого кода по столу в срочном порядке ее переименовать. Ну а комментировать кеш как «кеш» — это ж прямо как будто взято из той главы в Clean Code про повторяющиеся комменты. Ну а аннотация @implSpec так вообще выглядит вредной. Сегодня у меня одни делали реализации, а завтра другие и я конечно же забуду поменять коммент когда буду их обновлять.
НЛО прилетело и опубликовало эту надпись здесь
У меня с наследованием в итоге постоянно выходила какая-то непонятная фигня, сорты которой так чудесно расписаны в статье.

Наследование как раз и приводит к оверинжинирингу, на самом деле.


Композиция кажется сложнее из-за того, что популярные ООП-языки так устроены, что надо писать больше кода. Концептуально композиция проще.

Если наследования хватает, то просто поставьте final в последнем наследнике хотя бы

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

А ведь "ООП" как раз учат по наследованию, всякие там классы Animal от которых создаются другие животные. Из-за этого многие начинающие программисты во всю используют наследование, им кажется это такая такая замечательная фишка которая помогает не повторять множество кода.

А тут внезапно такая критика. Прям удар в самое сердце, в фундамент концепции "ООП на классах". Мне нравится, отличная статья.

Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации