Pull to refresh

Comments 180

решение для «Избегайте проверки типов (часть 1)» будет ещё лучше если интерфейсы использовать:
interface Movable { public function move(...); }
function travelToTexas(Movable $vehicle) { ... }
foreach ($locations as $location)

Сам так делаю, но начинаю сомневаться в правильности такого подхода. Переменные получаются излишне схожими (хоть за пределами цикла вторая обычно больше не используется), да и в скорости набора такой вариант проигрывает (я про то, что приличные ide подставляют по первым набранным буквам: тут приходится выбирать из двух вариантов).
Я обычно пишу
foreach ($locations as $loc)

Внутри блока обычно понятно, что такое $loc (или $p[erson], или $s[tudent]).
Аналогично, очень часто, делая нечто подобное:
$records = User::all();
foreach ($records as $record) {
    // some actions with $record->objects
}

автокомплит, к примеру у phpstorm внутри перебора foreach(){} сует именно records вместо ожидаемой record. Отсюда у меня так же имеется ряд сомнений относительно данной рекомендации (или адекватности автокомплита phpstorm).

я обычно пишу так


$userList = User::all();
foreach ($userList as $user){
//code
}

Но я бы не советовал так делать… так как вроде не стандарт, но визуально так проще и пропадает проблема с news...

на такой автокомплит грех жаловаться:
$mice = $children = $matches = $photos = $wives = $loaves = $women = $geese = $data = [];
foreach ( $mice as $mouse ) {}
foreach ( $children as $child ) {}
foreach ( $matches as $match ) {}
foreach ( $photos as $photo ) {}
foreach ( $wives as $wife ) {}
foreach ( $loaves as $loaf ) {}
foreach ( $women as $woman ) {}
foreach ( $geese as $goose ) {}
foreach ( $data as $datum ) {}

про data=>datum вовсе не знал пока впервые в самом же шторме не увидел, оказалось это совершенно корректно
Ну да, «data» = «данные»
А «datum» = «данный» (муж.р) или «данныя» (жен.р)?
Лучше оставить везде data. Просто $data_list as $data_item.
У вас User::all() может вернуть пустой массив или обьект. И у вас нету проверки, а сразу идет цикл
Вообще, код — чисто символичный и ничего он там не может вернуть другого, кроме коллекции объектов (см. laravel doc). И да, давно уже никто не проверяет результаты выборок (по крайней мере в разумных orm) на false, null или прочую дрянь, т.к. методы возвращают ожидаемые результаты одного типа (кроме ситуативных, типа ->first() или ->firstOrNull()).

интерпретатор не будет жаловаться… но как и написали этот код чисто символичный


$newsList = $newsRepository->all()
foreach($newsList as $news) {
//code
}
//$news = $newsList->first();
//$news = $newsList->last();

меня только одно интересует, многих раздражает слово list в конце, просто я постоянно использую такую практику а feedback пока не почувствовал, но опять повторюсь, не используйте...

Лучше использовать такой код

$news = $newsRepository->all();
foreach($news as $article) {
  // article как раз и есть ваша отдельная новость
}

Ну я примерно так и пишу, мне просто больше нравится в конце имени переменной видеть слово List, а не улавливать букву s, немного читабельнее лично для меня.

Я использую приставку cur, и, возможно, сокращаю имя переменной. Т.е.
foreach ($locations as $curLocation)

или
foreach ($locations as $curLoc)

Второй вариант выглядит не очень, но отлично срабатывает автокомплит, да и проблем с чтением кода не возникает.

Тут чушь написана. Такое просто не захватит переменную $name.
`
$name = 'Ryan McDermott';


function splitIntoFirstAndLastName() {
$name = preg_split('/ /', $name);
}


splitIntoFirstAndLastName();


var_dump($name); // ['Ryan', 'McDermott'];
// Не ['Ryan', 'McDermott'], а string(14) 'Ryan McDermott'
`


Хотя вот так прокатит:

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

Хотя вот так прокатит:
$name = 'Ryan McDermott';
$splitIntoFirstAndLastName = function () use (&$name) {
$name = preg_split('/ /', $name);
};
$splitIntoFirstAndLastName();

var_dump($name); // ['Ryan', 'McDermott'];

Но такой код ещё нужно додуматься написать

или внутри функции надо было объявить global $name;


function splitIntoFirstAndLastName() {
global $name;
$name = preg_split('/ /', $name);
}
За глобалсы надо отрубать всё, что выпирает. Подумайте над этим на досуге =))))

В этом и суть. По моему где-то даже высказывались о бессмысленности этого правила

Ой, значит я не правильно понял комментарий автора выше, прошу прощения.
UFO just landed and posted this here
Как минимум то, что кто угодно, когда угодно и где угодно может изменить, создать или удалить значение, даже не подозревая об этом.
UFO just landed and posted this here
Ну вот есть у нас небольшой набор вендоров, допустим 8к файлов, объёмом в 30 метров. Вы готовы проверить каждую строчку вендорного кода и поручиться, что каждый из тысяч людей, кто его писал всё делал предельно идеально?
UFO just landed and posted this here
Тут ключевой вопрос будет простой — вы все проекты делаете в одиночку? Помните все переменные, которые можно и нужно хранить в глобалах?
Ну или второй аргумент:
Есть у нас набор тестов, А и Б. В каком порядке они выполняются — неизвестно. Один тест пишет в глобалсы и другой. Получается, что в тесте А и Б результат выполнения Б, как следствие — первый падает, потому что в первом, ну например промизы используются для распараллеливания запросов на какое-нибудь апи (это вполне реальные ), а значит не блокируют исполнение (что есть хорошо).

