Разработка
Материал из BiTel WiKi
Содержание |
Цель и ценность ПО
Цель и ценность любой программы: выполнение каких-либо возложенных на неё функций. Мысль простая и очевидная не только для разработки ПО, но и для любого производства в условиях рыночной экономики, тем не менее почему-то порой игнорируемая.
Пользователя не интересует в конечном итоге стройность кода, наличие в нём комментариев, форматирования и т.п.
Интересует:
- функциональность, решение каких-либо необходимых пользователю задач
- удобство, простота, эстетичность
- надёжность
- cроки и цена реализации
Следовательно, разработчик должен добиться увеличение первых трёх показателей, при минимизации последнего. Все остальные проистекает из данного базового постулата. Требования к оформлению, тестированию, документированию и пр. проистекают уже из него.
Посему необходимо всегда перед тем или иным действием понимать для себя, какой базовый параметр вы этим улучшаете. Важная оговорка: улучшение следуюет учитывать с расчётом на действительную перспективу эксплуатации продукта. Вполне возможно, что в данный конкретный момент скопировать код может показаться более простым, чем вынести в отдельную функцию, но по мере дальнейшего развития проекта избыточные временные затраты на корректировку кода в двух местах превысят эту мнимую выгоду. С другой стороны, если вы совершенно уверены, что данный скрипт будет вами запущен только один раз, после чего необходимость в нём отпадёт - то городить в нём стройные ряды комментариев, возможно и нет особой необходимости.
Отличия ПО
Программное обеспечение по сравнению с материальными товарами обладет некоторыми особенностями, рассмотрим их:
1. Копирование программного продукта ничего не стоит
Тиражируемый продукт позволяет значительно увеличить совокупную пользу его применения и, как следствие - возможность получения выгоды для производителя. При этом затраты производителя практически не увеличиваются, продукт так же хорошо (или плохо) работает в новых копиях. После занятия доли рынка пользователи зачастую становятся зависимыми от конкретного продукта в силу своих производственных связей с другими его экспуатантами (общие форматы файлов, например). Кроме того, своими запросами в сети они постоянно повышают рейтинг используемого решения. Переход на новое решение сопряжён с необходимостью переобучения. Эффект нарастает лавинообразно, поэтому в производстве ПО так часты случаи доминирование в той или иной нише одного-двух крупных продуктов.
2. Распространение программного продукта также ничего не стоит
Рынок производителя кирпича ограничен тем расстоянием, на которое кирпич целесообразнее привезти, чем купить местный. Т.е. кирпичный завод-гигант в России при всём желании не сможет заполонить всю страну своим товаром. Затраты на транспортировку будут расти с ростом удалённости клиентов. Для программного обеспечения это не актуально и продукт чаще всего ограничен не территорией а средой пригодной для использования. Например, языковой, либо законодательной. Так, бухгалтерская программа реализованная под россиийские стандарты бухучёта не применима в Европе или США. Зато программа для распознавания изображений - вполне. При должной локализации, конечно.
3. ПО не подвержено износу
С течением времени день за днём ваш продукт либо будет приносить пользу и радовать клиента либо раздражать одной и той же ошибкой. Плохой продукт - это гораздо хуже, нежели слегка кривой ботинок, срок жизни которого в любом случае не слишком велик.
Разработка ПО - это как игра с большими ставками. Чуть лучший продукт получает всё, чуть худший - ничего. И оттеснить конкурента после можно лишь став значительно лучше, переломив привычки пользователей и сложившиюся вокруг инфраструктуру.
Базовые принципы разработки
Далее попытаемся разложить некотороые основные принципы в разработке ПО с учётом указанных выше базовых ценностей. Не принимая их как догму.
Документируйте
Отсутствие документации, на написание которой уходит менее 10% времени от разработки, полностью обесценивает реализованный функционал для большинства клиентов. Возможно, вы его реализовали и запустили у одного из клиентов. Но никто больше не сможет им воспользоваться. Ценность продукта уменьшается пропорционально количеству людей, которые могли бы использовать данный функционал. Самое печальное, что через некоторое время вашу суперфичу может удалить другой разработчик, не поняв, что делает этот нигде не документированный блок кода. После чего функционал перестанет работать и у едниственного его пользователя.
Не допускайте "мертвого" кода
Мышление человека в процессе разработки построенно так, что все действия связанны причинно-следственным образом для реализации некой цели. И нет ничего более обескураживающего для не посвящённого, чем вызов некого блока:
... doNothing( "Param1", 3 ); ...
В смятении ваш несчастный коллега будет долгое время размышлять над бессмертными и бессмысленными уже строками. Вполне возможно, что когда-то они имели смысл, но далее стали ненужными а кто-то поленился их убрать. В конце-концов их удалят, но сделать это мог бы гораздо проще и быстрее разработчик, сделавший данный код ненужным.
В меньшей степени заповедь относистся к закомментированным блокам кода. С наличием системы контроля версий возможно всегда вернуть предыдущую версию файла и содержание закоменнтированного старого кода бессмысленно и отвлекает.
Не копируйте код
Возникающие при этом проблемы:
- Разрастание объёмов кода, ухудшение читаемости. Раз за разом вы будете читать одинаковые блоки.
- Усложнение правок и исправлений. Корректировку придётся выполнять в нескольких местах.
В идеале у вас не должно быть одинаковых блоков в коде. Это почти всегда неверно.
Небольшой пример.
if ( request.getParameter("action").equals("auth") ) { try { Class.forName("com.mysql.jdbc.Driver"); con = DriverManager.getConnection(url, user, password); setUnicod1 = con.prepareStatement("set character set utf8"); setUnicod2 = con.prepareStatement("set names utf8"); setUnicod1.execute(); setUnicod2.execute(); selectData = con.prepareStatement("SELECT `login`,`password` FROM `users` WHERE `login` = ? AND `password` = ? "); selectData.setString(1, request.getParameter("login")); selectData.setString(2, request.getParameter("password")); ResultSet rs = selectData.executeQuery(); rs.next(); json.put("result", rs.getRow()); out.print( request.getParameter("callback") + "(" + json.toString() + ")" ); con.close(); HttpSession session = request.getSession(true); session.setAttribute("login", request.getParameter("login")); } catch(SQLException | ClassNotFoundException | JSONException e){ e.printStackTrace(); } } else if( request.getParameter("action").equals("reg") ) //регистрация { try { Class.forName("com.mysql.jdbc.Driver"); con = DriverManager.getConnection(url, user, password); setUnicod1 = con.prepareStatement("set character set utf8"); setUnicod2 = con.prepareStatement("set names utf8"); setUnicod1.execute(); setUnicod2.execute(); selectData = con.prepareStatement("SELECT `login` FROM `users` WHERE `login` = ? "); //Check the existing login in base selectData.setString(1, request.getParameter("login")); ResultSet rs = selectData.executeQuery(); rs.next(); if ( rs.getRow() == 0 ) //если такого логина в базе не найдено - регистрируем { insertReg = con.prepareStatement("INSERT INTO `users` (`login`, `password`) VALUES ( ?, ? )"); insertReg.setString(1, request.getParameter("login")); insertReg.setString(2, request.getParameter("password")); insertReg.execute(); con.close(); HttpSession session = request.getSession(true); session.setAttribute("login", request.getParameter("login")); json.put("result", "ok"); out.print( request.getParameter("callback") + "(" + json.toString() + ")" ); } else { json.put("result", "false"); out.print( request.getParameter("callback") + "(" + json.toString() + ")" ); } } catch(SQLException | ClassNotFoundException | JSONException e){ e.printStackTrace(); } }
Уберём копирование. Код вырван из контекста, тут важен только принцип.
.... String action = request.getParameter("action"); if ( action.equals("auth") ) { try { con = prepareConnection(); selectData = con.prepareStatement("SELECT `login`,`password` FROM `users` WHERE `login` = ? AND `password` = ? "); selectData.setString(1, request.getParameter("login")); selectData.setString(2, request.getParameter("password")); ResultSet rs = selectData.executeQuery(); rs.next(); sendResult( json, rs.getRow(), out ); con.close(); getSessionAndPutLogin( request ); } catch(SQLException | ClassNotFoundException | JSONException e){ e.printStackTrace(); } } else if( action.equals("reg") ) //регистрация { try { con = prepareConnection(); selectData = con.prepareStatement("SELECT `login` FROM `users` WHERE `login` = ? "); //Check the existing login in base selectData.setString(1, request.getParameter("login")); ResultSet rs = selectData.executeQuery(); rs.next(); if ( rs.getRow() == 0 ) //если такого логина в базе не найдено - регистрируем { insertReg = con.prepareStatement("INSERT INTO `users` (`login`, `password`) VALUES ( ?, ? )"); insertReg.setString(1, request.getParameter("login")); insertReg.setString(2, request.getParameter("password")); insertReg.execute(); con.close(); getSessionAndPutLogin( request ); sendResult( json, "ok", out ); } else { sendResult( json, "false", out ); } } catch(SQLException | ClassNotFoundException | JSONException e){ e.printStackTrace(); } } .... private sendResult( JSON json, String result, PrintWriter out ) { json.put("result", result ); out.print( request.getParameter("callback") + "(" + json.toString() + ")" ); } private HttpSession getSessionAndPutLogin( HttpServletRequest request ) { HttpSession session = request.getSession(true); session.setAttribute("login", request.getParameter("login")); } private Connection prepareConnection() { Class.forName("com.mysql.jdbc.Driver"); Connection con = DriverManager.getConnection(url, user, password); setUnicod1 = con.prepareStatement("set character set utf8"); setUnicod2 = con.prepareStatement("set names utf8"); setUnicod1.execute(); setUnicod2.execute(); return con; }
Теперь если нам понадобится передавать ответ не в "result" поле а в "result1" либо поменяется название параметра с "action" на "command" - правка потребуется всего в одном месте.
Вынесение промежуточных результатов в переменные
Частным случаем данной проблемы является вынесение результатов в переменные.
Ещё небольшой пример, JSP разделения строки по двоеточию. Первый параметр - некий workTypeId, далее минуты от и до.
workTypeTime.setWorkTypeId( Integer.valueOf( item.substring( 0, item.indexOf( ":" ) ) ) ); item = item.substring( item.indexOf( ":" ) + 1 ); workTypeTime.setDayMinuteFrom( Integer.parseInt( item.substring( 0, item.indexOf( ":" ) ) ) ); item = item.substring( item.indexOf( ":" ) + 1 ); workTypeTime.setDayMinuteTo( Integer.parseInt( item.substring( 0, item.indexOf( ":" ) ) ) );
Здесь постоянно используется значение item.indexOf( ":" ), правда оно меняется два раза. Я бы предложил такой подход:
String[] tokens = item.split( ":" ); if( tokens.length < 3 ) { continue; } workTypeTime.setWorkTypeId( Utils.parseInt( tokens[0] ) ); workTypeTime.setDayMinuteFrom( Utils.parseInt( tokens[1] ) ); workTypeTime.setDayMinuteTo( Utils.parseInt( tokens[2] ) );
Здесь мы сразу разбили строку, сохранив результат в массив из которого далее с помощью безопасной функции преобразования строки в целое присвоили значения. Более того, добавлен функционал проверки строки на правильное число токенов. И код не стал длинее, теперь он более читаем и работает быстрее.
Использование констант
При многократно используемых значениях имеет смысл выносить их в неизменяемые переменные.
if ( param == 0 ) { doSomth1(); sendResult( json, "ok", out ); } else if ( param == 1 ) { doSomth2(); sendResult( json, "ok", out ); } else if ( param == 2 ) { doSomth3(); sendResult( json, "ok", out ); } else doSomth4(); sendResult( json, "false", out ); }
Вынесем константы.
final static String RESULT_OK = "ok"; final static String RESULT_FALT = "false"; .... if ( param == 0 ) { doSomth1(); sendResult( json, RESULT_OK, out ); } else if ( param == 1 ) { doSomth2(); sendResult( json, RESULT_OK, out ); } else if ( param == 2 ) { doSomth3(); sendResult( json, RESULT_OK, out ); } else doSomth4(); sendResult( json, RESULT_FALSE, out ); }
Код во втором примере не стал ни быстрее ни короче. Какие же преимущества дают константы:
- Исключается возможность ошибки в значении + современные IDE вам предложат функции автодополнения, как только вы начнёте набирать RESULT.
- Если завтра положительный ответ станет не "ok" а "good" либо он такой и был всегда, но вы неверно поняли протокол - правка станет элементарной.
- Современные IDE с помощью функционала рефакторинга позволят посмотреть все места в коде, где используется данное значение.
Кроме числовых значений вы также можете использовать Enum - перечисления.
Не замалчивайте ошибки и не искажайте их
С помощью подобных "гениальных конструкций":
try { doSomth(); } catch( Exception e ) {}
Программа не будет выполнять задуманное и будет молчать, пользователь не сможет вам ничего сообщить, логи ничего не покажут.
Либо ещё вариант:
try { doSomth(); } catch( Exception e ) { System.out.println( "Неверный логин" ); }
Здесь имеет место сведение всех возможных ошибок к одной. Логин будет многократно перепроверен, а причина останется не понятой. В крайнем случае оставьте хотя бы подобную обработку:
try { doSomth(); } catch( Exception e ) { e.printStackTrace(); }