Превед \о/
Ты кто? Зачем читаешь мой блог? Интересно? Отстой?
Отметься, скажи, о чём мне лучше писать — про техническую жесть, или лучше болтологию. Вот давай прямо тут в формочке для комментов, не тяни:
Превед \о/
Ты кто? Зачем читаешь мой блог? Интересно? Отстой?
Отметься, скажи, о чём мне лучше писать — про техническую жесть, или лучше болтологию. Вот давай прямо тут в формочке для комментов, не тяни:
Превед \о/
Как-то недели 4 тому назад, маясь бессонницей поздним ночером, я тупил в Фейсбуке. Смотрю — Facebook’s Coding Puzzle, оно же opportunity получить работу в солнечной Калифорнии. Ну, думаю, — если убивать время, так не просто же так. Один чёрт, всё равно не высплюсь, так хоть мозги напрягу олимпиадной задачкой…
Вообще, к подобным вещам я отношусь, мягко говоря, прохладно. Олимпиады всех разновидностей с детства недолюбливаю — а заядлых олимпиадников так и вовсе терпеть не могу.
Да, задачи из реальной жизни нередко необходимо решать за строго ограниченное время при строго ограниченном бюджете ресурсов, но они при этом обычно имеют намного более протяжённую границу эксплуатацонного поля, чем олимпиадные экзерсисы, и их контрпродуктивно решать в духе «шнеллер тяп-ляп, и отъебись». Обычно такое решение может служить временной заплаткой, а настоящее — долгоиграющее — по-всякому придётся разрабатывать в процессе, и это уже рутина. А олимпиадники на рутине очень быстро сдуваются, в отличие от посредственностей типа меня.
Из-за этого олимпиадная задача, с моей точки зрения, не может являться позитивной затравкой для начала диалога с кандидатом, с другой стороны, если Фейсбуку нужны мающиеся от бессонницы программисты, то это их, конечно же, дело.
Итак, задачка мне попалась такая:
На стандартный вход программе передаётся текст, в первой строке которого — количество полей. Со второй строки — описания начальных состояний игровых полей в формате “<размер поля> <количество ходов>”, и далее состояние поля с буквами b и w для чёрных и белых фишек. Например,
1 4 5 bwbw bbbw wwwb wbwb
На стандартный выход требуется выдать состояния всех полей после просчёта всех ходов, но уже без каких-либо циферок.
Также навешены какие-то дурацкие ограничения, типа максимальный размер квадрата — 32х32 и количество ходов не более 50.
На выбор предлагалось что-то около 10 ЯП, от PHP до C# (я, понятное дело, выбрал Java), и час времени.
Из этого часа у меня минут 10 ушло на установку жабы на мою «восьмёрку» — инсталлятором не поставилась ни фига, пришлось выцепить какой-то пыльный архив одного из старых проектов, в котором была JVM, и прописать всякое там окружение.
Впрочем, можно было и без этого обойтись, так как предлагается онлайновый редактор и компилятор, но, правда, очень уж тормознутые, так что локальным пользоваться намного удобней. А также проверяльшик — вот он, как раз, весьма помог, потому что про перевод строки в конце вывода в условии сказано не было. А без него не принималось.
Ну что ж, поехали!
1: import java.util.Scanner;
2:
3: public class Solution {
4: public static void main(String[] args) {
5: Scanner sc = new Scanner(System.in);
6: if (sc.hasNextLine()) {
7: Integer boardCount = new Integer(sc.nextLine());
8: for (int i = 0; i < boardCount; i++) {
9: solveBoard(sc);
10: }
11: }
12: System.out.println();
13: }
14: }
Ну, тут комментировать особо нечего. Тупо читаем количество полей, и вызываем в цикле решалку для каждого из них. Если нам скормили первой строкой какую-нибудь пакость заместо количества — мы упадём. Олимпиадная ведь задача, какая тут, нафиг, проверка на выход за границы эксплуатационного поля? Однако, пустой вход нас уронить не должен.
Для большей семантичностибугога решение одного игрового поля вынесем в отдельный метод:
1: private static void solveBoard(Scanner sc) {
2: String[] boardDefinition = sc.nextLine().split(" ");
3: Integer size = new Integer(boardDefinition[0]);
4: Integer iterations = new Integer(boardDefinition[1]);
5:
6: String[] board = new String[size];
7:
8: for (int n = 0; n < size; n++) {
9: board[n] = sc.nextLine();
10: }
11:
12: for (int i = 0; i < iterations; i++ {
13: }
14: }
Итак, сначала считаем определение поля. Размер складываем в переменную size, количество ходов в iterations. Затем заводим массив из size строк, и считываем в него начальное состояние поля.
Почем массив из size строк, а char[size][size]? Ну что вы как маленькие, право слово? Во-первых, в жабе нету двумерных массивов, только массивы массивов, поэтому память на больших размерностях жрётся как чёрт знает что. Во-вторых, строка в жабе — это уже и так вполне себе неплохо оптимизированный по всем параметрам одномерный массив с произвольным доступом (см. String.charAt(int)).
Поэтому, делаем так:
1: String[] nextBoard = new String[size];
2:
3: int reduced = size - 1;
4: for (int j = 0; j < size; j++) {
5: if ((j == 0) || (j == reduced)) {
6: nextBoard[j] = board[j];
7: } else {
8: StringBuilder sb = new StringBuilder();
9:
10: for (int k = 0; k < size; k++) {
11: if ((k == 0) || (k == reduced)) {
12: sb.append(board[j].charAt(k));
13: } else {
14: // выбрать цвет фишки
15: }
16: }
17:
18: nextBoard[j] = sb.toString();
19: }
20: }
21:
22: board = nextBoard;
Сначала заводим массив строк для состояния поля после совершения хода. Кроме того, нужна переменная для уменьшенной на единицу размерности поля, чтобы каждый раз его не вычислять.
Поначалу я забыл про последний пункт постановки задачи, и минут 7 потратил впустую, пока пытался дождаться проверки заведомо неправильного решения (уж больно фейсбуковский облачный компилятор тормозит), но на тот момент в запасе у меня несколько минут оставалось. Забегая вперёд, скажу, что уложился я в 55 минут из 60 :)
Итак, вспомнив про последний пункт постановки, и обходя игровое поле построчно, мы сразу копируем первую и последнюю строки в следующее состояние. Также, при обходе средних строк, мы, ничего не вычисляя, скопируем первый и последний символ в целевое состояние.
Осталось дело за малым: обойти квадрат 3х3 с центром в рассматриваемой ячейке и посчитать количество чёрных фишек (так как фишек всего 9, и принимаем решение мы исходя из того, что чёрных должно быть строго больше, то нас устроит любое количество больше 4 штук.)
Если бы я был труЪ-программистом, то я написал бы этот кусок примерно так:
1: int black = 0;
2: char c = 'w';
3: enough: for (int jj = j - 1; jj <= j + 1; jj++) {
4: for (int kk = k - 1; kk <= k + 1; kk++) {
5: if (board[jj].charAt(kk) == 'b') black++;
6:
7: if (black > 4) {
8: c = 'b';
9: break enough;
10: }
11: }
12: }
13: sb.append(c);
Но я не Ъ, и поэтому в моём решении обход выглядит так:
1: int black = 0;
2:
3: if (board[j - 1].charAt(k – 1) == 'b') black++;
4: if (board[j - 1].charAt(k) == 'b') black++;
5: if (board[j - 1].charAt(k + 1) == 'b') black++;
6: if (board[j].charAt(k – 1) == 'b') black++;
7: if (board[j].charAt(k) == 'b') black++;
8: if (board[j].charAt(k + 1) == 'b') black++;
9: if (board[j + 1].charAt(k – 1) == 'b') black++;
10: if (board[j + 1].charAt(k) == 'b') black++;
11: if (board[j + 1].charAt(k + 1) == 'b') black++;
12:
13: sb.append((b > 4) ? 'b' : 'w');
Вот только не надо сейчас делать такие O_O глаза.
Во-первых, вложенности циклов уже и без того более чем достаточно. Во-вторых, я не одобряю ниндзюцу ни в каком виде — а тут без него никак, ибо в жабе иначе из двух циклов наружу не выбраться, а подобные вещи в столь высокоуровневом языке очень трудно читать.
Так что делаем циклу unroll, и он (бонусом) начинает выглядеть ближе к пункту 3 постановки задачи. Да вообще, буквально его имплементирует.
Собственно, всё. Только вывод на экран осталось добавить за внешним циклом, но это вообще полная фигня:
1: for (int n = 0; n < size; n++) {
2: system.out.println(board[n]);
3: }
Теперь ещё разок окинем взглядом всё это безобразие целиком:
1: import java.util.Scanner;
2:
3: public class Solution {
4: private static void solveBoard(Scanner sc) {
5: String[] boardDefinition = sc.nextLine().split(" ");
6: Integer size = new Integer(boardDefinition[0]);
7: Integer iterations = new Integer(boardDefinition[1]);
8:
9: String[] board = new String[size];
10:
11: for (int n = 0; n < size; n++) {
12: board[n] = sc.nextLine();
13: }
14:
15: for (int i = 0; i < iterations; i++ {
16: String[] nextBoard = new String[size];
17: int reduced = size - 1;
18:
19: for (int j = 0; j < size; j++) {
20: if ((j == 0) || (j == reduced)) {
21: nextBoard[j] = board[j];
22: } else {
23: StringBuilder sb = new StringBuilder();
24:
25: for (int k = 0; k < size; k++) {
26: if ((k == 0) || (k == reduced)) {
27: sb.append(board[j].charAt(k));
28: } else {
29: int black = 0;
30:
31: if (board[j - 1].charAt(k – 1) == 'b') black++;
32: if (board[j - 1].charAt(k) == 'b') black++;
33: if (board[j - 1].charAt(k + 1) == 'b') black++;
34: if (board[j].charAt(k – 1) == 'b') black++;
35: if (board[j].charAt(k) == 'b') black++;
36: if (board[j].charAt(k + 1) == 'b') black++;
37: if (board[j + 1].charAt(k – 1) == 'b') black++;
38: if (board[j + 1].charAt(k) == 'b') black++;
39: if (board[j + 1].charAt(k + 1) == 'b') black++;
40:
41: sb.append((b > 4) ? 'b' : 'w');
42: }
43: }
44:
45: nextBoard[j] = sb.toString();
46: }
47: }
48:
49: board = nextBoard;
50: }
51:
52: for (int n = 0; n < size; n++) {
53: system.out.println(board[n]);
54: }
55: }
56:
57: public static void main(String[] args) {
58: Scanner sc = new Scanner(System.in);
59: if (sc.hasNextLine()) {
60: Integer boardCount = new Integer(sc.nextLine());
61: for (int i = 0; i < boardCount; i++) {
62: solveBoard(sc);
63: }
64: }
65: System.out.println();
66: }
67: }
Проверку на все кейсы программулина прошла со свистом, и я с чистой совестью отправился спать.
«А что же Фейсбук?» спросите вы.
Ну а чё Фейсбук. Ну, написала мне ихняя HR, назадавала кучу вопросов на тему профессионального развития, и после шестого моего письма с ответами (не вру, реально шестого) предложила позвонить скайпом и взять у меня пятнадцатиминутное ынтарвью. И тут я честно сознался, что в устном английском не силён, в ответ на что получил категорическое «We have a very open and communicative environment, so fluent spoken English is a must. If you don’t see yourself as a fluent speaker, unfortunately, we cannot move on at this time.»
Так что не светит мне солнечная Калифорния. По крайней мере, до тех пор, пока не подточу английский. А там — посмотрим.
У вас была проблема, и вы захотели решать её при помощи Java?
Теперь у вас есть IProblem, ISolution, IProblemSolutionFactory,
и вы запутались в их имплементациях.
Превед \о/
В каждой шутке есть доля.
Я почти месяц не мог правильно сформулировать тему данного поста, да и его содержание для меня самого долгое время оставалось достаточно сумбурным; я переписывал черновик трижды, и уже даже почти выбрал другую тему, но сегодняшнее незапланированное ревью кода коллег по проекту, наконец, уложило хаос моих эмоций вокруг нескольких поинтов относительно регулярно, и я могу их сформулировать.
Объектно-ориентированное программирование — это всего лишь прикладная техника. Их вообще много, и каждая хороша в приложении на определённый класс задач: функциональное программирование рулит в сильно параллелизованных событийно-ориентированных средах, процедурное хорошо для детерминированных линейных алгоритмов, декларативное применяется для метапрограммирования и описания данных.
Чистое класс-ориентированное ООП, пригодное на самом деле только для лепки куличиков в управляемых песочницах, незаслуженно стало неким промышленным стандартом, и виной тому — сверхуспешный маркетинг жабы несколько лет назад.
Благодаря удачному дизайну виртуальной машины и особенному расположению звёзд, этот неуклюжий, плохо спроектированный, с крайне несбалансированным набором фич, заточенный только под эту разновидность ООП, язык программирования пустил корни в энтерпрайз. Да настолько глубоко пустил, что не выкорчевать его оттуда уже, судя по всему, никогда.
В итоге огромное множество программистов целенаправленно воспитывается с упором на ООП — в понимании Java. То есть: всё на свете есть экземпляр класса. А если не экземпляр, то синглтон. Если же ни то, ни другое, то его нужно обернуть.
Поэтому многие, если не все, — и, в особенности, молодые и не очень любящие включать мозг жаба-программисты, — лепят туеву хучу классов даже для таких частных случаев, как передача некоего промежуточного значения между двумя точками алгоритма, целиком умещающегося на один экран.
Это для них норма: завести по отдельной фабрике для оборачивания каждого паршивенько аннотированного класса, служащего только для доступа к записи таблицы БД, в класс, имплементирующий интерфейс, соответствующий сущности из домена предметной области. А если имеется две или более похожих сущности, то завести для них тупую базовую реализацию и обязательно унаследоваться, после чего упаковать выбор конкретной фабрики имплементаций в стратегию. Для доступа к которой — из осмысленного участка кода — непременно будет использоваться делегат, делающий это каким-нибудь очень хитрым способом.
Смотришь подобные портянки и ужасаешься: чтобы всего лишь логически сгруппировать несколько значений в нечто обособленное, и имеющее смысл максимум до границы пакета, в проект добавлен десяток вспомогательных, ничего полезного не делающих, тупых промежуточных классов. Забор из мёртвого дерева, за которым не видно леса. С неизбежными ошибками.
Подобная машинерия оправданна, если речь идёт о чём-то достаточно глобальном в рамках всей системы целиком, пронизывающем её сверху донизу, и сшивающем в единое целое. Но если такой расколбас происходит в границах каждого отдельно взятого модуля — и только в этих рамках, то мне становится грустно. Очень грустно.
Кроме того, такие горе-программисты зачастую не включают мозг, и не используют стандартные контейнеры из java.util, когда это сам доктор Здравый Смысл прописал: чтобы перемотать бечёвкой и положить в сторону пяток атрибутов до тех пор, пока они не понадобятся опять, а потом выбросить использованные на фиг. Но нет, вместо этого они городят очередной полностью состоящий из геттеров/сеттеров класс. Зато как бездумно они используют их во всех остальных случаях…
Особенно молодёжь, привыкшая к гигагерцам и гигабайтам: берут и юзают стандартный двусвязный список для массивчика из ~50000 элементов типа Integer, который формируется, проходится строго один раз в одном направлении (с обработкой чуть сложнее, чем просто сумму посчитать), и сразу же уничтожается. 15 раз в секунду. В 10 соседних потоках. И удивляются потом, куда шесть гигабайтов памяти ушло, и почему всё так тормозит…
Действительно, а зачем думать башкой? В проде будет вычислительный кластер, вот нехай лучше он потрудится.
Et cetera.
К несчастью, только очень небольшая доля программистов, пишущих тяжёлый энтерпрайз на жабе, держит свою оккамовскую бритву остро заточенной, и не забивает гвозди при помощи синхрофазотрона.
Большинство предпочитает мозг не включать, и действовать по однажды усвоенной схеме: интерфейс, класс имплементации, враппер, фабрика, стратегия, делегат, забор, надпись «ХУЙ». И никакого тебе колосящегося свежей зеленью леса.
Сопровождать, развивать, и в особенности исправлять код, написанный с таким отношением, крайне тяжело и неприятно, потому что его осмысленные участки напрочь забиваются этим информационным шумом.
Keep it simple, stupid.
Я стар! Я суперстар! Ох, как же я стар…
Превед \о/
Молодой программист, которому далеко до просветления, — хочет. Всё время хочет. Постоянно и настойчиво хочет.
Хочет он динамики. У него руки чешутся пробовать новые инструментальные средства, новые технологии, свежие прогрессивные методики. У него вертится шило в заднице, и не даёт ему спокойно и вдумчиво пилить кусочек чего-нибудь крупного в однозадачном режиме. Вместо этого он готов выдавать мелкий уголёк на гора, и лучше всего работать на три, пять проектов в параллель.
Казалось бы, ничего удивительного: легко жрать информацию тоннами, пока ППЗУ не деградировало от циклов перезаписи, и вообще — жги, пока молодой!..
Но всё, как обычно, не совсем так, как на самом деле.
А на самом деле средневзвешенная производительность труда среднестатистического программиста (как и любого другого представителя интеллектуального пролетариата) на долго тянущемся проекте пилообразная, и в виде графика выглядит примерно так:

По горизонтали время, по вертикали — эффективность. То, что над красной чертой — наивысшая эффективность, когда программиста откровенно прёт. Это примерно 15% всего рабочего времени. То, что под чёрной — тупняк. Его, наоборот, много, около 40%. Такова суровая правда жизни, и бороться с этим бесполезно, покуда мы сделаны из белка и воды. Оставшееся время — обычный рабочий режим средней расхлябанности, когда вроде и работается, но как-то не совсем в кайф, и субъективно оно воспринимается как езда по шоссе на 40км/ч.
А сидеть на работе, тупить и тормозить, как больная черепаха аж целых 85% времени, никому не нравится. Особенно это выматывает, если ты молод и горяч. И сроки давят, а ты сидишь бессильно кусаешь локти. И философски ты к этому относиться ещё не научился. И это, в конце концов, ужасно скучно. И вот растёт, и ширится, то самое неконтролируемое хотение динамики.
А когда работаешь над несколькими проектами параллельно и одновременно, и переключаешься между ними, не теряя контекста, получается уже вот такая картинка:

То есть, работаешь почти всегда с максимальной отдачей, а времени на потупить просто нет. С одной стороны это хорошо. Потому что не скучно. Чувствуешь себя в постоянном движении, прям как живой.
С другой стороны — мозг изнашивается намного быстрее. Потому что «тупление» — это на самом деле время работы подсознания, которое вредно запускать одновременно с логическим мышлением. После обработки подсознанием концепты получаются удачнее, а код — чище и оптимальней. И срединное время между чёрной и красной чертой на самом деле — время неспешного разгона перед следующим рывком.
Но — до определённого возраста этого просто не понимаешь. Поэтому многие молодые программисты, поработав полтора-два года над одним длинным проектом в хорошей крупной конторе, и уже почти зарекомендовав себя, вдруг срываются и улетают в какую-нибудь мелкую конторку, где жизнь нестабильнее, а проекты намного мельче, зато идут внахлёст, и разные всё время, отчего их эпически прёт.
И бесполезно пытаться удержать их. Они должны переболеть. Даже не тягой к перемене мест, а схлопотать просветляющий гипертонический криз от перегрузки мозгов.
К сожалению, за всё время общения с ув. заказчиками
мне ни разу не разрешили ни произнести вслух,
ни написать фразу, вынесенную в заголовок.
Превед \о/
Будучи самым опытным членом команды разработчиков в большей половине тех мелких контор, в которых мне довелось работать, я частенько участвовал в подготовительных этапах в качестве технического эксперта. Во всяческих пресэйлах, экспертизах, оценках трудоёмкости, и прочем взаимном обнюхивании, по итогам которого делается вывод «берёмся» или, — намного чаще, — «не берёмся».
С одной стороны, это всегда очень интересно, потому что узнаёшь кучу всякого нового и странного. С другой стороны, в 90% случаев заказчик — некомпетентный идиот, который сам не знает, чего он хочет, но хочет этого вчера и за смешные деньги, а начальство, наоборот, желает развести его на продажу конторе его бессмертной души. Из-за этого процесс постановки диагноза, пусть и тешащий внутреннего грегори хауса, является настолько муторным занятием, что моего терпения никогда не хватало более чем на пару встреч.
Да, непосредственное общение с заказчиком я очень не люблю… Впрочем, ближе к теме.
Расскажу на конкретном примере.
Заказчик — комитет по делам несовершеннолетних. Предмет обсуждения — настольная информационная система, предназначением которой является ведение протокола заседаний комитета.
Это довольно простая система. Имеется база малолетних нарушителей, справочник совершённых ими административных правонарушений, и раз в несколько дней комитет собирается на заседание, на котором по каждому пункту повестки дня проводится слушание, и выносится постановление. Кроме того, несколько раз в каждый отчётный период по накопленным вердиктам формируются всякие отчёты, которые в бумажном виде отдаются куда-то в МВД. Помимо этих периодических отчётов, по каждому пункту повестки должен формироваться некий пакет сопроводительных документов, которые тоже уходят потом бумажной почтой в разные места.
Собственно, всё. Структура базы личных дел довольно развесистая (аналогичная бумажным), но при том ничего особо сложного в ней нет — пара десятков таблиц. Сам КоАП вообще состоит из одной таблицы с номерами и названиями статей, а все без исключения отчёты имеют формы, в которых нужно всего лишь заполнять некоторые поля. Заседания замоделить — да два байта об асфальт, потому что повестка совершенно однотипная, этакий плоский список пунктов «слушали — постановили», плюс статус «проведено».
Собственно, руководствуясь такой постановкой задачи, один студент за полгода системку-то требуемую и наваял — на дельфях, с файербёрдом, да с кристал репортсом это нефиг делать.
Написал студент ИС, получил свою денежку, и свалил, исходников не оставив. Полгода заказчик был доволен как слон.
Но законодательство имеет обыкновение со временем меняться, соответственно, меняются и формы отчётности, и структура личных дел, да и сам КоАП тоже.
И вот устарела оная ИС настолько, что совсем перестали выходные документы удовлетворять вышестоящие органы. И встала задача: сделать такую систему настраиваемой и изменяемой силами самого заказчика, чтобы он мог под меняющиеся реалии её подгонять. Студент имел какое-то опосредованное отношение к конторе, в которой я тогда работал, поэтому заказчик первым делом сунулся к нам.
Моей первой реакцией было:
— А давайте мы им базу на MS Access запилим! Дёшево, сердито, настраиваемо.
— Ты что, Евдокимов, ебанулся? — ответило мне начальство, — Это же как потом из них бабло-то тянуть? С аксесом же никакого вендор локина не получится! Его же каждый дурак сможет потом переделывать как захочет.
— Если бы мог каждый дурак, то они сами бы наклепали уже себе такую базу.
— Нет, они не умеют!
— В таком случае, им не нужна такая система как продукт. Им нужно информационное сопровождение…
— Молчать! Ничего ты не понимаешь! Нам надо обязательно запилить супервелосипед с кучей крутилок и свистелок, мы сделаем на нём типовое решение (и пускай они его сами дотачивают, а нам только ежемесячно платят за использование), и будем продавать его, продавать $_$ И ещё 10 типовых решений для любых других похожих задач на нём сделаем! Разбогатеем, Евдокимов!
Офигев от взаимоисключающих параграфов, я сел, и закручинился. Ну вот зачем мне пилить какой-то очередной программируемый без программирования велосипед, когда буквально две недели назад микрософт выпустил бета-версию (бесплатную, и с лицензией go-live — шикарный вариант!) прекрасного инструмента, предназначенного именно для подобных простых ИС… Который, тем не менее, требует минимального навыка программирования хотя бы мышью в аксесе (и которого, как выясняется, у заказчика нет).
Тем более, что у микрософта там такой шикарный маркетинговый текст выложен, только перевести его, причесать… Не прокатило, потому что бета лайтсвича оказалась на английском языке, а ждать финального выпуска некогда — иначе заказчик сорвётся.
Окей, сделаем хорошую мину при плохой игре. Имея большой опыт разработки почти аналогичных систем, но предназначенных для веба, и писанных на джаваскрипте с пыхом в качестве интерфейса к БД, я оценил воссоздание подобного же, только настольного, инструмента, на втором дотнете (виндовс.форм наше всё), с сиквелайтом в качестве БД, и адо.нет прослойкой между ними, в 7 месяцев. Как ни странно, это прокатило. Правда, потом выяснилось, что начальство самовольно сократило срок до четырёх.
Надо было сразу уволиться, но меня разобрала злость: фиг ли нам, лосям, подсовывают такую подляну? Прекрасно понимая, что не нужна ув. заказчику такая система, я сел и набыдлокодил движок (ужасный; но даже уложился в обрезанный срок), а поверх него воссоздал имевшуюся среду старого дельфёвого приложения, потому что ТЗ на изменения так за это время и не дождался. Ещё пришлось написать 250-страничный талмуд по настройке — в виде комикса для полных идиотов, со скриншотами на любой мыслимый чих.
Естественно, что я был с самого начала прав. Заказчик оказался абсолютно не готов перенастраивать справочники под меняющиеся модели предметной области, тюнить воркфлоу, и, особенно, формы отчётов — самостоятельно, а начальство, рассуждающее на уровне одноклеточного, было не готово оказывать долговременную информационную поддержку даже за деньги.
Из той конторы я ушёл, как только представилась первая возможность.
А мораль, думаю, ясна.
Превед \о/
Прошлый раз мы запроектировали три простых таблицы, связанные между собой отношениями 1:М в одну сторону, и я вяло ругнулся насчёт нормализации, которая зло, но не объяснил, почему. Сейчас расскажу.
В третьей таблице, «Транспондеры», каждая запись содержит около 20 атрибутов, значения которых достаточно чётко стандартизированы. Например, избыточность битов в потоке (FEC) имеет дискретный ряд значений «1/2», «2/3», «7/8» и т.п., и при вводе (а любые информационные системы изначально заполняют люди) логично предлагать пользователю выпадайку с данными значениями, а не заставлять вбивать как строку.
То бишь, завести табличку «FEC», связать внешним ключом с «Транспондерами», внести в неё 5 записей…
Мда. Завести целую таблицу ради несчастных 5 записей. Лишний джойн при показе списка, да и любого одного транспондера. И отдельное UI для корректного дополнения таблицы (чтобы нельзя было туда ересь всякую вбивать), если какому-нибудь спутниковому провайдеру вдруг когда-нибудь приспичит использовать новое или нестандартное значение. Причём, обновить её придётся перед тем, как добавлять запись в «Транспондеры», что автоматически приводит к усложнению логики контроллера соответствующей формы.
Нет, я не то чтобы брюзжу. Но если подобных атрибутов ~20, то потраченный effort слишком велик, и окупится только тогда, когда ИС строится реально на века, и есть немаленький человеческий ресурс для создания качественной обвязки на каждую из вспомогательных таблиц, декомпозированных из основной. Обычно ресурса нет, и даже в случаях тяжёлого enterprise для подобных таблиц организуют в лучшем случае некий универсальный UI, от которого всех тошнит.
Любые компромиссы — по определению гадость, конечно же, но если оставить атрибут FEC в основной таблице, то в UI ввода записи всё равно можно организовать показ выпадайки с вариантами, сделав дополнительный SELECT DISTINCT(FEC) FROM «Транспондеры», и проиндексировав её по данному атрибуту. Индекс потом и для поиска пригодится.
То же самое верно для остальных 19 атрибутов, декомпозировав которые мы получим 19 дополнительных таблиц и внешних ключей с сопутствующими джойнами и прочей морокой.
А ещё в первой части я обмолвился о том, что ИС должна помнить о предыдущих состояниях сущностей из домена. Как этого достичь?
Да вот так прямо в лоб.
| Спутник | Антенна | Транспондер | ||
| id PK
ts DT Атрибуты |
1
М |
id PK
ts DT Атрибуты |
1
М |
id PK
ts DT Атрибуты |
| 1
М |
1
М |
1
М |
||
| История спутников | История антенн | История транспондеров | ||
| ver PK DESC id PK, FK ts DT DESC Атрибуты |
1
М |
ver PK DESC id PK, FK ts DT DESC Атрибуты |
1
М |
ver PK DESC id PK, FK ts DT DESC Атрибуты |
В таблицах домена заводится поле штампа времени, и к каждой из них прицепляется по одной дополнительной таблице истории, имеющей точно такую же структуру, но расширенную одним ключевым полем — версией записи, и отсортированную в обратном порядке по нему, и штампу времени.
Теперь, вместо изменения записей в основных таблицах, мы всегда делаем два действия: INSERT INTO «История» SELECT «История.ver»=MAX(«История.ver»)+1, «Таблица.*» FROM «Таблица», «История» WHERE «История.id»=«Таблица.id» AND «Таблица.id»=42, и потом уже REPLACE записи для «Таблицы» с id=42.
А теперь представьте, что было бы с базой в случае вынесения атрибутов в дополнительные таблицы, особенно если записи в основных обновляются достаточно часто, и историю вдруг нужно показать, или, ещё хуже, сформировать diff по двум состояниям… Автору этих строк встречались случаи, когда в «Историях» было по 900 записей с бурной биографией какой-нибудь особо выдающейся сущности, и самих таких сущностей тоже over 9000. Могу вас уверить: индексные tablespace-ы в таком случае жрутся просто со страшной силой.
Мораль: разумная денормализация при проектировании БД — не вредна, в отличие от бездумной нормализации, которая ведёт к тошнотворному результату как в коде, так и в UI.
И, кстати, о диффах. Как насчёт них? И как быть с историей в более сложных случаях, например, с отношениями М:М?
[Об этом речь пойдёт в следующий раз.]
Превед \о/
Я давно уже хотел начать писать этот блог, но окончательным побудительным мотивом для меня послужило неожиданное осознание того факта, что после всех уже почти 12 лет моего труда на ниве программирования, в окружающем мире в целом почти не осталось никаких его материальных результатов. Нематериальные остались, и немало — своей цели по служению цивилизации я достигаю, так как люди пользуются моими системами, потребляя с их помощью информацию, и создавая новую.
Но вот сами эти системы оказываются недолговечны. После запуска ИС проходит какие-нибудь 3–4 года, заходишь очередной раз посмотреть, как она там, — а нет её больше.
Обидно. Какой-нибудь слесарь-сантехник (с которыми я люблю сравнивать программистов — и те и другие занимаются чистейшей инфраструктурой) и то создаёт гораздо более долговечные конструкции. Какой-нибудь заштатный писатель областного значения (это тоже хорошая аналогия) со средним тиражом в 1500 экземпляров макулатуры в этом смысле в сотню раз круче — собственно, его макулатура долгие годы будет лежать на антресолях и в запасниках библиотек. Кто-нибудь лет через 50 да прочтёт, или хотя бы пролистает.
А моё творчество? Я вроде как неплохой специалист, почему же его результат так быстро оказывается не у дел?
Тому есть несколько объективных причин.
Во-первых, ИС достаточно быстро устаревает. Информация — это такая штука, которая, словно квант электромагнитного поля (из которых она состоит), который одновременно и частица, и волна — дуальна. Она одновременно и данные, и процесс.
А процесс, будучи однажды запущенным, развивается. Либо с положительной динамикой, либо с отрицательной, но стоять на месте он не может. Отсюда следует, что любая информационная система, развитие которой остановлено, неминуемо умрёт, так как быстро перестанет соответствовать процессу, в который вовлечены её данные.
Поэтому же нельзя сказать, что информационная система «готова». Она никогда не может быть готова, пока данные вовлечены.
К сожалению, ни один внешний заказчик не согласится на то, чтобы его ИС разрабатывалась вечно. Потому что у любого заказа есть бюджет и есть сроки. Заказчик внутренний, на который пашет отдел ИС в штате организации — ещё как согласится. Но тут мы приходим к проблеме, которую хочется вынести вторым пунктом.
Во-вторых, заниматься развитием одной и той же ИС долгие годы в одном и том же окружении с одними и теми же людьми — это невероятно, невозможно, выматывающе скучно.
Я однажды попробовал поработать в таком режиме. Продержался два года, потом ушёл. Сидеть клепать fulltime и не отвлекаясь на другие задачи одно и то же кошмарствие, которое писали десяток лет до тебя несколько разных человек, и будут дописывать после тебя ещё много лет другие несчастные — для этого нужно обладать поистине чугунной задницей.
Нет, это совершенно нормальная ситуация, — когда бизнес развивается, а ИС, на которой он основан, должна успевать, но при этом данные в ней должны быть доступны с самого начала. И совершенно нормально, что ИС в таком случае постоянно дорастает до текущих потребностей бизнеса. И совершенно нормально, что на 90% она состоит из legacy кода для устаревшей 10 лет назад платформы. И совершенно нормально, что люди, которые её ваяют, не растут как специалисты, потому что им тупо некогда — надо выдавать результат, и гарантировать, чтобы старое ядро под новым обвесом не развалилось.
И совершенно нормально, что люди эти получают за свой сизифов труд копейки. Но у меня, к несчастью, задница не чугунная, мне почему-то иногда хочется полетать чуточку повыше, чем над текущим ландшафтом.
Третья причина тоже кроется в людях. Дав однажды старт ИС, неминуемо приходится передавать её в чьи-то руки, руки людей с иными взглядами на жизнь и багажом практических навыков. Не факт, что они примут её такой, какова она есть, да и вообще, чтобы досконально разобраться в творении кого-либо — для этого надо быть немножечко этим человеком.
К сожалению, несколько лет назад я обнаружил, что 85% окружающих программистов знают и умеют меньше меня, а концепты, которые я закладываю в фундамент ИС, и которые с моей кочки выглядят просто, стройно и логично, — они инопланетянски сложны для их понимания. А из оставшихся 15% взгляды на архитектуру конкретной ИС совпадают с моими у 1/5.
И это было тем ещё шоком. Получается, что только один из 33 человек, принимающих в свои руки ИС, может развивать её бесшовно, по изначально определённому плану. Ещё четверо — просто развивать, пускай даже другим путём, и в другую сторону. А остальные 28 — гарантированно запорют, потому что не имеют нужного опыта.
К несчастью, тридцати трёх богатырей за эти годы ещё не набралось, и с месяц назад обнаружив, что все, все, вообще все мои прошлые проекты для меня мертвы, и разразившись гневным твитом, я решил, что напишу по ним всем постмортемы.
Возможно, они окажутся полезней.
Вот на этой неделе, например, будет вторая часть серии про проектирование базы данных.