Ну и третий, заключающий будет ответом на ваш тезис:
При грамотном программировании, глобальные переменные не будут кем попало переписываться.

Зачем нужна та вещь, которая на практике просто не нужна? Любую задачу можно решить без неё. По-этому, при грамотном программировании глобалсов вообще не будет, т.к. это ненужный атавизм. Да не покарает меня ЛММ, но даже goto иногда нужен (например при реализации конечных автоматов), в отличие от глобальных переменных.

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

UFO just landed and posted this here
Я ещё раз повторю свой тезис, который вы проигнорили: Глобалсы не нужны по простой причине — это атавизм, который просто ненужен. Любая задача решается проще и эффективнее без них; И никакого профита, удобства или плюшек они не несут.

Я не представляю ни одной ситуации, когда они могут понадобиться. Ну вот вообще ни одной задачи, даже высосанной из пальца. Сможете придумать?
UFO just landed and posted this here
$a = 5;
$b = 7;
echo $a + $b;
echo $a * $b;

Врядли я буду для таких целей писать классы…
ну тоесть я вижу применение глобальных переменных только в небольших скриптах, для простых вычислений или тестирования.

Если скрипт небольшой, то зачем там глобалы?

чтобы не тратить время.
Допустим есть сайт с каким-то разделом в котором реализована сложная логика.
Нужно добавить в него дополнительный функционал.
Проще сначала в отрыве от кодовой базы накидать алгоритм для нового функционала(небольшой скрипт), где будут глобальные переменные, не каких классов и объектов и после того как алгоритм будет готов уже нормально его встроить в архитектуру приложения… как правило это дешевле чем изучать архитектуру и сразу пытаться написать хороший код.

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

Если это приличный код, то там наверняка есть автозагрузка (своя или через композер). Сделать синглтон класс, да даже банально пустой класс с публичными статическими полями занимает пять секунд. Зато управляемости гораздо больше. Рефакторить потом гораздо проще

Да даже если вам надо накидать отдельный алгоритм, то в рамках однофайлового скрипта вам не нужны глобалы.

За последние 5 лет работы мне вообще не приходилось их использовать, что в больших проектах, что в малых

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

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

UFO just landed and posted this here

Переменные скрипта вне функций, классов и т. п. — глобальные.

оххххх, как жаль что я не могу плюсануть)) именно это я хотел написать, но Вы меня опередили))

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

А global — пробрасывание этих переменных внутрь функций :)

и вот именно за него предлагается бить линейкой по рукам

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

А как же делать реквайры в глобал только из фронт-контроллера-(ов), бинарника консоли и бутстрапа тестов? Не надо так. Поменяете вы переменную локально, а кто за вас поменяет ее в подключаемом скрипте? И в других местах, где подключен подключаемый скрипт

Я сейчас не рассматриваю битриксо-подобные системы, где реквайр на реквайре сидит и инклудом погоняет.

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

ну, в итоге вы практически процитировали мои сообщения выше, рад, что мы поняли друг друга.

можно конечно и подругому но как простой пример...


$filter = [/* some array */];

$fieldList = array_filter($fieldList, function ($field) {
    global $filter;
    return empty($filter[$field['name']]);
});
UFO just landed and posted this here

Рекомендую ознакомиться вот с этой заметкой. Там довольно подробно описана основная причина.


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

А ещё надо всё, что выпирает, отрубать за использование регулярок там, где они совершенно не нужны. Тут хватит обычного explode.
Только что проверил код с глобалом, всё работает как и у автора статьи. Вывод: вы немного поспешили со своим комментарием)
Внутри функции другая область видимости.

Можно захватить переменную извне тремя способами, в статье не используется ни один из них:
1. Лямбда с use, как описал выше;
2. Вариант nokimaro с global внутри функции;
3. Использовать $GLOBALS, но это дичь какая-то:
<?php
$name = 'Ryan McDermott';
function splitIntoFirstAndLastName() {
$GLOBALS['name'] = preg_split('/ /', $GLOBALS['name']);
}
splitIntoFirstAndLastName();
var_dump($name); // ['Ryan', 'McDermott'];


Чтобы не соврать, проверил все варианты (и вариант из статьи) в PHP 5.6 и 7.0, а также прочёл документацию, вдруг забыл что.
php.net/manual/en/language.variables.scope.php
Так и не понял, что за 86400. Можно же проще написать 24*60*60 и сразу всё понятно.
Внезапно ваш код оказывается на Марсе и надо использовать местные сутки.
24*60*60 уже не прокатит.
Вы наверное не понимаете сути такой записи: 86400 — сложно прочесть и на вскидку сказать сутки это или нет, тогда как записав форматом часы*минуты*секунды — получается более гибко для изменений и более читабельно.
А еще можно делать вот так:

