Adopt articles for astro
This commit is contained in:
@@ -1,11 +1,12 @@
|
|||||||
import { defineConfig } from 'astro/config';
|
import { defineConfig } from 'astro/config';
|
||||||
import tailwindcss from '@tailwindcss/vite';
|
import tailwindcss from '@tailwindcss/vite';
|
||||||
|
import mdx from '@astrojs/mdx';
|
||||||
import vue from '@astrojs/vue';
|
import vue from '@astrojs/vue';
|
||||||
import sitemap from '@astrojs/sitemap';
|
import sitemap from '@astrojs/sitemap';
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
site: 'https://vakhrushev.me',
|
site: 'https://vakhrushev.me',
|
||||||
integrations: [vue(), sitemap()],
|
integrations: [mdx(), vue(), sitemap()],
|
||||||
vite: {
|
vite: {
|
||||||
plugins: [tailwindcss()],
|
plugins: [tailwindcss()],
|
||||||
},
|
},
|
||||||
|
|||||||
7
package-lock.json
generated
7
package-lock.json
generated
@@ -8,6 +8,7 @@
|
|||||||
"name": "homepage",
|
"name": "homepage",
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@anwinged/predictor": "^0.2.1",
|
||||||
"@astrojs/mdx": "^4",
|
"@astrojs/mdx": "^4",
|
||||||
"@astrojs/rss": "^4",
|
"@astrojs/rss": "^4",
|
||||||
"@astrojs/sitemap": "^3",
|
"@astrojs/sitemap": "^3",
|
||||||
@@ -27,6 +28,12 @@
|
|||||||
"url": "https://github.com/sponsors/antfu"
|
"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": {
|
"node_modules/@astrojs/compiler": {
|
||||||
"version": "2.13.1",
|
"version": "2.13.1",
|
||||||
"resolved": "https://registry.npmjs.org/@astrojs/compiler/-/compiler-2.13.1.tgz",
|
"resolved": "https://registry.npmjs.org/@astrojs/compiler/-/compiler-2.13.1.tgz",
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
"@astrojs/mdx": "^4",
|
"@astrojs/mdx": "^4",
|
||||||
"vue": "^3",
|
"vue": "^3",
|
||||||
"tailwindcss": "^4",
|
"tailwindcss": "^4",
|
||||||
"@tailwindcss/vite": "^4"
|
"@tailwindcss/vite": "^4",
|
||||||
|
"@anwinged/predictor": "^0.2.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
125
src/components/PredictorDemo.vue
Normal file
125
src/components/PredictorDemo.vue
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
<template>
|
||||||
|
<div class="app" tabindex="0" @keyup="press">
|
||||||
|
<div v-if="isHumanWin">
|
||||||
|
<p>Победа! Было очень сложно, но вы справились, поздравляю!</p>
|
||||||
|
<button class="btn" @click.prevent="restart">Заново</button>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="isRobotWin">
|
||||||
|
<p>
|
||||||
|
Упс, железяка победила. Оказывается, предсказать выбор человека
|
||||||
|
не так уж и сложно, да?
|
||||||
|
</p>
|
||||||
|
<button class="btn" @click.prevent="restart">Заново</button>
|
||||||
|
</div>
|
||||||
|
<div v-else>
|
||||||
|
<p class="score">{{ predictor.score }}</p>
|
||||||
|
<p class="step">Ход {{ step }}</p>
|
||||||
|
<div class="buttons">
|
||||||
|
<button class="btn btn-left" @click.prevent="click(0)">Нечет</button>
|
||||||
|
<button class="btn btn-right" @click.prevent="click(1)">Чет</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, computed } from 'vue';
|
||||||
|
import Predictor from '@anwinged/predictor';
|
||||||
|
|
||||||
|
const MAX_SCORE = 50;
|
||||||
|
|
||||||
|
function makePredictor() {
|
||||||
|
return new Predictor({
|
||||||
|
base: 2,
|
||||||
|
daemons: [
|
||||||
|
{ human: 3, robot: 3 },
|
||||||
|
{ human: 4, robot: 4 },
|
||||||
|
{ human: 5, robot: 5 },
|
||||||
|
{ human: 6, robot: 6 },
|
||||||
|
{ human: 8, robot: 8 },
|
||||||
|
{ human: 12, robot: 12 },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const predictor = ref(makePredictor());
|
||||||
|
|
||||||
|
const isHumanWin = computed(() => predictor.value.score >= MAX_SCORE);
|
||||||
|
const isRobotWin = computed(() => predictor.value.score <= -MAX_SCORE);
|
||||||
|
const step = computed(() => predictor.value.stepCount() + 1);
|
||||||
|
|
||||||
|
function pass(value: number) {
|
||||||
|
if (Math.abs(predictor.value.score) >= MAX_SCORE) return;
|
||||||
|
predictor.value.pass(+value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function click(v: number) {
|
||||||
|
pass(v ? 1 : 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
function press(evt: KeyboardEvent) {
|
||||||
|
pass(evt.key === '1' ? 0 : 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
function restart() {
|
||||||
|
predictor.value = makePredictor();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.app {
|
||||||
|
display: block;
|
||||||
|
margin: 2em auto;
|
||||||
|
padding: 2em;
|
||||||
|
text-align: center;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app:hover {
|
||||||
|
border-color: #e6e6e6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.score {
|
||||||
|
font-size: 400%;
|
||||||
|
margin-top: 0.2em;
|
||||||
|
margin-bottom: 0.2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.step {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 3em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttons {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
display: inline-block;
|
||||||
|
color: #fff;
|
||||||
|
background-color: #1b5fad;
|
||||||
|
padding: 0.6em 1.2em;
|
||||||
|
border: none;
|
||||||
|
font-size: 100%;
|
||||||
|
font-family: inherit;
|
||||||
|
border-radius: 0.5em;
|
||||||
|
cursor: pointer;
|
||||||
|
min-width: 7em;
|
||||||
|
margin: 0.2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:hover {
|
||||||
|
background-color: #154a88;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-left {
|
||||||
|
border-top-right-radius: 0;
|
||||||
|
border-bottom-right-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-right {
|
||||||
|
border-top-left-radius: 0;
|
||||||
|
border-bottom-left-radius: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
15
src/content.config.ts
Normal file
15
src/content.config.ts
Normal file
@@ -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 };
|
||||||
134
src/content/articles/2019-05-01-predictor.mdx
Normal file
134
src/content/articles/2019-05-01-predictor.mdx
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
---
|
||||||
|
title: Гадалка Шеннона
|
||||||
|
description: Демо-версия электронной гадалки Шеннона
|
||||||
|
keywords: [гадалка, угадыватель, шеннон, чет-нечет]
|
||||||
|
---
|
||||||
|
import PredictorDemo from '../../components/PredictorDemo.vue';
|
||||||
|
|
||||||
|
В студенческое время я наткнулся на интересную статью об [игре "Чет-нечет"][game]
|
||||||
|
на домашней страничке пользователя [ltwood][ltwood].
|
||||||
|
|
||||||
|
Правила очень простые. Игрок загадывает один вариант из двух: "чет" или "нечет",
|
||||||
|
а оппонент пытается угадать выбор игрока. Если угадать не удалось, то очко получает
|
||||||
|
загадавший, а если угадать получилось - то угадывающий. Кто первым наберет 20 очков,
|
||||||
|
тот и молодец!
|
||||||
|
|
||||||
|
Кажется, что в этой игре все случайно. Случайно загадывается число, потом случайно
|
||||||
|
второй игрок пытается угадать что же было загадано. Я очень сильно удивился, когда
|
||||||
|
попробовал поиграть в эту игру с программой и за десять попыток так ни разу и не выиграл.
|
||||||
|
|
||||||
|
Парадокс в том, что мы _думаем_ что загадываем числа случайно. На самом деле все не так,
|
||||||
|
и последовательность загаданных чисел не случайна.
|
||||||
|
|
||||||
|
Исходного кода оригинальной гадалки в открытом доступе нет, есть только [описание алгоритма][algo],
|
||||||
|
по которому я сделал свою реализацию на TypeScrypt.
|
||||||
|
|
||||||
|
## Демоверсия
|
||||||
|
|
||||||
|
Попробуйте набрать 50 очков и выиграть. Чтобы выбирать вариант с клавиатуры,
|
||||||
|
кликните внутри серой рамки, а потом пользуйтесь клавишами "1" - нечет или "2" - чет.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<PredictorDemo client:visible />
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Как Это работает
|
||||||
|
|
||||||
|
Математически алгоритм на [странице][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
|
||||||
39
src/content/articles/2019-06-01-php-serialization.md
Normal file
39
src/content/articles/2019-06-01-php-serialization.md
Normal file
@@ -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 и специальные
|
||||||
|
функции для превращения объекта в массив и обратно.
|
||||||
85
src/content/articles/2019-06-28-storytelling.md
Normal file
85
src/content/articles/2019-06-28-storytelling.md
Normal file
@@ -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. Правда делает нас свободными. Верьте в то, что говорите. Аудитория хочет
|
||||||
|
правды, настоящности. Правда, сострадание, самоирония.
|
||||||
|
|
||||||
|
## Слайды
|
||||||
|
|
||||||
|
Цель слайда: напомнить, впечатлить, объяснить, доказать. Определить цель, делать
|
||||||
|
простым, смотреть на слайд со стороны аудитории.
|
||||||
|
|
||||||
|
- Фотографии. Размер (большой), смысл, честность, логика. Не использовать
|
||||||
|
фотографии для схем.
|
||||||
|
- Схемы. Простота, пошаговость, направление. Пошаговое разжевывание схем.
|
||||||
|
Схемы "как это устроено" и "как это работает". Схема-исория лучше
|
||||||
|
схемы-модели.
|
||||||
|
- Статистика. Результат (а не анализ), ничего лишнего, честность.
|
||||||
69
src/content/articles/2019-08-08-yandex-disk-image-hosting.md
Normal file
69
src/content/articles/2019-08-08-yandex-disk-image-hosting.md
Normal file
@@ -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
|
||||||
|
```
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
[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
|
||||||
25
src/content/articles/2019-09-26-highload-videos.md
Normal file
25
src/content/articles/2019-09-26-highload-videos.md
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
---
|
||||||
|
title: Как проектировать хайлоад, видео
|
||||||
|
keywords: [highload, высоконагруженные системы, онтико, олег бунин]
|
||||||
|
---
|
||||||
|
|
||||||
|
Нашел три замечательных видеоролика о высоконагруженных системах.
|
||||||
|
Архитектура, подходы, планирование, проблемы.
|
||||||
|
|
||||||
|
## Часть 1
|
||||||
|
|
||||||
|
<div class="youtube-embed-container">
|
||||||
|
<iframe src="https://www.youtube.com/embed/KmIE5K6adus" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
## Часть 2
|
||||||
|
|
||||||
|
<div class="youtube-embed-container">
|
||||||
|
<iframe src="https://www.youtube.com/embed/sCm4qUw28y4" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
## Часть 3
|
||||||
|
|
||||||
|
<div class="youtube-embed-container">
|
||||||
|
<iframe src="https://www.youtube.com/embed/MG8-HmgOXlk" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
|
||||||
|
</div>
|
||||||
@@ -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.
|
||||||
122
src/content/articles/2020-06-27-type-discriminant.md
Normal file
122
src/content/articles/2020-06-27-type-discriminant.md
Normal file
@@ -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` можно до определенного времени рассматривать как черный ящик,
|
||||||
|
просто зная, что это некий словарь параметров.
|
||||||
|
Когда будет понятно какой фильтр создавать, тогда этот набор параметров
|
||||||
|
послужит базой для создания объекта-фильтра.
|
||||||
|
|
||||||
|
## Заключение
|
||||||
|
|
||||||
|
- Если структура конфигурации предполагает создание нескольких разных объектов,
|
||||||
|
то лучше использовать специальное поле-дискриминатор для точного указания
|
||||||
|
типа создаваемого объекта.
|
||||||
|
- Все поля, которые зависят от типа, лучше перенести на дополнительный уровень,
|
||||||
|
тем самым сохранив структуру верхнего уровня постоянной (на верхнем уровне
|
||||||
|
будет всегда один и тот же набор полей).
|
||||||
177
src/content/articles/2020-11-08-nullable-fields.md
Normal file
177
src/content/articles/2020-11-08-nullable-fields.md
Normal file
@@ -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()` позволит обнаружить странное поведение.
|
||||||
|
- Если в пустом поле нет ничего не обычного, то подход "получить и проверить"
|
||||||
|
будет удобнее, все равно нужно делать проверку.
|
||||||
|
|
||||||
|
В любом случае, нужно смотреть на уместность того или иного подхода в каждом
|
||||||
|
случае, и не использовать их одновременно.
|
||||||
Reference in New Issue
Block a user