Ситуационный центр для мониторинга IT-инфраструктуры и UX — как такое решение может выглядеть на практике

Подобные системы применяют организации из самых разных отраслей — промышленные предприятия, нефтегазовые компании, облачные провайдеры и многие другие. Расскажем, что важно знать о ситуационных центрах.

/ Unsplash.com / Ibrahim Boran
/ Unsplash.com / Ibrahim Boran

Почему мы об этом говорим

Сегодня можно легко представить ситуацию, когда финансовая структура или ретейлер внезапно сталкиваются с технологическими проблемами, становятся «заложниками» сбоя или аварии в дата-центре. В силу обстоятельств они перестают обслуживать часть клиентов, а если речь идет о крупном маркетплейсе или банке из топ-20, их число может исчисляться тысячами.

Денежные и репутационные потери в таком случае, конечно же, приведут к серьезным последствиям. Подобные риски появились не вчера. Операторы дата-центров работают с ними с момента запуска облачных платформ как таковых, а организации-клиенты с каждым годом тратят все больше на обеспечение информационной безопасности, доступности и бесперебойной работы своих сервисов. Аналитики из Global Industry Analysts ожидают, что в ближайшие четыре года рынок систем управления производительностью и стабильностью приложений продолжит свое развитие и вырастет до $12 млрд. Для сравнения, еще в 2020-м его оценили в $6,3 млрд.

Решения, направленные на обеспечение работоспособности отдельных сервисов, достаточно разнообразны. Только Gartner выделяет порядка пятнадцати ключевых игроков. У них отличается технологический стек и функциональные подходы, зачастую они фокусируются на мониторинге процессов. При этом часть задач компании могут закрывать собственными силами, не привлекая сторонних поставщиков. Управлять таким «зоопарком» достаточно сложно — необходимы компетенции для связи и синхронизации инструментов, что тянет за собой дополнительные расходы.

В то же время даже минимальная децентрализация структуры мониторинга приводит к тому, что нештатные ситуации решаются с задержкой, а процесс устранения последствий затягивается. На эту проблему обращали внимание еще в 2015 году — тогда специалисты из аналитической фирмы Enterprise Management Associates установили, что большинство корпораций имеет более одиннадцати коммерческих платформ, отвечающих за мониторинг и бесперебойную работу сервисов. Две трети организаций отметили, что на поиск причины неполадок у них уходит от одного до трех часов (но эта цифра может достигать и шести часов). Дело в том, что в условиях децентрализации размывается ответственность за устранение инцидентов.

/ Unsplash.com / Ibrahim Boran
/ Unsplash.com / Ibrahim Boran

Одно из возможных решений — управление процессами с помощью централизованного ситуационного центра (СЦ). Он мониторит ключевые бизнес-операции (например, скорость обслуживания в офисе или колл-центре и необходимые для этого трудозатраты), а также выявляет потенциальные слабые места в инфраструктуре и ПО и заблаговременно предупреждает о сбое. Типичный спектр задач такой системы может включать и вопросы, связанные с управлением UX. Далее, рассмотрим все эти моменты подробнее.

Как это работает

Вообще, под ситуационным центром принято понимать единую систему, которая контролирует ключевые показатели бизнес-процессов компании и работу ИТ-инфраструктуры. В каком-то смысле она реализует data driven подход, позволяющий принимать взвешенные решения на основе данных. Плюс — она делает разработку новых продуктов и сервисов эффективной как с точки зрения ресурсов, так и информационной безопасности.

Такое решение становится основой для «зонтичного мониторинга» и единого центра ответственности. С его помощью можно координировать действия подразделений и контрагентов, чтобы оперативно устранять последствия аварий и сами уязвимости. Разумеется, к системам мониторинга можно подключить не только программное, но и аппаратное обеспечение. Например, вот так может выглядеть окно, позволяющее администратору оценить работоспособность отдельных рабочих станций.

При выборе централизованной системы для мониторинга следует обращать внимание на некоторые особенности вроде возможности анализа данных в контексте отдельно взятой компании, филиала или индивидуального рабочего места.

Также стоит обратить внимание на комплекс дополнительных решений, способных сэкономить время и ресурсы компании. Например, в некоторых случаях через ситуационный центр можно автоматизировать проведение конференц-звонков с руководителями ключевых подразделений при ЧС и настройку каналов для дистанционного доступа к корпоративной сети.

Еще один ключевой фактор — простота внедрения. Наличие исчерпывающих руководств и интуитивный интерфейс позволяют не тратить ресурсы на специальное обучение сотрудников, плюс — техподдержке проще реагировать на инциденты в процессе эксплуатации системы. Например, наш Watchman получает данные через унифицированные интерфейсы, что позволяет оперативно ставить на мониторинг новые и уже действующие приложения.

Что там с UX

В конечном счете — работа СЦ будет направлена на формирование благоприятного пользовательского опыта. В силу очевидной зависимости внешнего UX (клиенты и партнеры) от опыта внутренних потребителей (сотрудников), так как неэффективные процессы сказываются на общей удовлетворённости клиентов в не меньшей степени, чем технические сбои по «железу». Однако найти такие «тормозящие» процессы сложнее — зачастую их нельзя оценить визуально или с помощью каких-либо измерений.

В этом контексте стоит учитывать, что в составе СЦ обязательно должна быть многофункциональная система сбора данных о пользовательском опыте. Мониторинг такой информации является одновременно и элементом ситуационного центра, и инструментом для измерения эффективности его работы. Однако важно понимать, что метрики и параметры, на которых выстраиваются критерии анализа нештатных ситуаций, следует формировать на основе реальных данных.