strtotime('1 day', 0)
ИМХО конечно, но зачем городить лишние функции? Планета вроде не собирается менять орбиту, скорость вращения и наклон, то есть мы всегда знаем что в сутках 24 часа, в часе 60 минут, в минуте 60 секунд, а это означает, как минимум, что созданная давныыым давно запись часы*минуты*секунды и понятна и легко записывается одним простым int
Зачем городить? Она в PHP уже есть, просто заменить магическое число (умножение магических чисел) на вызов этой функции. Как мне кажется, это намного читабельнее.
Вызов доп функций не люблю, когда можно обойтись строкой на 10 символов (:
Она вызывается 1 раз в секции определения констант. Более того, с подобным подходом удобно определять необходимые константы промежутков вида «1 week» или «1 day +1 hour» / «1 day +1 minute».
Про константы не подумал, ваша правда.
В некоторых минутах 61 секунда :)
UFO just landed and posted this here
PHP зависит от системных вызовов, если система на NTP — да.
Я думаю zoonman имел виду, что костанта одна, а в вашем случае записей вида 24*60*60 может быть много и все их найти проблематично, особенно, если придется портировать код под Марсианские реалии.
А почему мы в константу не загоним 24*60*60?
Лично мне легко это прочесть и сказать, сутки это или нет. Увы, такие базовые вещи, как количество секунд сутках можно и знать.

Как было сказано в статье, вынесение этих чисел в константы с корректным именем решает проблему восприятия.
Не нужно потом запоминать, что там.

Могу привести немного высосанный из пальца пример.
В PHP есть встроенная константа π, но для наглядности допустим, что ее нет.

Если вы в коде будете повсюду писать что-то вроде 3.14, то вначале все будет хорошо.
Но если внезапно потребуется увеличить точность вычислений, то прийдется прошерстить весь код заменив 3.14 на 3.14159.
Если еще раз понадобится это сделать, то еще прийдется менять весь код.
Если вы где-то в одном месте недопишете одну цифру, например 3.1415, то автозамена уже не сработает. Это место будет приводить к неправильным результатам.
Если бы всюду использовали нечто Math::PI, то проблема была бы решена раз и навсегда.
> Как было сказано в статье, вынесение этих чисел в константы с корректным именем решает проблему восприятия.

Я думаю что с этим утверждением спорить глупо (:

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

Внезапно, 86400 секунд на Марсе это все равно 24*60*60 секунд.
Внезапно в марсианских сутках 88775 земных секунд.
Ну и что? Замена 86400 на 24*60*60 от этого не становится неправильной.
На Марсе в сутках 86400 марсианских секунд.

Смысл в том, что магические символы это всегда дурной тон в программировании...

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

можно вопрос, а часто вы сайтики на марсе запускаете?

Лол, ну попробуйте такое записать в свойство класса по умолчанию и посмотрите что скажет интерпретатор

UFO just landed and posted this here
оно то хорошо, хотя мы пока пишем 5.4 совместимый код. кстати, причем здесь константы?
UFO just landed and posted this here
это пока не так актуально, во-первый нет пока адекватных сборок для линукса с php 7, во-вторых, заметного прироста производительности не будет, т.к основные тормоза связаны с базой данных. поэтому уж точно нет смысла переписывать старый код, другое дело, что может есть смысл писать новый 100% совместимым под 7.0.
я просто комментарий писал к свойству класса, а не константе, но да, в данном случае правильнее использовать константу конечно
UFO just landed and posted this here
хорошо еще, что производительность битрикса не привели в качестве примера)
UFO just landed and posted this here
так не на запрос основное время уходит, а на подключение к субд. в твоей статистике время на это учитывается?
Рендер страницы = подключение к БД + запрос + обработка
во-первый нет пока адекватных сборок для линукса с php 7,
Есть, конечно, и давно.
т.к основные тормоза связаны с базой данных.
А вы всё-таки попробуйте. У меня разницы в два раза не получилось, но разница существенная и легко проверяемая по мониторингу.

Когда я увидел в коде коллеги const list = array(1, 2, 3); мне поплохело. Если так можно написать не значит что нужно.

В PHP нет readonly, так что всё, что не должно меняется надо защищать от изменения доступными методами. От чего вам поплохело?

Мне и моим голлегам проще читать цифры вида 3600 и 86400, хотя авторы со мной не согласны

Заголовок спойлера
class BankAccount {
...
    public function withdrawBalance($amount) {
        if ($amount > $this->balance) {
            throw new \Exception('Amount greater than available balance.');
        }
        $this->balance -= $amount;
    }
...
}


Вот объясните мне: разве это правильно бросать исключения в бизнес-логике, и, тем более, в сторону пользователя?
Лучше делать так, чтобы потом было легко отловить исключение:
<?php
class BalanceException extends \Exception { } // Ну или BankAccountException, зависит уже от конкретной ситуации

