Friday, July 09, 2010

Общее решение бывает тоже плохим

В этой заметке речь пойдёт о программировании, немного приправлено анекдотами.

Для начала я разберу цитату с блога my-tribune.blogspot.com(выделение курсивом моё), затем напишу свои мысли о ней, затем разберу ещё одну цитату оттуда же.


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

...слишком универсальные решения бывают, причём они имеют как минимум два минуса:
1) они могут быть слишком сложными, поэтому будут плохо выполнять свою функцию (компьютер с тв-тюнером может полностью заменить видеомагнитофон, но сможет ли им пользоваться ваша бабушка?),
2) ради расширения функциональности приходится заметно поднять цену (да, компьютер с тв-тюнером может автоматически вырезать рекламу, может самостоятельно скачивать программу передач и ещё много всего, но он стоит в 10 раз дороже видеомагнитофона).

http://my-tribune.blogspot.com/2009/09/blog-post_16.html

Начнём, с первой части цитаты:

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

http://my-tribune.blogspot.com/2009/09/blog-post_16.html

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

Ниже есть продолжение.

...Теперь, представим, что вы пишите код и в какой-то момент вы решаете, что часть функциональности неплохо бы сделать "инфраструктурой", т.е. такой, что

а) легко и удобно пользоваться;
б) можно будет использовать во многих подобных ситуациях в разных местах программы;

Не будем рассматривать вопрос о целесообразности написании такого вспомогательного кода (не всегда нужно это делать), не будем также разбирать вопрос кто кого должен вызывать (принцип Don't call me, I'll call you, который используется в современных Framework-ах), а остановимся на вопросе, насколько гибко должен быть написан код. Я напишу минимальные требования и максимальные требования, а потом напишу пояснение.

Минимальные требования:

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

Максимальные требования
а) С функциональной точки зрения, такая инфраструктура должна позволить решить текущие задачи;
б) Также она должна позволять решать их лёгкие вариация (кастомизация);
в) Также она должна позволять похожие задачи, которые, возможно, появятся в будущем;
г) Быть лёгкой в поддержке;
д) Быть простой в использовании;
е) Быть приемлемым по времени написания;

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


Физик провел эксперименты и, затем, смог построить систему уравнений, которые, кажется, объясняют его данные. Он просит, чтобы математик проверил их. Неделю спустя, математик звонит,
- я сожалею, но ваши уравнения - полная ерунда.
-Но эти уравнения точно предсказывают результаты экспериментов. Вы уверены, что они полностью неправильны?
-Чтобы быть точным, они - не всегда полная ерунда. Но единственный случай, в котором они являются верными - тривиальный, где пространство - евклидово... "


Второй, быть может, более понятный:

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

http://bars-minsk.narod.ru/an.html (там, кстати, очень много анекдотов про математику)

Перейдём к пункту б). Практически всегда не хватает времени. Поэтому, скажем, для простых задач не имеет смысла писать хорошую инфраструктура, т.к. на её написание нужно непропорционально большое время. Можно, конечно, попытаться убедить начальство в нужности инфраструктуры, чтобы оно-таки выделило необходимое время. Если для написания инфраструктуры не выделено время и оно занимает существенную часть от выделенного времени на всю задачу, то писать такую инфраструктуру не имеет смысла. Придёт dead line, с вас спросят о выполнении поставленной задачи, а вы скажите, что задачу вы не сделали, зато какую классную инфраструктуры вы пишите. Для решении этой проблемы используется, например, разделение труда, как между фирмами, так и внутри фирмы. У специализированной команды внутри фирмы или у другой фирмы нет необходимости сдать определённое задание к сроку, поэтому она может себе позволить написать хороший design и т.п. Естественно, оно должна получить requirements для этого, которые трудно предоставить. Можно описать только о текущих проблемах, с тем, с чем сейчас конкретно столкнулся, но т.к. специализированная команда имеет весьма слабое представление о том, как это будет использовано на практике, она может написать это или негибко или наоборот, через чур гибко, затратив лишнее время. Преимуществом разделения труда является то, что, возможно, не только вам нужна такая инфраструктура, а и другим группам\фирмам. Вы, скорее всего, об этом не знаете, а такая отдельная группа знает и может получить больше реальных use cases. Очевидным недостатком, однако, является непропорциональное удлинение времени написание этой инфраструктуры (даже если при этом её качество существенно возрастает). Таким образом, если вам надо сдать проект через неделю, вы не можете себе позволить ждать месяц пока будет готова инфраструктура.

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


public long getLastFourDigits(long num);


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


public long getLastDigits(long num, long numOfDigits);


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

Прежде чем я продолжу, я должен показать один из способов имплиментации последнего метода. Он состоит в том, что мы переведём num в String, затем возьмём последние numOfDigits у него, переведём обратно это в число и вернём. Минусом этого метода является его неэффективность. Если с неэффективностью по времени ещё можно спорить, т.к. такой метод всё равно работает достаточно быстро для нашего магазина, то неэффективность по памяти может доставить проблемы. Простая прикидка, количества объектов, которые будут созданы в памяти, при том, что после выхода из функции, их нужно уничтожить поражает. Тут есть сразу несколько тонкостей. Как известно, в Java есть Garbage collector, который эти объекты уничтожит. Однако, это будет сделано не сразу, и какое-то время эти объекты будут занимать память. В Java 1.6, однако, компилятор может сделать оптимизацию и определить все эти объекты на Stack-е, а не на Heap-е, тем самым, они будут уничтожены при выходе из функции. И всё же считаете плохим тоном опираться на оптимизацию компилятора. Более того, что делать, если используется более ранняя версия JDK?..

