Разработка

Материал из BiTel WiKi

Перейти к: навигация, поиск

Содержание

Цель и ценность ПО

Цель и ценность любой программы: выполнение каких-либо возложенных на неё функций. Мысль простая и очевидная не только для разработки ПО, но и для любого производства в условиях рыночной экономики, тем не менее почему-то порой игнорируемая.

Пользователя не интересует в конечном итоге стройность кода, наличие в нём комментариев, форматирования и т.п.

Интересует:

  1. функциональность, решение каких-либо необходимых пользователю задач
  2. удобство, простота, эстетичность
  3. надёжность
  4. 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 );
...

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

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

Не игнорируйте непонятное поведение

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

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

Не копируйте код

Возникающие при этом проблемы:

  1. Разрастание объёмов кода, ухудшение читаемости. Раз за разом вы будете читать одинаковые блоки.
  2. Усложнение правок и исправлений. Корректировку придётся выполнять в нескольких местах.

В идеале у вас не должно быть одинаковых блоков в коде. Это почти всегда неверно.

Небольшой пример.

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 );	
}

Код во втором примере не стал ни быстрее ни короче. Какие же преимущества дают константы:

  1. Исключается возможность ошибки в значении + современные IDE вам предложат функции автодополнения, как только вы начнёте набирать RESULT.
  2. Если завтра положительный ответ станет не "ok" а "good" либо он такой и был всегда, но вы неверно поняли протокол - правка станет элементарной.
  3. Современные 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 и было задействовано.

Используйте возможности языка

Приведённый ниже фрагмент Java кода использует C подобный стиль передачи сообщений об ошибках.

....  
  Result result = doSomth();
  if( !result.flagOk )
  {
     System.out.println( "Ошибка: " + result.value );
  }
  else
  {
     System.out.println( "Значение: " + result.value );
  } 
}
 
private static class Result
{
   // значение либо ошибка
   private String value;
   private boolean flagOk = false;
}   
 
private Result doSomth()
{
   Result result = new Result();
 
   if( .. )
   {
      result.value = "ERROR_1";
      return result;
   }
 
   result.value = "Value";
   result.flagOk = true;
 
   return result; 
}

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

....  
  try
  {
     System.out.println( "Значение: " + doSomth() ); 
  }
  catch( Exception ex )
  {
     System.out.println( "Ошибка: " + ex.getMessage() );
  }   
}
 
private String doSomth()
  throws Exception
{
   if( .. )
   {
      throw new Exception( "ERROR_1" );
   }
   return "Value";
}

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

Проверка внешних данных

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

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

int calendarId = form.getParamInt( "calendarId", 0 );
int from = form.getParamInt( "from", 0 );
int to = form.getParamInt( "to", 0 );
 
if( from == 0 || to == 0 )
{
   throw new BGException( "Выберите обе даты" );
}
 
if( from == to )
{
   throw new BGException( "Нельзя копировать самого в себя" );
}
 
new WorkTypeDAO( con ).copyWorkDaysCalendar( calendarId, from, to );
...

Если что-то может быть введено неверно или не введено вовсе - это случится.

Поддержка обратной совместимости

.. скоро будет ...

Конфигурации

.. скоро будет ...

Не стоит делать из языка религию

В тех или иных обстоятельствах вам придётся использовать разные языки, технологии, платформы. Язык - это всего лишь средство, применяемое для решения конкретной задачи. И обстоятельства могут сложиться так, что не совсем вам симпатичный язык будет оптимальным средством для решения очередной задачи. Например, JavaScript - де-факто стандарт разработки Web приложений. Можно ждать реализации C в браузере а можно смириться, понять то, что есть и расширить свои навыки. Разумеется, при прочих равных, удобнее группе разработчиков создавать проекты на сходных платформах и языках. Изучение нового тоже требует времени, причём оно никем не оплачивается и может быть оправдано лишь новыми свершениями.

Не допускайте догм

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

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

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

Мелкие нюансы

Использование особенностей поведения оптимизатора логических выражений

Пример кода на JSP, язык тут не важен:

${form.param.showEmptyParameters and empty contractParameter.getValue() or not empty contractParameter.getValue()}

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

Таким образом данное условие может быть переписано:

${form.param.showEmptyParameters or not empty contractParameter.getValue()}

Вообще говоря, именно для JSP эта конструкция работала бы без ошибок даже без оптимизатора, т.к. вычисление знаения от пустой переменной автоматически вернёт false. А для Java, например, эту особенность можно использовать в таких конструкциях:

if( value == null || value.before( BORDER ) )
{ ... }

Ну и аналогично:

if( value != null && value.after( BORDER ) )
{ ... }

Данные выражения не упадут в ошибку NPE, если value будет пустой ссылкой null.

Нецелые числа

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

float sum = 1222222.43f + 122222.47f;
System.out.println( sum );
 
DecimalFormat df = new DecimalFormat( "0.00" );
System.out.println( df.format( sum ) );

В выводе мы получим:

1344444.9
1344444,88

Проблема вызвана особенностью представления чисел с плавающей запятой. Число разбивается на мантиссу и порядок, причём и то и то в двоичном виде. Мантисса - двоичная дробь. Как известно, не всякая десятичная конечная дробь будет конечной же в двоичном представлении. Кроме того, там ограничена разрядность. Это в сумме вызывает проблему "пропавших копеек".

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

В первом выводе просто float числа (1344444.9) преобразование двоичного в десятичное число для распечатки происходит неизвестным мне образом. Вероятнее всего, там используется некое округление исходя из вида числа. Во втором выводе мы пытаемся округлить число до второго знака (вывод копеек с нулями) и получаем проблему.

Для решения следует использовать класс BigDecimal в котором хранить числа и проводить операции.

BigDecimal sum = new BigDecimal( "1222222.43" ).add( new BigDecimal( "122222.47" ) );
System.out.println( sum );

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

Аналогичные типы данных есть и в СУБД.

Для чего же нужен float и double? Для научных расчётов.

Личные инструменты