class BankAccount {
...
    public function withdrawBalance($amount) {
        if ($amount > $this->balance) {
            throw new BalanceException('Amount greater than available balance.');
        }
        $this->balance -= $amount;
    }
...
}
Я не об отлове (хотя и о нем тоже), а вообще об исключениях в бизнес-логике. Исключения должны обрабатывать ошибки в работе системы (такие как невозможность чтения из БД или неправильный тип аргумента функции), а не ошибки в действиях пользователя (такие как неверный пароль или недостаточная сумма на балансе, например). Я не ошибаюсь?
В целом, нет, не ошибаешься, но исключения, вида NotFoundHttpException, UnprocessableEntityHttpException и прочие — говорят об обратном.
Исключения должны обрабатывать ошибки в работе системы

не так.

Исключения нужны для передачи ошибок вверх по стеку вызовов
+ в них можно указать экстра-данные (если как «белый человек» расширяешь стандартный класс, добавляешь методы для get/set этих данных)
+ они избавляют все промежуточные классы/методы между throw и catch от необходимости
а. знать о кодах и типах ошибок «нижестоящих» классов
б. знать о том как их обработать
в. иметь лишнюю ответственность в виде «я могу/должен это обработать»
г. (как следствие) иметь лишний защитный код
д. иметь много типов возврата из метода (return false для ошибки, string для текста ошибки, string[] для набора ошибок)

Исключение прерывает поток исполнения, а обычная ошибка, которая отдается в return — это продолжение потока исполнения, как будто бы так и надо. Поэтому исключения нельзя использовать для нормального ветвления логики и следует использовать если из за конкретной ошибки нет возможности работать дальше (в текущем контексте, а не во всей программе). Типичный пример — «первая линия обороны» — проверка аргументов в методах где нет доверия входным данным.

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

Если брать конкретно этот простой пример — выброс исключения оправдан.
Можно ли продолжить операцию при отрицательном балансе? — нет, нельзя (хотя это зависит от бизнеса), поэтому тут именно исключение.
Намного проще поставить один try/catch где то наверху для критического кода и ожидать в нем исключения, нежели распихивать обработку в каждый класс, да еще и передачу по стеку вызов данных определенного типа и рассказывать каждому классу как работать с ними.
Недостаточная сумма на балансе — не ошибка пользователя в методе, изменяющем баланс. Ошибки пользовательского ввода должны проверяться раньше, чем попадут в бизнес-логику, например на фронте или в контроллере, а уж если попали в модель, то попытка овердрафта — это исключение, возможно требующее вмешательства СБ. Надо понимать разницу между правилом валидации «сумма операции не должна быть меньше баланса» и бизнес-правило «ни при каких операциях итоговый баланс не должен быть меньше нуля», даже если проверка осуществляется идентичным или даже одним и тем же кодом.
UFO just landed and posted this here
В данном случае это может быть оправдано.
UFO just landed and posted this here
селекты упрощаются немного, т.к. если есть табличка rooms и в ней PK room_id, то все ссылки на нее из других таблиц называются room_id, и тогда например можно писать такое:
SELECT * FROM persons LEFT JOIN rooms USING (room_id)

Если колонка PK называется всегда id, то потребуется полностью писать — ON (rooms.id = persons.room_id)
UFO just landed and posted this here
Сомнительное преимущество, писать везде лишний префикс, чтобы сэкономить пару символов в нескольких джойнах, которые во многих случаях делаются ORM.
Это вопрос религиозный — есть оракловский подход, называть свой ключ ID, а внешние ключи another_table_id.
и есть MS SQL подход — во всех таблицах называть ключи <table_name>_id.
Как указали ниже, это связано с синтаксисом джойнов в разных диалектах SQL
В Оракле длина названия сильно ограничена (31 символ, кажется), поэтому там длинные названия — боль :)
Аргументы по умолчанию не работают, если по умолчанию переменная ($this->default или self::$default), хотя это бывает необходимо
Принцип подстановки Барбары Лисков продемонстрирован достаточно сложно для понимания.
Дело в том, что существуют высота и ширина в квадрате равные между собой. Поэтому установка любого из этих свойств является корректной операцией.

$area = $rectangle->getArea(); // Плохо: Will return 25 for Square. Should be 20.

Самый безграмотный комментарий, который я встречал. It must be 25 for Square.

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

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

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

Лично мне больше понравилось описание на Stackoverflow.

DuckShaped

В двух словах я бы описал его так: когда у вас есть два похожих, но совершенно разных объекта, не наследуйте один от другого.

Поэтому установка любого из этих свойств является корректной операцией.

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


Если квадрат меняет стороны, значит у нас какой-то свой объект, который мы почему-то называем квадратом.


Проблема в том, что в современном ООП нет возможности описать связь, что квадрат это прямоугольник. Вот если бы тип менялся при изменении стороны, проблемы бы не было.


Может я не прав, но в примере из статьи я не вижу большой проблемы. Площадь квадрата будет нормально считаться после изменения сторон. Это не будет работать только в каких-нибудь тестах, где проверяется конкретный результат. Но в тестах лучше проверять потомков, а не родителя. Проблема будет, если будет меняться поведение или добавляться ограничения, например, setWidth() будет все под-объекты уничтожать, к которым есть доступ снаружи.

Проблема в том, что в современном ООП нет возможности описать связь, что квадрат это прямоугольник.

class Квадрат extends Прямоугольник?