В нашем портфолио есть такая система для комплексного бизнес-мониторинга — Watchman. Она построена на основе инструментов системного анализа, а также платформ для исследования бизнес-процессов — а именно, Process Mining и Process Discovery. Watchman получает данные мониторинга с рабочих мест и дает наиболее объективную картину ситуации с состоянием ПО и «железа». Такой подход дает возможность эффективнее оценивать то, как на самом деле выполняется тот или иной бизнес-процесс, в том числе и с помощью специализированных математических моделей.

Подобные решения по работе с данными, конечно же, подойдут не только условным предприятиям с разветвленной филиальной сетью, как упомянутый в начале статьи банк. Любая компания, имеющая в своем распоряжении широкий парк рабочих ПК и осуществляющая непрерывные бизнес-процессы, может выиграть от внедрения системы мониторинга. Так, для одного из наших клиентов мы добились увеличения доступности ИТ-сервисов с 99,3% до 99,7%, плюс — сократили число заявок в службу поддержки на 12%.


О других наших кейсах — в блоге на Хабре:


ссылка на оригинал статьи https://habr.com/ru/company/T1Holding/blog/680026/

Маркеры на Google Maps во Flutter: от простого к сложному

Я — Тим, разработчик в Гудитворкс. Недавно мы делали приложение-гид по ресторанам. Нам было нужно, чтобы на карте отображалась информация о ресторанах, а пользователь мог бы отмечать понравившиеся. Я расскажу, как работать во Flutter с картами, а также стандартными и нестандартными маркерами. В конце каждой части рассказа — ссылка на репозиторий с полным кодом примера.

Подключение карты

В качестве картографической основы я выбрал Google Maps. Для работы с ним во Flutter есть пакет google_maps_flutter. Пакет добавляется как зависимость в файл pubspec.yaml:

dependencies:   ...   google_maps_flutter: ^2.1.8   ...

Чтобы подключиться к картам, понадобится API-ключ: о том, как его получить, подробно написано в документации Maps SDK. Для Android добавляем ключ в файл android/app/src/main/AndroidManifest.xml:

<manifest ...    <application ...         <meta-data android:name="com.google.android.geo.API_KEY"                    android:value="API-КЛЮЧ"/>

После этого добавляем виджет с картой в файл main.dart:

import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:google_maps_flutter/google_maps_flutter.dart';  void main() => runApp(const MyApp());  class MyApp extends StatelessWidget {   const MyApp({Key? key}) : super(key: key);    @override   Widget build(BuildContext context) {     return const MaterialApp(       home: Scaffold(         body: CustomMap(),       ),     );   } }  class CustomMap extends StatefulWidget {   const CustomMap({Key? key}) : super(key: key);    @override   _CustomMapState createState() => _CustomMapState(); }  class _CustomMapState extends State<CustomMap> {   GoogleMapController? _controller;   static const LatLng _center = LatLng(48.864716, 2.349014);    void _onMapCreated(GoogleMapController controller) {     setState(() {       _controller = controller;     });      rootBundle.loadString('assets/map_style.json').then((mapStyle) {       _controller?.setMapStyle(mapStyle);     });   }    @override   Widget build(BuildContext context) {     return GoogleMap(       onMapCreated: _onMapCreated,       initialCameraPosition: const CameraPosition(         target: _center,         zoom: 12,       ),     );   } }

Стоит обратить внимание на:

  • метод _onMapCreated: он вызывается при создании карты и получает в качестве параметра GoogleMapController,

  • параметр initialCameraPosition: определяет первичное позиционирование карты,

  • GoogleMapController: управляет картой — позиционированием, анимацией, зумом.

Чтобы карта была красивее, я прописал стили в файле assets/map_style.json. Стилизовать карту удобно сервисом mapstyle.withgoogle.com. Теперь карта выглядит так:

Ветка репозитория: https://github.com/gooditcollective/flutter-google-maps-exmaple/tree/init-gm

Стандартные маркеры

На карту можно поместить стандартные маркеры. Для этого нужны координаты: в моем случае они, как и другие данные ресторанов — в файле datasource.dart

Метод _upsertMarker создает маркеры:

void _upsertMarker(Place place) {     setState(() {       _markers.add(Marker(         markerId: MarkerId(place.id),         position: place.location,         infoWindow: InfoWindow(           title: place.name,           snippet:               [...place.occasions, ...place.vibes, ...place.budget].join(", "),         ),         icon: BitmapDescriptor.defaultMarker,       ));     });   }

Класс infoWindow по тапу показывает пин с информацией о ресторане, а на карту маркеры добавляются с помощью атрибута markers виджета GoogleMap:

void _mapPlacesToMarkers() {   for (final place in _places) {     _upsertMarker(place);   } } ... @override initState() {   super.initState();   _mapPlacesToMarkers(); }  @override Widget build(BuildContext context) {   return GoogleMap(     onMapCreated: _onMapCreated,     initialCameraPosition: const CameraPosition(       target: _center,       zoom: 12,     ),     markers: _markers,   ); }

Выглядит это так:

Ветка репозитория: https://github.com/gooditcollective/flutter-google-maps-exmaple/tree/default-markers

Карточки по тапу

Но пина с информацией показалось недостаточно. Хотелось, чтобы была полноценная карточка с фотографией ресторана.

Добавлю переменную для хранения выбранного места и методы для его выбора в _CustomMapState. Карточка будет показываться по тапу на маркер (метод _selectPlace), а исчезать по тапу там, где маркера нет (метод _unselectPlace). Карточки подключаются с помощью виджета Positioned:

