Проводить оценку производительности при реальной нагрузке в продуктивном контуре, а не на тестовом стенде.
Не иметь метрик производительности информационной системы, оценивать производительность по обратной связи пользователей — «ой стало медленно работать».
Не проводить тестирование последствий изменений инфраструктуры на тестовом стенде, сразу проводить изменения в продуктивном контуре.
Воспринимать СУБД как черный ящик для хранения данных.
Не привлекать DBA к процессу дизайна и разработки.
[ Использовать ORM для взаимодействия с СУБД.] : по итогам дискуссии в комментариях - пункт исключён из списка особенностей.
Не анализировать коды ошибок СУБД в backend.
Не проводить нагрузочное тестирование.
Пытаться решать проблемы деградации производительности информационной системы путем подбора магической комбинации конфигурационных параметров СУБД и увеличением ресурсов серверов.
Запустить десяток SELECT ... FOR UPDATE и удивляться - почему всё тормозит.
Семь бед - один ответ. Ставь костыль изобрети велосипед.
Все началось с рабочей задачи по реализации весьма специфичной web-панели мониторинга и управления различным оборудованием, в которой нужно было получать данные и отправлять команды по разнообразным сценариям (периодический опрос, подписка на websocket-события, https-запросы и т.д.), а также выводить состояние и графики в реальном времени, динамически комбинируя различные источники данных в разных виджетах.
Кроме того, у панели было предусмотрено несколько настраиваемых режимов работы, которые переключались по запросу пользователя, что требовало массового управления маршрутами потоков данных внутри системы.
В первой итерации с использованием RxJS получилось много неструктурированного и сложно поддерживаемого кода, так как архитектура на RxJS вынуждала императивно описывать перестроение топологии при изменении правил маршрутизации «на лету». В нашем специфичном кейсе с динамическими виджетами это приводило к сайд-эффектам и сложностям в отладке. Кроме того, местами размывалась строгая типизация и мы лишались compile-time гарантий. Так появилась идея разработать собственное решение на основе альтернативной концепции — модели графа потоков данных.
В результате получившаяся модель продемонстрировала предсказуемое поведение и низкую связность компонентов, а кодовая база сократилась на ~30% и приобрела более декларативный вид. Убедившись в эффективности решения, мы решили оформить его в виде отдельной библиотеки с открытым исходным кодом — Transferum.
И что, получилась просто еще одна реактивная библиотека?
Не совсем. Классические FRP-библиотеки (RxJS, Bacon, Most) построены вокруг единственного примитива (Observable). Transferum же основан на композиции различных типов узлов с явно определенным поведением.
Каждый узел в графе потоков распространения данных явно декларирует свои способности: может ли он принимать данные через push, отдавать через pull, распространять полученный сигнал подписчикам, опрашивать источник, фильтровать, блокировать поток и т.д.
Объявленные узлом возможности являются одновременно флагами для использования в runtime и compile-time гарантиями наличия соответствующих методов, определяющих его поведение.
Ключевая идея: поведение системы описывается как композиция независимых возможностей, которые одновременно определяют тип, реализацию и правила взаимодействия.
Transferum предоставляет четыре слоя абстракции:
Трансферы — узлы графа (каналы, поллеры, мапперы, буферы, разветвители и концентраторы, реализации debounce, throttle, switchMap и т.д.).
Мосты — ребра графа — управляемые вентили между узлами с динамической маршрутизацией и гейтингом.
Операторы — stateless-трансформаторы и фильтры данных (используются трансферами, отвечающими за конвертацию данных).
Билдеры — fluent-конструкторы композитных трансферов из цепочек трансферов-примитивов.
Концептуальная и архитектурная основа — capability flags system. Каждый трансфер реализует CommunicationContractInterface — набор булевых флагов, определяющих его возможности.
Флаги isPushable, isPullable, isSubscribable, isGate и другие — это не просто свойства объекта. Это метаданные, которые:
Определяют TypeScript-интерфейс трансфера на этапе компиляции.
Управляют стратегией связывания с другими трансферами в рантайме (с помощью функции linkTransfers()).
Обеспечивают совместимость в билдерах без приведений типов.
Один набор флагов — три потребителя. Это единый источник истины для всей системы.
Когда флаг равен true, соответствующий метод входит в TypeScript-интерфейс трансфера. Это позволяет предоставлять трансфер пользователю вот так:
Transfer
// гомогенный трансфер: T -> [ Transfer ] -> T
Или вот так:
Transfer
// гетерогенный трансфер: TInput -> [ Transfer ] -> TOutput
Именно в таком формате типов фабрики в библиотеке возвращают трансферы. Например:
const pushChannel: Transfer;
const converter: Transfer;
// содержат методы push(), subscribe()
const manualBuffer: Transfer;
// содержит методы push(), pull(), trigger()
// trigger() в данном случае нужен, чтобы сделать запушенное значение
// доступным для чтения через pull()
const pollingProxy: Transfer
// содержит методы pull(), subscribe(), trigger(), activate(), deactivate(), toggle()
// может быть связан с pullable-источником благодаря поддержке поллинга
Эта «магия» работает в compile-time благодаря несколько замысловатой системе вычислимых типов:
Трансфер определяет своё поведение (push, pull, subscribe и др.), но никогда не ссылается и не проверяет класс другого трансфера. Он не знает, что является upstream или downstream — лишь выполняет свой контракт. Пользователь может создать свой трансфер, объявить и реализовать его возможности — и он органично и бесшовно впишется в экосистему.
2. Мосты не знают конкретных реализаций
Мост инспектирует capability flags, а не имена классов. Нет цепочки instanceof, нет переключения по имени класса. Любой output-трансфер может быть соединен с любым input-трансфером — при условии совместимости их флагов, о чем мы поговорим чуть ниже. Это применимо и к тем узлам, которые еще не существуют и будут созданы пользователем.
3. Значение undefined никогда не распространяется
В Transferum undefined означает «нет данных», а не «пустое значение». Оно подавляется на уровне внутренней реализации менеджера подписок — подписчики никогда не уведомляются с undefined. При этом для явных маркеров пустых значений можно использовать null. Мы сознательно пошли на этот компромисс, чтобы избежать runtime-оверхеда и сохранить нативную скорость работы на плотных потоках данных.
Связывание трансферов
Функция linkTransfers(lhs, rhs) соединяет output-трансфер (lhs) с input-трансфером (rhs) с автоматическим выбором стратегии связывания на основе возможностей этих трансферов:
Protocol-oriented design: механизм не спрашивает «какой это класс?» — он выясняет, какие у него есть возможности. Любая пара трансферов с совместимыми возможностями является linkable. Добавление нового класса трансфера требует только объявления его флагов и реализации соответствующих методов — как связать его с другим трансфером, связующий алгоритм разберется сам.
Синхронные и асинхронные трансферы сосуществуют и могут быть связаны между собой. linkTransfers() предпочитает sync-связывание, когда это возможно, а async-стратегии применяет только когда sync неприменим. Нет отдельного «асинхронного мира».
Поддержка backpressure
Ряд асинхронных трансферов (AsyncSinkTransfer, AsyncWriteTransfer, AsyncConvertTransfer, AsyncConditionTransfer) поддерживают необязательные поля в конфигурации: maxConcurrency, bufferSize и onBufferOverflow — для ограничения параллельных async-операций, очереди избыточных данных и graceful-обработки переполнения. По умолчанию — неограниченная обработка, без буферизации.
Локальная обработка ошибок
Transferum использует единую модель обработки ошибок для всех трансферов. Каждый трансфер, который может столкнуться с runtime-ошибкой, принимает опциональный onError-хэндлер в своей конфигурации.
А теперь — к примерам использования
Вот так можно просто и декларативно описать опрос и агрегирование данных из нескольких источников:
А вот как можно организовать динамический роутинг:
import {
createPushStoredChannelTransfer,
createBridgeMultiSelector,
createBridgeSelector,
createPassBridge,
} from 'transferum';
// Вариант 1. Выбор одного реципиента
const commandRouter = createBridgeSelector({
bridges: {
light: createPassBridge({ source: commandChannel, target: lightController, activated: false }),
thermostat: createPassBridge({ source: commandChannel, target: thermostatController, activated: false }),
lock: createPassBridge({ source: commandChannel, target: lockController, activated: false }),
},
initialKey: 'thermostat', // выбор по умолчанию
activated: true,
owned: true,
});
commandRouter.select('lock'); // выберем другой вариант
// Вариант 2. Выбор нескольких реципиентов
const loggersRouter = createBridgeMultiSelector({
bridges: {
prometheus: createPassBridge({ source: metricsChannel, target: prometheusWriter, activated: false }),
elk: createPassBridge({ source: metricsChannel, target: elkWriter, activated: false }),
sentry: createPassBridge({ source: metricsChannel, target: sentryWriter, activated: false }),
},
initialKeys: ['prometheus', 'elk'], // будут активированы по умолчанию
activated: true,
owned: true,
});
loggersRouter.check('elk'); // теперь выбраны все три
loggersRouter.uncheck('prometheus'); // выбраны elk, sentry
loggersRouter.select(['prometheus', 'sentry']); // выбраны prometheus, sentry
А вот пример c организацией игровой механики:
import {
createDebounceTransfer,
createConvertTransfer,
createMapOperator,
createBridgeSelector,
createPassBridge,
linkTransfers,
} from 'transferum';
// Источник: debounce нажатий клавиш (16ms ~ 1 кадр при 60 FPS)
const rawInputSource = createDebounceTransfer({ delay: 16 });
// Конвертер: KeyboardEvent -> код клавиши
const inputConverter = createConvertTransfer({
operator: createMapOperator((event) => event.code),
});
// Целевые системы-потребители пользовательских событий
const carSystem = { push: (cmd: string) => console.log(🚗 Car executing: ${cmd}) };
const planeSystem = { push: (cmd: string) => console.log(✈️ Plane executing: ${cmd}) };
const menuSystem = { push: (cmd: string) => console.log(📋 Menu processing: ${cmd}) };
// Роутер: динамическое переключение между игровыми контекстами
const gameplayRouter = createBridgeSelector({
bridges: {
driving: createPassBridge({ source: inputConverter, target: carSystem }),
flying: createPassBridge({ source: inputConverter, target: planeSystem }),
ui: createPassBridge({ source: inputConverter, target: menuSystem }),
},
initialKey: 'driving', // игрок начинает в машине
activated: true,
});
// Связывание источника с конвертером с автовыбором стратегии
linkTransfers(rawInputSource, inputConverter);
// Демонстрация:
// Игрок в машине нажимает "W"
rawInputSource.push(new KeyboardEvent('keydown', { code: 'KeyW' }));
// Через 16ms: 🚗 Car: KeyW
// ==================
// Переключаемся на самолёт
gameplayRouter.select('flying');
// Та же клавиша теперь управляет самолётом
rawInputSource.push(new KeyboardEvent('keydown', { code: 'KeyW' }));
// Через 16ms: ✈️ Plane: KeyW
Когда имеет смысл попробовать Transferum
Библиотека подойдет для:
TypeScript-first проектов — благодаря максимально строгой типизации и compile-time вычислению доступных методов любого трансфера на основе объявленных у него флагов возможностей.
Работы с pull-based источниками данных — polling API, датчиков, хранилищ с PollingProxy.
Смешанных sync/async пайплайнов — в единой модели без ручного преобразования.
Явного flow control — gates, bridges, selectors для runtime-маршрутизации.
Game development / IoT — frame-aligned tickers, idle polling, sensor aggregation.
Устойчивой обработки ошибок — локальная, non-fatal обработка: одна стадия не убивает пайплайн при условии переданного в конфиге обработчика ошибок, ничего не подавляется молча.
Результаты и планы
Библиотека уже используется в двух наших внутренних проектах и показывает свою эффективность. Код доступен на GitHub под лицензией MIT, библиотека не имеет внешних зависимостей и поставляется с подробной документацией (README + API Reference).
В одном из проектов граф состоит из ~80 узлов и стабильно обрабатывает несколько сотен событий в секунду без деградации. На основе этих данных в том числе рендерится 3D-сцена в Babylon.js со стабильным фреймрейтом ~60 FPS без микрофризов.
Тесты библиотеки покрывают не только отдельные трансферы, но и поведение системы в динамике: переподключение мостов, обработку ошибок в длинных асинхронных цепочках, а также разнообразные граничные случаи. Покрытие — 100%.
В дальнейшем планируем реализовать хуки и утилиты для более удобного и нативного использования Transferum с Vue и React. Если они окажутся в достаточной мере переиспользуемыми, оформим в отдельные пакеты-адаптеры.
Буду рад, если вы заглянете в репозиторий, попробуете библиотеку в деле и поделитесь замечаниями — обратная связь поможет сделать Transferum лучше.
P. S. Если вы сталкивались с похожими задачами и решили их как-то иначе — буду рад прочитать о вашем опыте в комментариях.
Сделал я курс по Экселю для новичков случайно. Просто так получилось естественным путем.
Сдал небольшой проект одной компании, а в этом проекте по желанию заказчика был кусок на Excel-е.
В свободное время, думая, как бы комфортно развлечься, я просматривал наиболее интересные моменты из этой экселевской части проекта.
И вдруг мне подумалось. Если в основной части эклелевского файла убрать специфические моменты, макрос сделать простым и понятным, а форму сделать познавательно-развлекательной, да еще переделать документацию, то тогда... Получается почти готовый и вроде совсем неплохой "Курс по Экселю для новичков".
Подумал - сделал. Другого пути нет. Иначе, эта идея будет вертеться в голове, не давая покоя. Лучше идеи реализовывать, хотя бы для того, чтобы они отстали.
Курс готов!
А тут еще сообразительная девчушка Светлана мимо пробегала. Я ее поймал и попросил пройтись по пошагово по этому курсу, с целью понять: реально ли научиться Экселю по моему новому курсу.
Ну, вот, пока она занималась, я фиксировал все ее телодвижения в видео формате.
В итоге получилось пять видео, вот их краткие описания.
Часть 1 - Подготовка. Запуск.
Что такое Excel и кому он нужен.
Где взять Excel, как установить. Лучшие альтернативы Excel.
Как запустить Excel. Где находится запускаемый файл Excel.
Часть 2 - Тренировка. Таблица умножения.
Разминочная тренировка. Выполняется на листе "Тренировка".
Всякие простейшие упражнения для разогрева.
Часть 3 - Задача "Распределение фонда премий".
Часть 4 - Турнирная таблица шахматный турнир.
Часть 5 - VBA (Visual Basic для приложений).
Использование VBA для расчета коэффициента Бергера и итоговых мест
Если есть у вас интерес быстро усвоить Excel (или альтернативную электронную таблицу), то добро пожаловать сюда:
Рекурсия – очень популярное явление и в нашем реальном мире, и в мире математической абстракции. Понять, что это такое, очень просто. Встаньте между двумя зеркалами. В результате вы увидите большое количество собственных отображений. Большое, но не бесконечное. В нашем мире нет ничего бесконечного, кроме надежд и обещаний. Мораль этой истории такая. Всегда следует учитывать объем имеющихся в распоряжении ресурсов, включая терпение задействованных в процессе персон.
Петр Иванович – очень богатый человек. У него своя собственная старая большая фирма. И также своя собственная молодая и стройная жена Тамара. Шли годы, и жизнь Петра Ивановича была стабильной, богатой, счастливой и гармоничной.
Но стали Петра вдруг одолевать мрачные мысли. За что его любит жена? За то, что он такой умный и духовно развитый? Или просто за то, что богатый? К тому же их сын почему-то очень уж похож на нанятого садовника. А дочь вообще на горничную. Поневоле задумаешься!
Садовника и горничную Петр уволил по-тихому, под предлогом, что те, якобы воровали серебряные ложки и редкие семена фиолетовых роз. Петр сделал также тщательную генетическую экспертизу, которая подтвердила его отцовство.
Но осадочек-то, в виде двух детей, остался! Эти дети очень раздражающе напоминали о бывших подозрительных работниках, что было крайне неприятно.
По совету друзей Петр обратился за помощью к опытному психологу, некоему Василию Ивановичу Паку, тонкому знатоку человеческих душ.
- Есть у меня подозрение, что когда я отдам Богу душу, жена моя не будет долго грустить, а, наоборот, обрадуется и начнет кутить на полную катушку, проедая мой капитал, - делился наболевшим Петр с опытным психологом.
- Давайте, сделаем так. Вы притворитесь мертвым, а мы посмотрим и зафиксируем реакцию Вашей жены, - предложил Пак, - так нам откроется истина.
Петр согласился. Было очень любопытно узнать, как среагирует жена.
- О, Боже, горе-то какое! – закричала Тамара в ходе тестирования и упала на колени перед гробом, - позвольте мне лечь в могилу рядом с ним! Если так нельзя, даю вечный обет безбрачия, и траур мой в черных одеждах будет длиться вечно. Надеюсь, что мода на черную классику никогда не пройдет.
Непосвященному человеку тут покажется, что все ясно. Но на самом деле, не все так просто. Тонкость в том, что получив гонорар от Петра, ушлый Василий Пак пошел к Тамаре. Предложил ей за небольшую премию рассказать важный секрет. Тамара согласилась и, благодаря полученной информации, разыграла этот спектакль.
Но и это еще не все! После того, как Тамара отыграла свою роль и ее в обморочном состоянии вынесли из комнаты скорби, психолог на ушко рассказал "покойнику" причину достойного поведения вдовы.
Конечно, мнимый покойник щедро вознаградил Василия Пака. А тот снова рванул к Тамаре с новой ценной информацией.
Тамара тяжело вздохнула, затем сняла с себя золотые сережки и отдала их психологу.
Василий Пак поморщился, он больше любил наличку, но сережки принял. На всякий случай, если вдруг его заластает полиция, он вставил они сережки себя в уши. Отмазка была заготовлена такая. Мол, это подарок от покойной бабушки, и он в память о ней носит эти сережки все время, никогда не снимая.
Весело позвякивая сережками, психолог ломанулся обратно к "покойному", на ходу сочиняя речь о том, что "вдова" в курсе всех предыдущих итераций, а также о новых коварных планах "вдовы".
Долго так бегал Василий Пак между двумя супругами, пока почти совсем их не обанкротил.
Наконец, терпение и финансы Петра Ивановича подошли к концу.
- Послушайте, у вас, вообще, совесть есть? – задал он риторический вопрос, - вам непонятно, что это не может продолжаться бесконечно? И что это добром для вас не закончится? Сколько у Вас сейчас хвостов? Хотите, чтобы я Вам еще один пристроил? Немедленно проснитесь!
Студент Василий поднялся и, под смешки аудитории, осоловевшими глазами посмотрел на Петра Ивановича.
Шла лекция по теме "Проблемы при использовании рекурсивных функций".
Тут преподаватель кафедры информатики Петра Иванович сделал небольшую паузу, задумчиво посмотрел на заспанное лицо Василия и продолжил.
- А теперь задание! Напишите на "Питоне" рекурсивную функцию, определите максимальное количество возможных вложений. И каким образом эта наглая функция "шантажист" наконец грохнется, исчерпав лимит памяти или доверия указанных в тексте персон. Помните, в нашем мире нет ничего бесконечного, кроме надежд и обещаний. А тому, кто отгадает, кто же именно, наконец, убьет Василия Пака, покойник или вдова, получит дополнительный балл за сообразительность. Работаем, работаем!
Что сподвигло меня сделать это курс по Ворду? Ведь я вместо этого мог бы сыграть в 3-4 шахматных турнирах с тщательной предварительной подготовкой. Или, вообще, плюнуть на все и уехать на дачу, копать червей для рыбалки... Дело не в том, какие дороги мы выбираем. Дело в том, что такое в нас самих заставляет нас выбрать ту или иную дорогу... Когда-то давным-давно, лет 5 тому назад, один наш программист сделал для бухгалтерии функционал, который периодически рассылал сотрудникам предупреждения о разных событиях, типа неожиданных премий и т.п. Функционал основан на шаблонах Ворда и по неизвестной причине перестал работать. Надо ремонтировать! А это оказалось не так просто, как показалось сначала.
Вызывает меня как-то зам главного бухгалтера (милая приятная женщина) и говорит трагическим голосом.
- Все пропало!
- Клиент уезжает, гипс снимают? - уточнил я.
Выяснилось следующее.
Когда-то давным-давно, лет 5 тому назад, один наш программист сделал для бухгалтерии функционал, который периодически рассылал сотрудникам предупреждения о разных событиях, типа неожиданных премий и т.п.
Примерно вот такого содержания.
"Сообщение о премии".
...
Уважаемый Иванов Иван Иванович! По итогам работы за квартал вам будет выплачена дополнительная премия в размере 30 тысяч рублей. Спасибо за отличную работу.
...
Программист, сделавший этот функционал, давно уволился, никто эту программу не поддерживал, да и потребности в этом вроде как не было. Бухгалтера нажимали нужные кнопки и все работало.
Ничто не предвещало, но все-таки это случилось. Программа перестала работать. Почему, непонятно. Документации, конечно, никакой не было. Бухгалтера начали все это делать вручную. Работа простая. Нужно сделать около сотни похожих файлов Ворда и разослать сотрудникам, попавшим на эту премию.
Один раз бухгалтера это сделали, но это им сильно не понравилось. Они хорошо помнили те славные времена, когда процесс проходил за несколько минут без всяких ошибок. Теперь они ковырялись с этими файлами больше недели, да еще сделали одну нелепую ошибку. Одному сотруднику приписали лишний ноль, т.е. 400 тысяч вместо 40. Сотрудник сначала сильно обрадовался, заранее потратил кучу денег, залез в кредит. А потом еще более сильно огорчился, когда получил реальные деньги, а не просто сообщение.
Бухгалтера поняли, что программу надо чинить и обратились к нам.
Зачем вообще нужны эти извещения? Ведь все равно получатель премии узнает о ней по факту прихода денег на карту.
Ответ простой. Это старая добрая корпоративная традиция, и не нам, разработчикам и бухгалтерам, ее ломать. Народу нравится! И этим все сказано.
Короче, взялся я за дело и через некоторое время с удивлением понял, что Ворда я не знаю, и никакие курсы, справки, сайты в Интернете мне не помогали. Вроде все просто. Есть переменная, вот она, живая, но почему-то не работает. Почему, непонятно.
Позвал на помощь товарищей по работе, устроили мозговой штурм. Залезли в жуткие вордовские дебри и с огромным трудом поймали за хвост истину.
Выяснилось следующие.
В Ворде есть переменные "старого" типа, а есть "нового" типа. Программист широко использовал и те и другие. Но они совсем разные, эти переменные. И косяки у них тоже разные. У "новых" переменных есть такая жесткая особенность, нельзя их исправлять прямо в тексте. Визуально такие исправления вроде как срабатывают, но по факту это не так. Внутри все стало работать неправильно. А чтобы увидеть дефект и исправить, надо разархивировать файл docx (а это обычный zip архив, в котором несколько файлов xml) и там уже править дефектный xml. Это уже задача не для новичков!
Или, что проще, просто с нуля аккуратно переделать файл docx непосредственно в Ворде.
Так или иначе, после того, как проблема была понята и осмысленна, решить ее было уже просто.
После того как я убедился, что этот функционал успешно работает, (я сам получил сообщение о премии), я решил сделать небольшой курс по Ворду.
Хотелось, чтобы если кто-то еще столкнется с подобными проблемами, то смог бы быстро их решить.
У меня уже был успешный опыт создания подобных курсов, например: курс по Экселю.
Почему бы не сделать еще один?
Тут я уже двигался по проторенной дорожке: текстовый курс с картинками, тщательное тестирование, путем многократного выполнения нужных действий курсанта по заданному плану, видео материалы.
Я решил подробно расписать все эти особенности, "ловушки" Ворда, чтобы сделать работу с этой программой максимально комфортной.
В итоге получилось: один длинный текст с картинками, а к нему еще примыкают 3 видео.
Идея такая. Кто-то больше любит читать тексты (как я, например), кто-то больше любит, чтобы ему показывали в видео формате.
Вот в таком виде, во всех форматах курс подойдет всем, никто не останется без нужных знаний.
Вот краткие описания основных составных частей курса.
Часть 1 - Подготовка. Запуск.
Что такое Word и кому он нужен. Запуск. Наш первый файл "Hello, Word" или "Привет, Ворд". Разметка страницы. Шрифты, настройка. Вставка картинки. Гиперссылка.
Часть 2 - Переменные "старые" и "новые". Макрос VBA.
Переменные "старого типа" и переменные "нового типа". Наш первый макрос в Ворде. Ключевые моменты макроса. Устанавливаем значения переменных.
Часть 3 - Автоматизация.
Как создать 99999 однотипных документов Ворда для приглашений на праздник клиентов и/или сотрудников, расписок работников о согласии на что-то, резюме искателя работы с указанием разных работодателей и т.п. Заключение. Лайфхаки. Ссылки. Пожелания. Развлечения.
Если есть у вас интерес быстро усвоить Word (или альтернативный редактор текстов), то добро пожаловать сюда:
Microsoft Word. Майкрософт Ворд. Курс для новичков.
Там все материалы в интуитивно понятной форме. Не требуется никакой регистрации и прочего. Просто сразу получаете нужную информацию и выполняете на практике нужные действия. Информация представлена как в виде текстов с картинками, так и в формате видео с озвучкой голосом на русском языке.
Успехов!
Полная авторская версия истории "Как я сделал курс по Ворду для новичков" здесь:
Комментарии