UFO just landed and posted this here
Честно говоря так и не понимаю ошибочности данного подхода. Наверное я слишком узко смотрю?
Если коротко, квадрат добавляет ограничения в реализацию предка-прямоугольника, что запрещено LSP.
Считается, что это нарушает SOLID. Тут есть обсуждение по этой теме.
Проблема в том, что в современном ООП нет возможности описать связь, что квадрат это прямоугольник. Вот если бы тип менялся при изменении стороны, проблемы бы не было.

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

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

Что никак не меняет того факта, что не всякий параллелограмм — квадрат.

Я тоже придерживаюсь правил, но когда устаю, то уже у меня получается как попало :( почти как быдло-кодинг
Как с этим бороться?

UFO just landed and posted this here
При этом нужно ментально перестроиться к тому, что к тебе не придираются, а наоборот помогают.
Многие новички воспринимают Code Review очень болезненно, как страшный суд. Поэтому задача старших коллег быть как можно осторожнее в выражениях и стараться объяснять, почему именно код плох и как его можно улучшить.
Я не пушу вечером. Лучше утром свежим взглядом просмотреть код и сделать push.
И code review рулит, конечно
отдыхайте и иногда возвращайтесь к старому коду, чтобы свежим взглядом оценить и поправить «хрень на скорую руку» и тому подобное ;)

ну либо делить работу на очень короткие итерации минут по 15-20. И не бояться сделать за те 15-20 минут что-то страшное, разобраться как оно выходит и потом удалить + переписать с нуля. Порой это быстрее чем пытаться что-то сделать разу нормально.

$locations = ['Austin', 'New York', 'San Francisco'];


ПЛОХО

foreach ($locations as $location) {
    doStuff();
    doSomeOtherStuff();
    dispatch($location);
});

В каждом цикле PHP будет пересоздавать переменную. Так же данный код работает заметно медленней, чем следующие два.

ПЛОХО

for ($i = 0; $i < count($locations); $i++) {
    $li = $locations[$i];
    doStuff();
    doSomeOtherStuff();
    dispatch($li);
}

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

ХОРОШО

for ($i = 0, $k=count($locations); $i < $k; $i++) {
    doStuff();
    doSomeOtherStuff();
    dispatch($locations[$i]);
}

Никаких лишних переменных и вычислений в циклах. Как по мне разница между $locations[$i] и $location не играет роли, а вот потери в скорости во втором случае при больших массивах будет существенно.
> $locations[$i] и $location не играет роли, а вот потери в скорости во втором случае при больших массивах будет существенно.

ох уж эти микрооптимизации… в плане производительности разницы не будет никакой
Не стоит забывать, что бы уже не в 2013-м году. Интерпретатор очень сильно поумнел в версии 7.0. И такие микрооптимизации бессмысленны.

Докажу тестом:
Исходный код теста
Доступен на GitHub Gist, вносите правки, если я где-то ошибся.

В тесте измеряем изменение в потребляемой памяти в байтах и время выполнения в микросекундах
<?php 
echo 'PHP ' . phpversion() . PHP_EOL . PHP_EOL;
$test = [];
for($i = 0; $i < 100000; $i++)
	$test[$i] = $i;
process_test(function() use ($test) {
	foreach($test as $t)
	    continue; 
});
process_test(function() use ($test) {
	for($a = 0; $a < sizeof($test); $a++)
		continue; 
});
process_test(function() use ($test) {
	for($a = 0, $b = sizeof($test); $a < $b; $a++)
		continue;  
});
function process_test($callback) {
	static $runNumber = 0;
	$runNumber++;

	$startedAt = microtime(true);
	$callback();
	echo 'Test #' . $runNumber . ': ' . ( microtime(true) - $startedAt ) . ' seconds' . PHP_EOL;
}

Результаты для PHP 5.6
Test #1: 0.018749952316284 seconds
Test #2: 0.011775016784668 seconds
Test #3: 0.0035569667816162 seconds
Результаты для PHP 7.0
Test #1: 0.0010159015655518 seconds
Test #2: 0.0030970573425293 seconds
Test #3: 0.0013790130615234 seconds

Интересно, я достаточно большой массив протестировал?
Как видно из теста, в PHP 7.0 код с foreach (Test #1) работает быстрее, чем ваш «оптимизированный» код (Test #3). Глупо использовать в новых проектах PHP 5.6, когда в 7.0 появилось столько вкусняшек.

И вот ответьте: стоит ли жертвовать читабельностью кода ради микрооптимизаций?
Хотите экономию памяти? Читайте про генераторы.
Хотите быструю работу? Используйте PHP 7, он почти в два раза быстрее PHP 5.6
Согласен, что для 7 актуальности не несет. Но, если я не ошибаюсь, доля 5.6 еще достаточно велика, чтобы не забывать о таких вещах)
Хочу на всякий случай заметить, что Laravel 5.5 LTS требует 7.0 в минималках, Symfony 4.0 LTS уже 7.1, Doctrine — тоже 7.1, Propel 3 — тоже 7.1, а версия 7.2 на данный момент находится на стадии релиз кандидата. Исходя из этих критериев можно заявлять, что у большинства современных или актуальных проектов на данный момент стоит 7.0 или 7.1.

