-
Notifications
You must be signed in to change notification settings - Fork 1
LOG: Anton
- В обучении есть темы, которые нужно закреплять практикой. Каждому студенту требуется свое количество практики. Одна из таких тем - это оценка сложности алгоритмов.
- Студенты не всегда готовы выделить время за компьютером для дополнительной практики, но готовы тренироваться по несколько минут в день на телефоне, в моменты, когда есть свободное время без доступа к полноценному компьютеру. Сейчас они тратят это время на соц-сети, а могут тратить на обучение.
- В рамках командного проекта сделать бота для телеграмма, который позволит тренироваться в оценке сложности алгоритмов в свободное время прямо на своем телефоне. Для этого разработать механику, плавного увеличения сложности задач, с учетом успеваемости пользователя. Задания должны быть каждый раз уникальными и генерироваться на лету, чтобы исключить возможность запоминания уже увиденных вариантов.
- Обобщить бота так, чтобы его можно было использовать для других тем, отличных от оценки сложности алгоритмов.
- Обобщить код так, чтобы незначительными доработками можно было сделать его доступным через другие платформы для чат-ботов.
Решение построено из набора микросервисов, взаимодействующих между собой по некоторому протоколу - в основном HTTP. Это позволяет добиться стабильности, более простой и независимой тестируемости. Кроме того, каждый разработчик получает возможность работать над сервисом независимо, сохраняя лишь внешнее API.
Здесь находится бизнес-логика викторины, построенная согласно DDD:
Для работы с задачами использована техника шаблонизации: человек, создающий задачу, может указать в ее тексте некоторые инструкции, которые система интерпретирует и заменяет необходимыми строками.
Внешнее REST API для работы с задачами и уровнями. Построено на фреймворке ASP.NET Core.
Внешнее REST API для работы с викториной и пользователями. Построено на фреймворке ASP.NET Core.
Интерфейс для работы с MongoDB: заданиями, пользователями и другой статистикой.
Построенный на ASP.NET Core webhook-bot для Telegram.
Интерфейс для работы с MongoDB: состояниями пользователей и их аутентификационными данными для Quiz API.
Веб-сервис для редактирования и дизайна уровней викторины. Фронтенд на React JS, бекэнд на ASP.NET Core
Для интересной и разнообразной викторины необходима система, позволяющая не создавать каждый вопрос вручную, а генерировать их каким-либо образом, используя случайные элементы внутри. Так же необходимо хранить данные в некотором хранилище.
Для взаимодействия с внешним API создана сущность генератора:
public abstract class TaskGenerator : Entity
{
public abstract Task GetTask(Random randomSeed);
}
где Task = struct { string Text; string Question; string Answer; string[] Hints; }
Для обеспечения "чистоты" генератора, генератор случайных чисел вынесен в аргумент функции, что позволяет адекватно тестировать генераторы в целом.
Так же это является точкой расширения, с помощью наследования и полиморфизма возможно создать генератор, который будет работать на любом принципе, не только на шаблонах.
Генератор, работающий на шаблонах, вынесен в отдельный класс:
public class TemplateTaskGenerator : TaskGenerator
{
public string[] PossibleAnswers { get; }
public string Text { get; }
public string Question { get; }
public string[] Hints { get; }
public string Answer { get; }
}
Шаблонный генератор одновременно является шаблоном для себя же: каждое поле в этом классе содержит текст вместе с некоторой подстановкой, которое в последствии преобразуется в объект Task
с помощью движка шаблонизации.
Шаблонизация реализована с помощью библиотеки Scriban.
Введем необходимые понятия:
Подстановка - некоторая строка вида "{{ smth }}
.
Шаблон - некоторая строка, которая может содержать в себе подстановки - "usual text {{ substitution }} usual text"
Рендеринг шаблона - процесс замены подстановок сгенерированными значениями.
Внутри подстановок можно писать выражения на одноименном языке, используя предоставленные системой функции и объекты. Можно определять переменные в рамках одного шаблона, при этом они являются глобальными для каждого элемента шаблона - вопроса, ответа и остальных.
Template
"{{ var1 = 5 }} Some number is {{ var1 }} and this is incremented number {{ var1 + 1 }}"
Rendered
" Some number is 5 and this is incremented number 6"
Методы библиотеки Scriban, осуществляющие парсинг и рендеринг шаблона, имеют следующую сигнатуру:
Template Parse(string rawText);
string Render(Template template, IScriptObject so);
Видно, что данные методы не позволяют обрабатывать целый пакет шаблонов - а именно набор полей TemplateTaskGenerator
. Для решения данной этого разработан модуль, который позволяет конкатенировать строки так, что после некоторого изменения данных строк как конкатенированной строки их можно разбить на исходные, сохранив изменения.
Основные методы данного модуля:
public sealed class Storage : IEquatable<Storage>
{
private Storage(ICollection<string> items)
{
if (items == null)
throw new ArgumentException("Can not split null");
Key = Guid.NewGuid().ToString();
Count = items.Count - 1;
Concatenated = items.Count == 0 ? "" : Join(Key, items);
}
private Storage(string value, string key, int count) => (Concatenated, Key, Count) = (value, key, count);
private int Count { get; }
private string Concatenated { get; }
private string Key { get; }
[Pure]
public static Storage Concat(params string[] items) => new Storage(items);
[Pure]
public string[] Split() => Concatenated?.Length == 0
? new string[0]
: Concatenated?.Split(Key)?.ToArray() ?? new string[0];
Логика работы такова: строки конкатенируются, при этом в качестве разделителя используется Guid
. В силу того, что его значение уникально, строку после преобразования можно разбить снова по его значению.
Метод TemplateTaskGenerator.GetTask
выглядит так:
public override Task GetTask(Random randomSeed)
{
var so = CreateScriptObject(randomSeed);
var simpleFieldsStorage = Concat(Text, Answer, Question);
var hintsStorage = Concat(Hints ?? new string[0]);
var answersStorage = Concat(PossibleAnswers ?? new string[0]);
var fields = new[] { simpleFieldsStorage, hintsStorage, answersStorage };
var ((code, answer, question), hints, answers)
= fields.MapMany(vs => Concat(vs).Map(s => Template.Parse(s).Render(so)).Split())
.Select(r => r.Split().ToArray())
.ToArray();
return new Task(code, hints, answer, Id, answers, question);
}
Все поля шаблона конкатенируются, прогоняюся через парсер, рендерятся и разбиваются на исходные поля, из которых собирается Task
.
В язык шаблонизации добавлено некоторое количество встроенных функций и переменных, которые позволяют делать более интересные и необычные задачи.
Например any_of
:
Шаблон
var template =
@"{{ var = any_of ["i", "j"] //| здесь в подстановке определяем глобальные переменные
c = any_of ["c", "const", "veryImportantVariable"] //| так как в подстановке только операции присваивания
inc = random 2 5 //| то подстановка возвращает ничего и результатом рендеринга
op = c + " += " + var + ";" //| будет пустая строка
}} for (var {{var}} = 0; {{var}} < n; {{var}} += {{inc}}) //| здесь используем ранее определенные переменные в шаблоне
{ //|
{{op}} //|
}" //|
Результат:
for (var i = 0; i < n; i += 4)
{
veryImportantVariable += i;
}
В проекте используется паттерн "Неизменяемая структура данных", в том числе и для генераторов.
В качестве БД выбрана MongoDB
. К сожалению, сериализатор по умолчанию не поддерживает работу с неизменяемыми классами, для чего в документации было найдено следующее решение:
public static class MongoHelpers
{
public static void AutoRegisterClassMap<TClass>(Expression<Func<TClass, TClass>> creatorLambda) =>
AutoRegisterClassMap<TClass>(cm => cm.MapCreator(creatorLambda));
public static void AutoRegisterClassMap<T>(Action<BsonClassMap<T>> additionalAction = null)
{
BsonClassMap.RegisterClassMap<T>(cm =>
{
var propertyInfos = typeof(T)
.GetProperties(BindingFlags.DeclaredOnly |
BindingFlags.Public |
BindingFlags.Instance)
.Cast<MemberInfo>()
.ToList();
foreach (var propertyInfo in propertyInfos)
cm.MapMember(propertyInfo);
additionalAction?.Invoke(cm);
});
}
}
Для каждого класса конфигурируется фабричный метод, которым де-факто является конструктор этого класса:
AutoRegisterClassMap<TemplateTaskGenerator>(c => new TemplateTaskGenerator(c.Id, c.PossibleAnswers,
c.Text, c.Hints,
c.Answer, c.Streak, c.Question));
на /level0 можно кликнуть и перейти к заданиям
a, y, m, константы и тело цикла рандомизированы
Затем следует новая задача
Кнопка пропадает, если подсказки кончились
После этого выдается новая задача
- Языки, грамматики, распознаватели - А. П. Замятин, А. М. Шур, 2007
- Telegram Bots Book @ https://telegrambots.github.io/book/
- ASP.NET Core in Action - Andrew Lock, 2018
- Руководство по языку C# @ https://docs.microsoft.com/ru-ru/dotnet/csharp/
- ASP.NET MVC 3 Framework с примерами на C# для профессионалов Адам Фримен , Стивен Сандерсон, 2011