• Как убедить клиента что пора переделать проект?

    Athanor
    @Athanor
    Лайк + Решение: не жмись, нажми
    Давайте по порядку.
    1. Про «делать проще» или «закладывать архитектуру». Вы пошли по первому пути вполне закономерно, потому как зачем клиенту платить больше для решения своей задачи, если можно заплатить меньше? Для него это противоречит здравому смыслу. Поделюсь опытом, мы например работаем так всегда, но с небольшой (на самом деле ключевой) поправкой — мы планируем, как проект будет развиваться дальше и обязательно вместе с клиентом. Когда есть этот план работа строится итеративно, сначала выкатывается минимальная работоспособная версия (mvp), которая покрывает критический контур системы (то, без чего этот продукт точно не будет работать), затем v0, v1 и так далее. Мысль в том, что это нормальная практика. На счет «закладывать архитектуру», а откуда вы знаете какая она должна быть? Хватит пальцев одной руки чтобы посчитать сколько я видел клиентов, которые четко знают, что им нужно и в конце проекта что-нибудь не менялось. Зная это, как можно просчитать архитектуру или хотя бы даже желаемый функционал? Лучше идти итеративно и постепенно достраивать систему.

    2. Теперь про постоянные фичи, костыли и прочее. Тут упираемся в бизнес-аналитику и сбор требований. Нельзя работать в режиме Клиент сказал сделать фичу — Сделали фичу. Да ещё и по нескольку раз в день. Важно моделировать бизнес-процессы и встраивать их в существующую систему, вы-то, видно, это понимаете, но клиент-то — нет. Чтобы он понял это, с ним нужно общаться на его языке и строить работу от бизнеса. Задайте на брифинге вопросы «Какую цель вы преследуете, внедряя эту фичу? Как ещё можно достигнуть этой цели, может есть решение, которое вообще не потребует внедрения никаких фич, может это можно сделать бесплатно? Например у нас есть бизнес-аналитик, который, занимается только этим — задаёт нужные вопросы, объясняет последствия тех или иных решений, затем сам формулирует задачи, переведя их с языка бизнеса на язык разработки и начинается работа.

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

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

    Клиент должен быть разрабу друг и партнёр, а не мудак :)

    Надеюсь помог и вы что-то извлечёте из этого текста для себя, вообще чтобы более предметно говорить про это нужно больше конкретики :)

    P.S. Картинки — ТОП орал долго, спасибо)

    Павел Паленин
    Head of design in Athanor
    Ответ написан
    1 комментарий
  • Как осуществить поиск по таблице в Postgres с разным регистром?

    Athanor
    @Athanor
    Лайк + Решение: не жмись, нажми
    Есть несколько вариантов, например:
    1. В запросе сделать условие WHERE full_name ilike %иван%. Ключевое здесь - "умный" оператор ilike, указывающий искать без учета регистра.
    2. В запросе сделать условие WHERE lower(full_name) like lower(%иван%) - примерно то же самое, но не так красиво.
    3. Использовать расширение CITEXT (https://www.postgresql.org/docs/9.4/citext.html), удобно если нужно учитывать например контроль уникальности в ключе. В столбцах CITEXT выражение 'foo@BAR.com' == 'FOO@bar.com'.
    Ответ написан
    Комментировать
  • Как составить наиболее оптимальный запрос с поиском по одинаковому полю в разных таблицах PostgreSQL с сортировкой по нему в конечном результате?

    Athanor
    @Athanor
    Лайк + Решение: не жмись, нажми
    Рекомендую приглядеться к materialized view, как уже советовали ранее.
    Особенность в том, что по полю для поиска name в materialized view вы сможете посторить gist/gin index и пользоваться быстрым поиском по %like% (см. https://postgrespro.ru/docs/postgrespro/9.5/pgtrgm...

    Обратите внимание, что materialized view необходимо обновлять вручную, периодически выполняя команду
    REFRESH MATERIALIZED VIEW mymatview;
    Ответ написан
    Комментировать
  • Как в PostgreSQL сделать уникальный идентификатор среди всех таблиц базы (глобально)?

    Athanor
    @Athanor
    Лайк + Решение: не жмись, нажми
    Здравствуйте. То, о чем вы говорите, называется GUID, но какого-то унифицированного одного алгоритма его генерации для всех нет. Но в postgres есть расширения, которые могут вам помочь и вам не придется генерить его на уровне приложения.
    Эта суперкороткая статья может быть вам полезна: https://postgrespro.ru/docs/postgrespro/9.5/dataty... и дать начальный стимул и понимание куда копать дальше. Удачи )

    Update (29.01.20):

    Сразу к делу и примерам. Сделать это можно, например, так:

    -- Начнем со схемы данных. На всякий случай проиллюстрирую как может выглядеть идея генерации guid базой, с которой
    -- я в самом начале и начал. Но вам, наверное, не будет смысла эту функцию использовать, т.к. сценарий использования
    -- у вас другой.
    --
    -- Обратите также внимание, что я поменял тип на BIGINT. У меня была гипотеза, что вы используете TEXT, т.к., возможно,
    -- не знали про BIGINT. Недостаток TEXT в том, что Postgres не построит по нему индекс и выборки по id будут все
    -- медленнее и медленнее. Я бы порекомендовал сразу отрефакторить все так, чтобы использовался именно BIGINT.
    --
    -- Все поля также назвал английскими словами, т.к. это можно считать индустриальным стандартом. NOT NULL добавил
    -- по наитию: логика подсказывает, что PRIMARY KEY у каждой таблице обязателен, как и данные. Просто для полноты.
    
    -- Начнем с функции, которая будет генерировать нам ID для примера.
    
    CREATE OR REPLACE FUNCTION guid()
        RETURNS BIGINT AS
    $BODY$
    BEGIN
        -- Количество секунд с начала эпохи Линукса и домножаем на какой-то множитель, чтобы увеличить точность
        -- и получить 1580307917143.431 вместо 1580307917.143431
        RETURN CAST(EXTRACT(EPOCH FROM NOW()) * 1000 AS BIGINT);
    END;
    $BODY$
        LANGUAGE 'plpgsql' VOLATILE;
    
    -- Теперь перейдем к самой схеме данных и создадим ее.
    
    CREATE TABLE users (
       id BIGINT PRIMARY KEY NOT NULL DEFAULT guid(),
       pseudonym TEXT NOT NULL
    );
    
    CREATE TABLE posts (
       id BIGINT PRIMARY KEY NOT NULL DEFAULT guid(),
       content TEXT NOT NULL
    );
    
    CREATE TABLE comments (
      id BIGINT PRIMARY KEY NOT NULL DEFAULT guid(),
      text TEXT NOT NULL
    );
    
    -- Для того чтобы повесить CONSTRAINT на id целевых таблиц, понадобится сделать VIEW, котоый будет содержать все
    -- id из всех этих таблиц, а также функцию, которую мы сможем использовать для CONSTRAINT. По сути, делаем то же,
    -- что делали бы на уровне приложения, но на уровне БД.
    
    CREATE OR REPLACE VIEW all_ids AS
    SELECT id FROM users UNION
    SELECT id FROM posts UNION
    SELECT id FROM comments;
    
    -- Теперь перейдем к функции, которая и будет выполнять всю грязную работу.
    
    CREATE OR REPLACE FUNCTION is_unique_id (BIGINT)
        RETURNS BOOLEAN AS 'SELECT CASE WHEN
                                       (SELECT 1
                                        FROM all_ids
                                        WHERE  id = $1) > 0
                            THEN FALSE ELSE TRUE END'
        LANGUAGE 'sql' WITH  (isstrict);
    
    -- Осталось только повесить CONSTRAINT
    
    ALTER TABLE users ADD CONSTRAINT id CHECK (is_unique_id(id));
    ALTER TABLE posts ADD CONSTRAINT id CHECK (is_unique_id(id));
    ALTER TABLE comments ADD CONSTRAINT id CHECK (is_unique_id(id));
    
    -- А теперь внимание. Теперь вам придется быть очень внимательным при добавлении таблиц, в рамках которых id должен
    -- быть уникален. При добавлении новой таблцы будет необходимо:
    --   1. Пересоздать VIEW, дополнив запрос новыми таблицами.
    --   2. Не забыть повесить аналогичный CONSTRAINT на новую таблицу.
    --
    -- Также обратите внимание, что при вставке новой записи в любую из таблиц будет проверяться весь созданный VIEW
    -- и очень важно чтобы это был не полнотекстовый поиск, а работали индексы, поэтому так важно отрефакторить все в BIGINT.
    
    -- Пришло время тестирования. Вставляем данные.
    
    INSERT INTO users (pseudonym) VALUES ('Первый');
    INSERT INTO users (pseudonym) VALUES ('Второй');
    
    INSERT INTO posts (content) VALUES ('О том как надо');
    INSERT INTO posts (content) VALUES ('О том как не надо');
    
    INSERT INTO comments (text) VALUES ('Я думаю что решение...');
    INSERT INTO comments (text) VALUES ('Хорошо я пропробую сделать...');
    
    -- И глянем что получилось.
    
    SELECT * FROM users;
    -- 1580326610797	Первый
    -- 1580326611809	Второй
    
    SELECT * FROM posts;
    -- 1580326613690	О том как надо
    -- 1580326613712	О том как не надо
    
    SELECT * FROM comments;
    -- 1580326613779	Я думаю что решение...
    -- 1580326613797	Хорошо я пропробую сделать...
    
    -- Время X: тестируем нашу проверку, пытаясь вставить в таблицу users id из таблицы comments:
    
    INSERT INTO users (id, pseudonym) VALUES (1580326613779, 'tiabc');
    -- [23514] ERROR: new row for relation "users" violates check constraint "id" Detail: Failing row contains (1580326613779, tiabc)
    
    -- Profit!


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

    В вашем же случае, честно говоря, схему очень сильно хочется упростить, а не усложнить. Хочется добавить поле created_at с DEFAULT CURRENT_TIMESTAMP() и хочется добавить реляционные связи. Либо же в принципе уйти на нереляционную БД и задать структуру сущностей там (что опять же должно быть обосновано).

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

    Помимо этого, поскольку вы делаете процессинг на основе id и его порядковых номеров, не могу не порекомендовать обратить внимание на одни из базовых принципов проектирования ПО. Конечно, вся картина не видна, но я тут вижу нарушение буквы S (Single Responsibility), что id у вас и за порядок отвечает, и за уникальность (да еще и между несколькими таблицами). Хочется как-то их развязать. Со временем жизни продукта это часто оказывается полезно.

    Опять же, беспокойств о том, что вы хотите достичь, достаточно много, но если отвечать именно конкретно на ваш вопрос и давать дополнительно какие-то рекомендации, то как-то так )

    Желаем удачи, образования и всех благ )

    С уважением,
    Иван Томилов
    CEO of Athanor
    Ответ написан
    2 комментария
  • Как обезопасить bitcoin кошелек на сервере?

    Athanor
    @Athanor
    Лайк + Решение: не жмись, нажми
    С одним из наших партнеров у нас была такая задача: необходимо было сделать хранение как фиата, так и крипты. Когда ставится задача что-то обезопасить, важно правильно сформулировать threat model: от каких именно действий и проникновений должна быть защита и уже на основе этого проектировать архитектуру.

    Например, в случае ответа Dark_Scorpion, threat model можно коротко сформулировать следующим образом:
    1. Злоумышленник получил доступ к виртуалке с API.
    2. Злоумышленник получил доступ к виртуалке с bitcoind и ключами и ко всем ключам.

    Дальше думаем как уменьшить негативный эффект в каждом из случаев.

    1 случай. Злоумышленник теперь может подписывать запросы самостоятельно. Решением может быть, например, multisig, где часть ключа хранится у пользователя, а часть у сервера. Таким образом, злоумышленник уже не сможет эти запросы подписывать. Но тут важно еще понимать в какой юрисдикции будет находится продукт, т.к. есть юридические нюансы. Мы с ними сталкивались. Другим решением может быть возможность принудительного отключения виртуалки с bitcoind каким-то простым способом: даунтайм лучше потерянных денег.

    2 случай. Тут уже ничего не поделаешь. Однако, есть продукты вроде https://www.thalesgroup.com/en, которые предоставляют железку, которая безопасно хранит ключи и которая считается невзламываемой. При этом, ключи доступны до тех пор, пока физическая (или виртуальная) карта вставлена в кардридер. Таким образом, если система была скомпрометирована, то достаточно будет вытащить карту. Есть и другие продукты, более бюджетные.

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

    С точки зрения архитектуры как-то так, удачи в реализации )

    С уважением,
    Иван Томилов
    CEO of Athanor
    Ответ написан
    1 комментарий
  • При оценке качества кода, на что следует обратить внимание?

    Athanor
    @Athanor
    Лайк + Решение: не жмись, нажми
    • Работу с api лучше вынести в отдельный слой, а уже в компонентах вызывать его методы
    • воздержаться от использования инлайн стилей
    • action: (item) => {
       this.showConfirmDialogDeleteRestaurants(item)
      }
      можно записать как
      action: this.showConfirmDialogDeleteRestaurants
    • диалоговое окно можно вынести в отдельный компонент
    • добавить на v-textarea v-model
    • Разобраться с неймингом — везде рестораны в множественном числе, хотя действие производится над одним рестораном, кроме того, я бы начинал название методов с действия, а не с того, над чем это действие происходит — restaurantsRemove => removeRestaurant. Это позволит быстрей ориентироваться в коде. Так же вам стоит определиться с форматом именования сущностей, у вас сейчас snake_case вперемешку с camelCase. Надо выбрать что-то одно.
    • Такие комментарии над методами не несут никакой пользы, по названию метода и так все это понятно
    • Метод restaurantsAdd выглядит довольно объемным, из него, например, можно попробовать вытащить саму логику отображения тостера
    • Не используйте относительные импорты когда они не нужны. Это позволит вам в будущем гораздо легче делать изменения в структуре вашего приложения.
    Ответ написан
    5 комментариев
  • Как узнать первую цифру числа?

    Athanor
    @Athanor
    Лайк + Решение: не жмись, нажми
    const a = 99;
    const b = a.toString();
    console.log(b[0])
    Ответ написан
    1 комментарий