Доля php 5.6, подозреваю, такая же, как и 5.5, как и 5.4 и прочих, может чуть более. Каждый год образуется некий процент людей, которые живут (написали или поддерживают) в легаси коде и не могут переехать на актуальные версии по этим или иным причинам (ну например у них говнобитрикс или просто лень).

5.6 всё-таки нельзя ещё считать легаси, поскольку она активно поддерживается, наряду с 7.0 и 7.1, в статусе current stable.

5.6 не «активно поддерживается», а «поддерживается в режиме патчей безопасности»: php.net/supported-versions.php — это значит что-то вроде «deprecated». Т.е. пока есть, но в будущем мы избавимся.

Статус «current stable» применяется для текущих актуальных версий с полной поддержкой, т.е. 7.0 и 7.1. Через 3 месяца PHP 7.0 будет точно так же задепрекейчен и признан устаревшим, а через год прекратится его полная поддержка.

http://php.net/downloads.php — немного другая картина.


В любом случае экслуатировать сейчас 5.6 — не признак отсталости, но переход на 7 в течение года надо планировать.

7.0 на той же странице помечен как «Old stable» (можно сравнить с графиком поддержки), так что переход на устаревшую версию, думаю, не имеет смысла. Подозреваю, что пометка 5.6 как «Current stable» — это просто опечатка, учитывая то, что время поддержки такое же, как у 7.0, а он помечен как устаревший.
UFO just landed and posted this here
Именно по-этому время жизни 5.6 такое же, как и 7.0. Но на данный момент прошло, напоминаю, уже два года с момента выхода 7.0.
UFO just landed and posted this here
Он не будет пересоздавать переменную, он будет перезаписывать её значение, стоимость чего значительно ниже чем стоимость создания, это без учёта возможных оптимизаций в трансляторе

Travelcamnet, кстати, отвечая на ваши примеры (а точнее объясняя причину минусов), рассказываю: цикл for будет самым медленным из списка, а foreach всегда является самым эффективным и быстрым решением, всегда (думаю, кроме SplFixedArray, но не уверен).


Почему for — плохо:
1) Массивы представлены в виде хеш-мапы, а значит для поиска $i элемента в массиве — потребуется линейное время N
2) $locations может быть итератором, в т.ч. non-countable, например корутиной, а значит count не сработает.
3) Пример не красивый


foreach самый быстрый и эффективный:
1) $location содержит память только на текущий элемент и выделяется только на один zval за всё время цикла (далее просто изменяется адресация). Количество ссылок на объект можно посмотреть функцией zval dump. Под конец цикла должна оставаться ровно один указатель на объект, чтобы существующая память выделилась под следующий "тик" итератора (в этом случае, я кстати, не уверен, надо смотреть). Так что аргументация на тему "создания новой переменной" не только не аргументирована, но ещё и прямо противоположна реальности и работе Zend VM.
2) foreach ровно на N-1 быстрее цикла for, т.к. происходит вызов next у итератора, а не поиск по хеш-мапе.
3) Функция count не является синонимом для максимального индекса (maxIndex != length - 1) хеш-мапы. foreach игнорирует индексы и является полноценным способом итерации, т.е. в случае удаления элемента из хеш-мапы (которая массив) — он не начнёт кидаться ошибками, как for при поучении несуществующего элемента.
4) foreach итерируется по корутинам и другим итераторам, в т.ч. бесконечным, т.к. ему вообще пофигу что внутри, в отличие от примеров с for, где итератор обязан имплементировать ArrayAccess.

спор foreach vs for должен начинаться и заканчиваться статистикой ошибок в условиях цикла.

Хорошо:

class UserAuth {
    private $user;
    public function __construct($user) {
        $this->user = user;
    }

    protected function verifyCredentials() {
        // ...
    }
}

class UserSettings {
    private $user;
    public function __construct($user) {
        $this->user = $user;
        $this->auth = new UserAuth($user);
    }

    public function changeSettings($settings) {
        if ($this->auth->verifyCredentials()) {
            // ...
        }
    }
}

PSR — Что такое?
DI — И так сойдет)))

Вцелом статья хорошая, но нужно было конечно кому то показать)

Автор оригинала содрал код отсюда и даже не особо заморачивался с переводом в PHP код (1, 2, 3, 4).

Формальное определение: «Если S — это подтип Т, то объекты типа Т могут быть заменены объектами типа S (например, вместо объектов типа Т можно подставить объекты типа S) без изменения каких-либо свойств программы (корректность, задачи и т. д.)». Ещё более пугающее определение.

Можно объяснить проще: если у вас есть родительский и дочерний классы, тогда они могут быть взаимозаменяемы без получения некорректных результатов.

Более простое объяснение не соотвествует формальному. Не взаимозаменяемы, а родительский заменяем на дочерний.


Сцепление — очень плохой шаблон разработки, затрудняющий рефакторинг кода.

Сцепление не шаблон, а характеристика, метрика кода.

Господа, прошу, не нужно учить весь мир создавать пустые конструкторы.
Это все приводит к тому, что народ делает вещи наподобие:
$lionOptions=['size' => 'big', 
                     'color' => 'black']
$lion=new Lion($lionOptions);


И если в php7 при указании типов это хоть как то переживаемо, то в php5 код превращается в неюзабельное нечто.