class _CustomMapState extends State<CustomMap> {   ...   final List<Place> _places = places;   Place? _selectedPlace;      void _unselectPlace() {     setState(() {       _selectedPlace = null;     });   }      void _selectPlace(Place place) {     setState(() {       _selectedPlace = place;     });   }    void _upsertMarker(Place place) {     setState(() {       _markers.add(Marker(         ...         onTap: () => _selectPlace(place),         ...       ));     });   }   ...     @override   Widget build(BuildContext context) {     return Stack(       ...       children: <Widget>[         GoogleMap(           ...           ),           markers: _markers,           onTap: (_) => _unselectPlace(),         ),         if (_selectedPlace != null)           Positioned(             bottom: 76,             child: PhysicalModel(               color: Colors.black,               shadowColor: Colors.black.withOpacity(0.6),               borderRadius: BorderRadius.circular(12),               elevation: 12,               child: Container(                 decoration: const BoxDecoration(                   color: Colors.white,                   borderRadius: BorderRadius.all(Radius.circular(12)),                 ),                 child: MapPlaceCard(                   place: _selectedPlace!,                 ),               ),             ),           ),       ],     );   }

Теперь карта — с карточками:

Ветка репозитория: https://github.com/gooditcollective/flutter-google-maps-exmaple/tree/cards-on-tap

Меняющиеся маркеры

Было бы здорово, чтобы пользователь мог отмечать понравившиеся рестораны, и маркер бы от этого менялся. Для этого понадобятся иконки:

Маркеры будут добавляться методом _upsertMarker:

Future<void> _upsertMarker(Place place) async {     final selectedPrefix = place.id == _selectedPlace?.id ? "selected_" : "";     final favoritePostfix =         _likedPlaceIds.contains(place.id) ? "_favorite" : "";      final icon = await BitmapDescriptor.fromAssetImage(       const ImageConfiguration(),       "assets/icons/${selectedPrefix}map_place$favoritePostfix.png",     );      setState(() {       _markers.add(Marker(         markerId: MarkerId(place.id),         position: place.location,         onTap: () => _selectPlace(place),         icon: icon,       ));     });   }

Сердечко-лайк ставится методом _likeTapHandler:

void _likeTapHandler() async {     if (_selectedPlace == null) return;     setState(() {       if (_likedPlaceIds.contains(_selectedPlace!.id)) {         _likedPlaceIds.removeAt(_likedPlaceIds.indexOf(_selectedPlace!.id));       } else {         _likedPlaceIds.add(_selectedPlace!.id);       }     });      _upsertMarker(_selectedPlace!);   }

Метод вызывается в виджете MapPlaceCard:

@override   Widget build(BuildContext context) {     return Stack(       ...       children: <Widget>[         ...         if (_selectedPlace != null)           Positioned(             ...             child: PhysicalModel(               ...               child: Container(                 ...                 child: MapPlaceCard(                   place: _selectedPlace!,                   isLiked: _likedPlaceIds.contains(_selectedPlace!.id),                   likeTapHandler: _likeTapHandler,                 ),               ),             ),           ),       ],     );   }

Когда пользователь выбирает другое место, иконка должна вернуться к прежнему состоянию. Это делает метод _unselectPlace — он снимает выбор с места и обновляет его иконку:

class _CustomMapState extends State<CustomMap> {   ...   Future<void> _unselectPlace() async {     if (_selectedPlace == null) return;      final place = _selectedPlace;     setState(() {       _selectedPlace = null;     });      await _upsertMarker(place!);   }    Future<void> _selectPlace(Place place) async {     await _unselectPlace();      setState(() {       _selectedPlace = place;     });      await _upsertMarker(place);   }   ...   @override   Widget build(BuildContext context) {     return Stack(       ...       children: <Widget>[         GoogleMap(           ...           ),           markers: _markers,           onTap: (_) => _unselectPlace(),         ),         if (_selectedPlace != null)           Positioned(             bottom: 76,             child: PhysicalModel(               color: Colors.black,               shadowColor: Colors.black.withOpacity(0.6),               borderRadius: BorderRadius.circular(12),               elevation: 12,               child: Container(                 decoration: const BoxDecoration(                   color: Colors.white,                   borderRadius: BorderRadius.all(Radius.circular(12)),                 ),                 child: MapPlaceCard(                   place: _selectedPlace!,                   isLiked: _likedPlaceIds.contains(_selectedPlace!.id),                   likeTapHandler: _likeTapHandler,                 ),               ),             ),           ),       ],     );   } }

Теперь наша карта выглядит так:

Слева — неотмеченный ресторан, справа — отмеченный
Слева — неотмеченный ресторан, справа — отмеченный

Ветка репозитория: https://github.com/gooditcollective/flutter-google-maps-exmaple/tree/different-icons

Нестандартные маркеры

Осталось немного — чтобы название ресторана было видно всегда, а не только по тапу. Для этого пришлось сделать отдельную утилиту для рисования маркера utils/custom_marker_drawer.dart

... class CustomMarkerDrawer {   ...   Future<CustomMarker> createCustomMarkerBitmap({     ...   }) async {     ...     PictureRecorder recorder = PictureRecorder();     Canvas canvas = Canvas(       recorder,       Rect.fromLTWH(         0,         0,         scaledCanvasWidth,         scaledCanvasHeight,       ),     );     ...     Picture picture = recorder.endRecording();     ByteData? pngBytes = await (await picture.toImage(       scaledCanvasWidth.toInt(),       scaledCanvasHeight.toInt(),     ))         .toByteData(format: ImageByteFormat.png);      Uint8List data = Uint8List.view(pngBytes!.buffer);      final marker = BitmapDescriptor.fromBytes(data);     const double anchorDx = .5;     final anchorDy = imageHeight / scaledCanvasHeight;      return CustomMarker(marker: marker, anchorDx: anchorDx, anchorDy: anchorDy);   }   ... }

