А пользователь кто? Моделирование пользовательских ролей и описание персон

В гибкой разработке как никогда популярно использование пользовательских историй (user story). И если вы слышали или работали с ними, то в курсе, что они пишутся от имени разных пользователей (user types). Определение пользователей важно не только для этого инструмента, но и если используются сценарии (use cases) или иное представление требований. Эта статья расскажет о моделировании персон, о том, как можно определить пользовательские роли для своего продукта.

Сначала еще раз про user story

Пользовательская история (user story, US) содержит описание функциональности, которая будет представлять ценность для пользователя или покупателя программного обеспечения (ПО).

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

Пример пользовательской истории:

Пользователь может создать список покупок.

Кроме такого лаконичного описания могут быть примечания, которые нужны для обсуждения («Он должен быть доступен всем в доме? Сохранять автоматически или по кнопке?») и список тестов, которые раскрывают суть реализации («Проверить: создание списка, добавление пунктов, удаление пунктов, возможность задать заголовок для списка, возможность указать количество товара, возможность не указывать количество товара» и т.д.). Само описание тестов может быть разным, но главное, чтобы по тестам было понятно, когда история действительно реализована.  Также, если требуется, после обсуждения появляются дочерние истории, более мелкие.

Вообще формат истории может быть разным, самое важное в этом инструменте — обсуждение!

Для тех, кто привык к стандартному описанию требований, такой подход может быть непривычен («Как так без конкретного описания, что сделать?», «Неужели тесты – это часть user story»?). Приемочные тесты как раз должны описывать, что должно быть сделано, чтобы история считалась решенной. По крайней мере, задумка такова. В реальности зачастую к историям прибавляются задачи на разработку (tasks), в которых дается больше конкретики по принятым решениям.

В отличие от классических спецификаций (Software requirements specification, SRS) пользовательские истории краткие, не содержат в себе описания конкретной реализации, не являются контрактными обязательствами (!). Тем не менее, их использование хорошо зарекомендовало себя, так как отлично ложится на итеративную разработку (как extreme programming, agile или конкретно scrum), позволяет максимально привлекать заказчика в проект, стимулирует общение в команде и откладывает проработку деталей до момента непосредственной реализации (а не на старте проекта). В целом, использовать пользовательские истории можно даже в государственных контрактах, так как в первоначальном ТЗ может не быть конкретных деталей (а как раз, по сути, список US), однако по результатам разработки необходимо будет написать весь пакет требуемой документации.

На мой взгляд, в случае заказной разработки отдельно необходимо работать с разрешением споров, так как пользовательские истории (еще раз) – не контрактные обязательства. Нельзя обратиться к ним и сказать: «Вот тут так написано, мы так и сделали». Конечно, за счет привлечения заказчика к обсуждению и постоянного контакта с ним не должно возникать ситуации «сделали не то, что хотелось», но часто может возникать ситуация «сделали не всё, что хотелось», ведь у контрактов есть цена, рассчитанная на определенные трудозатраты. Этот момент необходимо прорабатывать отдельно, но он не является блокирующим для использования user story в проекте. Часто в подобных ситуациях команды приходят к некоторому гибридному варианту, что тоже имеет право на жизнь.

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

Пользовательские роли, персоны

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

Для разделения историй по разным типам пользователей придуманы пользовательские роли.

Пользовательская роль (user role) – описание некоторой совокупности пользователей и предполагаемого взаимодействия с системой.

Кроме описания группы пользователей аналитики часто задают некоторый образ, дают роли имя, возраст, профессию, это часто помогает команде представить, для кого делается продукт, также помогает и выявить упущенные истории. Такое описание называется персоной или личностью (persona).

Существует несколько способов выявить, смоделировать роли пользователей.

Способ 1. Реальные пользователи

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

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

Способ 2. Мозговой штурм

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

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

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

В ходе мозгового штурма были сгенерированы следующие роли, которые расположили по близости и пересечению:

После обсуждения каждой роли было принято решение оставить следующие (розовые стикеры):

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

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

Пример — описание роли «Мама»:

Естественно, не обязательно, чтобы в самом приложении фигурировало название этой роли.

Способ 3. Сегментирование

Лично мне нравится другой способ моделирования ролей – от целевой аудитории. В данном случае также можно (даже рекомендуется) использовать мозговой штурм, но не сразу для названия ролей, а для определения критериев по разделению ЦА.

Способ рассмотрим так же на примере определения пользователей приложения для планирования домашних дел.

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

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

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

После этого с помощью критериев необходимо определить категории целевой аудитории, и для каждой категории задать название или сразу описать персону. То есть необходимо придумать роль, человека, который сочетает в себе разные значения критериев для разделения ЦА. Например, член семьи, который распределяет и контролирует обязанности по дому – мать Анжела. Сосед в съемной квартире или общежитии, который заинтересован в понимании, кто когда что делает по дому – Санёк. Человек, который не заинтересован в наведении порядка, но вынужден что-то делать по дому – подросток Володька.

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

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

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

Способ 4. Экстремальные персонажи

Что еще? Еще есть дополнительная методика – использование экстремальных персонажей (extreme characters). Вместо проектирования продукта для типичного пользователя предлагается подумать о пользователях с неординарными личностными качествами. Так, при проектировании персонального цифрового помощника предлагается подумать не о бизнес-консультанте в костюме, а о наркоторговце и о папе римском. Это может помочь нащупать новые полезные фичи, но не факт, что их целесообразно будет включать в продукт. Во всяком случае, это интересный инструмент развития креативности.

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

Что дальше

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

Резюме

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

  • Определение пользовательских ролей (user types) полезно для разных методов документирования разработки.

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

  • Для полноты картины следует использовать все способы моделирования пользовательских ролей.

  • Персона (persona) – воображаемое представление пользовательской роли (её очеловечивание), помогает членам команды лучше представлять пользовательскую роль.

  • Само моделирование пользовательских ролей уже позволяет выявить новые пользовательские истории (или требования).

Михайлова Анна

Начальник отдела интеллектуального анализа данных, главный системный аналитик,
Консорциум «Кодекс»

 


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

Tiny-qORM: рассказ без счастливого конца

Чаще всего на хабре люди делятся историями своего успеха. Вроде, «Ребята, я написал свою ORM, качайте, ставьте ллойсы!» Эта история будет немного другая. В ней я расскажу о неуспехе, который считаю своим серьёзным достижением.


Ожидание — реальность.

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

С чего всё началось? Предсказуемо, с лени. Как-то раз появилась задача (де-)сериализации структур в SQL. Большого числа структур, несколько сотен. Естественно, с разными уровнями вложенности, с указателями и контейнерами. Помимо прочего, имелась определяющая особенность: все они уже имели привязку к QJSEngine, то есть имели полноценную метасистему Qt.

С такими вводными не мудрено было придти к написанию своей ORM и поставить весьма амбициозные цели:
1) Минимальная модификация сохраняемых структур. В лучшем случае, без оной вообще, в худшем — Ctrl+Shift+F.
2) Работа с любыми типами, контейнерами и указателями.
3) Не самые страшные таблицы с возможностью их использования вне ORM.

И обозначить предсказуемые ограничения:
1) Таблицы создаются только для классов с метаинформацией (Q_OBJECT\Q_GADGET) для их свойств (Q_PROPERTY). Все зарегистрированные в метасистеме типы, не имеющие метаинформации, будут сохраняться либо в виде строк, либо в виде сырых данных. Если преобразование не существует или тип неизвестен, он пропускается.

Забегая вперёд, получилось следующее:

До ORM