И даже если сеттеров не очень много, не очень правильно создавать неполноценных львов, вы не находите?
«Не пишите в глобальные функции» — и сделали синглтон. Сделали ЕЩЕ хуже. Синглтон так же глобален, только без слова глобал.

Очень часто можно услышать "не используйте ххх" без аргументации "почему не использовать" (да ещё и капслоком накричали). У синглтона две проблемы — это глобальный стейт и скрытые зависимости в местах использования.


Проблема глобалсов же — в неконтролируемости data-flow (плюс всё, перечисленное выше для синглтона). При этом, в случае синглтона мы получем контроль за состоянием, а значит и допустимость stateless, например в тестировании, через какой-нибудь вспомогательный метод setInstance(?Singleton $instance): void


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


В любом случае синглтон, максимум для чего может понадобиться — это для DI контейнера. Больше я хз где его можно применять.


Хочется услышать ваше мнение где я ошибаюсь. Почему синглтон хуже глобалсов? Лично я категорически не согласен, это глобалсы на несколько порядков хуже синглтонов.

При этом, в случае синглтона мы получем контроль за состоянием

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

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

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


Но всё равно мой тезис о том, что "глобалсы — это треш и если уж кто-то упоролся до глобального стейта, то лучше уж синглтоны" остаётся в силе =)

Вообще-то вы очень сильно поспешили с переводом.
Там куча косяков. Большую часть я поправил, но например реализация того же LSP не правильная.
А про использование SECONDS_IN_A_DAY я вообще молчу.


Если уж сделали перевод, поделитесь с сообществом.

И актуализируйте пожалуйста перевод до текущей версии. Перевод сильно отстал.

В разъяснении принципа Лисков серьёзная ошибка:
Можно объяснить проще: если у вас есть родительский и дочерний классы, тогда они могут быть взаимозаменяемы без получения некорректных результатов

Они не взаимозаменяемы. Принцип говорит о том, что экземпляр родительского класса можно заменить экземпляром дочернего класса, и всё будет хорошо. В обратную сторону заменять, разумеется, нельзя: смысл дочернего класса в расширении функционала родителя, и если вы возьмёте экземпляр родительского класса и попробуете обратиться к этому расширенному функционалу — вас ждут неприятности.
UFO just landed and posted this here

Это сделано для демонстрации проблемы LSP
Есть пример с immutable классами.

UFO just landed and posted this here

Пример с прямоугольником и квадратом самый популярны при описании принципа LSP.


В PR код завязан на конкретные типы потому, что квадрат и прямоугольник это разные типы. В этом и суть LSP

UFO just landed and posted this here

Спасибо за замечание. Уже убрал интерфейс Shape.

UFO just landed and posted this here

рука_лицо.джпег! при том что вынесение одной в подтип другой является нарушением LSP, о чем и говорит весь раздел


Если вам не нравится пример, предложите лучше, а не критикуйте.
Это открытый проект.

UFO just landed and posted this here

Вы не поверите. Я не только Википедию читал, но и ещё с десяток статей по теме.
Но вы похоже не поняли о чем статья и о чем проект.


Вы мне раз за разом пытаетесь объяснить как правильно реализовать LSP.
Задача же проекта показать проблему, то есть пример нарушения LSP, и привести пример решения конкретной проблемы, то есть решение которое не будет нарушать LSP. Как ваше решение, так и моё не нарушает LSP, что и требуется.


Код решат разные задачи потому и правильное решение будет разным.


Ну и авторам больше по нраву другой пример.

UFO just landed and posted this here
Как же вы читали то Википедию?

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


Фигур много, вам придется под каждую писать свою версию клиентского кода! Вы завязаны на реализацию, а не на абстракцию.

Естественно клиентский код завязан на реализации. Фигур то много, и свойства у них разные и действия над ними можно выполнять разные. Вы все свойства сущностей выносите в абстракцию?


Вы вынесли в абстракцию, метод расчета площади и думаете что всё хорошо. А вот и нет.


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


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


Если вернуться к вашему решению, то в нем есть несколько проблем.


  1. Вы сделали класс неизменяемым как и я. Но посмотрите на реализацию. Одной лишь неизменяемости достаточно чтоб решить описанную проблему.

Ваша реализация
interface Shape
{
    public function area();
}

class Rectangle implements Shape
{
    private $width;
    private $height;

    public function __construct($width, $height)
    {
        $this->width = $width;
        $this->height = $height;
    }

    public function area()
    {
        return $this->width * $this->height;
    }
}

class Square implements Shape
{
    private $length;

    public function __construct($length)
    {
        $this->length = $length;
    }

    public function area()
    {
        return pow($this->length, 2);
    }
}

function printArea(Shape $shape)
{
    echo $shape->area();
}

Упрощенная форма
class Rectangle
{
    private $width;
    private $height;

    public function __construct($width, $height)
    {
        $this->width = $width;
        $this->height = $height;
    }

    public function area()
    {
        return $this->width * $this->height;
    }
}

class Square extends Rectangle
{
    public function __construct($length)
    {
        parent::__construct($length, $length);
    }
}

function printArea(Rectangle $rectangle)
{
    echo $rectangle->area();
}

Обратите внимание, что квадрат стал подтипом прямоугольника и код работает правильно. И в таком виде он не нарушает LSP.


