Порівнюємо код pet-проєктів, що перевіряють наявність світла
Усім привіт! Ще нещодавно в Україні була актуальна проблема зі світлом. Багато розробників замислювалися над тим, щоб створити застосунок, який буде сповіщати про наявність світла вдома. Найпростіші реалізації зводилися до того, щоб пінгувати статичну адресу роутера, і в разі відсутності / появи світла сповіщати про це через месенджер. Проблема може знову стати актуальною взимку, отже розглянемо ці реалізації. Вони цікаві тим, що ми можемо порівняти код застосунків на різних мовах, що вирішують майже одну й ту саму просту задачу.
У цій статті я хотів би порівняти два застосунки:
- єСвітло — телеграм-канал, що повідомляє про те, чи є світло. Створений за допомогою AWS\Lambda\Python. Репозиторій на GitHub.
- Electricity — вебсайт, що відображає на карті точки моніторингу світла та надсилає сповіщення через телеграм, якщо світло зникло або навпаки зʼявилося. Репозиторій на GitHub. Cтворено за допомогою low-code Fractal.

Оскільки технології, що використовувались для створення єСвітло зрозумілі та описані в статті, спробуємо розібратися з другою «темною конячкою» — Fractal. Маємо приблизно таке порівняння:
Спробуємо розібрати код проєкту, так би мовити, на атоми.
Database
База даних Electricity має документоорієнтований вигляд і складається з кількох колекцій.
Dashboard
Dashboard — це колекція, що виконує роль source для основної сторінки.
Колекція містить основний документ в JSON-форматі, що описує дані для трьох контролів: Location, NewLocation та Map.
{
"Locations": "Locations",
"NewLocation": "New Location",
"Map": {
"Key": "",
"Title": "Electricity points",
"Zoom": 15,
"Center": {
"Lat": "50.4329658690059",
"Lng": "30.57958332067064"
},
"Points": [
{
"Lat": "50.4329658690059",
"Lng": "30.57958332067064"
},
{
"Lat": "49.84395251649464",
"Lng": "24.026309322098676"
}
]
}
}
Типи цих контролів описані в UI Dimension.
{
"Style": "Save:false;Cancel:false",
"Map": {
"ControlType": "Map"
},
"NewLocation": {
"ControlType": "Button"
},
"Locations": {
"ControlType": "Button"
}
}
Locations
Locations — це колекція, що зберігає points (або точки на карті), де ми можем моніторити наявність світла. Типовий документ для тесту виглядає так:
{
"Address": "Kyiv",
"Description": "",
"HasElectricity": true,
"IPAddress": "127.0.0.1",
"LastPingTime": "09/27/2023 15:12:04",
"Lat": "50.4329658690059",
"Lng": "30.57958332067064",
"TelegramUserID": "5018512422",
"TextMessages": [
{
"IsSent": true,
"Message": "Electricity available",
"Provider": "Telegram",
"Receiver": 5018512422
}
]
}
Окрім уже знайомого нам UI Dimension, який може налаштовувати відображення певних JSON-атрибутів у світі UI, у нас також тут є TextMessages Dimension.
Його роль дуже проста: він спостерігає за документом, та якщо в документ було додано новий обʼєкт в масив TextMessages, він, використовуючи свою конфігурацію, намагається надіслати повідомлення через сконфігурований провайдер. Надіславши, переводить прапорець IsSent в true, або ж логує помилку, а також вказує причину, через яку не зміг відправити, і намагається це зробити знову за деякий час. У такому випадку цей Dimension сконфігурований так, щоб відправляти повідомлення через телеграм.
Ця колекція також має Timer Dimension, який свідчить, що документ повинен оброблятися за таймером кожні 300 секунд. Але як це працює — ми глянемо згодом, розібравши код в евенті OnTimerDimension.
NewLocation
NewLocation — це колекція, що зберігає шаблон для створення нового документу, який буде додано до Locations колекції.
{
"Address": "",
"IPAddress": "",
"Lat": "",
"Lng": "",
"TelegramUserID": 0,
"Description": "",
"HasElectricity": true,
"LastPingTime": ""
}
Крім уже знайомого UI Dimension, ця колекція має Validation Dimension, що інформує нас про те, як треба провалідувати поля перед записом на сервер.
{
"Address": {
"IsRequired": true,
"MinLen": 3,
"MaxLen": 256
},
"IPAddress": {
"IsRequired": true,
"MinLen": 7,
"MaxLen": 12
},
"Lat": {
"IsRequired": true,
"Type": "float"
},
"Lng": {
"IsRequired": true,
"Type": "float"
},
"TelegramUserID": {
"Type": "number"
}
}
Тож підсумуємо. Ми маємо в базі три прості колекції. Dashboard відповідає за нашу основну сторінку. Locations зберігає точки для моніторингу світла, а NewLocation зберігає шаблон для створення нової точки моніторингу світла.
Application
Тепер усе, що нам потрібно, це написати трохи C# коду в функціональному стилі, щоб звʼязати всю бізнес-логіку до купи.
Ping host
Передусім нам потрібна функція, що буде перевіряти доступність хосту через пінг.
private bool PingHost(string host)
{
try
{
var ping = new Ping();
var pingReply = ping.Send(host, 1000);
return pingReply.Status == IPStatus.Success;
}
catch
{
return false;
}
}
OnStart
Цей код викликатиметься завжди, коли наш застосунок стартує. Для цього ми звертаємося до нашої колекції Dashboard, отримуємо перший документ через GetFirstDoc()
, та викликаємо OpenForm()
public override void OnStart()
{
Client.SetDefaultCollection("Dashboard")
.GetFirstDoc()
.OpenForm();
}
У самій Dashboard колекції вже описано, як саме відображати цей документ.
OnEventDimension
Далі в нас у формі є дві кнопки: Locations та New Location, тож потрібно подивитися, що саме повино трапитися після їх натискання.
public override bool OnEventDimension(EventInfo eventInfo)
{
switch (eventInfo.Action)
{
case "NewLocation":
Client.SetDefaultCollection("NewLocation")
.WantCreateNewDocumentFor("Locations")
.OpenForm(result =>
{
if (result.Result)
{
var gps = result.Collection
.GetFirstDoc()
.Values("{'Lat':$,'Lng':$}");
Client.SetDefaultCollection("Dashboard")
.GetFirstDoc()
.Update("{'Map':{'Points':[Add,{'Lat':@Lat,'Lng':@Lng}]}}", gps[0], gps[1]);
result.NeedReloadData = true;
}
});
return true;
case "Locations":
Client.SetDefaultCollection("Locations")
.GetAll()
.OpenForm();
return true;
default:
return base.OnEventDimension(eventInfo);
}
}
Для Locations-кнопки код дуже простий. Ми знову звертаємося до колекції Locations, через GetAll()
отримуємо звідти всі документи та відкриваємо форму через OpenForm()
.
Для NewLocation код трішки складніший, але схожий на загальний шаблон виразів. Ми звертаємося до колекції NewLocation, далі кажемо через WantCreateNewDocumentFor("Locations")
, що ми хочемо створити новий документ для колекції Locations, а в лямді в кінці перевіряємо через result.Result
, чи натиснув користувач кнопку Create (Save). Якщо так, ми отримуємо координати через вираз:
var gps = result.Collection
.GetFirstDoc()
.Values("{'Lat':$,'Lng':$}");
Та додаємо ці координати в наш Dashboard як нову точку на мапі:
Client.SetDefaultCollection("Dashboard")
.GetFirstDoc()
.Update("{'Map':{'Points':[Add,{'Lat':@Lat,'Lng':@Lng}]}}", gps[0], gps[1]);
OnTimerDimension
В нас лишилась основна бізнес-логіка цього застосунку, яка періодично моніторить доступність хостів з колекції Locations.
public override bool OnTimerDimension(TimerInfo timerInfo)
{
var locations = Client.SetDefaultCollection("Locations")
.GetAll()
.Select<Location>();
foreach (var location in locations)
{
if (location.HasElectricity != PingHost(location.IPAddress) && location.TelegramUserID > 0)
{
location.HasElectricity = !location.HasElectricity;
Client.SetDefaultCollection("Locations")
.GetWhere("{'Address':@Address}", location.Address)
.Update(@"{'HasElectricity':@HasElectricity,
'TextMessages':[Add,{'Provider':'Telegram',
'Receiver':@Receiver,
'Message':@Message,
'IsSent':false}]}",
location.HasElectricity,
location.TelegramUserID,
location.HasElectricity ? $"{location.Address} HAS electricity" : $"{location.Address} HAS NO electricity");
}
Client.SetDefaultCollection("Locations")
.GetWhere("{'Address':@Address}", location.Address)
.Update("{'LastPingTime':@Now}");
}
return true;
}
Розберемо її покроково. OnTimerDimension-функція викликається кожні 300 секунд (5 хвилин). Перше, що вона зробить: видаляє з бази даних усі документи з колекції Locatons:
var locations = Client.SetDefaultCollection("Locations")
.GetAll()
.Select<Location>();
та десеріалізує їх в це dto:
public class Location
{
public string Address { get; set; }
public string IPAddress { get; set; }
public long TelegramUserID { get; set; }
public bool HasElectricity { get; set; }
}
Далі ми перевіряємо, чи не змінився статус хосту, порівнявши прапорець HasElectricity з поточним станом доступності хосту. Якщо стан змінився, ми інвертуємо цей правопрець:
location.HasElectricity = !location.HasElectricity;
Далі ми звертаємося до колекції Locations, шукаємо документ з потрібною адресою, оновлюємо прапорець HasElectricity та вставляємо в масив TextMessages новий обʼєкт з прапорцем IsSent=false. Як ми вже говорили, спеціальний SendMessages dimension моніторить цей масив і надсилає звідти повідомлення.
Client.SetDefaultCollection("Locations")
.GetWhere("{'Address':@Address}", location.Address)
.Update(@"{'HasElectricity':@HasElectricity,
'TextMessages':[Add,{'Provider':'Telegram',
'Receiver':@Receiver,
'Message':@Message,
'IsSent':false}]}",
location.HasElectricity,
location.TelegramUserID,
location.HasElectricity ? $"{location.Address} HAS electricity" : $"{location.Address} HAS NO electricity");
І останній вираз оновлює LastPingTime атрибут в документі, незалежно від того, чи статус з світлом змінився. Це нам допомогає просто переглядати у вебформі, коли останній раз пінгували наш Location:
Client.SetDefaultCollection("Locations")
.GetWhere("{'Address':@Address}", location.Address)
.Update("{'LastPingTime':@Now}");
Висновки
Якщо в великих проєктах ми загалом обмежені у виборі технологій, то в малих проєктах, стартапах, а також в pet-проєктах ми можемо вибирати ті технології, що в десять та більше разів скорочують час розробки, роблять код більш простим, функціональним, гнучким і зрозумілим.
Також я хочу лишити тут маленьку інструкцію того, що потрібно зробити, аби задеплоїти свій перший проєкт для експериментів.
Для цього створено пʼять пісочниць (Sandbox1, Sandbox2, Sandbox3, Sandbox4, Sandbox5), ви можете використовувати будь-яку вільну з них.
Для цього потрібно:
- Установити Visual Studio Community.
- Установити .NET Core 3.1.
- Клонувати репозиторій.
- Установити в конфізі потрібний проєкт для деплою.Наприклад, якщо ви хочете деплоїти свій код в Sandbox3, то змінити цей рядок на:
"AppNames": ["Sandbox3"]
- Запустити FractalPlatform.Deployment проєкт на виконання. Він миттєво задеплоїть усі необхідні файли з Sandbox-проєкту в інфраструктуру Fractal і відкриє в браузері задеплоєний проєкт з потрібним url.
Також я створив телеграм-групу @FractalPlatform, ви можете долучатися та ставити будь-які питання. Також чекаю на ваші думки тут в коментарях.
22 коментарі
Додати коментар Підписатись на коментаріВідписатись від коментарів