From 4b04afb912975fc0e866dac4b3180a4c2d8451b7 Mon Sep 17 00:00:00 2001 From: Anton Vakhrushev Date: Sat, 7 Mar 2026 11:14:50 +0300 Subject: [PATCH] Adopt articles for astro --- astro.config.mjs | 3 +- package-lock.json | 7 + package.json | 3 +- src/components/PredictorDemo.vue | 125 +++++++++++++ src/content.config.ts | 15 ++ src/content/articles/2019-05-01-predictor.mdx | 134 +++++++++++++ .../articles/2019-06-01-php-serialization.md | 39 ++++ .../articles/2019-06-28-storytelling.md | 85 +++++++++ .../2019-08-08-yandex-disk-image-hosting.md | 69 +++++++ .../articles/2019-09-26-highload-videos.md | 25 +++ ...020-06-27-interesting-programming-blogs.md | 16 ++ .../articles/2020-06-27-type-discriminant.md | 122 ++++++++++++ .../articles/2020-11-08-nullable-fields.md | 177 ++++++++++++++++++ 13 files changed, 818 insertions(+), 2 deletions(-) create mode 100644 src/components/PredictorDemo.vue create mode 100644 src/content.config.ts create mode 100644 src/content/articles/2019-05-01-predictor.mdx create mode 100644 src/content/articles/2019-06-01-php-serialization.md create mode 100644 src/content/articles/2019-06-28-storytelling.md create mode 100644 src/content/articles/2019-08-08-yandex-disk-image-hosting.md create mode 100644 src/content/articles/2019-09-26-highload-videos.md create mode 100644 src/content/articles/2020-06-27-interesting-programming-blogs.md create mode 100644 src/content/articles/2020-06-27-type-discriminant.md create mode 100644 src/content/articles/2020-11-08-nullable-fields.md diff --git a/astro.config.mjs b/astro.config.mjs index 03a5aa6..e67fc71 100644 --- a/astro.config.mjs +++ b/astro.config.mjs @@ -1,11 +1,12 @@ import { defineConfig } from 'astro/config'; import tailwindcss from '@tailwindcss/vite'; +import mdx from '@astrojs/mdx'; import vue from '@astrojs/vue'; import sitemap from '@astrojs/sitemap'; export default defineConfig({ site: 'https://vakhrushev.me', - integrations: [vue(), sitemap()], + integrations: [mdx(), vue(), sitemap()], vite: { plugins: [tailwindcss()], }, diff --git a/package-lock.json b/package-lock.json index 484c288..802bdff 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,7 @@ "name": "homepage", "version": "2.0.0", "dependencies": { + "@anwinged/predictor": "^0.2.1", "@astrojs/mdx": "^4", "@astrojs/rss": "^4", "@astrojs/sitemap": "^3", @@ -27,6 +28,12 @@ "url": "https://github.com/sponsors/antfu" } }, + "node_modules/@anwinged/predictor": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@anwinged/predictor/-/predictor-0.2.1.tgz", + "integrity": "sha512-817M9xiPesxLtvUH/qZNs3EBNw5HBOR/W8T3HYLqyNvMRCEJ/h86uJJB5BW+FzZc+mQsVQcRX+NI8pSOBr3jwg==", + "license": "MIT" + }, "node_modules/@astrojs/compiler": { "version": "2.13.1", "resolved": "https://registry.npmjs.org/@astrojs/compiler/-/compiler-2.13.1.tgz", diff --git a/package.json b/package.json index 2ad560b..6988dd6 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "@astrojs/mdx": "^4", "vue": "^3", "tailwindcss": "^4", - "@tailwindcss/vite": "^4" + "@tailwindcss/vite": "^4", + "@anwinged/predictor": "^0.2.1" } } diff --git a/src/components/PredictorDemo.vue b/src/components/PredictorDemo.vue new file mode 100644 index 0000000..87d6d9d --- /dev/null +++ b/src/components/PredictorDemo.vue @@ -0,0 +1,125 @@ + + + + + diff --git a/src/content.config.ts b/src/content.config.ts new file mode 100644 index 0000000..4a24c4c --- /dev/null +++ b/src/content.config.ts @@ -0,0 +1,15 @@ +import { defineCollection, z } from 'astro:content'; +import { glob } from 'astro/loaders'; + +const articles = defineCollection({ + loader: glob({ pattern: '**/*.{md,mdx}', base: './src/content/articles' }), + schema: z.object({ + title: z.string(), + description: z.string().optional(), + keywords: z.array(z.string()).optional(), + tags: z.array(z.string()).optional(), + draft: z.boolean().default(false), + }), +}); + +export const collections = { articles }; diff --git a/src/content/articles/2019-05-01-predictor.mdx b/src/content/articles/2019-05-01-predictor.mdx new file mode 100644 index 0000000..eba9697 --- /dev/null +++ b/src/content/articles/2019-05-01-predictor.mdx @@ -0,0 +1,134 @@ +--- +title: Гадалка Шеннона +description: Демо-версия электронной гадалки Шеннона +keywords: [гадалка, угадыватель, шеннон, чет-нечет] +--- +import PredictorDemo from '../../components/PredictorDemo.vue'; + +В студенческое время я наткнулся на интересную статью об [игре "Чет-нечет"][game] +на домашней страничке пользователя [ltwood][ltwood]. + +Правила очень простые. Игрок загадывает один вариант из двух: "чет" или "нечет", +а оппонент пытается угадать выбор игрока. Если угадать не удалось, то очко получает +загадавший, а если угадать получилось - то угадывающий. Кто первым наберет 20 очков, +тот и молодец! + +Кажется, что в этой игре все случайно. Случайно загадывается число, потом случайно +второй игрок пытается угадать что же было загадано. Я очень сильно удивился, когда +попробовал поиграть в эту игру с программой и за десять попыток так ни разу и не выиграл. + +Парадокс в том, что мы _думаем_ что загадываем числа случайно. На самом деле все не так, +и последовательность загаданных чисел не случайна. + +Исходного кода оригинальной гадалки в открытом доступе нет, есть только [описание алгоритма][algo], +по которому я сделал свою реализацию на TypeScrypt. + +## Демоверсия + +Попробуйте набрать 50 очков и выиграть. Чтобы выбирать вариант с клавиатуры, +кликните внутри серой рамки, а потом пользуйтесь клавишами "1" - нечет или "2" - чет. + +--- + + + +--- + +## Как Это работает + +Математически алгоритм на [странице][algo] сайта ltwood. +Я рассмотрю простой пример, чтобы показать принцип. + +В основе алгоритма находится популяция "демонов" - автоматов, которые на основании ходов +игрока и предсказанных значениях выдают новое предсказание. Демонами управляет +супервайзер. Задача супервайзера в том, чтобы опросить всех демонов, выбрать ответ +от одного их них, а после получения ответа игрока пометить тех, кто выдал правильный ответ. + +Алгоритм состоит из двух шагов: + +- предсказать следующих ход игрока; +- учесть реальный ход игрока, добавив веса тому демону, который предугадал ход. + +Рассмотрим работу на примере одного демона. + +Пусть у нас есть демон, который смотрит на последний хода игрока +и на свое последнее предсказание. + +Строим два вектора: + +- `[<1 ход демона>, <1 ход игрока>, 0]` +- `[<1 ход демона>, <1 ход игрока>, 1]` + +В самом начале, когда у демона нет никакой информации о ходах игрока, эти векторы +будут выглядеть как `[0]` и `[1]`. Но с накоплением данных, они всегда будут каждый +по 5 элементов. + +После чего смотрим, который из таких наборов в прошлом приносил победу чаще, +и соответственно выбираем или вариант с 0, или с 1. + +После получения действительного хода игрока, мы увеличиваем вес того набора, +который оказался верным. И далее снова предсказываем ход. + +Теперь с числами. + +#### Ход 1 + +У демона нет информации, наборы `[0]` и `[1]` равнозначны, выбираем `[0]`, +а значит предсказываем ход игрока 0. + +Игрок загадывал 1. Обновляем веса: + +``` +[0] = 0 +[1] = 1 +``` + +#### Ход 2 + +Строим векторы на основе последних ходов: + +``` +0: [0, 1, 0] +1: [0, 1, 1] +``` + +Для этих векторов тоже еще нет весов, так что снова выбираем первый, предсказываем 0. + +Игрок снова выбрал 1. Обновляем веса (помним, что еще были прошлые вектора из одного элемента): + +``` +[0] = 0 +[1] = 1 +[0, 1, 0] = 0 +[0, 1, 1] = 1 +``` + +#### Ход 3 + +Картина такая же, как на втором ходу, но отличие в том, что у нас есть веса с прошлого хода: + +``` +0: [0, 1, 0] - 0 +1: [0, 1, 1] - 1 +``` + +Выбираем вариант 1, игрок снова выбирает 1. Предсказание удалось! + +## Расширение алгоритма + +Это был самый элементарный вариант. Понятно, что на таком далеко не уедешь, +и никого не обыграешь. Чтобы хорошо предугадывать ходы игроков, используется +несколько демонов с разной величиной просматриваемой истории. Следит за ними +"супервайзер", который ведет для каждого демона рейтинг. На основе этого рейтинга +выбираются ответы тех демонов, которые были наиболее успешны в своих предсказаниях. + +## Ссылки + +- [Код гадалки][repo] +- [Описание алгоритма][algo] +- [Описание игры у ltwood][game] + +[ltwood]: https://sites.google.com/site/ltwood/ +[game]: https://sites.google.com/site/ltwood/projects/heshby +[algo]: https://sites.google.com/site/ltwood/projects/heshby/algorithm +[repo]: https://github.com/anwinged/predictor diff --git a/src/content/articles/2019-06-01-php-serialization.md b/src/content/articles/2019-06-01-php-serialization.md new file mode 100644 index 0000000..4ab963b --- /dev/null +++ b/src/content/articles/2019-06-01-php-serialization.md @@ -0,0 +1,39 @@ +--- +title: Сериализация в PHP +description: Проблема долговременного хранения сериализованных данных +keywords: [php, serialization, сериализация, пхп] +--- + +В PHP есть две функции для сериализации и десериализации данных: `serialize()` и +`unserialize()`. Функции встроены в язык, не требуют дополнительных модулей. + +В один момент кто-то решает использовать их для долговременного хранения +объектов. В базе данных, на диске, еще где-то. + +``` +namespace Test\Serialize; + +class A {} + +$a = new A(); +serialize($a); +``` + +И тут начинаются проблемы. + +Дело в том, что при сериалзации объектов классов кроме самих данных объекта +сохраняется еще и информация о классе. Его имя, пространство имен. + +Результатом сериализации в примере выше будет: + +``` +O:16:"Test\Serialize\A":0:{}O:16:"Test\Serialize\A":0:{} +``` + +Если теперь произвести рефакторинг, переместить класс, изменить пространство +имен, то десериализация уже не сработает. И будет больно. + +Не делайте так. + +Контролируйте процесс сериализации. Например, используйте JSON и специальные +функции для превращения объекта в массив и обратно. diff --git a/src/content/articles/2019-06-28-storytelling.md b/src/content/articles/2019-06-28-storytelling.md new file mode 100644 index 0000000..35eba67 --- /dev/null +++ b/src/content/articles/2019-06-28-storytelling.md @@ -0,0 +1,85 @@ +--- +title: Сторителлинг +description: Конспект видеолекций Алексея Каптерева о сторителлинге и презентации +keywords: [story, storytelling, lectures, история, сторителлинг, каптерев] +--- + +Это конспект трех лекций Алексея Каптерева, где он рассказывает о презентациях. +Как придумать историю, как подготовить слайды и что рассказывать. + +- [https://youtu.be/PaoKhNLyxdk](https://youtu.be/PaoKhNLyxdk) +- [https://youtu.be/18M9ZJRU2wI](https://youtu.be/18M9ZJRU2wI) +- [https://youtu.be/KyyAgiw8B6I](https://youtu.be/KyyAgiw8B6I) + +## История + +Самый главный вопрос: "Чего я хочу?", а второй - "Что им нужно?" Далее нужно +найти пересечение ответов на эти два вопроса. "Что я хочу, чтобы у вас +получилось?" + +Эмоции = Мотивация + +Я хочу, чтобы они + +- поняли, что... +- изменили мнение о... +- сделали... + +Кто? Кому? Что? Зачем? Как? + + История = интересная тема + близость темы + сценарий + + История = факты + смысл + эмоции + +Основные составляющие истории: + +- **Герой** Кто хочет? Я сам, клиент, клиент клиента. +- **Цель** Что хочет? +- **Проблема / слабость** Что мешает, какая проблема? Почему он не может без + этого жить? +- **Злодей** Кто мешает? +- **Решение** Пути обхода? В чем инсайт? +- **Цена** Плата за решение? +- **Мораль** В чем призыв к действию? Если вы сделаете Х, то будет Y, иначе Z. + +Факт -> Проблема -> Решение -> Но не все так просто -> Теперь уж точно + +План, зум (уточнение) частей плана - деревья представления. + +Концовка - самое тяжелое, начните с концовки. + +Говорите то, что вас зажигает. + +Чтобы заинтересовать человека в письменном виде, нужно сделать письмо +максимально похожим на устную речь. С героем, целью и так далее. + +## Презентации + +Таблицы для анализа, презентации для результата. + +1. Монотонность - враг истории. +2. Дело не в "умении выступать" (владейте сутью, слова найдутся). +3. История - это не сложно. Экспозиция, проблематизация, решение, выводы. +4. Не начинайте с PowerPoint. +5. Вы не можете сказать всего (Скажите 5% от того, что вы знаете). Одна хорошая + мысль и _очень_ много дисциплины. +6. Лучше один раз увидеть (чем 100 раз услышать и 10 раз потрогать). +7. Слайды - они как дети. Уродливые, зато свои. Не увлекаться форматированием. + Чтобы выделить главное, нужно ответить на вопрос "Какова цель этого слайда?" +8. Дизайн - это вычитание. Пропорции, цвета, типографика. +9. Контакт глаз. При выступлении смотрите людям в глаза. Это значит, что вы не + врете. +10. Правда делает нас свободными. Верьте в то, что говорите. Аудитория хочет + правды, настоящности. Правда, сострадание, самоирония. + +## Слайды + +Цель слайда: напомнить, впечатлить, объяснить, доказать. Определить цель, делать +простым, смотреть на слайд со стороны аудитории. + +- Фотографии. Размер (большой), смысл, честность, логика. Не использовать + фотографии для схем. +- Схемы. Простота, пошаговость, направление. Пошаговое разжевывание схем. + Схемы "как это устроено" и "как это работает". Схема-исория лучше + схемы-модели. +- Статистика. Результат (а не анализ), ничего лишнего, честность. diff --git a/src/content/articles/2019-08-08-yandex-disk-image-hosting.md b/src/content/articles/2019-08-08-yandex-disk-image-hosting.md new file mode 100644 index 0000000..9869bd4 --- /dev/null +++ b/src/content/articles/2019-08-08-yandex-disk-image-hosting.md @@ -0,0 +1,69 @@ +--- +title: Яндекс.Диск для хостинга картинок +keywords: [яндекс.диск, хостинг картинок, yandex disk, image hosting, hosting] +--- + +У [Яндекс.Диска][ya-disk] есть замечательная функция. Он может создавать превью +загруженных фотографий. Эта функциональность не афишируется, но описана +в [документации][ya-api-preview]. + +У меня есть фотография на Диске `/img/kemsky.jpg`. Чтобы получить ее превью, +нужно выполнить запрос: + +``` +GET /img/kemsky.jpg?preview&size=XS +User-Agent: my_application/0.0.1 +Host: webdav.yandex.ru +Authorization: OAuth 0c4182a7c2cf4521964a72ff57a34a07 +``` + +Но есть проблема. Для запросов нужен токен. Без токена не получится использовать +это API для публичного хостинга. + +Решение - сервер [Caddy][caddy] в качестве прокси. Caddy очень +удобно использовать в качестве фронтенда для внутренних сервисов. +Он просто настраивается, а самое главное - поддерживает автоматический +выпуск и обновление SSL-сертификатов буквально одной строчкой конфига. +Скроем токен в конфигурации сервера, и будем передавать его при обращении +к Яндекс.Диску: + +``` +preview.vakhrushev.me { + proxy /img https://webdav.yandex.ru { + transparent + header_upstream User-Agent "yandex-disk-previewer/1.0" + header_upstream Authorization "OAuth 0c4182a7c2cf4521964a72ff57a34a07" + } + + tls anwinged@ya.ru +} +``` + +Директива `proxy /img` будет направлять все запросы с `preview.vakhrushev.me/img` +на `https://webdav.yandex.ru/img`. Таким образом во внешний +мир будет смотреть только директория `img`, а остальные останутся скрытыми. + +Кроме OAuth авторизации можно использовать Basic, передавая логин и +[пароль приложения][app-password]. Мне этот способ удобнее, +чтобы не заморачиваться с OAuth. Логин и пароль я храню +зашифрованными с помощью [Ansible Vault][vault]. +И строчка с заголовком тогда будет выглядеть так: + +``` +header_upstream Authorization "Basic {{ '{{' }} (yandex_disk.login ~ ':' ~ yandex_disk.password) | b64encode {{ '}}' }}" +``` + +А так будет выглядеть ссылка на картинку: + +``` +https://preview.vakhrushev.me/img/kemsky.jpg?preview&size=XXL +``` + +![Кемский поселок](https://preview.vakhrushev.me/img/kemsky.jpg?preview&size=XXL) + +[ya-disk]: https://disk.yandex.ru +[ya-api]: https://yandex.ru/dev/disk/doc/dg/concepts/quickstart-docpage/ +[ya-api-preview]: https://yandex.ru/dev/disk/doc/dg/reference/preview-docpage/ +[caddy]: https://caddyserver.com/ +[app-password]: https://yandex.ru/support/passport/authorization/app-passwords.html +[vault]: https://docs.ansible.com/ansible/latest/user_guide/vault.html diff --git a/src/content/articles/2019-09-26-highload-videos.md b/src/content/articles/2019-09-26-highload-videos.md new file mode 100644 index 0000000..a02e80b --- /dev/null +++ b/src/content/articles/2019-09-26-highload-videos.md @@ -0,0 +1,25 @@ +--- +title: Как проектировать хайлоад, видео +keywords: [highload, высоконагруженные системы, онтико, олег бунин] +--- + +Нашел три замечательных видеоролика о высоконагруженных системах. +Архитектура, подходы, планирование, проблемы. + +## Часть 1 + +
+ +
+ +## Часть 2 + +
+ +
+ +## Часть 3 + +
+ +
diff --git a/src/content/articles/2020-06-27-interesting-programming-blogs.md b/src/content/articles/2020-06-27-interesting-programming-blogs.md new file mode 100644 index 0000000..dd6c451 --- /dev/null +++ b/src/content/articles/2020-06-27-interesting-programming-blogs.md @@ -0,0 +1,16 @@ +--- +title: Интересные блоги о программировании +description: Сборник интересных блогов и статей о программирования для себя +keywords: [блоги, программирование, сборник, чистый код, ооп, haskell] +--- + +## Дизайн приложений + +- Сайт Мартина Фаулера - [martinfowler.com](https://martinfowler.com/). + Архитектура, рефакторинг, чистый код. +- Блог Роберта Мартина, или дядюшки Боба, о чистом коде - [The Clean Code Blog](https://blog.cleancoder.com/). + +## Функциональное программирование + +- Хорошие статьи о функциональном программировании с примерами на F# - [F# for fun and profit](https://fsharpforfunandprofit.com/). +- Блог [Alexis King](https://lexi-lambda.github.io/) о Haskell и Racket. diff --git a/src/content/articles/2020-06-27-type-discriminant.md b/src/content/articles/2020-06-27-type-discriminant.md new file mode 100644 index 0000000..c36b351 --- /dev/null +++ b/src/content/articles/2020-06-27-type-discriminant.md @@ -0,0 +1,122 @@ +--- +title: О полях-дискриминаторах +description: Заметка о том, как записывать конфигурацию для сложных объектов +keywords: [чистый код, дискриминатор, php, конфигурация] +--- + +Поля-дискриминаторы - это удобный прием для обработки нескольких типов +данных со схожей структурой. + +Лучше начать с примера. + +Допустим, у нас есть объект-фильтр для целых чисел. Можно применить фильтр +к последовательности чисел и получить новую последовательность. +Его параметры выглядят следующим образом: + +```json +{ + "from": 0, + "to": 10 +} +``` + +Отлично. +Фильтр пропустит только числа от 0 до 10. +По такой конфигурации без проблем можно создать объект. + +Теперь добавим второй фильтр - он будет отсекать нечетные числа. + +```json +{ + "odd": false +} +``` + +## Создаем фильтр на основе структуры + +Теперь есть два фильтра, и нужно понимать какой их них создать. +Простое и наивное решение - смотреть на структуру полей. +Если есть поле `odd`, то фильтр нечетных чисел, иначе - фильтр по диапазону. + +```php +if (isset($config['odd'])) { + // фильтр нечетных чисел +} else { + // фильтр диапазона +} +``` + +У этого решения масса недостатков. +Например, если поле `odd` понадобится двум фильтрам сразу, то условие усложнится. +Или появятся поля с разными названиями, но одинаковым смыслом. + +## Добавляем поле-дискриминатор + +Решение проблемы в добавление специального поля, в котором содержится имя фильтра. +Назовем это поле `type`. Тогда конфигурации фильтров будут выглядеть следующим образом: + +```json +{ + "type": "range", + "from": 0, + "to": 10 +} +``` + +```json +{ + "type": "odd_control", + "odd": false +} +``` + +Поле `type` - это и есть поле-дискриминатор. +По нему можно точно определить какой фильтр перед нами. + +```php +switch ($config['type']) { + case 'range': + // фильтр диапазона + break; + case 'odd_control': + // фильтр нечетных чисел + break; + default: + throw new \LogicException(sprintf( + 'Unknown filter type "%s"', $config['type'])); +} +``` + +## Выделить зависимые поля + +Решение еще можно улучшить. +Сейчас на одном уровне в структуре конфига есть и обязательные поля, и необязательные поля. +Мы можем перенести все необязательные поля на дополнительный уровень, +например, в поле `params`. +Эти поля необязательны для всей конфигурации, но обязательны для конкретного фильтра. + +```json +{ + "type": "range", + "params": { + "from": 0, + "to": 10 + } +} +``` + +Что дает такой маневр? +На верхнем уровне конфигурации у нас будет постоянная структура. +Поле `params` можно до определенного времени рассматривать как черный ящик, +просто зная, что это некий словарь параметров. +Когда будет понятно какой фильтр создавать, тогда этот набор параметров +послужит базой для создания объекта-фильтра. + +## Заключение + +- Если структура конфигурации предполагает создание нескольких разных объектов, + то лучше использовать специальное поле-дискриминатор для точного указания + типа создаваемого объекта. +- Все поля, которые зависят от типа, лучше перенести на дополнительный уровень, + тем самым сохранив структуру верхнего уровня постоянной (на верхнем уровне + будет всегда один и тот же набор полей). diff --git a/src/content/articles/2020-11-08-nullable-fields.md b/src/content/articles/2020-11-08-nullable-fields.md new file mode 100644 index 0000000..1e04535 --- /dev/null +++ b/src/content/articles/2020-11-08-nullable-fields.md @@ -0,0 +1,177 @@ +--- +title: Организация доступа к nullable полям класса +description: Заметка о том, лучше организовать доступ к полям класса, которые могут содержать значение null +keywords: [чистый код, php, "null", поля класса] +--- + +Нередкая ситуация, когда в классе есть поле, которое может содержать `null`. + +```php +class User +{ + private Email $email; + private ?string $name; +} +``` + +Пользователь может указать имя, а может и не указывать, +ограничившись только почтовым адресом. + +А далее мы пишем код, которые работает с полем имени. + +```php +class User +{ + private ?string $name; + + public function hasName(): bool + { + return $this->name !== null; + } + + public function getName(): ?string + { + return $this->name; + } + + public function setName(?string $name): void + { + $this->name = $name; + } +} +``` + +И использование этого кода: + +```php +/** @var User $user */ + +if ($user->hasName()) { + do_something_with_name($user->getName()); +} + +function do_something_with_name(string $name) {} +``` + +Выглядит хорошо. +Сначала убедились, что имя установлено, а потом использовали его. + +Но статический анализатор нам обязательно припомнит, что мы пытаемся передать +в функцию `do_something_with_name` значение типа `string|null`, хотя функция +ожидает значение типа `string`. +И получается дурацкая ситуация, что формально мы должны дописать +еще одну проверку. + +```php +/** @var User $user */ + +if ($user->hasName()) { + $name = $user->getName(); + if ($name !== null) { + do_something_with_name($name); + } +} + +function do_something_with_name(string $name) {} +``` + +Статический анализатор наш друг, он помогает находить ошибки и несоответствия +в коде. +И здесь он нашел такое формальное несоответствие типов. + +Статический анализатор прав, а мы, как проектировщики интерфейса, не правы. +На самом деле мы смешали два подхода, когда описывали методы в нашем классе: + +1. Получить и проверить +2. Проверить и получить + +## Получить и проверить + +И сразу начнем с примера использования. + +```php +$name = $user->getName(); +if ($name !== null) { + do_something_with_name($name); +} +``` + +Сначала мы получаем значение поля, а потом проверяем, соответствует ли это +значение нашим требованиям. Класс при этом будет построен вот так: + +```php +class User +{ + private ?string $name; + + public function getName(): ?string + { + return $this->name; + } + + public function setName(?string $name): void + { + $this->name = $name; + } +} +``` + +Заметьте, здесь уже нет метода `hasName()`, потому что этот метод перестал быть +нужным. Его роль исполняет метод `getName()`. + +## Проверить и получить + +Второй подход: сначала проверяем значение, а потом работаем с ним: + +```php +if ($user->hasName()) { + do_something_with_name($user->getName()); +} +``` + +Структура класса: + +```php +class User +{ + private ?string $name; + + public function hasName(): bool + { + return $this->name !== null; + } + + public function getName(): string + { + if ($this->name === null) { + throw new \LogicException('Name is not set'); + } + + return $this->name; + } + + public function setName(?string $name): void + { + $this->name = $name; + } +} +``` + +Смотрите отличия. +Метод `hasName()` остается. +А вот метод `getName()` теперь возвращает значение типа `string`. +Он выбросит исключение, если мы попытаемся получить значение, +которое не установлено. + +## Использование + +Теперь встает вопрос, когда и какой подход следует использовать. + +- Если ситуация, когда поле не установлено, скорее исключительная, нежели + обычная, то можно использовать второй подход, а проверку опустить. + Исключение в методе `getName()` позволит обнаружить странное поведение. +- Если в пустом поле нет ничего не обычного, то подход "получить и проверить" + будет удобнее, все равно нужно делать проверку. + +В любом случае, нужно смотреть на уместность того или иного подхода в каждом +случае, и не использовать их одновременно.