function printSquareArea(Square $square)
{
    echo $square->area();
}

Но с развитием проекта могут вылезти новые несоответствия и тогда появится необходимость разделить их на отдельные классы. И это уже относится к принципу YAGNI.


Также обратите внимание, что зависимость printArea() от Rectangle, а не от Shape это нарушения DIP, а не LSP.


  1. Ваш пример слишком сильно отличается от оригинала. Новички и майнтейнер не поймут изменений.

Bad
class Rectangle
{
    protected $width;
    protected $height;

    public function __construct()
    {
        $this->width = 0;
        $this->height = 0;
    }

    public function render($area)
    {
        // ...
    }

    public function setWidth($width)
    {
        $this->width = $width;
    }

    public function setHeight($height)
    {
        $this->height = $height;
    }

    public function getArea()
    {
        return $this->width * $this->height;
    }
}

class Square extends Rectangle
{
    public function setWidth($width)
    {
        $this->width = $this->height = $width;
    }

    public function setHeight(height)
    {
        $this->width = $this->height = $height;
    }
}

function renderLargeRectangles($rectangles)
{
    foreach ($rectangles as $rectangle) {
        $rectangle->setWidth(4);
        $rectangle->setHeight(5);
        $area = $rectangle->getArea(); // BAD: Will return 25 for Square. Should be 20.
        $rectangle->render($area);
    }
}

$rectangles = [new Rectangle(), new Rectangle(), new Square()];
renderLargeRectangles($rectangles);

Good
interface Shape
{
    public function name(): string;

    public function area(): float;
}

class Square implements Shape
{
    private $length;

    public function __construct(float $length)
    {
        $this->length = $length;
    }

    public function name(): string
    {
        return 'Square';
    }

    public function area(): float
    {
        return $this->length * $this->length;
    }
}

class Rectangle implements Shape
{
    private $width;
    private $height;

    public function __construct(float $width, float $height)
    {
        $this->width = $width;
        $this->height = $height;
    }

    public function area(): float
    {
        return $this->width * $this->height;
    }

    public function name(): string
    {
        return 'Rectangle';
    }
}

function printShape(Shape $shape)
{
    echo sprintf('%s has area %.2f.', $shape->name(), $shape->area()).PHP_EOL;
}

$shapes = [
    new Rectangle(3.6, 7.1),
    new Square(3),
];

foreach ($shapes as $shape) {
    printShape($shape);
}

  1. Вы сделали объекты неизменяемым, но частой задачей является создание нового объекта на основе старого.
    В задаче нужно посчитать и вывести площадь фигуры с заданными размерами. Ваш код не делает этого. Он делает что-то совершенно другое.

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


Сотрясать воздух каждый может, а как на счёт дела? Пока что я практически в одиночку пытаюсь привести проект в божеский вид.

UFO just landed and posted this here

Давайте попробуем сформулировать задание.


Задача:


  • Написать Bad код в котором будет явная ошибка из-за нарушения LSP.
  • Написать Good код который решает ошибку в Bad коде.
  • Good код должен не очень сильно отличаться от Bad кода чтобы новички могли понять как из одного получилось второе. (Майнтейнер тоже туговат и сложные примеры отклоняет)
  • Код Bad и Good примеров должен быть не слишком сложен чтоб новички могли в нем разобраться. Допустим лимит 100 строк.

Не обязательно реализовывать задание именно на квадратах. Можно взять уток. Мне например, нравится пример с бойлерами, но он более сложный на мой взгляд.


В целом, ваше решение мне нравится. У меня была такая же мысль, но я откланил её потому, что Good код в результате слишком сильно отличаться от Bad.
Возможно мы придем к чему-то похожему.

UFO just landed and posted this here

Это на ваше усмотрение. Можете площадь посчитать.

UFO just landed and posted this here

В примере с птицами мы получаем ту же проблему что есть сейчас:


void ArrangeBirdInPattern(Bird* aBird)
{
    if(aBird instanceof FlightfulBird)
        ArrangeBirdInSky(aBird);
    else
        ArrangeBirdOnGround(aBird);
}
UFO just landed and posted this here

Нет. Я его не проигнорировал, а как раз таки наоборот, я его использовал.


Как по вашему функция ArrangeBirdInPattern() будет определять может ли птица летать?


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

А если у вас есть водоплавающие птицы? космические? Проблема с instanceof легко решается тем, что создается некий ArrangmentResolver с методом ArrangementResolver::getArrangement, возвращающий Arrangement с методом Arrange(Bird* aBird) для необходимого класса птиц.

Да, больше сложности, зато более SOLIDно, т.к. нам больше не придется модифицировать этот метод при появлении новых видов птиц.

Это уже получается шаблон Strategy.
Тоже неплохое решение хотя слишком сложное для примера.

Уберите, пожалуйста, закрывающие круглые скобки у циклов. И в двух следующих функциях тоже.
function tokenize($code) {
    $regexes = [
        // ...
    ];

    $statements = split(' ', $code);
    $tokens = [];
    foreach($regexes as $regex) {
        foreach($statements as $statement) {
            $tokens[] = /* ... */;
        });
    });

    return $tokens;
}
[...]
Sign up to leave a comment.