• Учет расходов

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

    Почти целый год мы с женой ведем учет трат: когда, сколько и на что потратили. Записываем абсолютно все, неважно насколько мала или велика сумма. Купил булочку за 10 рублей – внес в учет. Оплатил 400 т.р за отдых всей семьей – тоже внес.

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

    На вопрос “сколько ваша семья тратит в месяц” многие отвечают расплывчато. Ну, около N тысяч. Без учета эта цифра далека от реальности в меньшую сторону.

    Правило: вы всегда тратите больше, чем кажется.

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

    Я познал на личном опыте, каково это: смотришь на отчет и видишь, что уже за первую неделю потрачено 30 тысяч. Первая реакция – отрицание. Не может такого быть, я же только за продуктами ходил. Смотришь по дням: перевел денег родителям, купили школьную форму, заплатил налоги за ИП… вот тридцаточка и набежала.

    Все по-честному. Цифры неумолимы.

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

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

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

    Для учета я использую приложение “Дребеденьги”. Ссылку не даю, гуглите сами. Напомню, это не рекламная заметка. Я не размещу рекламу без метки в начале текста.

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

    Имеются версии для Айфона и Андроида, синхронизация проходит нормально.

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

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

    Есть даже функция “дать в долг”, но я ни разу не пользовался.

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

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

    Разработчик грозился, что приложение может сканить чек и вносить сумму автоматом. У меня тупо вылетает.

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

    И все-таки, приложение отличное. Главный и решающий плюс в том, что за все время оно не обросло миллионом фич. Знаете же, бывает так, что приложение простое и удобное, добавить уже нечего. Но маркетологи начинают его УЛУЧШАТЬ. Интерфейс усложняется, код тормозит, пользоваться невозможно.

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

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

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

  • Посоветуйте книгу

    Странный вопрос, который задают из года в год: “посоветуйте книгу по X”. Не могу понять, что мотивирует писать подобное.

    О любой промышленной технологии уже написано уйма книг. Литературой о ПХП, Питоне или Джаве можно отопить небольшой город в новогодний сезон. Даже про Кложу успели написать три экрана книжек.

    Стеллажи в магазинах ломятся. В сети книг навалом. Давно написаны отзывы, рецензии, выставлены рейтинги.

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

    Книги для новичков (а это практические все книги) примерно одного формата. Если раньше еще встречался треш, то сегодня процесс регламентирован. Какой-нибудь условный O’Reilly, штампующий книги пачками, уже не выпустит откровенно провальную книгу.

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

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

    Просто гуглите “python/php/etc books”. Откройте первые две книги на Амазоне. У которой больше звездочек, ту и покупайте.

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

    Но в общем случае разницы нет. Достаточно просто погуглить.

  • Пруф

    У меня простое правило: кому нужны пруфы, тот сам их ищет.

    Если раньше на вброс “ну и кто пишет на твоей Кложе” я начинал кидаться ссылками, то теперь отвечаю: найди ответ самостоятельно. Дил уыз ыт.

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

    Предположим, я не знаю, кто пишет на Кложе. Значит ли это, что не пишет никто? Это как утверждать, что человек, с которым ты не знаком, не существует. Реальность-то никак не меняется.

    Конечно, я делаю исключение если интерес собеседника искренний. Но случается это крайне редко.

  • Database optimization

    Sometimes, especially on interviews, developers are asked about what would be their steps to fix a database that works slowly. That’s a really extensive subject to discuss. Here, I decided to summarize my experience. I hope it help to not only increase the performance but do that in acceptable time and with less misery.

    First, of all, fixing the database queries is the last thing you should do. Fixing code first is premature optimization that is a root of all evil as we know. It could take huge amount of time whereas the real problem is kept in a wrong configuration file.

    So in my prospective, the right steps to make your database more peppy are something like follows below. Keep in mind that it is not a technical tutorial with code snippets that you could paste and make the job done.

    Ensure you use SSD drives. It sounds silly but still you should check it. SSD gives you up to 10 times performance boost immediately. Ensure that SSD drive is a highlighted feature in you hoster’s plan.

    Check you database config before tweaking something else. Usually, the default configurations are set up poorly to make the DB work on cheap computers. Some distributions carry config templates which names include memory or CPU values. Read them, they are well-documented. Google for best practices when configuring your database. Read books.

    Consider a case when the entire database is kept in memory. That also involves DBA and Ops teams so probably you won’t be allowed to perform such a move by your own. But since memory is cheat nowadays, it is possible. For backups, replicate such a database using streaming replications on servers that are run on hard drives. PostgreSQL supports various replication scenarios as well. You may also dump memory images into files and restore them on demand.

    I used to work in a project where they ran 400Gb database into RAM and it worked quite fast.

    Check you application uses connection pooling when communicating to the DB. Suddenly, such wildly spreaded frameworks as Django do not use them. On each HTTP request, they open a fresh connection and close it once the response has been sent. Now imagine your application serves 20 requests per second. Network communication will be quite expensive in that case!

    Consider using either PGBouncer in front your Postgres installation or any library for your language that deals with reusable connections by its own. Say, for Clojure, HikariCP would be a good choice.

    Everything that might be read from replica should be read from it, not the prod DB instance. Don’t run reports against the production. All the analytics and statistics should not touch it. Modern frameworks allow you to declare multiple database backends and switch between them on demand. For example, in Django, calling .using('replica') method on a Queryset instance dispatches a query into another database, not the default one.

    For quite complicated queries, use materialized views that may be turned into physical tables on demand. Querying them would be much faster than processing a query full of joins and aggregates every time. Setup a cron job to rebuild such views every night for example when the load is low.

    Select only those fields that you really need to process a request. Avoid using select * from table since it takes extra time for the database to find out what fields to fetch exactly. With Django ORM, either specify needed fields with .only() method or bypass some of them calling .defer(). But keep in mind that touching a non-fetched field hits the database again.

    When writing unit tests, add a case that counts a number of database queries made during request. Sometimes, it leads to terrifying findings. Touching fields of foreign entities in a cycle without joining them initially may end up with up to 100 queries or more.

    Be carefull when passing a collection of IDs as a parameter. On the database level, it turns into something like

    STATEMENT: SELECT * FROM users WHERE id IN (?, ?, ?, <100 more of them>)
    PARAMETERS: $1=10035, $2=10036, $3=10037, $4=10038, $5=10039,...
    

    When not limiting the number of such IDs, one may end up with 1000 of them what is quite harmful for the database. Either process them by chunks or try to compose a single query with raw SQL.

    For fast data processing, don’t use ORM at all. Building a model instance from a low-lever tuple that the database driver returns is really costly. Even SQLAlchemy (Python’s the most powerful ORM) spends double time on building instances than reading data from psycopg2 (Python PostgreSQL driver) directly.

    Denormalize your tables. Sometimes, it really worth moving any columns into foreign tables and joining them on demand. A good example might be ranking system. You’ve got a users table full of fields. Now you need to add rank field and recalculate it every 6 hours. Adding yet another fields would be a bad choice since our users table is already under load.

    But having the users_ranks table that has a unique foreign key to a user and a rank value would be a good choice. Now that, we may rewrite it as we wish without touching actual user’s data. When needing to sort users by their rank, we join that table and order the result by rank as well.

    The same relates to a search document. When implementing full search on some entity, we need to build a special document known as a search vector. Storing that vector inside an entity is a bad choice because it is not the data but just technical information.

    Join tables only by foreign and primary keys but not any custom conditions like strings equality, regex matching and so on. When you need such a join, probably it is better to fetch both parts of it and compose the result in your app’s code.

    When a query really works badly, examine it with EXPLAIN operator. If you don’t understand its output completely, use online services that turn it into more human-friendly format. Ensure the query hits indexes. Don’t add multiple indexes since having them a lot may slow down writing procedure.

    Finally, never be afraid of raw SQL. No matter how powerful your ORM is, check what queries it produces exactly. On my dev machine, I always start Postgres not as a service but as a process passing -E flag to print all the queries:

    postgres -E -D /usr/local/var/postgres
    

    That gives me the whole vision of what’s going on with the database.

    Summarizing all together:

    1. use modern hardware;
    2. configure your database properly;
    3. know all the shadowed parts of your ORM;
    4. rise your SQL skills.
  • Clojure in Highload Cup

    1st September was the day when the Highload Cup competition has finished. I’m proud of I took participation in it. The Cup was a quite interesting event full of drive, enthusiasm and night coding. Although I haven’t taken any prize, I’ve got lots of fun and new knowledge that I’ve partially shared with you in my previous publications.

    In that post, I’m going to highlight some technical details unmentioned before.

    A minute of vanity: I’m a single developer who used Clojure/Datomic stack to implement a solution. And by the way a single member from my hometown Voronezh (my colleagues who live here argued on Cup passionately but still without a result).

    The task was easy only at first glance. For non-Russian speakers, let me retell it quickly. You’ve got three entities: a user, a location and a visit. They represent, respectively, a physical person, a place in the world and a fact that somebody visited a place and put mark for it. Say, the last year John Doe visited Siberia and was so excited that he put 5 stars.

    Your task is to ship a Docker container that carries a REST server. The server should implement basic CRUD operations on users, locations and visits. All the data pass in and out using JSON.

    In addition to CRUD, there are two aggregate APIs. The first one is to return all the places visited by specific user. The second one is to return an average mark assigned to specific location by all the users who have ever visited it. Both APIs accept optional query string arguments to filter the results by foreign entities. Say, fromAge parameter stands for we need to consider only those people who are alder than that number, distanceTo limits those locations with distances less than the passed value and so on.

    Once you’ve built a Docker container, you submit it to the central server where it is shot with a special software. It considers lots of such facts as proper response codes, incorrect data filtering, response time and so on. Than it calculates your rank. The less is the rank, the better your position is.

    Sounds simple, but I spent a week trying to implement fast Clojure solution. TL/DR: finally, the C++ guys have come and taken the top of the rank table. Some of them wrote their own HTTP server. But still, it was quite fun to compete with them.

    As the Cup has finished, you are welcome to review my code (it was private before due to Cup rules). The final version uses Clojure 1.9, Datomic free edition and clojure.spec to validate incoming data. There were some experiments with SQLite database kept in memory but at the end I finished with Datomic (more on that below).

    So here are some technical details that I wanted to discuss.

    Reading ZIP on the fly

    According to the Cup’s rules, when your application starts, it finds the input data in /tmp/data directory. There is a single zip archive with JSON files inside. The Docker container is mount with read-only file system, so you cannot unzip it using standard Unix tools. Instead, you should read the data directly using streams.

    Thanks to Java, it ships java.util.zip package with all we need inside. Surprisingly, I ended up with quite short code to read the file:

    (defn read-zip [path]
     (let [zip (java.util.zip.ZipFile. path)
        entries (-> zip .entries enumeration-seq)]
      (for [e entries]
       (.getInputStream zip e))))
    

    It accepts path to a zip file and returns a lazy sequence of input streams. Each stream might be read into a data structure with a function:

    (defn read-stream [stream]
     (json/parse-stream (io/reader stream) true))
    

    , where json is an alias to the Cheshire library included as [cheshire.core :as json] at the top of the namespace.

    The data backend

    Since the beginning it was obvious to keep the data in memory but not on the disk. I was thinking on whether I should use in-memory SQLite backend or use Datomic within in-memory storage. After all, I’ve tried both options and ended up with Datomic finally.

    With SQLite, I’ve got only one trouble when connecting to the database. I described the problem in details in my previous post “In-Memory SQLite Database In Clojure”. For the rest, it worked fine. I used HugSQL to compose queries like this:

    -- :name get-location-avg :? :1
    select
      avg(v.mark) as avg
    from visits v
    /*~ (when (or (:fromAge params) (:toAge params) (:gender params)) */
    join users u on v.user = u.id
    /*~ ) ~*/
    where
      v.location = :location_id
      /*~ (when (:fromDate params) */
      and v.visited_at > :fromDate
      /*~ ) ~*/
      /*~ (when (:toDate params) */
      and v.visited_at < :toDate
      /*~ ) ~*/
      /*~ (when (:fromAge params) */
      and u.birth_date < :fromAge
      /*~ ) ~*/
      /*~ (when (:toAge params) */
      and u.birth_date > :toAge
      /*~ ) ~*/
      /*~ (when (:gender params) */
      and u.gender = :gender
      /*~ ) ~*/
    

    Then I switched to Datomic backend. I was wondering whether it would be slower than good old SQLite. The results were in favor of Datomic: it was about 1.5 times faster when returning responses.

    For in-memory backend, you do not need a registered version or a license key. Just add [com.datomic/datomic-free "0.9.5561.54"] into dependencies list and I’ve done. Then pass something like "datomic:mem://highloadcup" when connecting to the database.

    It was a good decision to create common functions for CRUD operations (create-user, update-user, etc). In fact, I had only three general functions to create, update and read for something, and the entity-specific functions became just partials on them.

    Having that, I could quickly switch from SQLite-powered backed to Datomic.

    The only think I’ve got stuck on was applying optional filters to a query. That became a reason to write “Conditional Queries in Datomic” article.

    You may examine Datomic database backend in master branch whereas SQLite version lives in a self-titled branch.

    JSON validation

    A system that tests you server tends to send incorrect data. If you accept it without returning 400 Bad Request status you will get penalty score. So the validation is a major part of our application.

    Before, I used Schema module for that purpose. I know it well including some of its shadowed parts. But having Clojure 1.9 on board was a great chance to try clojure.spec that is still in alpha but works great.

    After some REPL experiments, I ended up with my own highloadcup.spec namespace that carried wrappers around the original spec. One of them is validate function that does the following:

    1. validates the data against a spec;
    2. coerces string numbers into integers when needed;
    3. returns nil when the data is invalid.

    Its code is

    (ns highloadcup.spec
     (:require [clojure.spec.alpha :as s]))
    
    (def invalid :clojure.spec.alpha/invalid)
    
    (defn validate [spec value]
     (let [result (s/conform spec value)]
      (when-not (= result invalid)
       result)))
    

    Pay attention it’s a good practice to declare invalid constant at the top. Once the library becomes stable, its namespace will get rid of “alpha”.

    Another point, spec was designed to be used with full-qualified keys. But in my case, all the keys were without namespaces. That’s normal for non-Clojure applications. Declare your specs as usual, but once you compose a map of them, pass :opt-un parameter (stands for “unqualified”):

    (def enum-gender #{"m" "f"})
    
    (s/def :user/id int?)
    (s/def :user/email string?)
    (s/def :user/first_name string?)
    (s/def :user/last_name string?)
    (s/def :user/gender enum-gender)
    (s/def :user/birth_date int?)
    
    (s/def :user/create
     (s/keys :req-un [:user/id
                      :user/email
                      :user/first_name
                      :user/last_name
                      :user/gender
                      :user/birth_date]))
    

    This is a spec for creating a user where every field is required. For updating a user, there is a similar spec with all the fields optional:

    (s/def :user/update
     (s/keys :opt-un [:user/email
                      :user/first_name
                      :user/last_name
                      :user/gender
                      :user/birth_date]))
    

    When applying query string filters, they are all plain strings even when represent numbers. Turning them to the proper type is also knowing as coercion. To coerce a string value during validation, use conformer:

    (defn x-integer? [x]
     (if (integer? x)
      x
      (if (string? x)
       (try
        (Integer/parseInt x)
        (catch Exception e
         invalid))
       invalid)))
    
    (def ->int (s/conformer x-integer?))
    
    (s/def :opt.visits/fromDate ->int)
    (s/def :opt.visits/toDate ->int)
    (s/def :opt.visits/country string?)
    (s/def :opt.visits/toDistance ->int)
    
    (s/def :opt.visits/params
     (s/keys :opt-un [:opt.visits/fromDate
              :opt.visits/toDate
              :opt.visits/country
              :opt.visits/toDistance]))
    

    Now, then you validate parameters taken from the Ring request against :opt.visits/params spec, all the numbers represented with strings will be turned into integers as well.

    Docker

    Let’s talk a bit about building a Docker container. I don’t see any reason to compile uberjar inside Docker. It’s Java so that it is “compiled once, works everywhere” (usually I’m sceptical on that point, but not now). All you need is to copy an uberjar file into container and setup CMD properly.

    Do not use the official Java template. Under the hood, it ships OpenJDK that is 1.5 times slower than OracleJDK, unfortunately. So I had to inherit my image from Vlad Frolov’s one. I know that’s illegal to distribute Java runtime as a part of your application. But the difference in score was more important these days.

    JVM flags also could help to tweak performance. The web pages I’ve found googling for “java docker” said that Java has troubles detecting heap size when running in Docker. So at least "-Xmx" and "-Xms" should be specified. Next two, "-da" and "-dsa" reduce all the assert statements. See the rest of flags in my Dockerfile.

    All the project duties (lein, docker, files) should be automated with good old Make utility. Ideally, the default target should build the entire project from scratch.

    Acknowledgments

    I want to thank Mail.ru team who were responsible for handling that Cup. They’ve done really huge amount of work. I thought they didn’t sleep at all. During the three weeks term, they’ve been answering all the questions in chat, fixing the infrastructure, solving bugs, writing wiki and adapting the official website.

    Thank you guys, your are real professionals!

    PS to my Russian readers: you are welcome to share that post with your foreign colleagues. Next time, let them join Highload Cup too!

  • Мобильное приложение

    Ох, что я в интернете прочел.

    Значит, проблема: люди тупят в телефон на ходу. Спотыкаются, врезаются в столбы, падают в фонтаны.

    Знаете, что предлагает автор? Готовы? Написать мобильное приложение, которое будет читать камеру, распознавать препятствия и сигналить пользователю.

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

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

    Это технические трудности, предположим, мы их решили.

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

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

    Поможет ли Васе приложение? Сомневаюсь.

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

    В толпе люди разделены метром. Человек впереди это препятствие?

    Даже если представить, что приложение использует новейшие технологии (нейросети, глубокое обучение), это делает его хуже. Оно даст ложное чувство защиты. Человек будет выходить на середину дороги и попадать под автобус.

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

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

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

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

  • Центрирование курсора

    editors

    В редакторах или IDE крайне редко встречается следующая функция.

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

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

    Слева на картинке Саблайм. Файл закончился, набирать текст приходится внизу, смотаться нельзя. Емакс проматывается в середину сам. Сочетание Ctrl+L центрирует буфер по требованию, повторное нажатие перематывает вверх, оставляя одну строчку. Обратите внимание, что буфер кончился, слева нет нумерации строк, но для перемотки это не проблема.

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

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

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

    Почему об этом думали в 1970-бородатом году, а сейчас никого не волнует?

  • Вконтакт

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

    Предположим, хочу повесить объявление в группе жильцов ЖК. В шапке группы жирная кнопка “написать сообщение”. Открывается диалог:

    vk

    Минуточку, мне нужно, чтобы отвечали все, а не только администраторы. Или остальные тоже могут? Как понять?

    Что за ссылка “перейти к диалогу с сообществом”? А сейчас что, не диалог? На всякий случай нажал, там пустой экран. Ясно, этим не пользуются. Зачем ссылка?

    В группе есть “обсуждения”: жильцы второго дома, любители котят и тд. Этакий форум из нулевых. Объявление в одном обсуждении не увидят в другом.

    Сбоку ссылка на чат. Что-то вроде Телеграма, только внутри ВК. Слева личные сообщения.

    Черт, как разместить текст, чтобы его прочли все жильцы?

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

    Что за дизайнеры верстали все это? Кто это проектировал?

    Я слышу: сперва добейся, потом предъявляй. Но совершенно верно сказал Лебедев: если известная субстанция приносит много денег, быть ею от этого она не перестает.

  • Дети

    Чтобы ребенку было интересно, нужно, чтобы родителю было интересно. Чтобы ребенку нравилось, нужно, чтобы родителю тоже нравилось.

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

    Дети все прекрасно понимают. В силу физиологии они не могут развернуто сформировать претензию или обиду. Но халтуру чуют за версту. Это особенно заметно в период с 2 до 3 лет, когда речь еще толком не поставлена, но уже есть устойчивые вкусы и понимание всего, что происходит вокруг.

    Чтобы ребенок с упоением читал, нужно читать ему каждый день несколько лет. Чтобы с интересом смотрел мультик и понимал сюжет, нужно смотреть вместе с ним.

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

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

    Никогда не делайте скидку на то, что это ребенок. Он такой же человек, как и вы. Критерии качества у детей такие же высокие.

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

    Может, и ваш ребенок одупляет телевизор третий час, пока вы это читаете?

  • Ворчание ягнят

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

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

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

    UPD стрим в процессе создания:

Страница 16 из 49