Разработка
Материал из BiTel WiKi
Содержание
|
Цель и ценность ПО
Цель и ценность любой программы: выполнение каких-либо возложенных на неё функций. Мысль простая и очевидная не только для разработки ПО, но и для любого производства в условиях рыночной экономики, тем не менее почему-то порой игнорируемая.
Пользователя не интересует в конечном итоге стройность кода, наличие в нём комментариев, форматирования и т.п.
Интересует:
- функциональность, решение каких-либо необходимых пользователю задач
- удобство, простота, эстетичность
- надёжность
- cроки и цена реализации
Следовательно, разработчик должен добиться увеличение первых трёх показателей, при минимизации последнего. Все остальные проистекает из данного базового постулата. Требования к оформлению, тестированию, документированию и пр. проистекают уже из него.
Посему необходимо всегда перед тем или иным действием понимать для себя, какой базовый параметр вы этим улучшаете. Важная оговорка: улучшение следуюет учитывать с расчётом на действительную перспективу эксплуатации продукта. Вполне возможно, что в данный конкретный момент скопировать код может показаться более простым, чем вынести в отдельную функцию, но по мере дальнейшего развития проекта избыточные временные затраты на корректировку кода в двух местах превысят эту мнимую выгоду. С другой стороны, если вы совершенно уверены, что данный скрипт будет вами запущен только один раз, после чего необходимость в нём отпадёт - то городить в нём стройные ряды комментариев, возможно и нет особой необходимости.
Отличия ПО
Программное обеспечение по сравнению с материальными товарами обладет некоторыми особенностями, рассмотрим их:
1. Копирование программного продукта ничего не стоит
Тиражируемый продукт позволяет значительно увеличить совокупную пользу его применения и, как следствие - возможность получения выгоды для производителя. При этом затраты производителя практически не увеличиваются, продукт так же хорошо (или плохо) работает в новых копиях. После занятия доли рынка пользователи зачастую становятся зависимыми от конкретного продукта в силу своих производственных связей с другими его экспуатантами (общие форматы файлов, например). Кроме того, своими запросами в сети они постоянно повышают рейтинг используемого решения. Переход на новое решение сопряжён с необходимостью переобучения. Эффект нарастает лавинообразно, поэтому в производстве ПО так часты случаи доминирование в той или иной нише одного-двух крупных продуктов.
2. Распространение программного продукта также ничего не стоит
Рынок производителя кирпича ограничен тем расстоянием, на которое кирпич целесообразнее привезти, чем купить местный. Т.е. кирпичный завод-гигант в России при всём желании не сможет заполонить всю страну своим товаром. Затраты на транспортировку будут расти с ростом удалённости клиентов. Для программного обеспечения это не актуально и продукт чаще всего ограничен не территорией а средой пригодной для использования. Например, языковой, либо законодательной. Так, бухгалтерская программа реализованная под россиийские стандарты бухучёта не применима в Европе или США. Зато программа для распознавания изображений - вполне. При должной локализации, конечно.
3. ПО не подвержено износу
С течением времени день за днём ваш продукт либо будет приносить пользу и радовать клиента либо раздражать одной и той же ошибкой. Плохой продукт - это гораздо хуже, нежели слегка кривой ботинок, срок жизни которого в любом случае не слишком велик.
Разработка ПО - это как игра с большими ставками. Чуть лучший продукт получает всё, чуть худший - ничего. И оттеснить конкурента после можно лишь став значительно лучше, переломив привычки пользователей и сложившиюся вокруг инфраструктуру.
Базовые принципы разработки
Далее попытаемся разложить некотороые основные принципы в разработке ПО с учётом указанных выше базовых ценностей. Не принимая их как догму.
Документируйте
Отсутствие документации, на написание которой уходит менее 10% времени от разработки, полностью обесценивает реализованный функционал для большинства клиентов. Возможно, вы его реализовали и запустили у одного из клиентов. Но никто больше не сможет им воспользоваться. Ценность продукта уменьшается пропорционально количеству людей, которые могли бы использовать данный функционал. Самое печальное, что через некоторое время вашу суперфичу может удалить другой разработчик, не поняв, что делает этот нигде не документированный блок кода. После чего функционал перестанет работать и у едниственного его пользователя.
Комментируйте, когда есть что
Добавленный в нужном месте комментарий может очень быстро уразуметь суть происходящего.
// сортировка, чтобы kernel.xml оказался первым Collections.sort( actionFiles, new Comparator<String>() { @Override public int compare( String o1, String o2 ) { if( o1.startsWith( "kernel" ) ) { return -1; } if( o2.startsWith( "kernel" ) ) { return 1; } return o1.compareTo( o2 ); } } );
Всё тут, конечно, субъективно, лично я в данном случае не стал бы добавлять комментарий.
for( String actionFile : actionFiles ) { // если имя файла оканчивается на xml if( actionFile.endsWith( ".xml" ) ) { permissionNodes.add( getPermissionTree( DIRECTORY + "/" + actionFile ) ); } }
В следующем примере комментарием отмечено место, которое следует исправить. Разработчик частично разобрался с проблемой и создал заметку на будущее.
//FIXME если сюда приходит незаполненное полностью что-то (в узле что-то не введено, например), то всё падает в NPE при инициализации дерева super.init(data, deep, nodeId);
Не допускайте "мертвого" кода
Мышление человека в процессе разработки построенно так, что все действия связанны причинно-следственным образом для реализации некой цели. И нет ничего более обескураживающего для не посвящённого, чем вызов некого блока:
... 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(); }
Используйте готовое
Если вы столкнулись с какой-то новой задачей или проблемой, почти наверняка кто-то сталкивался с ней ранее и вы сможете найти реализующее требуемое библиотеку либо подходящее решение в сети. Только воспользуйтесь англоязычным поиском этим вы многократно увеличите вероятность нахождения нужного.
Примеры из жизни1
В системе понадобилась возможность гибкой конфигурации неких условий с помощью математических выражений. Можно попробовать написать парсер разбирающий хитросплетения плюсов и минусов. А можно взять готовый. Например, JEXL для Java. И получить на перспективу массу дополнительных возможностей.
Примеры из жизни2
Требуется система сборки пакетов. Можно попробовать изобразить её на Shell скриптах, либо поискать и найти гору специализированных решений, одно из которых Apache ANT и было задействовано.
Не допускайте канонов
В том числе и не принимайте как канон всё указанное выше. Если рекомендации целесообразны для вас - используйте их. Если нет - игнорируйте. Желательно их просто знать.
Область разработки ПО полна канонов. Следует всегда понимать, чем то ли иное будет полезно для вас кроме "идеологической правильности", "концептуальной целостности" и т.п. Т.е. понять предлагаемое решение и оценить возможные плюсы.
Если задача решена, код легко понимается вами и окружающими и не сулит проблем в дальнейшем - всё отлично.