И да, далеко не всегда попадаются внешние библиотеки, где интересный нам функционал, который нам надо модифицировать, вынесен под интерфейс.
Именно об этом я и говорю, ценность таких библиотек сильно низка, так как авторы не заботятся о контрактах, и не гарантируют его соблюдение в будущем. Отсюда или фокри или постоянные проблемы при обновлении версий.
И именно это и вызывает основную боль. А голословно утверждать что парни давайте все зафиналим, это плохо. У любого подхода есть плюсы и минусы, это надо четко понимать.
А реализация интерфейса — это, по большому счёту, то же наследование, вид с боку. Только на немножко другом уровне абстракции.
Мне жаль если вы не понимаете разницу.
Если вы посмотрите на принципы 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 на классы — и все они финальны без контракта, то такой код не может быть качественно пере использован, так как хоть он и классный, но он полностью фиксирован реализацией.
Пример с Logger я привел, как самый очевидный, никто не будет спорить, что он имеет множество реализаций.
Но даже DTO объекты в рамках какой-либо vendor библиотеки, не могут быть финальными без контракта, если хотя бы один публичный метод любого класса этой библиотеки принимает его в качестве аргумента. Принцип подстановки Барбары Лисков. Вы сами говорили о SOLID, но при этом этот принцип полностью вычеркиваете. Финальный класс без контракта требуемый в качестве аргумента, это запрещение использования данного принципа. Это запрещение разработчику который хочет воспользоваться вашим кодом, расширения его частей (даже через композицию), для его задач в рамках его программной инфрастуктуры.
Пример из жизни, это многострадальный usb разъем и то, что было до него. А был зоопарк разъемов. Этот зоопарк разъемов — это ваши финальные классы без контракта. Производитель выпускал оборудование с фиксированной реализацией иных аксессуаров к нему, только под своей маркой.
Usb же это контракт, который позволил пользоваться каким-то оборудованием одного производителя, но и заменой части аксессуаров от иных производителей. Это и есть пере использование.
Очень длинная статья. Но в итоговом виде, порядок я бы поменял, так как в случае публичного api не имеет смысла вообще выпускать final классы, без интерфейсов. Поэтому, если хочешь написать final — будь добр одновременно с этим описать и контракт в виде интерфейса.
Очень часто встречаю код, коллег, где они видимо прочитали про принцип закрытости и низкого зацепления, и все свои классы всегда объявляют финальными (забывая выделять контракт). И получаем в коде вот такое:
final class Logger { /* какая-то реализация */ }
final class SomeService {
public function setLogger(Logger $logger);
}
Специально взял пример с логером у которого может быть множество реализаций, но контракт един (см. psr/logger)
Если ты объявил финальный класс без выделения контракта, и потом его ожидаешь в ином классе, ты фиксируешь реализацию, а должен фиксироваться контракт.
Поэтому повторюсь, финальные классы без контрактов — это больше антипатерн, аля Singletone. Финальные классы нужно использовать всегда совместно с контрактом. Тогда код будет легко переиспользовать, он будет менее сцеплен, и более гибок.
Также стоит добавить и ложку дегтя про final. Не все библиотеки и фрейморки умеют корректно работать с таким классами, хорошим примером является symfony lazy сервис, symfony не сможет сделать сервис lazy если класс сервиса финальный.
Соглашусь, что сейчас у детей море и телега развлечений, особенно игр на планшетах и т.д. Их в принципе на учебу (любую) мотивировать сложно.
Но вспоминая себя, у меня в детстве тоже были игры (Денди, Сега), компа не было, но я отвлекался и копался в радиоэлектронике, что-то паял, собирал и т.д. Никто, меня не заставлял. И не отбирал игры.
К программированию, я уже в старших классах пришел, когда у родителей на работе и в школе добрался до компа. По сравнению с радиоэлектроникой это был творческий рай. Электроника, в те годы 90е, это месяцы поиска нужных компонентов, схем и т.д. и когда собираешь и включаешь — а оно не работает, это недели поиска причин и новых компонентов. А тут, написал код, запустил — увидел, что не работает, и подсказку компилятора, поправил и запустил снова, сокращение времени итерации на порядок, твори не хочу. Это, сделало моё будуще.
А вот с современными детьми, пока все не понятно. Как подступиться, в каком возрасте начать, и с чего.
Про спорные ощущения уже писали, рекламный осадок остался. Но судя по коду на гитхабе и истории коммитов, больше похоже, что пост не рекламный.
Тем не менее читать было интересно. Я часто задумываюсь, как «показать» мир кодинга, своему ребенку, или супруге. Учитель из меня плохой, ввиду того, что примеры мне приходят более сложные, чем то, с чего стоит начинать и проекты или объяснения.
Это как ребенку сразу про solid говорить, не понимая, что он даже ветвления не знает.
Спасибо возьму вашу задачу с простой консольной игрой, как отправную точку.
Вам, же советую, на этой же задачей потренироваться в ООП стиле.
Либо опечатка, либо за новость выдают старую информацию :)
Судя по всему это запись симуляции из фрейворка ROS. В статье написано, что
Именно об этом я и говорю, ценность таких библиотек сильно низка, так как авторы не заботятся о контрактах, и не гарантируют его соблюдение в будущем. Отсюда или фокри или постоянные проблемы при обновлении версий.
И именно это и вызывает основную боль. А голословно утверждать что парни давайте все зафиналим, это плохо. У любого подхода есть плюсы и минусы, это надо четко понимать.
Мне жаль если вы не понимаете разницу.
Предлагаемый подход не решает, SOLID из коробки, он его кастрирует, и вводит большинство читателей в заблуждение, приносит в код огромное количество костылей.
Вы читали мою мысль? Давайте на примере, библиотеки с http клиентом и набором классов (сервисов)
Так, вот я говорю, что таким кодом нельзя пользоваться. Так как он полностью закрывает возможность его расширения, в том числе в рамках SOLID.
В коде есть метод который в аргументе принимает final класс. То есть закрыта даже композиция.
Допустим мы подключили эту библиотеку в проект. А через некоторое время приходит бизнес требование. К примеру: журналировать все внешние обращения, мерить время при внешних обращениях и т.д.
Самое простое и правильное решить это композицией над SomeHttpClient
И в коде где конфигурируется SomeService инжектим туда нашу обертку.
Все задача решена в лучших традициях SOLID.
НО!!! Нет, у нас же требуется передать в SomeService именно финальный HttpClient. То есть жестко фиксированная реализация. И вместо подобного решения, все, что нам остается, это во всех местах в проекте, где вызывается методы из someService — там мерить время. Это полный ужас.
Именно поэтому я написал, да можно финалить классы, НО, если этот финальный класс где-то ожидается как аргумент метода иного класса, ВЫ обязаны закрыть его контрактом и ожидать именно контракт!
В итоге должно быть:
Только так. Иначе, то что описывает автор, приведет к ухудшению, а не улучшению кодовой базы.
В случае описываемом в статье, если у нас финальный класс, наследовать нельзя, интерфейса нет. То и принцип не применим. То есть из SOLID можно вычеркнуть одну букву.
Ну по такой аналогии, можно еще кучу статей написать и вычеркнуть оставшиеся.
Пример с Logger я привел, как самый очевидный, никто не будет спорить, что он имеет множество реализаций.
Но даже DTO объекты в рамках какой-либо vendor библиотеки, не могут быть финальными без контракта, если хотя бы один публичный метод любого класса этой библиотеки принимает его в качестве аргумента. Принцип подстановки Барбары Лисков. Вы сами говорили о SOLID, но при этом этот принцип полностью вычеркиваете. Финальный класс без контракта требуемый в качестве аргумента, это запрещение использования данного принципа. Это запрещение разработчику который хочет воспользоваться вашим кодом, расширения его частей (даже через композицию), для его задач в рамках его программной инфрастуктуры.
Пример из жизни, это многострадальный usb разъем и то, что было до него. А был зоопарк разъемов. Этот зоопарк разъемов — это ваши финальные классы без контракта. Производитель выпускал оборудование с фиксированной реализацией иных аксессуаров к нему, только под своей маркой.
Usb же это контракт, который позволил пользоваться каким-то оборудованием одного производителя, но и заменой части аксессуаров от иных производителей. Это и есть пере использование.
Очень часто встречаю код, коллег, где они видимо прочитали про принцип закрытости и низкого зацепления, и все свои классы всегда объявляют финальными (забывая выделять контракт). И получаем в коде вот такое:
Специально взял пример с логером у которого может быть множество реализаций, но контракт един (см. psr/logger)
Если ты объявил финальный класс без выделения контракта, и потом его ожидаешь в ином классе, ты фиксируешь реализацию, а должен фиксироваться контракт.
Поэтому повторюсь, финальные классы без контрактов — это больше антипатерн, аля Singletone. Финальные классы нужно использовать всегда совместно с контрактом. Тогда код будет легко переиспользовать, он будет менее сцеплен, и более гибок.
Также стоит добавить и ложку дегтя про final. Не все библиотеки и фрейморки умеют корректно работать с таким классами, хорошим примером является symfony lazy сервис, symfony не сможет сделать сервис lazy если класс сервиса финальный.
Но вспоминая себя, у меня в детстве тоже были игры (Денди, Сега), компа не было, но я отвлекался и копался в радиоэлектронике, что-то паял, собирал и т.д. Никто, меня не заставлял. И не отбирал игры.
К программированию, я уже в старших классах пришел, когда у родителей на работе и в школе добрался до компа. По сравнению с радиоэлектроникой это был творческий рай. Электроника, в те годы 90е, это месяцы поиска нужных компонентов, схем и т.д. и когда собираешь и включаешь — а оно не работает, это недели поиска причин и новых компонентов. А тут, написал код, запустил — увидел, что не работает, и подсказку компилятора, поправил и запустил снова, сокращение времени итерации на порядок, твори не хочу. Это, сделало моё будуще.
А вот с современными детьми, пока все не понятно. Как подступиться, в каком возрасте начать, и с чего.
Тем не менее читать было интересно. Я часто задумываюсь, как «показать» мир кодинга, своему ребенку, или супруге. Учитель из меня плохой, ввиду того, что примеры мне приходят более сложные, чем то, с чего стоит начинать и проекты или объяснения.
Это как ребенку сразу про solid говорить, не понимая, что он даже ветвления не знает.
Спасибо возьму вашу задачу с простой консольной игрой, как отправную точку.
Вам, же советую, на этой же задачей потренироваться в ООП стиле.
Успехов, ему.