Мы рисуем на виртуальном Canvas, который потом преобразуем в Picture с помощью PictureRecorder. Результат преобразуем в Uint8List — список 8-битных беззнаковых целых чисел, который отправляем в BitmapDescriptor — объект, который определяет битмэп, которую Google Maps потом отрисовывает на карте.

Для рендера Flutter использует логические пиксели. Но, в зависимости от устройства, на один логический пиксель может приходиться несколько реальных. Чтобы иконки выглядели корректно независимо от устройства, используется параметр scale.

Вот как это выглядит в main.dart:

class _CustomMapState extends State<CustomMap> {   GoogleMapController? _controller;   final Set<Marker> _markers = {};     final CustomMarkerDrawer _markerDrawer = CustomMarkerDrawer();   double _scale = 1;    ...   @override   initState() {     super.initState();     Future.delayed(Duration.zero, () {       _likedPlaceIds.addAll([_places[0].id, _places[3].id]);       _scale = MediaQuery.of(context).devicePixelRatio;       _mapPlacesToMarkers();     });   }   ... }

Этот параметр отдает только класс MediaQueryData — он может быть только у потомков Material-виджетов, его нет в корневом виджете приложения. MediaQueryData.of(context) сработает только после полной инициализации initState, поэтому я обернул его в Future.delayed(Duration.zero, () {...} — это переводит исполнение лежащего в нем кода на следующий тик обработки, в котором initState уже полностью завершён.

Финальный вид карты:

Ветка репозитория

Итак, мы увидели, как во Flutter подключить Google Maps, как использовать стандартные и нестандартные маркеры. Если у вас остались вопросы — с удовольствием отвечу.


ссылка на оригинал статьи https://habr.com/ru/post/680092/

Frontend архитектура MVP (Model-View-Presenter)

Frontend сейчас сильно разрастается, всё больше компаний переписывают свои старые решения на SPA. В компании которой я работаю это не обошло стороной.

По умолчанию был выбран фреймворк Nuxt.js, т.к Vue лучше React :))
В общем суть не в фреймворке, а с чего начинаем.

Проблемы

  1. Скорость порождает говнокод в плане связей, архитектуры и т.п

  2. Многие разрабы в голове видят архитектуру фронта по разному

  3. Стандартные подходы Vue, где во Vuex экшенах делаются запросы и кладутся в стор и т.п не расширяемые

  4. Сильные зависимости от фрейморка, сложно его обновлять на что-то мажорное если понадобиться

Требования

  1. Кодовая база в едином стиле, чтобы любой мог зайти, изучить документацию и сразу понял что где

  2. Легкая масштабируемость, чтобы не бояться менеджеров с их запросами

  3. Полный контроль состояний каждого блока на странице

  4. Чёткие слои в архитектуре

  5. Чтобы всё было типизировано и был удобный поиск и переход в IDE

  6. Переиспользуемость компонентов

Model View Presenter

Данный шаблон в основном используется в мобильной разработке, и если глянуть то там, в презентер передается вьюшка, и любое действие со стороны вью тригерило действие в презентере, и презентер вызывал нужные методы из вьюшки.

Во фронте мне данный подход не понравился, т.к мы делаем зависимость от представления, а хочется прям жёстко разделить бизнес логику, модель и сервисы от фреймворка в целом.

Новый подход позволит метнуться и на реакт, и на вью3 или другое решение.

Идеология

Бизнес блок — это конкретные компоненты на странице объединенные по смысловой нагрузке и общему состоянию. Все компоненты одного бизнес блока не имеют права обращаться к данным других бизнес блоков. Полная изоляция в рамках своего бизнеса. Это позволяет заюзать кнопку создания заказа где угодно, и ей ничего не надо будет. Она сама всё сделает внутри себя.

Примеры:

  • Каталог: список позиций, кнопка показать больше позиций, карточка позиции.

  • Чайник: слайдер с фотками чайника, описание чайника, тайтл чайника, цена чайника.

  • Корзина: кнопка купить товар, список товаров корзины в шапке, ссылка на переход в корзину.

Модель — состояние бизнес блока, в нем содержится описание всех типов, стор, события и т.п что характеризует бизнес блок. Это конечно не православная активная\пассивная модель в DDD например, но так проще ориентироваться и понимать что происходит.
Тоже самое по изоляции, модель ничего не знает за границами своего стора.

Презентер — то, где находится вся бизнес логика, он является основной точкой работы с бизнес блоками, только презентер может изменить состояние и больше никто. В нем идет обращение к сервисам, мапперам и ко всему тому, что требуется.
Презентер не имеет права вызывать другие презентеры других бизнес блоков, только в рамках своего бизнеса.

Каждый публичный метод презентера начинается с префикса on это важно, презентеру говорят о том, что что-то произошло, сделай что нибудь. А не приказывают)
Методы ничего не возвращают, только изменяют своё состояние, на которое уже подписаны вьюшки и другие. Бывают исключения что удобнее что-то вернуть, тогда да, например получить ссылку на скачивание.

Сервис — слой где делаются запросы.

Связи с внешним миром: роутинг, уведомления и т.п — через единую шину событий. Что-то произошло в презентере, в шину кидаем событие с данными, и в медиаторе или странице обработали.