Пункт в) говорит о том, что ваша инфраструктура должна уметь предвидеть и будущие изменения. Легко представить, что завтра этим методом захотят воспользоваться совсем в другом месте. Например, захотят определить чётность числа (будем считать, что про булевы операции мы не знаем). Можно, конечно, вызвав этот метод, получить последнюю цифру и затем, если это 0,2,4,6,8 сказать, что число чётное, иначе - нечётное. Но что, если нам это надо сделать во многих местах? Писать отдельный метод


public boolean isEven(long num);


А если послезавтра, надо будет проверить признак делимости на 4, мы найдём последние цифры, проверим их на делимость на 4 и напишем тоже отдельный метод? Написание отдельного метода не так уж и плохо, само по себе, тем самым мы создаём ещё один уровень абстракции, в частности увеличиваем читабельность кода. Плохо, что нам в каждом конкретном случае надо делать дополнительные действия, проверки делимости однозначного числа на 2 или двузначного на 4. Хотя, мы делаем reuse нашей "инфраструктуре", видно, что она требует доводки. Это можно сделать добавить, если в качестве второго параметра передавать не количество последних цифр, а число mod. Естественно и имя метода должно поменяться.


public long mod(long num, long m);


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


public long mod(long num, long m);


в качестве первого параметра и 2 в качестве второго. Оно вернёт остаток от деления на 2 и дальше надо его всего лишь сравнить с 0. Аналогично, передадим 4 для определения делимости на 4.

...При этом надо быть осторожным со способом имплиментации этого метода. Наивный способ следующий:


public long mod(long num, long m){
return num%m;


Подумайте, какие в этом способе естть потенциальные проблемы? Оператор % находит остаток от деления, верно? Почти. Подумайте, что произойдёт, если num отрицательный? В Java оператор % это такой оператор, что для всех целых a и b ($b!=0$, т.к. для всех целых n $n/0$ не определено):

$(a / b) * b + (a % b) = a$
где $a / b$ обозначает целочисленное деление.

Таким образом, к примеру, $-7 % 2$, будет

$(-7 / 2) * 2 + (-7 % 2) = -7$

Тогда:

$(-6) * 2 + (-7 % 2) = -7$
$-6 + (-7 % 2) = -7$
$(-7 % 2) = -1$

См. Java Language Specification 16.17.3 для подробностей.

Таким образом, вызвав


public long mod(long num, long m);


из


public long getLastDigits(long num, long numOfDigits);


нужно помнить, что mod может вернуть отрицательное значение...


Здесь, однако, нас ожидает ловушка. Мы можем написать гибкий код под сценарии, которые в реальной жизни могут не произойти. Если мы пишет online магазин, нам вряд ли понадобится узнать делиться ли число на 4. Более того, жизнь может преподнести сценарии о которых мы не думали в момент написания кода.

Перейдём, однако, к пункту г). Т.к. именно вы написали инфраструктуру, код должен написан так, чтобы его легко можно было поддерживать. Например, должно быть легко поменять имплиментацию. Например, сначала нужно было определить является ли число чётным, допустим, по чётным числам вы предоставляете скидку. Был написали метод


public boolean isEven(long num){
return (num & 0x1) == 0;


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


public long mod(long num, long m);


Вы можете изменить её имплиментацию на


public boolean isEven(long num){
return mod(num, 2);


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

Пункт д) говорит о лёгкости в использовании. Если вам нужны последние 4 цифры, а вы написали инфраструктуру, включающую только


public long mod(long num, long m);


таким методом неудобно пользоваться. Поэтому должна быть также метод для частых случаев (Façade design pattern упомянутый выше)


public long getLastDigits(long num, long numOfDigits);


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


public long getLastFourDigits(long num);


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

...Может, также случится, что расширяя задачу, мы её существенно усложняем. Т.е. мы можем решить "узкую" задачу, но если мы хотим быть перфекционистами, задача получается практически не решаемая. Процитирую опять Илью Весенньего:


Вспоминается по этому поводу история, которую рассказывал Терехов Андрей Николаевич - генеральный директор ТЕРКОМ. Когда он только начинал делать программно-аппаратные комплексы для различных военных задач, ему довелось участвовать в обсуждении установки, которую создали за многие годы до того разговора. И он произнёс довольно резкие слова по поводу производительности той системы - «как же так надо было проектировать, что включение установки и её подготовка к работе происходят целых полторы минуты?». На что неожиданно отреагировал очень взрослый и опытный конструктор (как оказалось, это был создатель обсуждаемого комплекса): «Во-первых, личный состав по нормативам должен успевать приготовиться к выполнению боевой задачи за две минуты, поэтому старт за полторы - это вполне достаточно, а во-вторых, попробуйте сначала сами сконструировать систему, которая будет надёжно работать в боевых условиях» (если я правильно помню, то вычислительное устройство могли монтировалось на ГАЗ-66 и БМП, подвеска которых не отличается мягкостью).

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

http://my-tribune.blogspot.com/2009/09/blog-post_16.html

Поэтому иногда нужно делать только то, что просят.

Сферический конь в вакууме (ЮМОР)


http://lurkmore.ru/Сферический конь в вакууме
у него есть любопытные свойства, авось в быту пригодится. :-)