спагетти код что это
Что такое «спагетти код»?
Код «Спагетти» (от английского словосочетания spaghetti code) — простыми словами, это уничижительное жаргонное описание некачественного программного продукта, который в силу различных факторов приходит ко всё менее оптимизированному виду. Иногда неправильно называют «лапша код» (относится к штрих-коду).
В качестве примера «спагетти» кода можно привести ситуацию, когда несколько разработчиков добавляют или изменяют код в течение многих лет, пытаясь оптимизировать существующую инфраструктуру программирования. В результате возникает запутанная структура кодирования с многочисленными операторами — проект со временем становится всё сложнее (и дороже) поддерживать.
Почему спагетти код — это ругательство?
Предприниматель может обвинить разработчика в создании кода спагетти и выставить неустойку. Организация в этой ситуации здорово рискует. После долгих лет работы и инвестиций в проект, ИТ-менеджеров и другие ресурсы, поддержка становится всё более дорогостоящей. В конце концов, спагетти код сделает её нерентабельной.
Как избежать спагетти кода?
Попытки привлечь других программистов приводят к значительным потерям по времени и не гарантируют результат. Новым исполнителям приходится исследовать и перекодировать инфраструктуру проекта только для того, чтобы что-то перестало «ломаться». Затем им приходится изучать годы выполненной работы, часто управляемой разными разработчиками, чтобы определить источник проблемы, если её вообще возможно решить.
По этой причине спагетти-код считается серьёзной неприятностью для разработчиков и ИТ-менеджеров. Корпоративным предприятиям, которым приходится управлять своими ресурсами, такой ситуации следует избегать как огня. Сделать это можно следующими способами.
Усердие и внимание к деталям
По нашей в ZEL-Услугах оценке, многие компании в погоне за результатами не уделяет внимание архитектуре разработки. Фундаментальный подход к деталям зачастую на корню пресекает все возможные путаницы с кодом в будущем даже при смене разработчика — все работают с унифицированным и интуитивным строением кода.
Тестирование модулями
Регулярные тесты должны выполняться модульно. Комплексный подход хорош на прохождении определённых этапов развития проекта. Однако внимание деталям уделяется больше всего на промежуточных модульных тестированиях.
Проверяйте программистов
Дополнительная пара глаз не помешает. Если вы столкнетесь с признаками кода спагетти, обратитесь к другому программисту или другой команде разработчиков (например, на аутсорсинге), чтобы убедиться в опасениях и внести изменения пока ещё не поздно.
Фреймворки полегче
В 2021-м году чем легче и проще фреймворки, тем лучше. Оптимизация структуры по этому принципу настраивает всю компанию на работу с простыми решениями впредь. Отличный пример профилактики кода спагетти.
Принцип «слоёного теста»
В крупных проектах «Spaghetti Code» — неизбежность. Реализуйте в таком случае архитектуру «слоёной» разработки. Так легче локально корректировать запутанный код и сохранять функциональность других слоёв.
В англоязычном сообществе софт ещё обзывают:
PS: приятного аппетита, наши дорогие айтишники и предприниматели!
Обратитесь в компанию ИТ-аутсорсинга для дальнейшей экспертной поддержки и консультации по этой теме и любым другим техническим вопросам.
Убираем спагетти-код
Два подхода к упорядочиванию хаоса.
Когда мы делали свой Трелло-планировщик из Бутстрапа и нашего списка задач, у нас появился спагетти-код. Это значит, что для простоты и скорости решения мы скопировали одинаковые куски кода, которые делают почти одно и то же. У них внутри одинаковая логика, но разные входные данные, и мы их просто скопировали-вставили.
У такого подхода к программированию есть несколько проблем:
Единственное, когда можно использовать такой подход — если нужно что-то быстро протестировать и понять, работает оно или нет. Если не работает — значит, мы сэкономили время и не стали тратить его на неудачное решение. А если всё работает как нужно, значит, настало время переписать код в нормальном виде. Для этого нам понадобится некоторое подобие объекта, но не на основе класса, а созданное вручную.
Мы хотели ООП, но не смогли
Изначально мы хотели на этом примере показать силу классов и объектов в объектно-ориентированном программировании. Но, разобравшись, поняли, что классы только всё усложнят, не принеся нам существенной пользы.
Поэтому мы будем использовать объекты, но без классов. А силу классов мы показали в игре на Питоне — посмотрите, вам понравится.
Новый тип переменной: объект
У переменной типа «объект» есть одна отличительная особенность: она состоит как бы из двух частей — свойства и значения. Мы про это говорили в статье про объекты в ООП, и здесь всё то же самое — у объекта меняются только значения свойств. Поясним на примере языка JavaScript.
Сделаем переменную-объект, у которой будет два свойства — имя и возраст:
Чтобы поменять имя, нужно написать название переменной и через точку указать название свойства, которое хотим изменить:
Свойств у объекта может быть сколько угодно:
Но если у объекта получается много однотипных свойств, можно сделать их массивом и обращаться к ним по номерам. А если элементы массива тоже сделать объектами с одинаковыми названиями свойств, получится вот такая магия:
Смотрите, так как у каждого элемента массива одинаковое название свойств, то, меняя просто номер элемента, мы можем запоминать разные свойства у одинаковых по сути элементов. Это нам пригодится на следующем шаге.
Собираем дубли
Пройдёмся по нашему старому коду из прошлой статьи и соберём все одинаковые переменные, которые отличаются только цифрами:
Первые две переменные задают ссылку на объект и маску для сравнения элементов, а вторые две отвечают за количество задач в колонках.
Свернём эти 16 переменных в один большой объект:
Теперь, зная только номер колонки, мы можем обратиться к объекту, маске и количеству задач в колонке. Сначала это может показаться громоздким, но на самом деле сокращает наш финальный объём программы в 4 раза.
Чтобы избавиться от дублей в коде, используют циклы или выносят повторяющийся код в отдельную функцию. На самом деле вариантов больше, но основные — эти.
Используем цикл
Циклы применяют в тех случаях, когда спагетти-код по очереди используется несколько раз, где отличаются только порядковые номера элементов. В нашем случае это функция showTasks(). Она берёт по очереди все элементы из локального хранилища и по очереди же сравнивает их с шаблоном — для первой, второй, третьей или чётвёртой колонки. Единственное, чем отличаются фрагменты — маской и колонкой, куда их отправлять:
Сделаем то же самое в цикле, используя нашу большую переменную-объект. Для этого мы организуем цикл от 0 до 3 (потому что нумерация элементов массива начинается с нуля) и по очереди проверяем все значения:
Код сократился в 4 раза, читать стало проще, и всё меняется в одном месте. Красота.
Делаем отдельную функцию
У нас есть вот такой огромный кусок кода, который делает одно и то же, только с разными элементами.
Здесь 4 раза задаётся обработчик нажатия клавиш в поле ввода у каждой колонки. Очевидно, что проще вынести повторяющийся код в отдельную функцию и вызывать её по мере необходимости:
Что дальше
Мы только что убрали почти весь спагетти-код из нашего проекта. Почти — потому что у нас осталось два неопрятных фрагмента:
Оба фрагмента можно оптимизировать, например, с помощью jQuery.
Попробуйте сделать это сами, пока это не сделали мы 🙂
🚀 Объектно-ориентированное программирование – самая большая ошибка компьютерных наук
Почему же ООП так опасен?
Представьте, что вы садитесь в свою хорошо знакомую машину и едете по хорошо знакомому маршруту. И вдруг что-то идет не так, как всегда. Вы отпускаете педаль газа, но машина продолжает ускоряться! Давите на тормоз – не помогает!
Страшно? Подобный инцидент произошел в 2007 году с Джин Букаут – и еще с сотнями водителей Toyota Camry. Десятки человек погибли.
Производитель ссылался на залипание педалей, человеческий фактор и даже половые коврики, однако истинным виновником оказалось программное обеспечение автомобилей.
Команда экспертов 18 месяцев разбирала кодовую базу Toyota – и нашла сотни потенциальных причин возникновения непреднамеренного ускорения. Код в целом был запутанным и беспорядочным – то, что на сленге называется «спагетти». Из-за этого Toyota пришлось отозвать более 9 млн автомобилей и выплатить более 3 млрд долларов.
Проблема спагетти
Подобные инциденты не уникальны – и это пугает. Например, два самолета Boeing 737 Max потерпели крушение из-за ошибки, вызванной спагетти-кодом (346 жертв, 60 млрд долларов ущерба). То же самое может случиться, например, на атомной электростанции или в реанимационной палате.
Как ни странно, программный код пишется прежде всего для людей. Мартин Фаулер говорил, что любой дурак может написать код, понятный компьютеру, а хорошие программисты пишут код, понятный людям. Если код непонятен, то очень скоро он перестанет работать.
Спагетти-код непонятен, связи между его частями не очевидны. Любое изменение может что-нибудь сломать. На такой код невозможно писать тесты, его сложно расширять и больно поддерживать.
Откуда берется спагетти-код?
Со временем любой код может превратиться в спагетти. По мере усложнения он становится все более и более запутанным, энтропия растет – и в один прекрасный момент вы уже имеете дело с клубком зависимостей.
Чтобы бороться с этим нужны строгие ограничения, вроде ограничения скорости на дорогах. Причем такие ограничения должны быть максимально автоматизированы и не зависеть от человеческого фактора. В идеале их должна накладывать сама парадигма программирования.
ООП – корень зла
ООП не накладывает на код никаких ограничений, которые могли бы предотвратить его запутывание. Безусловно, есть лучшие практики разработки – внедрение зависимостей, TDD, Domain Driven Design, которые реально помогают. Однако они не внедрены в парадигму, не обеспечиваются ей, и нет инструментария, который мог бы следить за их соблюдением.
Встроенные фичи ООП только добавляют путаницы. Например, инкапсуляция скрывает и рассеивает состояние по всей системе. Преимущества полиморфизма еще сомнительнее – мы не знаем точно, какой путь выполнения выберет наша программа. Если вдобавок ко всему этому приходится иметь дело с несколькими уровнями наследования, спагетти-код обеспечен.
Ссылочные типы
В большинстве объектно-ориентированных языков данные передаются по ссылке, то есть разные участки программы могут иметь дело с одной и той же структурой данных – и менять ее.
Но в современном ООП одни «клетки» проникают внутрь других и меняют их состояние. Это приводит к большой связанности кода. Изменения в одном месте программы могут привести к неожиданным последствиям в другом.
Предсказуемость
Склонность к спагеттификации – не единственная проблема ООП-парадигмы.
Если 2+2 будет равно 5 хотя бы один раз из миллиона, это может привести к ужасным последствиям.
Если эта цитата вам не нравится, то это потому, что недетерминированность в целом никуда не годится.
Вот пример кода, который просто вызывает функцию:
Мы не знаем, что делает эта функция, но кажется, что она всегда возвращает один и тот же результат для одних и тех же входных данных.
Эта функция вернула разные значения для одних и тех же входных параметров. Функция computeb недетерминирована. Она может выдавать ожидаемое значение, но это не гарантируется.
Что делает функцию детерминированной или недетерминированной?
Детерминированный код предсказуем, недетерминированный – нет.
Непредсказуемость
Давайте рассмотрим простую функцию сложения:
В большинстве языков программирования операция сложения реализуется на аппаратном обеспечении, то есть за нее отвечает очень надежный процессор. Поэтому мы всегда можем быть уверены, что функция add детерминирована.
Теперь немного усложним задачу – введем в бой объекты:
Пока все идет хорошо. Давайте внесем небольшое изменение в тело функции add :
Что случилось? Внезапно результат перестает быть предсказуемым! В первый раз код сработал нормально, но с каждым последующим запуском его вывод становился все более и более неожиданным.
Другими словами, функция больше не детерминирована.
Почему это вдруг произошло? Потому что функция начала вызывать побочные эффекты, изменив значение за пределами своей области действия.
Каковы последствия недетерминированного кода? В нем легко появляются дефекты – баги, которые заставляют разработчиков тратить драгоценное время на отладку и значительно ухудшают качество работы ПО.
Чтобы сделать наши программы более надежными, мы должны в первую очередь заняться их детерминизмом.
Побочные эффекты
Что такое побочный эффект? Если вы принимаете лекарство от головной боли, но оно вызывает у вас тошноту, то тошнота является побочным эффектом. Проще говоря, этот что-то нежелательное и не связанное с основной задачей препарата.
Вернемся к нашей функции сложения:
Чистота
Разобравшись с детерминизмом и побочными эффектами, мы готовы говорить о чистоте.
Чистая функция – это функция, которая одновременно детерминирована и не имеет побочных эффектов. То есть у нее всегда предсказуемый результат работы, и она не делает ничего лишнего.
Чистые функции имеют множество преимуществ:
И так далее. Проще говоря, чистые функции возвращают радость в программирование.
Насколько чисто ООП?
Для примера давайте поговорим о двух фичах ООП — геттерах и сеттерах.
Результат геттера зависит от внешнего состояния — состояния объекта. Многократный вызов геттера может привести к различным выходным данным, в зависимости от состояния системы. Это делает геттеры изначально недетерминированными.
Сеттеры предназначены для изменения состояния объекта, что делает их по своей сути побочными эффектами.
Таким образом, все методы в ООП (кроме, возможно, статических) либо не детерминированы, либо вызывают побочные эффекты. Следовательно, ООП – это что угодно, только не чистое программирование.
Чистое решение
Есть ли что-то, что может спасти программирование – луч надежды в мрачном мире программных сбоев? Что-то достаточно надежное и детерминированное, чтобы на нем можно было строить качественное ПО? Это математика.
А на чем основано современного ООП? Уже не на биологических законах клеток, как планировал Алан Кей. Оно базируется на множестве нелепых идей вроде классов и наследования, склеенных скотчем, чтобы лучше держались.
Основной строительный блок функционального программирования – функция в ее математическом понимании. В большинстве случаев – чистая функция, детерминированная и предсказуемая. Программа, составленная из таких функций, тоже становится предсказуемой.
Значит ли это, что в такой программе нет багов? Конечно нет. Однако и баги в ней детерминированы. Для одних и тех же данных всегда будет возникать одна и та же ошибка, что существенно облегчает ее исправление.
Как я тут оказался?
Очень похожий процесс происходит сейчас с ООП. Только вместо вопроса «как я попал в эту точку исполнения», мы спрашиваем «как я попал в это состояние».
ООП (и императивное программирование в целом) не может нам ответить. Когда все передается по ссылке, любой объект может быть изменен другим объектом – и парадигма не накладывает никаких ограничений, чтобы это предотвратить. Инкапсуляция совсем не помогает – вызов метода для мутации некоторого поля объекта ничем не лучше, чем его непосредственная мутация. Программы быстро превращаются в месиво зависимостей и глобального состояния.
Функциональное программирование избавит нас от сложных вопросов. Когда состояние остается неизменным, оно не вызывает проблем.
А как же спагетти-код?
В ООП есть много лучших практик, которые теоретически должны помочь справиться со спагеттификацией, например, предпочтение композиции наследованию. Однако сама парадигма ООП не накладывает никаких ограничений и не следит за применением этих практик. Можете ли вы поручиться, что джуниоры в вашей команде будут их соблюдать?
В функциональном программировании есть только композиция. Функции вызывают другие функции, большие функции состоят из малых. Это единственный способ построения программ. Иначе говоря, парадигма программирования сама навязывает лучшую практику, она является нативной, естественной. Когда у нас нет мешанины связей и зависимостей, разработка, тестирование и рефакторинг становятся удовольствием.
Но ООП и ФП дополняют друг друга!
Жаль вас разочаровывать, но это не так.
Объектно-ориентированное программирование – полная противоположность функциональному. Оно нарушает многие фундаментальные принципы:
Программисты ООП тратят большую часть своего времени на исправление ошибок. Программисты ФП – на написание работающего кода.
Действуйте, пока не поздно
ООП было очень большой и ужасно дорогой ошибкой. Давайте все, наконец, признаем это. Если ваш автомобиль работает на объектно-ориентированном ПО, вы не можете быть спокойны.
Пришло время принять меры. Мы все должны осознать опасность ООП и начать делать маленькие шаги в сторону функционального программирования. Это не быстрый процесс, реального сдвига можно ожидать не раньше, чем через 10 лет, однако он должен произойти.
В ближайшем будущем ООП-программисты станут «динозаврами», как сейчас разработчики на COBOL. C++, Java, C# умрут. TypeScript тоже умрет.
10 принципов хорошего кода и хорошего программиста
10 принципов хорошего кода и хорошего программиста
Спагетти-коды, огромные цепочки «if-else» и программы, которые ломаются после изменения переменной, функции размером в сотни строк и раздражающие имена переменных и классов? Это лишь некоторые из постоянно встречающихся в работе недочётов. И результат того, что будет, если попытаться превратить надвигающийся дедлайн в готовый продукт, внутри которого скрывается проблемный и перегруженный код.
Попробуем написать код, думая о его ремонтопригодности, а не только о том, чтобы он «работал!». Код, который поймёт и сможет поддерживать любой (разумеется любой, кто знаком с программированием). Будем следовать 10 принципам хорошего программиста, которые выведут вашу работу на новый уровень!
KISS расшифровывается как «чем проще — тем лучше». Этот принцип стоит использовать для решения разных жизненных задач, и он очень помогает в написании кода.
Например, вы собираетесь разработать мобильную игру — и у вас появился шанс это сделать. Не факт, что вы создадите следующую Candy Crush или Clash Royale. Поэтому постарайтесь оставить проект маленьким и простым. А если он уже прост — сделайте его ещё проще. Дополнительные функции появятся, когда вы сделаете стабильно работающую базу. Впрочем, можете не придерживаться этого правила, перегрузить софт функциями и «взорвать» весь проект.
Во время работы старайтесь сделать код простым, потому что сложный быстро становится трудоёмким. Чем больше времени вы тратите на одну программу — тем больше багов и ошибок получите. И в конечном итоге это приведёт к трудностям, если вы захотите изменить что-то в проекте позже.
DRY означает «не повторяйся», и это следует понимать буквально. Если ваш код повторяется и повторяет одни действия, вы нарушаете это правило. Несколько функций делают одно и то же? Рефакторинг в одну функцию. Содержат ли несколько переменных одни и те же данные? Рефакторинг в одну переменную.
Против принципа DRY выступает WET: «пиши всё дважды» или «потрать время зря». Основной вопрос для определения нарушителей правил заключается в следующем: сколько мест необходимо изменить, чтобы пофиксить программу? Если ваш ответ «больше одного», значит, вы действовали вопреки принципу DRY.
Представьте, что вы разрабатываете музыкальное приложение с тремя страницами: альбомы, названия и плейлисты. Для каждой из этих страниц существует собственный класс, а для каждого класса — своя функция «fetchMusic= ()». Так почему же в коде есть три места, которые ведут себя одинаково? Извлеките эту функцию и не действуйте против принципа DRY.
Открытый/Закрытый
Этот принцип хорошего программиста означает, что уже реализованные логические функции должны оставаться таковыми, и их нет смысла переписывать. В то же время новые требования или элементы можно добавлять к существующим классам, и расширять эти самые классы, а не менять. Так что они «открыты» для расширений, но «закрыты» для модификаций.
Это — ключ к созданию хорошего API, особенно если вы хотите выпустить библиотеку. Не соблюдайте этот принцип, и пользователи будут использовать вашу библиотеку «как есть», либо не использовать её вообще. И если вы захотите дать им возможность расширять её, они смогут менять ваш код в соответствии со своими потребностями. Вряд ли это то, чего вы добивались.
Если во время основного релиза кто-то серьёзно модифицирует код, требуется слияние всех изменений. Это занимает много времени и может привести к появлению багов. Не говоря уже о том, что те, кто переписывал код, наделают своих ошибок. Следование принципу «открытый/закрытый» защищает ваш софт от таких проблем, потому что простые расширения не могут взломать ни один существующий код.
Построение, а не наследование
Это означает, что поведение программ надо прописывать, а не брать со стороны. Почему? Потому что по мере роста древо наследования становится всё более запутанным, и каждая его «ветвь» получает свой собственный набор поведений. И попытки перенести модель поведения из одной «ветви» в другую могут оказаться трудными.
Прописанное с нуля поведение легче обрабатывать и поддерживать. Таким образом, также можно производить бесконечное количество моделей поведения. И из каждой комбинации можно получить свой класс.
Одним из примеров могут быть враги в видеоигре. На первом уровне они могут только бить. На следующем уровне — бить и пинать или бить и плеваться, но не пинать. Или плевать ядом и бить ногами, но не кулаком. Теперь представьте себе, что этот набор навыков растёт для каждого уровня. Попробуйте нарисовать дерево наследования, и вы скоро увидите, что построение с нуля гораздо удобнее.
Отдельная ответственность
Этот принцип хорошего программиста гласит, что каждый класс должен заботиться о предоставлении только одного бита.
Если вы придерживаетесь принципа KISS или у вашего проекта мало фишек, то большинство классов просто и без ошибок выполняют один тип работы за раз. Но с ростом требований к коду и приближением дедлайна большинство классов начинают делать по несколько вещей и нарушают этот принцип.
Чтобы придерживаться пятого правила, задайте себе вопрос, где и когда меняется каждая функция? Если ответ будет «более чем в одном месте и более чем по одной причине», вы нарушаете этот принцип.
Разделение задач
Этот принцип похож на предыдущий, но его стоит рассматривать на более высоком уровне абстракции. Программа состоит из нескольких частей, которые в лучшем случае не связаны друг с другом.
Хорошо известен пример MVVM-Pattern (Модель, Представление и Модель представления), где приложение разбивается на три не пересекающихся части. Модель данных содержит необработанные данные и выполняет некоторый алгоритм. Модель представления содержит агрегированные данные, которые должны отображаться, но не знает, как отобразить их. А Представление отображает эти данные наилучшим образом. Кроме того, только у него есть возможность взаимодействовать с пользователем и реагировать на него напрямую.
Таким образом, Модель представления и Модель не знают, на какую кнопку или область нажал пользователь. Представление обрабатывает действия человека и доставляет данные в Модель представления, которая выполняет собственные алгоритмы, не контактируя с пользователем. Это даёт возможность создавать модульные коды и параллельно разрабатывать каждую его часть.
YAGNI
Принцип хорошего программиста «Тебе это не нужно» хочет, чтобы вы не прописывали функции, которые могут и не понадобиться в будущем. Потому что велика вероятность, что они вам вообще не понадобятся. Зато усложнят код.
Можете рассматривать это как строгое соблюдение принципов DRY и KISS. В основном его нарушают неопытные разработчики. Они пишут очень абстрактный код и в итоге получают нечто раздутое и невозможное в использовании.
Избегайте преждевременной оптимизации
Смотрите на это как на противоположность принципа YAGNI. Первый призван, чтобы избежать реализации ненужных фрагментов кода, второй нужен, чтобы предотвратить слишком раннюю оптимизацию.
Вы не можете обнаружить недоработку, пока она не обнаружит вас! Программа должна сама показать вам «узкое место», потому что вы не найдете его, изучая код самостоятельно.
Рефракторинг, рефракторинг, рефракторинг
Многие знакомы с этим принципом хорошего программиста, но принимают не все: «код редко получается совершенным с первого раза» — Роберт К. Мартин.
Лучше ещё раз просмотреть и переработать сделанное ранее, потому что это помогает убедиться в правильности кода.Это нормально, такова природа вещей.
Если же вы посмотрели на старый код и поняли, что не можете его улучшить, есть только 2 варианта:
Я очень сомневаюсь, что кто-то может когда-либо прийти к варианту 1.
Поэтому возвращаться к старым участка кода и улучшать их — нормально. Особенно учитывая тот факт, что кодовые базы постоянно расширяются. Относитесь к этому принципу как к «правилу большого пальца» или как к рутине, но не забывайте совершенствовать свой код.
Чистый код лучше, чем «умный» код
Оставьте музу дома и напишите чистый код вместо шедеврального. Потому что шедевральный код — это загадка, которую будут решать ваши товарищи! Раскрою небольшой секрет: «умный код» довольно быстро станет загадкой и для вас. Никому нет дела до «умного» когда, если его невозможно прочесть.
«Умный код» делает как можно больше в одной строке вместо того, чтобы разбить всё для лучшего восприятия. Или содержит странные имена переменных/методов/классов вместо выразительных. Всё, что заставляет кого-то сказать «Подождите, что?», можно идентифицировать как «умный код».
Хороший программист пишет читаемый код и оставляет комментарии, если это действительно необходимо. Комментарии, которые объясняют, почему, а не как всё делается. Придерживайтесь руководств по стилю и пишите код, соответствующий языку.
Наконец, упомяну, что каждый разработчик программного обеспечения должен прочитать две книги, чтобы прокачать себя. Это «Чистый код» Роберта К. Мартина и «Совершенный код» Стива МакКоннелла. Прочитав их, я почувствовал себя заново рождённым в мире программирования, но сохранил опыт своей прежней жизни.
Хинт для программистов: если зарегистрируетесь на соревнования Huawei Cup, то бесплатно получите доступ к онлайн-школе для участников. Можно прокачаться по разным навыкам и выиграть призы в самом соревновании.
Перейти к регистрации