Медиатор — компонент который имеет доступ ко всем состояниям и презентерам, этакое место связей блоков между собой.

Примеры:

  • Надо по изменению состояния корзины обновить счётчик акции в баннере

  • При подаче заявки в процедуру обновить в шапке текущий статус процедуры

Правила построения бизнес блоков — по умолчанию бизнес растет в горизонталь, и каждый из бизнесов не знает про соседние, только в медиаторах или страницах.

Но бывает так что нужный общий главный родитель, который будет содержать общий контекст для остальных.

Для этого создаем главный бизнес блок, и его состояние могут использовать во вьюшках и медиаторах его дочерние бизнес блоки. Изменить состояние опять же родителя нельзя, только вызывать доступные публичные методы его презентера.

Состояние родителя передается в презентеры дочерних через методы initWithContext(…data)

Состояние во вьюшках можно получить напрямую из родительского в режиме readonly.

Допускается в соседние бизнес блоки в медиаторах передавать в пропсах базовые данные, например ID или что-то очень маленькое для инициализации запроса или еще чего. Нельзя в пропсы передавать больше объекты и т.п, только через презентер.

Примеры

Начнем с директорий

Директория business содержит конечные бизнес блоки с архитекрутой MVP.
Внутри уже store это Vuex модуль, служит чисто для удобства работы со стейтом (реактивность и т.п)

Директория view содержит компоненты бизнес блока которые чаще всего не принимают никаких снаружи пропсов и т.п, всё берут со стора. Могут еще тригерить события, на которые подпишутся в медиаторе например.

Директория mediator содержит агрегирующий компоненты нескольких бизнес блоков

Директория pages страницы для роутинга приложения, по сути это тоже медиатор, но медиатор можно более скрупулезно разделить по ОО в отличии страниц. В страницах чаще всего просто юзаются компоненты медиаторов.

Остальные директории уже относят больше к Nuxt.js.

Посмотрим на код

В домене описываются все возможные интерфейсы, типы, и состояния бизнес блока, вот пример из демо. Презентер инициализирует стор и описывает методы из интерфейса.

Domain.ts
import { namespace } from 'vuex-class'; import { IVuexObservable, TFetchState } from '~/mvp/store';  export type TWidgetData = {   id: string;   title: string;   description: string; };  export enum EModal {   NONE,   WIDGET_CREATE }  export type TModalData = Partial<TWidgetData>;  // ОБЯЗАТЕЛЬНО // Конечное состояние конкретной бизнес логики, домейн export type TState = TFetchState & {   disabled: boolean;   list: TWidgetData[];   showedModal: EModal;   dataModal: TModalData | null; };  // ОБЯЗАТЕЛЬНО // презентер, в нем вся логика, обращение к модели за данными, заполнение стора и оповещение вьюшки. export interface IPresenter extends IVuexObservable {   onCreate(): void;   onCloseModal(): void;   onOpenModal(type: EModal): void;   onTogglePermissionCreate(): void;   onCreateWidget(title: string, description: string): void; }  // если есть сервис то и для сервиса описываем интерфейс  // ОБЯЗАТЕЛЬНО базовое состояние для вьюшки export const initialState = (): TState => ({   isLoading: true,   isError: false,   statusCode: 200,   disabled: true,   errorMessage: '',   list: [],   showedModal: EModal.NONE,   dataModal: null });  export enum EEvents {   CREATE_WIDGET = 'mvp:main:createWidgetEvent' }