struct Mom {     Q_GADGET     Q_PROPERTY(QString name MEMBER m_name)     Q_PROPERTY(She is MEMBER m_is) public:     enum She {         Nice,         Sweet,         Beautiful,         Pretty,         Cozy,         Fansy,         Bear     }; Q_ENUM(She) public:     QString m_name;     She m_is;     bool operator !=(Mom const& no) { return m_name != no.m_name; } }; Q_DECLARE_METATYPE(Mom)  struct Car {     Q_GADGET     Q_PROPERTY(double gas MEMBER m_gas) public:     double m_gas; }; Q_DECLARE_METATYPE(Car)  struct Dad {     Q_GADGET     Q_PROPERTY(QString name MEMBER m_name)     Q_PROPERTY(Car * car MEMBER m_car) public:     QString m_name;     Car * m_car = nullptr; // lost somewhere     bool operator !=(Dad const& no) { return m_name != no.m_name; } }; Q_DECLARE_METATYPE(Dad)  struct Brother {     Q_GADGET     Q_PROPERTY(QString name MEMBER m_name)     Q_PROPERTY(int last_combo MEMBER m_lastCombo)     Q_PROPERTY(int total_punches MEMBER m_totalPunches) public:     QString m_name;     int m_lastCombo;     int m_totalPunches;     bool operator !=(Brother const& no) { return m_name != no.m_name; }     bool operator ==(Brother const& no) { return m_name == no.m_name; } }; Q_DECLARE_METATYPE(Brother)  struct Ur {     Q_GADGET     Q_PROPERTY(QString name MEMBER m_name)     Q_PROPERTY(Mom mom MEMBER m_mama)     Q_PROPERTY(Dad dad MEMBER m_papa)     Q_PROPERTY(QList<Brother> bros MEMBER m_bros)     Q_PROPERTY(QList<int> drows MEMBER m_drows) public:     QString m_name;     Mom m_mama;     Dad m_papa;     QList<Brother> m_bros;     QList<int> m_drows; }; Q_DECLARE_METATYPE(Ur) 

bool init() {          qRegisterType<Ur>("Ur");         qRegisterType<Dad>("Dad");         qRegisterType<Mom>("Mom");         qRegisterType<Brother>("Brother");         qRegisterType<Car>("Car"); } bool serialize(QList<Ur> const& urs) {      /* SQL hell */  } 

После ORM

struct Mom {     Q_GADGET     Q_PROPERTY(QString name MEMBER m_name)     Q_PROPERTY(She is MEMBER m_is) public:     enum She {         Nice,         Sweet,         Beautiful,         Pretty,         Cozy,         Fansy,         Bear     }; Q_ENUM(She) public:     QString m_name;     She m_is;     bool operator !=(Mom const& no) { return m_name != no.m_name; } }; ORM_DECLARE_METATYPE(Mom)  struct Car {     Q_GADGET     Q_PROPERTY(double gas MEMBER m_gas) public:     double m_gas; }; ORM_DECLARE_METATYPE(Car)  struct Dad {     Q_GADGET     Q_PROPERTY(QString name MEMBER m_name)     Q_PROPERTY(Car * car MEMBER m_car) public:     QString m_name;     Car * m_car = nullptr; // lost somewhere     bool operator !=(Dad const& no) { return m_name != no.m_name; } }; ORM_DECLARE_METATYPE(Dad)  struct Brother {     Q_GADGET     Q_PROPERTY(QString name MEMBER m_name)     Q_PROPERTY(int last_combo MEMBER m_lastCombo)     Q_PROPERTY(int total_punches MEMBER m_totalPunches) public:     QString m_name;     int m_lastCombo;     int m_totalPunches;     bool operator !=(Brother const& no) { return m_name != no.m_name; }     bool operator ==(Brother const& no) { return m_name == no.m_name; } }; ORM_DECLARE_METATYPE(Brother)  struct Ur {     Q_GADGET     Q_PROPERTY(QString name MEMBER m_name)     Q_PROPERTY(Mom mom MEMBER m_mama)     Q_PROPERTY(Dad dad MEMBER m_papa)     Q_PROPERTY(QList<Brother> bros MEMBER m_bros)     Q_PROPERTY(QList<int> drows MEMBER m_drows) public:     QString m_name;     Mom m_mama;     Dad m_papa;     QList<Brother> m_bros;     QList<int> m_drows; }; ORM_DECLARE_METATYPE(Ur) 

bool init() {          ormRegisterType<Ur>("Ur");         ormRegisterType<Dad>("Dad");         ormRegisterType<Mom>("Mom");         ormRegisterType<Brother>("Brother");         ormRegisterType<Car>("Car"); } bool serialize(QList<Ur> const& urs) {           ORM orm;          orm.create<Ur>(); // if not exists          orm.insert(urs); } 

Diff

        Q_DECLARE_METATYPE(Mom)     -> ORM_DECLARE_METATYPE(Mom)         Q_DECLARE_METATYPE(Car)     -> ORM_DECLARE_METATYPE(Car)         Q_DECLARE_METATYPE(Dad)     -> ORM_DECLARE_METATYPE(Dad)         Q_DECLARE_METATYPE(Brother) -> ORM_DECLARE_METATYPE(Brother)         Q_DECLARE_METATYPE(Ur)      -> ORM_DECLARE_METATYPE(Ur)          qRegisterType<Ur>("Ur");         -> ormRegisterType<Ur>("Ur");         qRegisterType<Dad>("Dad");        -> ormRegisterType<Dad>("Ur");         qRegisterType<Mom>("Mom");        -> ormRegisterType<Mom>("Ur");         qRegisterType<Brother>("Brother");-> ormRegisterType<Brother>("Ur");         qRegisterType<Car>("Car");        -> ormRegisterType<Car>("Ur");           /* sql hell */ -> ORM orm;                            orm.create<Ur>(); // if not exists                            orm.insert(urs); 

Making of…

Шаг 1. Получать метаинформацию, список полей класса и их значения.

Спасибо разработчикам Qt, мы это можем делать по щелчку пальцев и id метакласса. Выглядит это примерно так:

const QMetaObject * object = QMetaType::metaObjectForType(id); if (object) {     for (int i = 0; i < object->propertyCount(); ++i) {         QMetaProperty property = object->property(i);         columns << property.name();         types << property.userType();     } }

Чтение и запись так же не вызывают проблем. Почти. QMetaProperty имеет пару методов чтения-записи для объектов. И ещё одну пару для гаджетов. Поэтому на этапе чтения-записи нам нужно определиться, в кого мы пишем. Делается это так:

bool isQObject(QMetaObject const& meta) {     return meta.inherits(QMetaType::metaObjectForType(QMetaType::QObjectStar)); }

Тогда чтение и запись производятся следующим образом:

inline bool write(bool isQObject, QVariant & writeInto,                    QMetaProperty property, QVariant const& value) {     if (isQObject) return property.write(writeInto.value<QObject*>(), value);     else return property.writeOnGadget(writeInto.data(), value); }  inline QVariant read(bool isQObject, QVariant const& readFrom,                       QMetaProperty property) {     if (isQObject) {         QObject * object = readFrom.value<QObject*>();         return property.read(object);     }     else {         return property.readOnGadget(readFrom.value<void*>());     } }

Казалось бы, readOnGadget, в конце концов, вызывает тот же read, так что зачем городить весь этот код? Совместимость и отсутствие гарантий, что такое поведение не изменится.

И ещё один нюанс. При сохранении Q_ENUM в QVariant его значение кастуется в int. В базу данных тоже поступает int. Но записать int в свойство типа Q_ENUM мы не можем. Поэтому перед записью мы должны проверить, является ли указанное свойство перечислением — и вызвать явное преобразование в таком случае. Звучит страшнее, чем есть на самом деле.

if (property.isEnumType()) {    variant.convert(property.userType()); }

Шаг 2. Создавать произвольные структуры по метаинформации.

Снова бьём челом разработчикам за класс QVariant и его конструктор QVariant(int id, void* copy). С его помощью можно создать любую структуру с пустым конструктором — и это хорошая новость. Плохая новость: наследники QObject в список не входят. Хорошая новость: их можно делать с помощью QMetaObject::newInstance().

Создание экземпляра произвольного типа будет выглядеть примерно так:

QVariant make_variant(QMetaObject const& meta) {     QVariant variant;     if (isQObject(meta)) {         QObject * obj = meta.newInstance();         if (obj) {             obj->setObjectName("orm_made");             obj->setParent(QCoreApplication::instance());             variant = QVariant::fromValue(obj);         }     }     else {         variant = QVariant((classtype), nullptr);     }     if (!variant.isValid()){         qWarning() << "Unable to create instance of type " << meta.className();     }     if (isQObject(meta) && variant.value<QObject*>() == nullptr) {         qWarning() << "Unable to create instance of QObject " << meta.className();     }     return variant; }

Шаг 3. Реализовать сериализацию тривиальных типов.

Под тривиальными типами будем понимать числа, строки и бинарные поля. Вроде бы задача простая, снова берём QVariant и в бой. Но есть нюанс. В ряде случаев нам может захотеться сделать «тривиальными» иные типы, например, изображение. С одной стороны, можно было бы просто проверять, есть ли у метатипа нужные конвертеры и использовать их. Но это не самый удачный способ, тем более, что он чреват возникновением конфликтов, так что лучше иметь списки типов и способы их сохранения: в строку, в BLOB или отдать на откуп Qt. На этом же шаге лучше заиметь список тех типов, с которыми вы предпочтёте не связываться. Из стандартных это могут быть JSON-объекты или QModelIndex. Опять же, никакой магии, статические списки.

Шаг 4. Реализовать сериализацию нетривиальных типов: структур, указателей, контейнеров.

И опять, разработчики постарались: их QVariant решает эту задачу. Или нет?

Проблема 1: связность указателя и типа, шаблона и типов-параметров.

Для произвольного метакласса нельзя ни получить связные с ним метаклассы указателей (или структуры), ни получить тип, хранимый в шаблоне. Это очень печально, хотя и вполне предсказуемо. Откуда ей взяться?

Неоткуда.

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

template <typename T> int orm::Register(const char * c) {             int type = qMetaTypeId<T>();             if (!type) {                 if (c) {                     type = qRegisterMetaType<T>(c);                 }                 else {                     type = qRegisterMetaType<T>();                 }             }             Config::addPointerStub(orm::Pointers::registerTypePointers<T>());             orm::Containers::registerSequentialContainers<T>();             return type; }

А вместе с ней и статический массивчик под это дело. Точнее, QMap, где ключом будет id метакласса, а значением — структура, хранящая все связные типы.

Выглядит это, конечно, пошловато, но работает.

Серьёзно, вы вряд ли тут найдёте что-нибудь принципиально новое.

// <h> struct ORMPointerStub {     int T =0;  // T     int pT=0; // T*     int ST=0; // QSharedPointer<T>     int WT=0; // QWeakPointer<T>     int sT=0; // std::shared_ptr<T>     int wT=0; // std::weak_ptr<T> }; // <cpp> static QMap<int, orm_pointers::ORMPointerStub> pointerMap; void ORM_Config::addPointerStub(const orm_pointers::ORMPointerStub & stub) {     if (stub. T) pointerMap[stub. T] = stub;     if (stub.pT) pointerMap[stub.pT] = stub;     if (stub.ST) pointerMap[stub.ST] = stub;     if (stub.WT) pointerMap[stub.WT] = stub;     if (stub.sT) pointerMap[stub.sT] = stub;     if (stub.wT) pointerMap[stub.wT] = stub; } // <h> template <typename T> void* toVPointer (                T  const& t)     { return reinterpret_cast<void*>(const_cast<T*>(&t       )); } template <typename T> void* toVPointerP(                T *       t)     { return reinterpret_cast<void*>(                t        ); } template <typename T> void* toVPointerS(QSharedPointer <T> const& t)     { return reinterpret_cast<void*>(const_cast<T*>( t.data())); } template <typename T> void* toVPointers(std::shared_ptr<T> const& t)     { return reinterpret_cast<void*>(const_cast<T*>( t.get ())); }  template <typename T> T* fromVoidP(void* t)     { return                    reinterpret_cast<T*>(t) ; } template <typename T> QSharedPointer <T> fromVoidS(void* t)     { return QSharedPointer <T>(reinterpret_cast<T*>(t)); } template <typename T> std::shared_ptr<T> fromVoids(void* t)     { return std::shared_ptr<T>(reinterpret_cast<T*>(t)); }  template <typename T> ORMPointerStub registerTypePointersEx() {     ORMPointerStub stub;     stub.T = qMetaTypeId<T>();     stub.pT = qRegisterMetaType<T*>();     stub.ST = qRegisterMetaType<QSharedPointer <T>>();     stub.WT = qRegisterMetaType<QWeakPointer   <T>>();     stub.sT = qRegisterMetaType<std::shared_ptr<T>>();     stub.wT = qRegisterMetaType<std::weak_ptr  <T>>();      QMetaType::registerConverter<                T , void*>(&toVPointer <T>);     QMetaType::registerConverter<                T*, void*>(&toVPointerP<T>);     QMetaType::registerConverter<QSharedPointer <T>, void*>(&toVPointerS<T>);     QMetaType::registerConverter<std::shared_ptr<T>, void*>(&toVPointers<T>);      QMetaType::registerConverter<void*,                 T*>(&fromVoidP<T>);     QMetaType::registerConverter<void*, QSharedPointer <T>>(&fromVoidS<T>);     QMetaType::registerConverter<void*, std::shared_ptr<T>>(&fromVoids<T>);     return stub; }

Как вы могли заметить, тут уже были зарегистрированы конвертеры T>void*, T*>void* и void*>T*. Ничего особенного, они нам потребуются для спокойной работы с QMetaProperty, так как в select, где будут создаваться элементы, мы будем делать простые указатели, а передавать вообще универсальный void*. Нужный тип указателя будет создан самим QVariant в момент записи.

Проблема 2: обработка контейнеров.

С контейнерами не всё так плохо. Для последовательных есть простой способ узнать, является ли переданный нам тип зарегистрированным:

bool isSequentialContainer(int metaTypeID){     return QMetaType::hasRegisteredConverterFunction(metaTypeID,                  qMetaTypeId<QtMetaTypePrivate::QSequentialIterableImpl>()); }

Пробежаться по нему:

QSequentialIterable sequentialIterable = myList.value<QSequentialIterable>(); for (QVariant variant : sequentialIterable) {     // do stuff }

И даже получить ID хранимого метатипа (осторожно — глаза!)

inline int getSequentialContainerStoredType(int metaTypeID) {     return (*(QVariant(static_cast<QVariant::Type>(metaTypeID))                 .value<QSequentialIterable>()).end()).userType(); // да, .end()).userType(); // мне стыдно, хорошо? }

Так что сохранение данных становится делом чисто техническим. Остаётся лишь справиться со всем многообразием контейнеров. Моя реализация затрагивает лишь те, которые можно получить кастами из QList. Во-первых, потому, что результатом QSqlQuery является QVariantList, а, во-вторых, потому, что он может кастоваться во все основные Qt и std контейнеры. (Есть и третья причина, шаблонная магия std плохо впихивается в универсальные короткие решения.)

template <typename T> QList<T> qListFromQVariantList(QVariant const& variantList) {     QList<T> list;     QSequentialIterable sequentialIterable = variantList.value<QSequentialIterable>();     for (QVariant const& variant : sequentialIterable) {         if(v.canConvert<T>()) {             list << variant.value<T>();         }     }     return list; } template <typename T> QVector    <T>   qVectorFromQVariantList(QVariant const& v)      { return qListFromQVariantList<T>(v).toVector              (); } template <typename T> std::list  <T>   stdListFromQVariantList(QVariant const& v)      { return qListFromQVariantList<T>(v).toStdList             (); } template <typename T> std::vector<T> stdVectorFromQVariantList(QVariant const& v)      { return qListFromQVariantList<T>(v).toVector().toStdVector(); }  template <typename T> void registerTypeSequentialContainers() {     qMetaTypeId<QList      <T>>() ? qMetaTypeId<QList      <T>>()                                    : qRegisterMetaType<QList      <T>>();     qMetaTypeId<QVector    <T>>() ? qMetaTypeId<QVector    <T>>()                                    : qRegisterMetaType<QVector    <T>>();     qMetaTypeId<std::list  <T>>() ? qMetaTypeId<std::list  <T>>()                                    : qRegisterMetaType<std::list  <T>>();     qMetaTypeId<std::vector<T>>() ? qMetaTypeId<std::vector<T>>()                                    : qRegisterMetaType<std::vector<T>>();     QMetaType::registerConverter<QVariantList, QList      <T>>(&(    qListFromQVariantList<T>));     QMetaType::registerConverter<QVariantList, QVector    <T>>(&(  qVectorFromQVariantList<T>));     QMetaType::registerConverter<QVariantList, std::list  <T>>(&(  stdListFromQVariantList<T>));     QMetaType::registerConverter<QVariantList, std::vector<T>>(&(stdVectorFromQVariantList<T>)); }

С ассоциативными контейнерами и парами дела обстоят хуже. Несмотря на то, что для них есть аналогичный по функциональности с QSequentialIterable класс QAssociativeIterable, некоторые сценарии его использования приводят к вылетам программы. Поэтому нас снова ожидают старые друзья: структура и статический массив, которые нужны для выяснения хранившегося в контейнере типа. Кроме того, нам потребуется тип-прокладка, который бы смог сохранить промежуточные результаты select для каждой строки. Можно было бы использовать QPair<QVariant,QVariant>, но я решил создать собственный тип, чтобы избежать конфликтов преобразования.

// Код становится всё больше и всё скучнее. Если интересно, https://github.com/iiiCpu/Tiny-qORM/blob/master/ORM/orm.h

Скрытый текст

Я смотрю, ты упорный. На.

     struct ORM_QVariantPair //: public ORMValue     {         Q_GADGET         Q_PROPERTY(QVariant key MEMBER key)         Q_PROPERTY(QVariant value MEMBER value)     public:         QVariant key, value;         QVariant& operator[](int index){ return index == 0 ? key : value; }     };      template <typename K, typename T> QMap<K,T> qMapFromQVariantMap(QVariant const& v)     {         QMap<K,T> list;         QAssociativeIterable ai = v.value<QAssociativeIterable>();         QAssociativeIterable::const_iterator it = ai.begin();         const QAssociativeIterable::const_iterator end = ai.end();         for ( ; it != end; ++it) {             if(it.key().canConvert<K>() && it.value().canConvert<T>()) {                 list.insert(it.key().value<K>(), it.value().value<T>());             }         }         return list;     }      template <typename K, typename T> QList<ORM_QVariantPair> qMapToPairListStub(QMap<K,T> const& v)     {         QList<ORM_QVariantPair> psl;         for (auto i = v.begin(); i != v.end(); ++i) {             ORM_QVariantPair ps;             ps.key = QVariant::fromValue(i.key());             ps.value = QVariant::fromValue(i.value());             psl << ps;         }         return psl;     }      template <typename K, typename T> void registerQPair()     {         ORM_Config::addPairType(qMetaTypeId<K>(), qMetaTypeId<T>(),                                 qMetaTypeId<QPair <K,T>>() ? qMetaTypeId<QPair <K,T>>() : qRegisterMetaType<QPair <K,T>>());         QMetaType::registerConverter<QVariant, QPair<K,T>>(&(qPairFromQVariant<K,T>));         QMetaType::registerConverter<QVariantList, QPair<K,T>>(&(qPairFromQVariantList<K,T>));         QMetaType::registerConverter<ORM_QVariantPair, QPair<K,T>>(&(qPairFromPairStub<K,T>));         QMetaType::registerConverter<QPair<K,T>, ORM_QVariantPair>(&(toQPairStub<K,T>));     }     template <typename K, typename T> void registerQMap()     {         registerQPair<K,T>();          ORM_Config::addContainerPairType(qMetaTypeId<K>(), qMetaTypeId<T>(),                                          qMetaTypeId<QMap <K,T>>() ? qMetaTypeId<QMap <K,T>>() : qRegisterMetaType<QMap <K,T>>());         QMetaType::registerConverter<QMap<K,T>, QList<ORM_QVariantPair>>(&(qMapToPairListStub<K,T>));         QMetaType::registerConverter<QVariantMap            , QMap<K,T>>(&(qMapFromQVariantMap<K,T>));         QMetaType::registerConverter<QVariantList           , QMap<K,T>>(&(qMapFromQVariantList<K,T>));         QMetaType::registerConverter<QList <ORM_QVariantPair>, QMap<K,T>>(&(qMapFromPairListStub<K,T>));     }  uint qHash(ORM_QVariantPair const& variantPair) noexcept; Q_DECLARE_METATYPE(ORM_QVariantPair) 

Проблема 3: использование контейнеров.

У контейнеров есть ещё одна проблема: они не являются структурой. Вот такой вот внезапный удар поддых от Капитана Очевидности! На самом деле, всё просто: у контейнеров нет полей и метаобъекта, а, значит, мы должны их обрабатывать отдельно, пропихивая заглушки. Точнее, не так. Нам нужно обрабатывать отдельно последовательные контейнеры с тривиальными типами и отдельно — ассоциативные контейнеры, так как последовательные контейнеры из структур запросто обрабатываются, как простые структуры. С первыми можно схитрить, преобразовав их в строку или BLOB (нужные методы в QList есть из коробки). Со вторыми же ничего не поделать: придётся дублировать все методы, пропихивая вместо настоящих Q_PROPERTY заглушки key и value.

До

QVariant ORM::meta_select(const QMetaObject &meta, QString const& parent_name,                            QString const& property_name, long long parent_orm_rowid) {     QString table_name = generate_table_name(parent_name, property_name,                                               QString(meta.className()),QueryType::Select);     int classtype = QMetaType::type(meta.className());     bool isQObject = ORM_Impl::isQObject(meta);     bool with_orm_rowid = ORM_Impl::withRowid(meta);     if (!selectQueries.contains(table_name)) {         QStringList query_columns;         QList<int> query_types;         for (int i = 0; i < meta.propertyCount(); ++i) {             QMetaProperty property = meta.property(i);             if (ORM_Impl::isIgnored(property.userType())) {                 continue;             }

После

QVariant ORM::meta_select_pair  (int metaTypeID, QString const& parent_name,                                 QString const& property_name, long long parent_orm_rowid) {     QString className = QMetaType::typeName(metaTypeID);     QString table_name = generate_table_name(parent_name, property_name, className, QueryType::Select);     int keyType = ORM_Impl::getAssociativeContainerStoredKeyType(metaTypeID);     int valueType = ORM_Impl::getAssociativeContainerStoredValueType(metaTypeID);     if (!selectQueries.contains(table_name)) {         QStringList query_columns;         QList<int> query_types;         query_columns << ORM_Impl::orm_rowidName;         query_types << qMetaTypeId<long long>();         for (int column = 0; column < 2; ++column) {             int userType = column == 0 ? keyType : valueType;             QString name = column == 0 ? "key" : "value";             if (ORM_Impl::isIgnored(userType)) {                 continue;             }

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

Шаг 5. Написать SQL запросы.

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

 QString ORM::generate_update_query(QString const& parent_name,                  QString const& property_name, const QString &class_name,                  const QStringList &names, const QList<int> &types,                  bool parent_orm_rowid) const {     Q_UNUSED(types)     QString table_name = generate_table_name(parent_name,                         property_name, class_name, QueryType::Update);     QString query_text = QString("UPDATE OR IGNORE %1 SET ").arg(table_name);     QStringList t_set;     for (int i = 0; i < names.size(); ++i) {         t_set << normalize(names[i], QueryType::Update) + " = " +                  normalizeVar(":" + names[i], types[i], QueryType::Update);     }     query_text += t_set.join(',') + " WHERE " +                    normalize(ORM_Impl::orm_rowidName, QueryType::Update) + " = :" +                    ORM_Impl::orm_rowidName + " ";     if (parent_orm_rowid) {         query_text += " AND " + ORM_Impl::orm_parentRowidName + " = :" +                        ORM_Impl::orm_parentRowidName + " ";     }     query_text += ";";     return query_text; }

О чём не стоит забывать:
1) Нормализация имён. Дело не только в регистре, типы могут содержать в себе скобки и запятые шаблонов, двоеточия пространств имён. От всего этого многообразия следует избавляться.

QString ORM::normalize(const QString & str, QueryType queryType) const {     Q_UNUSED(queryType)     QString s = str;     static QRegularExpression regExp1 {"(.)([A-Z]+)"};     static QRegularExpression regExp2 {"([a-z0-9])([A-Z])"};     static QRegularExpression regExp3 {"[:;,.<>]+"};     return "_" + s.replace(regExp1, "\\1_\\2")                  .replace(regExp2, "\\1_\\2").toLower()                  .replace(regExp3, "_"); }

2) Приведения типов. Если работа ведётся с SQLite, то всё просто: кто бы ты ни был, ты — строка. Но если используются другие БД, порой, без каста не обойтись. Значит, при вставке или обновлении нормализованное значение (плейсхолдер) нужно дополнительно преобразовать, да и при выборе тоже.

И в чём же проблема? Почему «неуспех»?

Думаю, многим ответ уже очевиден. Скорость работы. На простых структурах падение скорости составляет 10% на запись и 100% на чтение. На структуре с глубиной вложенности 1 — уже 30% и 700%. На глубине 2 — 50% и 2000%. С повышением вложенности скорость работы падает экспоненциально.

Simple sqlite[10000]:
ORM: insert= 2160 select= 56
QSqlQuery: insert= 1352 select= 53
RAW: insert= 1271 select= 3

Complex sqlite[10000]:
ORM: insert= 7231 select= 24095
QSqlQuery: insert= 4594 select= 127
RAW: insert= 1117 select= 7

Simple

    struct U1 : public ORMValue     {         Q_GADGET         Q_PROPERTY(int index MEMBER m_i)     public:         int m_i = 0;         U1():m_i(0){}         U1& operator=(U1 const& o) { m_orm_rowid = o.m_orm_rowid; m_i = o.m_i; return *this; }     };

Complex

        struct U3 : public ORMValue     {         Q_GADGET         Q_PROPERTY(int index MEMBER m_i)     public:         int m_i;         U3(int i = rand()):m_i(i){}         bool operator !=(U3 const& o) const { return m_i != o.m_i; }         U3& operator=(U3 const& o) { m_orm_rowid = o.m_orm_rowid; m_i = o.m_i; return *this; }     };     struct U2 : public ORMValue     {         Q_GADGET         Q_PROPERTY(Test3::U3 u3    MEMBER m_u3)         Q_PROPERTY(int       index MEMBER m_i )     public:         U3 m_u3;         int m_i;         U2(int i = rand()):m_i(i){}         bool operator !=(U2 const& o) const { return m_i != o.m_i || m_u3 != o.m_u3; }         U2& operator=(U2 const& o) { m_orm_rowid = o.m_orm_rowid; m_u3 = o.m_u3; m_i = o.m_i; return *this; }     };     struct U1 : public ORMValue     {         Q_GADGET         Q_PROPERTY(Test3::U3* u3 MEMBER m_u3)         Q_PROPERTY(Test3::U2 u2 MEMBER m_u2)         Q_PROPERTY(int index MEMBER m_i)     public:         U3* m_u3 = nullptr;         U2 m_u2;         int m_i = 0;         U1():m_i(0){}         U1(U1 const& o):m_i(0){ m_orm_rowid = o.m_orm_rowid; m_u2 = o.m_u2; m_i = o.m_i; if (!o.m_u3) { delete m_u3; m_u3 = nullptr; } else { if (!m_u3) { m_u3 = new U3();} *m_u3 = *o.m_u3; } }         U1(U1 && o):m_i(0){ m_orm_rowid = o.m_orm_rowid; m_u2 = o.m_u2; m_i = o.m_i; delete m_u3; m_u3 = o.m_u3; o.m_u3 = nullptr; }          ~U1(){ delete m_u3; }         U1& operator=(U1 const& o) { m_orm_rowid = o.m_orm_rowid; m_u2 = o.m_u2; m_i = o.m_i; if (!o.m_u3) { delete m_u3; m_u3 = nullptr; } else { if (!m_u3) { m_u3 = new U3();} *m_u3 = *o.m_u3; } return *this; }     };

Причина тому ровно одна. Метасистема Qt. Она устроена так, что в ней происходит очень много копирований. Вернее, в ней производится минимально необходимое число копирований для реалтайма, но, тем не менее, весьма большое. Когда производится сериализация данных, нужно один раз скопировать значение в QVariant, и больше никаких копирований не производится. Когда же происходит десериализация — это песня! Копирование структур происходит на каждом вызове write\writeOnGadget — и от них совершенно нельзя избавиться.

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

struct Car {     Q_GADGET     Q_PROPERTY(double gas MEMBER m_gas) public:     double m_gas; };  struct Dad {     Q_GADGET     Q_PROPERTY(Car car MEMBER m_car STORED false)     Q_PROPERTY(ormReferenсe<Car> car READ getCar WRITE setCar SCRIPTABLE false) public:     Car m_car;     ormReferenсe<Car> getCar() const { return ormReferenсe<Car>(&m_car); }     void setCar(ormReferenсe<Car> car) { if (car) m_car = *car; } }; 

Такое решение позволяет значительно ускорить ORM. Падение скорости работы всё ещё значительное, в разы, но уже не на порядки. Тем не менее, решение это flawed by design, требующее изменять кучу кода. А если это в любом случае нужно делать, не проще ли сразу написать генератор SQL запросов? Увы, проще, и работает такой код разительно быстрее. Потому моя достаточно большая и интересная работа осталась пылиться в углу.

Вместо вывода

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

Послесловие

Статья, как и сам код, были написаны 4 года назад и отложены для проверки и правки. За эти 4 года вышло 2 стандарта C++ и одна мажорная версия Qt, но никаких существенных правок внесено не было. Я даже не проверил, работает ли ORM в 6-ой версии. (UPD: Работает после небольших правок deprecated методов и типов) Тем не менее, вернувшись назад, я посчитал, что её стоит опубликовать. Хотя бы для того, чтобы воодушевить других на исследование. Ведь если они достигнут большего успеха, чем я, — я тоже останусь в выигрыше. Будет на одну полезную библиотеку больше! А если не достигнут — то, как минимум, они будут знать, что они не одни такие, и что их результат, каким бы разочаровывающим он не был, — это всё равно результат.


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

Корпоративный договор между бизнес-партнёрами от «А» до «Я»: какие условия в него включить и как грамотно оформить

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

Что такое корпоративный договор?

Если очень коротко и по существу, то:

Корпоративный договор — это договор, по которому его стороны (бизнес-партнёры) добровольно ограничивают себя в тех или иных корпоративных правах для достижения бизнес-задач.

Если пошутить, то с высокой долей правды, корпоративный договор — это бизнес-БДСМ договор, заключаемый бизнес-партнёрами в целях обеспечения защиты своих интересов и интересов компании =)

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

Зачем нужен корпоративный договор?

Корпоративный договор нужен для:

  • Закрепления договоренностей бизнес-партнёров, которые по тем или иным причинам нельзя отразить в уставе компании (к таким причинам можно отнести временный характер действия договоренностей, распространение того или иного ограничения/запрета не на всех партнёров и т.д.)

  • Обеспечения стабильности и гибкости корпоративного управления компанией

  • Защиты интересов бизнес-партнёров и/или третьих лиц, имеющих тот или иной интерес к компании (иневсторы, потенциальные покупатели, бенефициары и т.д.)

  • Обеспечения выполнения целей и задач бизнеса (компании)

Кто может быть стороной (участником) корпоративного договора?

По общему правилу участниками корпоративного договора могут быть все или несколько участников (совладельцев) компании (ООО).

Однако в соответствии с пунктом 9 статьи 67.2 Гражданского кодекса РФ участниками корпоративного договора могут быть также и иные (третьи) лица, имеющие в отношении компании охраняемый законом интерес — кредиторы, инвесторы, потенциальные покупатели компании, потенциальные покупатели доли в компании и т.д.

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

В своей практике я также в целях обеспечения безопасности бизнеса нередко включаю в качестве стороны корпоративного договора генерального директора компании и прописываю в договоре:

Участие Общества в Договоре в лице генерального директора и принятие Обществом (генеральным директором Общества) запретов и ограничений, предусмотренных настоящим Договором, обеспечивается подписанием Договора генеральным директором Общества.

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

В какой форме заключается корпоративный договор и нужно (можно) ли его зарегистрировать?

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

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

Регистрировать корпоративный договор в ФНС (государственных реестрах) не обязательно. Однако для того, чтобы обеспечить максимальную надежность и работоспособность корпоративного договора, сведения о нём лучше внести в ЕГРЮЛ. Делается это с помощью представления в соответствующую налоговую инспекцию экземпляра заключенного корпоративного договора и заявления по форме Р13014 с отметкой, к примеру, указанной в прикрепленном ниже скриншоте (фрагмент реальной формы Р13014, поданной в ФНС в целях внесения сведений и корпоративном договоре в ЕГРЮЛ):

По результатам такой регистрации корпоративного договора в ФНС в выписке из ЕГРЮЛ в отношении соответствующей компании появится отметка о наличии корпоративного договора (как указано в прикрепленном ниже скриншоте):

Зачем вносить сведения о корпоративном договоре в ЕГРЮЛ?

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

Так, к примеру, при отображении сведений о корпоративном договоре в ЕГРЮЛ практически невозможно будет обойти временный запрет на отчуждение (продажу, дарение и т.д.) доли в компании или выход из компании. Поскольку и нотариус, и налоговики будут видеть, что в компании есть корпоративный договор (корпоративные договоры), то ни нотариус, ни налоговики не допустят проведения сделки, противоречащей требованиям корпоративного договора. А как известно, практически все сделки по отчуждению доли в ООО или выходу из ООО, подлежат нотариальному заверению. Поэтому в случае наличия сведений о корпоративном договоре в ЕГРЮЛ либо нотариус откажет в удостоверении противоречащей ему сделки, либо налоговики по той же причине откажут во внесении соответствующих изменений в ЕГРЮЛ.

Более того, в соответствии с пунктом 6 статьи 67.2 Гражданского кодекса РФ сделка, заключенная стороной корпоративного договора в нарушение этого договора, может быть признана судом недействительной по иску участника корпоративного договора только в случае, если другая сторона сделки знала или должна была знать об ограничениях, предусмотренных корпоративным договором. Внесение сведений о корпоративном договоре в ЕГРЮЛ автоматически (в контексте реализации принципов добросовестности и осмотрительности) создает условие, при котором «другая сторона сделки должна знать об ограничениях, предусмотренных корпоративным договором».

Сколько корпоративных договоров может быть в компании?

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

Когда можно заключить корпоративный договор?

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

Так, пункт 10 статьи 67.2 Гражданского кодекса РФ прямо говорит, что правила о корпоративном договоре применяются к соглашению о создании хозяйственного общества, если иное не установлено законом или не вытекает из существа отношений сторон такого соглашения. Таким образом, условия корпоративного договора фактически можно включить в соглашение о создании ООО ещё до момента создания самого ООО, что в значительной степени повышает надёжность первоначальных договоренностей бизнес-партнёров.

Что главнее устав или корпоративный договор и что, если корпоративный договор противоречит уставу?

Конечно, главнее устав компании. Устав — это базис, «священное писание» компании. Корпоративный договор работает на базе устава компании, весь механизм корпоративного договора работает на базе устава.

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

А если такое противоречие всё-таки имеет место (в силу недоразумения/ошибки), то положения корпоративного договора в принципе не могут сработать в таком случае, ибо отсутствует необходимая для этого база. При этом пункт 7 статьи 67.2 Гражданского кодекса РФ говорит о том, что стороны корпоративного договора не вправе ссылаться на его недействительность в связи с его противоречием положениям устава хозяйственного общества. Однако этим положением устанавливается не приоритет корпоративного договора перед уставом, а обеспечивается стабильность действия корпоративного договора (остальной его части, не противоречащей уставу) в случае наличия такого противоречия.

Какие вопросы могут быть урегулированы корпоративным договором?

Ниже приведен перечень вопросов, ответы на которые очень желательно бизнес-партнёрам отразить в корпоративном договоре:

1. Запрещается ли участникам компании отчуждать (продавать, дарить, менять и т.д.) свои доли в компании в течение определённого срока? Если да, то в течение какого срока, какие есть исключения (например, можно отчуждать при согласии на сделку всех участников компании или можно продавать по цене не ниже X долларов), на каких участников распространяется запрет отчуждения (на всех или некоторых)? 

2. Запрещается ли участникам выходить из компании (путём отчуждения своей доли компании)? Если да, то в течение какого срока, на каких участников распространяется запрет отчуждения (на всех или некоторых)?

3. Устанавливается ли минимальный и (или) максимальный предел стоимости доли в компании при её продаже третьим лицам кем-либо из участников компании? 

4. Каких лиц (потенциальных участников компании) не допускается принимать в компанию (указать критерии)? В каких случаях все участники компании обязаны проголосовать «ПРОТИВ» или «ЗА» принятие в компанию инвестора (нового участника)? 

5. Вводится ли условие tag-along, при котором в случае продажи кем-либо из участников своей доли в компании третьему лицу (покупателю) остальные участники вправе присоединиться к сделке и также продать свои доли такому покупателю на аналогичных условиях? При каких условиях действует (не действует) tag-along? 

6. Вводится ли условие drag-along, при котором в случае продажи кем-либо из участников своей доли в компании третьему лицу (покупателю) остальные участники обязаны присоединиться к сделке и также продать свои доли такому покупателю на аналогичных условиях? При каких условиях действует (не действует) drag-along?

7. При несоответствии каким критериям то или иное лицо не вправе избираться на должность генерального директора (управляющего) компании? В каких случаях участники компании не вправе голосовать «ЗА» избрание того или иного лица на должность генерального директора (управляющего) компании?

8. В каких случаях участники обязаны проголосовать «ЗА» (или «ПРОТИВ») распределение чистой прибыли компании (выплату дивидендов)? В течение какого срока действует такое условие? 

9. Запрещается ли участникам (участнику) передавать в залог принадлежащие им доли в компании? Если да, то в течение какого срока, при каких условиях (есть ли исключения)? 

10. В каких случаях все участники компании обязаны проголосовать «ЗА» образование в компании совета директоров (с составом и компетенцией, предусмотренными уставом компании)? Нужно ли вообще сейчас предусматривать в компании совет директоров, если да, то какой у него состав (количество участников) и компетенция? 

11. В каких случаях (по каким вопросам) тот или иной участник (участники) компании обязан (обязаны) присоединиться к голосу (решению) другого участника компании на общем собрании участников? Если такие случаи есть, то кто именно обязан присоединиться к мнению какого именно участника? 

12. Какие участники компании вправе внести дополнительный вклад (по номинальной стоимости) в уставный капитал компании при входе в компанию нового участника (инвестора) (то есть кто из участников защищён от размытия долей)? В каких случаях данное условие работает или не работает, в течение какого срока действует такая привилегия? 

13. Обязан ли какой-либо участник (участники) компании инициировать созыв общего собрания участников в случае получения соответствующего требования от миноритария (участника, владеющего долей в компании в размере менее 10 процентов)?

14. Не чаще скольки раз за определенный период времени участник (участники) компании вправе запрашивать информацию о деятельности компании?

15. Какая информация является конфиденциальной? В течение какого срока такая информация не может разглашаться участниками компании?

16. В каких случаях участнику (участникам) компании запрещается приобретать долю (часть доли) другого участника компании (например, если в результате такой сделки будет превышен максимально допустимый размер доли)?

Примечание: imho такое условие допустимо в корпоративном договоре вне зависимости от положений п. 3 ст. 14 Закона об ООО. 

17. В каких случаях участники компании обязуются не реализовывать преимущественное право покупки доли (части доли)? Например, в случае, если доля продается по опциону сотруднику компании или в случае, если доля продаётся близкому родственнику.

18. Запрещается ли участнику предъявлять Обществу требование о приобретении принадлежащей ему доли или части доли в соответствии с п. 2 ст. 23 Закона об ООО?

19. В каких случаях участники компании обязаны проголосовать «ЗА» внесение вкладов в имущество компании (в рамках дополнительного финансирования компании)? Как будет определяться общая сумма вкладов? В каких случаях участники компании обязаны внести вклады непропорционально размеру своих долей в компании?

20. Запрещается ли участникам компании приобретать доли в уставном капитале компаний-конкурентов, работать на них (по трудовому или гражданско-правовому договору), участвовать иным образом в управлении такой компанией (например, быть членом совета директоров)? Запрещается ли участникам компании осуществлять иную деятельность, конкурирующую с деятельностью компании (например, в качестве ИП)? Если да, то в течение какого срока действуют указанные запреты, каким критериям должна отвечать компания-конкурент? Какие ещё ограничения вводятся в отношении участников компании в целях противодействия их возможной «помощи» конкурентам в будущем? 

21. Обязаны ли участники компании зарезервировать часть своей доли в компании для опционной программы (программы мотивации сотрудников и иных лиц)? Кто какую часть доли участники обязаны зарезервировать? 

22. Согласны ли участники с направлением им юридически значимых сообщений по электронным каналам связи (адресам электронной почты участников, указанным в списке участников компании и(или) корпоративном договоре)? 

23. В случае возникновения тупиковой ситуации, при которой на общем собрании участников не получается принять то или иное решение, каким образом будет разрешена такая ситуация?

Варианты: участник более молодой по возрасту обязан присоединиться к решению более взрослого участника, при решении вопроса будет иметь приоритет мнение бОльшего количества участников, иное? В случае, если мирно решить вопрос не получится, будет ли использоваться «русская рулетка» или иной механизм, направленный за вывод из компании одной из противоборствующих сторон? 

24. По каким вопросам все участники компании обязательно должны проголосовать «ЗА» или «ПРОТИВ» (например, «ЗА» выплату компанией кредитору (кредиторам) участника—должника действительной стоимости его доли (части доли) или «ЗА» перевод активов компании в новую компанию при соблюдении определенных условий, в том числе при соблюдении условия получения участниками соответствующих долей в уставном капитале новой компании)?

25. Какие заверения (гарантии) дают участники компании? 

Варианты: что не привлекались к уголовной ответственности, не имеют долгов на определенную сумму, не подвергались санкциям со стороны тех или иных государств, не объявлялись ранее банкротом, не являются резидентом того или иного государства, не являются участниками тех или иных организаций, не владеют прямо или косвенно (через аффилированных лиц) долями (акциями) компаний-конкурентов и так далее. 

26. Какую ответственность несет участник в случае нарушения того или иного положения (условия) корпоративного договора? 

Варианты: денежная неустойка, лишение участника его доли (части доли) в компании по опциону, иное?

27. Обязательно ли для приобретения доли в компании потенциальный покупатель должен сначала присоединиться к корпоративному договору? Каким образом будут присоединяться к корпоративному договору новые участники компании?

Как обеспечить действие (исполнение) корпоративного договора с помощью опциона?

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

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

Лишение бизнес-партнёра (участника корпоративного договора) доли в компании не влияет на действие корпоративного договора (если, конечно, после этого количество сторон корпоративного договора не стало меньше двух). Пункт 8 статьи 6772 Гражданского кодекса РФ прямо говорит, что «прекращение права одной из сторон корпоративного договора на долю в уставном капитале общества не влечет прекращения действия корпоративного договора в отношении остальных его сторон, если иное не предусмотрено этим договором.»

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

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

Может ли корпоративный договор содержать в себе NDA (соглашение о конфиденциальности)?

Да, корпоративный договор может содержать в себе NDA.


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

С уважением, Евгений Рябов, предприниматель, инвестиционный и корпоративный юрист, автор книги «Стартап и инвестор: правила игры»

Почта

Телеграм

+7 (987) 207 73 80


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

Человеческим языком про метрики 3: перцентили для чайников

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

Лучше, конечно, объяснять эту тему вживую и с доской, но у нас будет простой python – потому что это то же самое, что писать математику вручную, только короче.

Оглавление цикла:

  1. Потерянное введение

  2. Prometheus

  3. Перцентили для чайников

  4. PromQL

Зачем?

Какую задачу решают перцентили, и зачем эти сложности? На самом деле, они нужны для того, чтобы посмотреть на массив данных и описать их одним числом. Например «большинство HTTP запросов обработались в пределах 5 секунд». Это нужно, чтобы было удобно смотреть на графике, как ведет себя приложение «в целом». Иногда запросы залипают или завершаются раньше времени, но нам это не важно, если большинство запросов ведут себя предсказуемо. В общем, это способ сжать историю наблюдений до одного числа, при этом по пути потерять неудобные для нас данные, чтобы не мешали смотреть на общую картину.

Данные

Мы рассматриваем перцентили в контексте метрик. Напомню, метрика — это временной ряд, то есть набор значений во времени. Пока забудем про timestamp-ы. Тогда останутся данные вида [1,2,0,10,50,42,38]. Вот с такими данными мы и будем работать в этой статье: просто каким-то массивом чисел.

Подготовка

Для начала разберемся с тем, что часто встречается и проще всего для понимания: что такое «среднее арифметическое» и «медиана». Работать будем вот с этими массивами:

a = [1, 2, 0, 10, 50, 42, 38] b = [1, 2, 0, 10, 50, 42, 38, 5]

Среднее арифметическое

Это сумма всех элементов, деленная на их количество:

sum(a)/len(a) # 20.428571428571427 - среднее арифметическое массива a  sum(b)/len(b) # 18.5 - среднее арифметическое массива b

Медиана

Чтобы не разглядывать весь массив, можно выбрать какой-то один хороший элемент
Чтобы не разглядывать весь массив, можно выбрать какой-то один хороший элемент

Медиана – это «средний элемент», то есть буквально в середине массива (если его упорядочить). Чтобы ее найти:

  1. Упорядочим массив

  2. Если длина массива нечетная, берем элемент посередине

  3. Если длина массива четная, берем два элемента посередине и считаем их среднее арифметическое

Случай с четной длиной — это просто такой математический костыль.

sort_a = sorted(a) # [0, 1, 2, 10, 38, 42, 50]  len(sort_a) # 7 - длина массива нечетная, элемент посередине - с индексом 3  sort_a[3] # 10 - медиана массива a

Случай с массивом четной длины:

sort_b = sorted(b) # [0, 1, 2, 5, 10, 38, 42, 50]  len(sort_b) # 8 - длина массива четная, элементы посередине - с индексом 3 и 4  sort_b[3] # 5  sort_b[4] # 10  (sort_b[3] + sort_b[4])/2 # 7.5 - медиана массива b

Можно сказать, что это свойства нашего массива. Если мы поменяем в нем значения, то его свойства изменятся: будет другое среднее арифметическое и другая медиана. А какие еще есть полезные свойства?

max(a) # 50  max(b) # 50  min(a) # 0  min(b) # 0

Это нам пригодится чуть ниже.

Перцентили

Вообще, есть разные слова, которые обозначают похожие вещи: квантиль (quantile), перцентиль или процентиль (percentile), квартиль (quartile). Пока давайте забьем на все кроме перцентиля, остальное разберем в конце

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

Опять медиана

У медианы есть такая особенность: половина элементов массива больше либо равна ей, а другая половина элементов — меньше либо равна. Мы же достали ее из середины сортированного массива, логично? Остановитесь и подумайте над этим:

# a  [0, 1, 2, 10, 38, 42, 50] '''           ^^           10 [   <=    ]            [     >=     ] '''  # b [0, 1, 2, 5, 10, 38, 42, 50] '''             ^            7.5 [     <=   ]              [     >=      ] '''

Вот возьмем ту часть, которая меньше либо равна. Можно поиграться терминами и перефразировать так:

50% элементов массива <= какого-то числа

Так исторически сложилось, что это «какое-то число» часто используется и имеет свое название, медиана. Еще часто используется вот такое значение:

Все элементы массива <= какого-то числа
или другими словами
100% элементов массива <= какого-то числа

Это, внезапно, максимальный элемент, то есть max(): все элементы массива меньше либо равны ему. Ну и наконец, часто встречается вот это:

Ни один элемент массива не < какого-то числа
или другими словами
0% элементов массива < какого-то числа

Тут опять втыкается математический костыль, потому что в данном случае оказалось удобно заменить <= на <, чтобы в качестве «какого-то числа» взять минимальный элемент, то есть min(). Не заморачивайтесь, почему и зачем, мы тут не за этим…

Поздравляю, теперь вы знаете три перцентиля: 0-й, 50-й и 100-й.

Обобщаем

Заметное свойство массива можно выбирать под свою задачу
Заметное свойство массива можно выбирать под свою задачу

А что если пошатать определение медианы и взять какой-то произвольный процент? Ну или как-то обобщить min, max и медиану?

N-й перцентиль — это такое число, что N% элементов массива меньше или равны этому числу
или чуть короче
X — это N-й перцентиль, если N% элементов массива <= X

Под это определение попадают медиана (N=50), max (N=100) и min (N=0), с небольшой поправкой на равенство в случае с min. Они описывают понятные вещи: средний элемент, максимальный, минимальный. А что описывает, например, 95-й перцентиль? Подставим его в определение:

95-й прецентиль — это такое число, что 95% элементов массива меньше или равны этому числу.

И что с этим делать? Оказывается, очень удобно использовать эту штуку, чтобы описать большинство элементов массива, при чем степень точности регулируется: 80%, 95%, 99%, 99.9%, … Для чего бывает полезно описывать «большинство из массива»? Чтобы выбросить пики! Например, у вас есть HTTP-сервер, и вы считаете время обработки запросов. Иногда запросы зависают надолго, потому что сеть моргнула, нода перезапускалась, БД приуныла, пришел GC, клиент сам отвалился, … В принципе это нормально, мы же не регистрируем сбой из-за каждого подвисшего запроса. Вы хотите знать, что в целом у вас запросы отрабатывают за какое-то разумное время. Вот в этой фразе «в целом» на математическом языке заменяется на «какой-то перцентиль», а «разумное время» — это его значение. Например:

95 перцентиль от времени обработки запросов = 5 секунд

То есть, большинство (95%) запросов мы обработали за 5 секунд или меньше. А остальные 5% обрабатывались дольше. Можно еще посчитать 99-й перцентиль и сравнить с 95-м, тогда станет понятно, что большинство запросов укладываются в 5 секунд, а подавляющее большинство, скажем, в 6 секунд, что вообще-то тоже неплохо. При этом максимум может оказаться каким-нибудь гигантским, скажем, 60 секунд и встречаться крайне редко (оставшийся 1%), поэтому нам он не интересен. А еще до кучи посчитаем медиану (50-й перцентиль), она может оказаться сильно меньше 95-го перцентиля (например 1 секунда). Это поможет понять, что в среднем запросы отрабатывают быстро и есть запас по производительности.

Перцентили позволяют нарезать слоями ваш массив и понять, на какие группы делятся элементы: в среднем они такие-то, в основном такие-то, в подавляющем большинстве такие-то и т.д

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

Для интересующихся, функция на чистом питоне, которая считает перцентиль из сортированного массива
# взято отсюда: https://stackoverflow.com/a/2753343  import math  def percentile(arr, n):     k = (len(arr)-1) * n     f = math.floor(k)     c = math.ceil(k)     if f==c:         return arr[int(k)]     d0 = arr[int(f)] * (c-k)     d1 = arr[int(c)] * (k-f)     return d0+d1  # пример: # percentile(sort_a, 0.95) # percentile(sort_b, 0.5)

Если все понятно с определением — отправляемся назад, в будущее!

Добавляем время

Историю изменений в массиве тоже можно наблюдать издалека
Историю изменений в массиве тоже можно наблюдать издалека

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

Большие массивы чисел сами по себе «в лоб» не понятно как вообще можно визуализировать, поэтому в метриках и используются перцентили

В самом начале мы считали медиану, min и max на небольших массивах. Если мы начнем записывать время обработки HTTP запросов, и не будем перезапускать приложение месяцами, у нас могут накопиться миллионы значений, которые еще нужно сортировать и обсчитывать, когда мы захотим узнать перцентили. Ну или как-то хитро хранить в сортированном виде и находить место для очередного значения… Короче, это все потенциально разрастается в бесконечность, поэтому так никто не делает.

Семплирование

Обычно для метрик мы семплируем данные, то есть из всего множества выбираем только некоторые элементы, чтобы из них уже считать перцентили и другие свойства. Как из бесконечного ряда даных выбрать конечное количество элементов? Придется чем-то пожертвовать, для этого есть разные алгоритмы. Мы храним, например, до 100 элементов, а когда надо записать новый, мы решаем, какой элемент нужно выкинуть:

  • Sliding Window: просто выбрасываем самый старый элемент. Очень просто в реализации и понимании

  • Forward Decay: выбрасываем случайный элемент, но чем он старее, тем больше вероятность, что он будет выброшен. Вероятность может расти как угодно, например, экспоненциально. Идея в том, что старые значения все еще чуть-чуть влияют на свойства массива, поэтому такая выборка немного точнее описывает наш бесконечный ряд данных

Сжатие

Можно считерить и вообще хранить не сами элементы, а «корзины» или «бакеты». Это просто счетчики того, сколько раз элемент попал в такой-то диапазон. На примере будет понятнее: берем время обработки HTTP запросов. Определимся, какое время считать хорошим, какое плохим (пример из прошлой части):

  • <= 0.1 сек — хороший запрос

  • <= 1 — сойдет

  • <= 5 — подозрительно

  • больше 5 — вообще плохо. Для однообразия можно сказать, что это <= infinity

Пришел запрос, померяли время X, и добавили +1 к тем бакетам, у которых подходят условия: X <= 0.1, X <= 1 и т.д.

А дальше в дело вступает магия, которую не обязательно знать, и поэтому мы ее разбирать не будем, но из бакетов тоже можно посчитать перцентили. Конечно, если бакетов мало, мы теряем точность, зато экономим ресурсы! Алгоритм можно посмотреть в этой статье — там нет ничего сложного, тоже инженерно-алгоритмические фокусы, только более громоздкие.

Prometheus

С перцентилями можно столкнуться при использовании Histogram и Summary. Подробнее о том, как они выглядят, было в прошлой части. Гистограмма — это бакеты, которые вы считаете в своем приложении, а prometheus можно потом попросить посчитать из них перцентили. Причем если бакеты одинаковые, а теги разные (например, гистограмма с разных реплик или слоев), то можно без проблем их посчитать вместе. Summary — это перцентили, которые вы уже посчитали в приложении, а prometheus просто сохраняет их как числа. Обычно считается с помощью семплирования, поэтому точнее, чем бакеты. Ее можно просто в лоб вывести на график, но нельзя агрегировать! Причина, упрощенно, в том, что у вас разные выборки, поэтому результат агрегации будет отражать непонятно что. Вот показательный пример:

Не агрегируйте Summary

Пример, который демонстрирует, что с виду одинаковые Summary с двух реплик нельзя как-то сложить вместе: предположим у вас две реплики app1 и app2, они получают HTTP-запросы и записывают время обработки.

app1 = [9]*5  # пять запросов по 9 секунд app2 = [1]*100  # сто запросов по секунде  percentile(app1, 0.95) # 9 - 95й перцентиль app1  percentile(app2, 0.95) # 1 - 95й перцентиль app2

Каждая пишет в prometheus метрику типа Summary с 95-м перцентилем и с тегом, чтобы различать с какой реплики сняты данные.

http_time{quantile="0.95", instance="app1"} 9 http_time{quantile="0.95", instance="app2"} 1

Вы думаете, что неплохо бы нарисовать один общий график «95й перцентиль от времени обработки HTTP» и делаете такой запрос к prometheus:

avg(http_time{quantile="0.95"}) without (instance)

avg — потому что нужна же какая-то функция, чтобы сложить вместе два значения, почему бы не взять среднее арфиметическое между ними? Не сумму же. В результате получается (9+1)/2 = 5, вроде бы все нормально, да? А теперь давайте по-честному посчитаем 95 перцентиль на общих данных от app1 и app2:

all = sorted(app1+app2) percentile(all, 0.95) # 1

Честный 95-й перцентиль равен 1, а avg от двух метрик получился 5, то есть вы видите что-то на графике, но это совершенно бессмысленное значение. Если вам нужно агрегировать summary, подумайте, для чего вы на них смотрите. Например, в случае со временем обработки HTTP, скорее всего вас интересует, не превышает ли 95-й перцентиль какой-то порог. Поэтому можно взять максимум из метрик вместо avg:

max(http_time{quantile="0.95"}) without (instance)

Вы будете видеть один график, и если какая-то из реплик работает в основном медленно, то это будет видно. Конечно, решение не идеальное. Например, может привести к false-positive алертам: тормозит только одна реплика, а не все приложение, но вам уже звонят посреди ночи.

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

Рецепты

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

  • Для начала попробуйте сделать гистограмму.

  • Сделайте бакетов пять, вроде «отлично/хорошо/сойдет/плохо/недопустимо».

  • Если вообще не представляете, какие нужны бакеты, придется взять Summary.

  • Вам скорее всего нужен 95-й перцентиль (вне зависимости от типа метрики).

  • Сравните визуально 95-й перцентиль и медиану. Медиана покажет что-то максимально усредненное, 95-й перцентиль покажет изменения с большей точностью, но спрячет выбросы.

  • Если у вас Summary, не агрегируйте ее или делайте очень осторожно, никакого avg()!

  • У Summary неплохо бы понимать параметры семплирования, читайте доку к вашей библиотеке для метрик.

Вариации на тему

Короткий словарик похожих терминов, как и обещано:

  • Перцентиль мы уже знаем. Это число N от 0 до 100, такое, что N% элементов массива меньше него.

  • Процентиль это другой способ перевести percentile на русский язык. То есть синоним слова перцентиль.

  • Квартиль это четверти: 25%, 50%, 75%, 100%. То есть бывает первый, второй третий и четвертый квартиль. И еще иногда используют нулевой.

  • Квантиль — это, условно, перцентиль без процентов. Используется в статистике, где бывает удобно указывать абсолютную вероятность, а не в процентах

  • Еще можно встретить дециль — это 10%, 20% и т.д.

Есть еще разночтения самого определения, которое, по сути, гнется и шатается под вашу задачу, например:

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

  • Можно в определении использовать < > >= вместо <= и это все можно тоже называть перцентилями, например «95% элементов больше X».

  • Можно считать перцентиль так, чтобы он всегда был элементом массива, это нужно в некоторых задачах (как и медиану не считать средним арифметическим, а брать ближайший больший/меньший элемент).

В целом все это создает путаницу, особенно для тех, кто еще не знаком с самим понятием и не переваривает сложные математические определения. Поэтому здесь разобран один вариант, наиболее близкий к тому, как работает prometheus, чтобы можно было как-то пользоваться Histogram, Summary и понимать, что вообще происходит.

Бонус

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


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


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

(не) Безопасный дайджест: атака «от скуки», любовь к штрафам и этичный взлом

Собрали подборку ИБ-инцидентов, о которых стало известно в сентябре. Сегодня в программе: искренние извинения за утечку, доверчивые сотрудники, взлом забавы ради и банковский холдинг Morgan Stanley, которому, кажется, нравится платить штрафы за потеряю данных своих клиентов.

Неподдельная искренность?

Что случилось: Данные 44 миллионов пользователей онлайн-кинотеатра START оказались в открытом доступе из-за неисправленной уязвимости.

Как это произошло: Киберпреступники опубликовали в открытом доступе базу данных объемом 72 гб. В утекшей базе имена, фамилии, email-адреса, хешированный пароль, IP-адрес, страна регистрации, дата начала и окончания подписки в онлайн-кинотеатре. Киберэксперты считают, что утечка могла произойти у подрядчика сервиса. Представители онлайн-кинотеатра подтвердили факт утечки и заявили, что уже исправили уязвимость и закрыли доступ к данным. START заверил, что информация в слитой базе не полностью актуальна, не содержит паролей в открытом виде, истории просмотров или данных банковских карт. Также в официальном Telegram-канале стримингового сервиса появился пост, где START, кажется, искренне извиняется перед пользователями за случившийся инцидент.

Новый штраф подкрался незаметно 

Что случилось: Банковский холдинг Morgan Stanley оштрафовали на 35 миллионов долларов за разглашение личной информации 15 миллионов клиентов. 

Как это произошло: Комиссия по ценным бумагам заявила, что холдинг не смог «безопасно» утилизировать жесткие диски и сервера с данными клиентов. Согласно заявлению комиссии, Morgan Stanley нанимал сторонние компании для перевозки и утилизации носителей информации. Но, как оказалось, подрядчики не были «компетентны» в вопросе уничтожения данных и просто перепродавали тысячи устройств Morgan Stanley третьим лица, которые потом выставляли их на продажу в интернете. В результате 42 сервера банковского холдинга, которые могли содержать незашифрованную конфиденциальную информацию, бесследно пропали.

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

Этичный взлом

Что случилось: Индийская авиакомпания Akasa Air раскрыла личные данные пассажиров в первый же день работы.

Как это произошло: Утечку данных обнаружил киберэксперт. Он рассказал, что информация о пользователях оказалась в открытом доступе из-за сбоя в системе регистрации профиля. В результате ошибки раскрывались такие данные о пользователях сайта, как имена, email-адреса, номера телефонов. 

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

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

Губительные ссылки?

Что случилось: Крупнейшая сеть отелей InterContinental Hotels Group PLC пострадала в результате хакерской атаки.

Как это произошло: 5 сентября хакеры атаковали компанию IHG Hotels & Resorts, это привело к отключению некоторых ИТ-систем. IHG привлекла к расследованию сторонних киберэкспертов и уведомила регуляторов. В заявлении компания рассказала, что системы бронирования и некоторые сервисы были сильно повреждены. Хотя представители сети отелей не раскрывают подробности кибератаки, но есть предположение, что хакеры положили систему, используя программу-вымогателя. По данным Hudson Rock, киберпреступники скомпрометировали около 15 аккаунтов сотрудников и получили доступ к данным 4 тысяч пользователей мобильного приложения сети отелей InterContinental Hotels Group.

Ошибочка вышла

Что случилось: Google случайно выплатила выкуп в размере 250 тысяч долларов не тому хакеру.

Как это произошло: Ошибку обнаружил Сэм Карри, который занимается поиском различных уязвимостей в компаниях и работает инженером. О том, что компания Google загадочным образом заплатила ему почти 250 тысяч долларов он рассказал своим подписчикам в соцсети. 

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

Сами виноваты

Что случилось: 18 летний хакер взломал Uber, потому что «у сервиса была слабая защита».

Как это произошло: Киберпреступник рассказал The New York Times, что получил доступ к электронной почте, облачному хранилищу и репозиторию кода Uber. Позже и сам сервис сообщил о том, что ИТ-система сервиса была взломана и им пришлось к отключить некоторые системы. 

Специалист по безопасности Сэм Карри (да, тот самый Сэм, которому Google случайно отправила 250 тысяч долларов) сообщил, что состоит в переписке с предполагаемым злоумышленником. Хакер сообщил Сэму, что имеет полный доступ к системе Uber.

Незадолго до того, как система сервиса была отключена, некоторые сотрудники получили сообщение от хакера, в котором говорилось, что произошла утечка данных и перечислялось несколько баз данных, которые ему удалось скомпрометировать. В сообщении также говорилось, что водители Uber должны получать более высокую оплату. Представители компании выяснили, что хакер взломал учетную запись Slack сотрудника и использовал ее для отправки сообщения. Сам киберпреступник подробнее рассказал газете The New York Times, как взломал Uber. Оказалось, что он отправил сообщение сотруднику компании, в котором представился специалистом по корпоративным ИТ-технологиям. Затем хакеру удалось убедить работника написать его пароль, который и позволил получить доступ к системам Uber. 

Потеряли данные

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

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

Легко пришли. Легко ушли

Что случилось: Из-за ошибки у стороннего провайдера Coinbase, грузинские трейдеры получили стократную прибыль.

Как это произошло: Как сообщает blockworks, из-за ошибки у провайдера Coinbase в курсе доллара к грузинской валюте лари запятая съехала на два знака. Благодаря этому в течение семи часов 1 доллар приравнивался к 290 лари вместо положенных 2,90 лари. Потерявшуюся запятую компания объяснила «технической ошибкой» у одного из ее партнеров. Предположительно багом воспользовалось около 900 человек, а по подсчетам криптоэкспертов, Coinbase потеряла от 14 до 140 миллионов долларов. Правда, не долго музыка играла. Местные банки отследили подозрительные транзакции и заблокировали счета предприимчивым трейдерам. Сейчас Coinbase пытается вернуть незаконно выведенные средства.

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


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