Presenter.ts
import eventEmitter from '~/modules/eventbus/EventEmitter';  export default class Presenter   extends VuexObservable<TState, MainVuexModule>   implements IPresenter {   constructor(store: Store<TState>) {     super(store, initialState(), STORE_NS);   }    onCreate(): void {     setTimeout(() => {       this.onChangeState({ isLoading: false });       eventEmitter.emit<TNotification>('notification', {         status: 'success',         title: 'Уведомление',         content: 'Модуль загружен',         position: 'top'       });     }, 700);   }    onCloseModal(): void {     this.onChangeState({ showedModal: EModal.NONE });   }    onOpenModal(type: EModal): void {     this.onChangeState({ showedModal: type });   }    onTogglePermissionCreate(): void {     this.onChangeState({ disabled: !this.state.disabled });   }    onCreateWidget(title: string, description: string): void {     // тут допустим уходит запрос в сервис, возвращаются данные и сетим уже в стейт     this.onChangeState({ title, description }, 'addWidget');     // шлём в общую шину событий уведомление     eventEmitter.emit(EEvents.CREATE_WIDGET, title);   } }

Service.ts из соседнего блока
import { IService, TPost } from '~/demo/business/post/Domain';  export default class Service implements IService {   async fetchListPosts(): Promise<TPost[]> {     const response = await fetch('https://jsonplaceholder.typicode.com/posts');     return await response.json();   } }

Что нибудь из компонентов. Как можете увидеть в компоненты ничего не передается, каждый из компонентов возьмет своё состояние. Вызывает свой презентер и т.п

DemoMediator.vue
<template>   <cds-grid>     <cds-row align-v="stretch">       <cds-col cols="16">         <create-widget-button class="cds-mb-m" />         <widget-list />         <create-widget-modal v-if="state.showedModal === EModal.WIDGET_CREATE" />       </cds-col>     </cds-row>      <cds-row>       <cds-col cols="4">         <menu-posts />       </cds-col>       <cds-col cols="12">         <load-posts-button class="cds-mb-m" />         <post-list />       </cds-col>     </cds-row>   </cds-grid> </template>  <script lang="ts"> import { Component, Vue, Watch } from 'nuxt-property-decorator'; import eventEmitter from '~/modules/eventbus/EventEmitter'; import { EModal, storeModule, TState } from '~/demo/business/main/Domain'; import { postStoreModule, TState as TPostState } from '~/demo/business/post/Domain'; import { TNotification } from '~/demo/@types';  import CreateWidgetButton from '~/demo/business/main/view/action/CreateWidgetButton.vue'; import LoadPostsButton from '~/demo/business/post/view/action/LoadPostsButton.vue';  const PostList = () => import('~/demo/business/post/view/PostList.vue'); const MenuPosts = () => import('~/demo/business/post/view/action/MenuPosts.vue'); const WidgetList = () => import('~/demo/business/main/view/WidgetList.vue'); const CreateWidgetModal = () => import('~/demo/business/main/view/modal/CreateWidgetModal.vue');  @Component({   components: { PostList, LoadPostsButton, MenuPosts, WidgetList, CreateWidgetModal, CreateWidgetButton } }) export default class DemoMediator extends Vue {   EModal = EModal;   // в медиаторе есть доступ ко всем состояниям модуля   @storeModule.State('internalState') state: TState;   @postStoreModule.State('internalState') postState: TPostState;    // можно подписаться на любое состояние и вызывать презентер другого бизнеса и др.   @Watch('postState.list')   onLoadPosts() {     eventEmitter.emit<TNotification>('notification', {       status: 'success',       title: 'Уведомление',       content: 'Список постов загрузился'     });   }    mounted() {     // есть доступ ко всеми презентерам, в медиаторе происходит связь состояний, и постройка базовой логики     this.$presenter.mainInstance.onCreate();     this.$presenter.postInstance.onCreate();   } } </script> 

CreateWidgetButton.vue
<template>   <cds-button :disabled="state.disabled" @click="onClick">     Создать виджет   </cds-button> </template>  <script lang="ts"> import { Component, Vue } from 'nuxt-property-decorator'; import { EModal, storeModule, TState } from '~/demo/business/main/Domain';  @Component export default class CreateWidgetButton extends Vue {   @storeModule.State('internalState') state: TState;    onClick() {     this.$presenter.mainInstance.onOpenModal(EModal.WIDGET_CREATE);   } } </script>

А где же инициализация всего и вся?

В nuxt.js за это отвечают плагины, вот например код плагина и стора.

На ваше усмотрение можно сделать полноценный DI или описание базовый ServiceManager. Так же можно сделать какую нить абстратную штуку которая будет автоматически всё регистрировать в системе вашего фреймворка.
В демо версии я не стал упарываться)

presenter.ts
import { Plugin } from '@nuxt/types';  import * as Main from '~/demo/business/main/Domain'; import * as Post from '~/demo/business/post/Domain';  import MainPresenter from '~/demo/business/main/Presenter'; import PostPresenter from '~/demo/business/post/Presenter';  export interface IPresenterPlugin {   mainInstance: Main.IPresenter;   postInstance: Post.IPresenter; }  const presenter: Plugin = (context, inject) => {   let presenterMainInstance: Main.IPresenter;   let presenterPostInstance: Post.IPresenter;   inject('presenter', {     get mainInstance(): Main.IPresenter {       if (presenterMainInstance) {         return presenterMainInstance;       }        presenterMainInstance = new MainPresenter(context.store);       return presenterMainInstance;     },     get postInstance(): Post.IPresenter {       if (presenterPostInstance) {         return presenterPostInstance;       }        presenterPostInstance = new PostPresenter(context.store);       return presenterPostInstance;     }   }); };

store.ts
import * as Main from '~/demo/business/main/Domain'; import * as Post from '~/demo/business/post/Domain';  import MainVuexModule from '~/demo/business/main/store/MainVuexModule'; import PostVuexModule from '~/demo/business/post/store/PostVuexModule';  export default ({ store }: Context) => {   store.registerModule(     Main.STORE_NS,     MainVuexModule   );   store.registerModule(     Post.STORE_NS,     PostVuexModule   ); }; 

Касаемо архитектуры в Nuxt.js

Нукст можно расширять модулями, лучше так и делать как в демо проекте, это позволит потом при расширении большого проекта вынести в отдельный проект кусочки. Это уже не монолит получается.

Для остальных же фреймворков можно сделать всё тоже самое)

Исходник проекта, можно запустить потыкать: https://github.com/gustoase/habr-mvp-nuxt2


ссылка на оригинал статьи https://habr.com/ru/post/680096/

Визуализация данных GeoJSON с использованием GeoPandas и Python

Ежесекундно в мире появляется огромное количество информации, которую обрабатывать и понимать человеку в большинстве случаев невозможно. Благодаря визуализации данных (построении графиков, диаграмм и нанесении данных на карты), люди могут анализировать информацию, делать выводы и строить прогнозы.

Автором данной статьи является доктор Саумен Атта — постдокторант Лаборатории математических наук в Нантском университете, Франция. Ознакомиться с его блогом можно по ссылке. Оригинал статьи вы можете найти тут.

Фотография Brett Zeck из Unsplash
Фотография Brett Zeck из Unsplash

GeoJSON — открытый формат, предназначенный для хранения географических структур данных, основан на JSON. (Википедия)

В этом уроке мы узнаем, как визуализировать данные GeoJSON с помощью библиотеки Python GeoPandas, используя данные, доступные по этой ссылке. Предоставленный файл GeoJSON соответствует картографическим данным муниципальной корпорации Калькутты с информацией о каждом районе.

Импортирование пакета GeoPandas

Мы можем импортировать пакет GeoPandas, используя следующую команду:

import geopandas as gpd

Если в вашей системе не установлен пакет GeoPandas, вы можете использовать следующую команду pip:

pip install geopandas

Чтение и обработка файла GeoJSON

Теперь мы готовы прочитать данные из файла GeoJSON. Это можно сделать с помощью следующей команды:

df = gpd.read_file(‘kolkata.geojson’)

Предоставленные данные содержат информацию о районах муниципальной корпорации Калькутты в Западной Бенгалии, Индия.

В текущем руководстве данные используется только в образовательных целях. Автор туториала не проверяет подлинность и правильность информации в файле GeoJSON.

Построение карты с помощью GeoJSON-данных

С помощью библиотеки GeoPandas очень просто построить файл GeoJSON. Команда показана ниже:

df.plot()

Приведенная выше команда покажет следующий рисунок:

Визуализация с помощью функции plot() без применения дополнительных параметров
Визуализация с помощью функции plot() без применения дополнительных параметров

Мы также можем передать несколько параметров в функцию plot(), чтобы изменить вид карты следующим образом:

df.plot(figsize=(10,10), edgecolor=‘purple’, facecolor=‘green’)

Таким образом, карта обретет следующий вид:

Визуализация с помощью функции plot(), с применением параметров цвета
Визуализация с помощью функции plot(), с применением параметров цвета

Анализ файла GeoJSON

Мы также можем проанализировать файл GeoJSON, определив форму входных данных следующим образом:

df.shape

На выходе будет кортеж, показанный ниже:

(141, 2)

Следовательно, файл kolkata.geojson имеет 141 строку и 2 столбца (т. е. атрибуты). Это также означает, что муниципалитет Калькутты имеет 141 район согласно файлу входных данных.

Мы также можем просмотреть первые пять строк входных данных следующим образом:

df.head()

Результат показан ниже:

Результат функции print(df.head())
Результат функции print(df.head())

Мы видим, что имена атрибутов — «WARD» и «geometry».

Больше опций для построения визуализаций

Возможно, что мы хотим нанести только границы районов. Это легко сделать с помощью следующей команды:

df.boundary.plot(figsize=(10,10))

Результат показан ниже:

Границы всех районов Калькутты
Границы всех районов Калькутты

Если мы хотим начертить границу определенного района, скажем, округа номер 23 в муниципалитете Калькутты, мы можем использовать следующие команды:

df_23 = df.loc[df[‘WARD’] == ‘23’]

df_23.boundary.plot(figsize=(10,10))

Также, мы также можем использовать альтернативные команды следующего вида:

df_23 = df[df.WARD==‘23’]

df_23.boundary.plot(figsize=(10,10))

Полученная карта показана ниже:

 Граница района 23 в Калькутте
Граница района 23 в Калькутте

Итак, мы научились извлекать данные из файла GeoJSON с помощью библиотеки Python GeoPandas, а также узнали, как отображать данные GeoJSON различными способами.

Примечание

Помимо перевода данной статьи, я была озадачена тем, чтобы предоставить полностью рабочий код. Но возникли следующие сложности:

  1. Модуль geopandas не устанавливался. Команда «pip install geopandas» выдавала ошибку. Изучив информацию, я узнала, что для установки данной библиотеки требуется предварительная установка следующих модулей:

    • pip install wheel

    • pip install pipwin

    • pipwin install numpy

    • pipwin install pandas

    • pipwin install shapely

    • pipwin install gdal

    • pipwin install fiona

    • pipwin install pyproj

    • pipwin install rtree

    • pip install geopandas

и лишь после этого код заработал.

  1. Несмотря на то, что код работал, картинка с изображением карты не выводилась. Для этого требуется в конце кода ввести: «plt.show()». А до этого требуется установить модуль matplotlib с помощью команды «pip install matplotlib», после чего импортировать.

  2. Для вывода данных df.head() я воспользовалась функцией print(): «print(df.head())».

Надеюсь данные комментарии помогут лучше ознакомиться с библиотекой geopandas и ее преимуществами. А также я предоставляю ссылку на github.


ссылка на оригинал статьи https://habr.com/ru/post/680100/

Профессиональные и непрофессиональные клиенты на фрилансе

Это глава из «Книги нормального фрилансера», в которой я делюсь накопившимся опытом проектировщика интерфейсов, работающего на себя.

Нужно выбирать клиента только близкого по духу.
Пусть он платит очень мало, но зато свой человек.
Объяснит тебе задачу, назовёт свои дедлайны
И подробно обоснует все задержки в платежах.

При выходе на фриланс логично применять опыт, полученный на прошлом месте работы, а не осваивать профессию с нуля. После работы в студии по созданию сайтов я решил не менять направление деятельности и продолжил развиваться в проектировании интерфейсов. Кроме того, за время работы научился ладить с шефом и коллегами, справляться с авральными и сложными ситуациями и решать задачи самостоятельно, не докучая окружающим лишними вопросами.

Первое, что я сделал, — разослал электронные письма в петербургские и московские студии с предложением посотрудничать. Я обещал буквально в одиночку заменить собой целый отдел проектирования, попутно объясняя, что получится дешевле, чем нанимать сотрудников в штат. Так как письма я писал уже неплохо и умел работать с текстом, как с интерфейсом, призывающим к действию и закрывающим возражения потенциального клиента, несколько студий откликнулись и между нами завязались партнёрские отношения.

Так я вышел на профессиональных клиентов — буквально на работодателей в моей нише. Тех, кто может оценить мой профессиональный уровень, знает рыночную стоимость моих услуг и берёт на себя многие риски по управлению проектами, поэтому к моим срывам сроков или некачественным промежуточным результатам относился бы тоже профессионально. Определения «профессиональных» и «непрофессиональных» клиентов я придумал сам и нигде их не встречал.

Профессиональные клиенты очень похожи на наших предыдущих работодателей, поэтому, начиная сотрудничать с ними на фрилансе, я чувствовал себя, как рыба в воде. Но проработав с ними в течение трёх лет, я вырос из такого формата и столкнулся с рядом ограничений, которые не позволяли зарабатывать больше.

  • Иногда не было прямого доступа к финальному заказчику. Общение происходило через «испорченный телефон» в виде менеджера проекта. Это мешало качественно выполнять работу.

  • Даже когда спрос превышал предложение, сложно было повышать цены. Профессиональные клиенты в какой-то момент находили вместо меня другого исполнителя со среднерыночными расценками. И я упирался в финансовый потолок.

  • Продолжая работать с профессиональными клиентами, я не прокачивал навыков, необходимых для привлечения клиентов, улучшения коммуникации, повышения уровня сервиса.

  • Я делал довольным не финального клиента, который хотел решения своей проблемы, а менеджера проектов со стороны профессионального клиента, который хотел получить максимально возможную прибыль от моей работы, зачастую жертвуя ради этого качеством.

  • Оплата зависела от финального заказчика и от процессов внутри компании, с которой я сотрудничал. В таких условиях было довольно сложно рассчитывать на 100% предоплату. Возникали задержки в выплатах.

Разумеется, бывали и исключения. Некоторые студии допускали меня к своим клиентам и позволяли самому вести всю коммуникацию. Бывали и такие, которые соглашались на высокие ценники, а также вносили 100% предоплату. С ними я работаю и по сей день.

В период сотрудничества со студиями, я создавал ролики, обучавшие использованию программы по проектированию, а также периодически писал статьи о моей профессии. Эти материалы в конце концов привели первых непрофессиональных клиентов. Тех, кому нужно было спроектировать что-то для себя. Это были начинающие стартаперы и команды с уже готовым продуктом, но без проектировщика в команде.

И поначалу я не мог сделать их довольными, хоть и старался изо всех сил. Всё потому, что не все навыки для выполнения этой задачи можно получить в найме или при работе с профессиональными клиентами. И вот те вещи, к которым я не был готов:

  • Если я нарушал сроки, то ко мне не приходил менеджер с вопросом «ну как дела?» Никто не «журил» меня за опоздание и не просил назвать следующую дату. Я оказывался один-на-один со своим провалом и должен был сам сообщить клиенту, что произошло и как это повлияет на проект. И поначалу это вызывало ситуации, когда я боялся связаться с заказчиком, пропадал на несколько дней, пытаясь всё доделать так, чтобы «клиент не заметил». Это, в свою очередь, снижало качество работы.

  • Для совершения дорогостоящих сделок пришлось заводить ИП, самостоятельно готовить договоры, выставлять счета, закрывать выполненные работы актами.

  • Такие клиенты не могли оценить моих профессиональных навыков. Единственное, на что они сразу обращали внимание, — это уровень оказываемого сервиса.

  • Результат работы в первую очередь должен быть понятен клиенту и нравиться ему, а уже во вторую быть объективно эффективным (многие могут возмутиться такому подходу, но дальше в книге я постараюсь объяснить, как делать свою работу максимально качественно и при этом так, чтобы она нравилась клиенту).

  • Между мной и клиентом не было менеджера, который умел бы брать удар на себя и сглаживать острые углы в конфликтных ситуациях. Вся ответственность за процесс и результат лежала на мне.

Зато теперь я мог повышать цены при высоком спросе («вас много, а я один»), брать 100% предоплату, работать над персональным брендом и делать клиентов довольными, чтобы они возвращались снова и снова и рекомендовали меня другим (кстати, профессиональные клиенты меня никогда никому не рекомендовали, чтобы случайно не лишиться хорошего исполнителя, когда он окажется занят в других проектах).

Особенно большой прорыв в этом направлении произошёл в тот день, когда я сам впервые оказался на месте клиента на услуги проектирования. Я бы даже рекомендовал начинающему фрилансеру заказать работы у одного из конкурентов и посмотреть, что к чему. Обратите внимание на те места в процессе оказания услуги, которые доставили удовольствие, и на те, которые вызвали раздражение, после чего используйте полученный опыт в своей работе. Без такого опыта, можно всю дорогу оказывать услуги, основываясь лишь на своих предположениях о том, что хочет клиент, и что делает его довольным.

Итого. Профессиональные клиенты похожи на наших бывших работодателей и с ними легко начать взаимодействовать при выходе на фриланс. Это может быть промежуточным шагом на пути к непрофессиональным клиентам. Последние же более требовательны к сервису и меньше ориентируются в средней стоимости. Их гораздо больше, чем профессиональных клиентов и, научившись находить их и делать довольными, можно почувствовать себя на фрилансе так же уверенно, как до этого в найме (а может даже и увереннее).


«Книга нормального фрилансера» ещё в работе. Я потихоньку пишу её с 2019 года. Планирую в этом году закончить. Следить за прогрессом, а также читать другие мои фрилансерские истории можно в Телеграме и Вконтакте.

Прикладываю картинку с оглавлением. Жирным выделены дописанные и отредактированные главы. Плюсиками — главы, к которым я написал «вредные советы».


ссылка на оригинал статьи https://habr.com/ru/post/680110/