суббота, 12 сентября 2015 г.

Упрощаем код и пишем красиво

Работая в команде, мне часто приходится читать чужой код. И, периодически я вижу простые вещи написанные весьма громоздко и совершенно нечитаемо. Например, много повторяющихся блоков или, еще хуже, лишние операции. Такой код хорош, если ваша цель сделать так, что бы никто не разобрался, однако, в большинстве случаев, такая цель перед нами не стоит.
bool a = SomeCondition();
/* Как упростить следующий код? */
bool b;
if( a.ToString().Length == 5 )
{
   b = true;
} else
{
   b = false;
}
Этот небольшой отрывок я привел отчасти ради шутки, а отчасти для того, что бы показать о чем идет речь. Разумеется, я не думаю, что кто-то реально так пишет. Но, в реальности наши задачи несколько сложнее и, по мере поступления новых вводных, мы часто перестаем видеть знакомые шаблоны и, как следствие, начинаем писать в стиле приведенного выше шуточного примера.

Товары и услуги

Давайте рассмотрим менее очевидный и более приближенный к реальности пример. Заказчик попросил нас вывести на экран такую вот табличку.

Все данные хранятся в базе данных. Однако, там только цифры. Товары в базе имеют специальные коды, а расшифровку этих кодов заказчик дал в бумажном виде. Вы уточняете требования заказчика и получаете ответы в стиле стандартного заказчика:
  • Как часто будет меняться перечень товаров и услуг? Пока менять не планируют, но через несколько месяцев фирма будет расширяться, может добавиться еще пара-тройка таблиц, возможно добавятся строки к существующим.
  • Куда выводить таблички - десктопное приложение, веб-странички, принтер? Пока только десктоп, потом, возможно понадобится принтер.
  • Можно ли вносить изменения в базу данных? База данных уже работает с существующими системами - любые изменения будут перезабиты ближайшим слиянием данных.
Обсудив задачу с коллегами, вы разбили ее на два блока - визуальная часть и работа с базой данных. Ваш специалист по UI (user interface) тут же выдал вам интерфейс добавления таблиц:
public interface IDataViewer
{
   void ShowGrid( string caption, IEnumerable<RowInfo> rows );
}

public class RowInfo
{
   public string Title { get; set; }
   public string Count { get; set; }
   public string Comment { get; set; }
}
И пошел работать над реализацией для десктопа. В свою очередь, специалист по базам данных, разобравшись со структурой хранения, написал методы для работы с каждой таблицей:
public interface IDataRetriever
{
   int GetPurchases( string depotCode, int code, bool spentOnly );
   int GetServices( string gangKey, int accessLevel, int code, bool paidOnly );
}
А что за дополнительные параметры мы тут видим? Пока это константы, а в дальнейшем будут определяться при входе в программу. Хорошо, осталось совсем немного - соединить все это в вместе.

Решение "в лоб"

Первое, что приходит в голову, это самое что ни на есть простое решение - для каждой строки таблицы вызвать соответствующий метод, заполнить класс RowInfo, добавить его в список и, потом, весь список передать методу ShowGrid. Давайте посмотри, как это будет выглядеть:
public void SillyFillGrid( IDataRetriever retriever, IDataViewer viewer )
{
   var depotCode = GetDepotCode();
   var gangKey = GetGangKey();
   var accessLevel = GetAccessLevel();

   var rows = new List<RowInfo>();
   var row = new RowInfo();
   var count1 = 0;
   var count2 = 0;

   row.Title = "Молотки";
   count1 = retriever.GetPurchases( depotCode, 1, false );
   row.Count = count1 > 0 ? count1.ToString() : "--";
   count2 = count1 > 0 ? retriever.GetPurchases( depotCode, 1, true ) : 0;
   row.Comment = count2 > 0 ? string.Format( "из них использовано {0} шт", count2 ) : "";
   rows.Add( row );

   row.Title = "Гвозди";
   count1 = retriever.GetPurchases( depotCode, 2, false );
   row.Count = count1 > 0 ? count1.ToString() : "--";
   count2 = count1 > 0 ? retriever.GetPurchases( depotCode, 2, true ) : 0;
   row.Comment = count2 > 0 ? string.Format( "из них использовано {0} шт", count2 ) : "";
   rows.Add( row );

   row.Title = "Электрошокер бытовой";
   count1 = retriever.GetPurchases( depotCode, 3, false );
   row.Count = count1 > 0 ? count1.ToString() : "--";
   count2 = count1 > 0 ? retriever.GetPurchases( depotCode, 3, true ) : 0;
   row.Comment = count2 > 0 ? string.Format( "из них использовано {0} шт", count2 ) : "";
   rows.Add( row );

   row.Title = "Патроны к пистолету Макаров";
   count1 = retriever.GetPurchases( depotCode, 4, false );
   row.Count = count1 > 0 ? count1.ToString() : "--";
   count2 = count1 > 0 ? retriever.GetPurchases( depotCode, 4, true ) : 0;
   row.Comment = count2 > 0 ? string.Format( "из них использовано {0} шт", count2 ) : "";
   rows.Add( row );

   viewer.ShowGrid( "Закуплено товаров", rows );
}
Фууух! Одну табличку заполнили. Теперь еще нужно написать то же самое для второй. Однако, уже по первой таблице мы видим множество повторяющихся операций. А что, если завтра у некоторых товаров сменятся коды. К примеру электрошокер станет не 3-м, а 18-м или 11-м. Нам придется выискивать в дебрях кода нужные цифры, а там каждая в двух экземплярах, что дает вероятность ошибиться - заменить в одном месте и забыть про второе.

Упрощаем код

А что если вынести коды и названия в массив и "пробежать" по нему в цикле foreach.
public void AverageFillGrid( IDataRetriever retriever, IDataViewer viewer )
{
   var depotCode = GetDepotCode();
   var gangKey = GetGangKey();
   var accessLevel = GetAccessLevel();

   var pInfo = new[]{
      new{code = 1, title = "Молотки"},
      new{code = 2, title = "Гвозди"},
      new{code = 3, title = "Электрошокер бытовой"},
      new{code = 4, title = "Патроны к пистолету Макаров"},
   };
   var sInfo = new[]{
      new{code = 14, title = "Мелкие хулиганства"},
      new{code = 22, title = "Беседа с пристрастием"},
      new{code = 24, title = "Возврат долга"},
      new{code = 31, title = "Последний разговор"},
   };

   var rows = new List<RowInfo>();
   foreach( var p in pInfo )
   {
      var row = new RowInfo();
      row.Title = p.title;
      var count1 = retriever.GetPurchases( depotCode, p.code, false );
      row.Count = count1 > 0 ? count1.ToString() : "--";
      var count2 = count1 > 0 ? retriever.GetPurchases( depotCode, p.code, true ) : 0;
      row.Comment = count2 > 0 ? string.Format( "из них использовано {0} шт", count2 ) : "";
      rows.Add( row );
   }
   viewer.ShowGrid( "Закуплено товаров", rows );

   rows = new List<RowInfo>();
   foreach( var s in sInfo )
   {
      var row = new RowInfo();
      row.Title = s.title;
      var count1 = retriever.GetServices( gangKey, accessLevel, s.code, false );
      row.Count = count1 > 0 ? count1.ToString() : "--";
      var count2 = count1 > 0 ? retriever.GetServices( gangKey, accessLevel, s.code, true ) : 0;
      row.Comment = count2 > 0 ? string.Format( "из них оплачено {0}", count2 ) : "";
      rows.Add( row );
   }
   viewer.ShowGrid( "Оказано услуг", rows );
}
Как мы видим, в таком варианте примерно тот же объем кода покрывает уже две таблички. Кроме того, коды и названия представлены в читаемом виде и менять их будет гораздо проще.

Доводим до конца

Однако, это еще не предел. Блоки кода по заполнению первой и второй табличек очень похожи, хотя в них и есть некоторые различия. А что, если вынести общую часть в отдельный метод.
void FillGridHelper<T>( IDataViewer viewer, string caption, IEnumerable<T> data, Func<T, string> getTitle, Func<T, bool, int> getCount, string commentFormat )
{
   viewer.ShowGrid( caption, data.Select( d =>
   {
      var row = new RowInfo { Title = getTitle( d ) };
      var count1 = getCount( d, false );
      row.Count = count1 > 0 ? count1.ToString() : "--";
      var count2 = count1 > 0 ? getCount( d, true ) : 0;
      row.Comment = count2 > 0 ? string.Format( commentFormat, count2 ) : "";
      return row;
   } ) );
}
У нас получилась функция с достаточно простым и относительно компактным кодом. При этом, все различия мы вынесли в параметры функции. Давайте посмотрим, как преобразится наш конечный метод с использованием данной вспомогательной функции:
public void CleverFillGrid( IDataRetriever retriever, IDataViewer viewer )
{
   var depotCode = GetDepotCode();
   var gangKey = GetGangKey();
   var accessLevel = GetAccessLevel();

   var pInfo = new[]{
      new{code = 1, title = "Молотки"},
      new{code = 2, title = "Гвозди"},
      new{code = 3, title = "Электрошокер бытовой"},
      new{code = 4, title = "Патроны к пистолету Макаров"},
   };
   var sInfo = new[]{
      new{code = 14, title = "Мелкие хулиганства"},
      new{code = 22, title = "Беседа с пристрастием"},
      new{code = 24, title = "Возврат долга"},
      new{code = 31, title = "Последний разговор"},
   };

   FillGridHelper( viewer, "Закуплено товаров", pInfo, p => p.title, ( p, so ) => retriever.GetPurchases( depotCode, p.code, so ), "из них использовано {0} шт" );
   FillGridHelper( viewer, "Оказано услуг", sInfo, s => s.title, ( s, po ) => retriever.GetServices( gangKey, accessLevel, s.code, po ), "из них оплачено {0}" );
}
Уже значительно симпатичнее и компактнее. Или, может быть можно еще упростить? ;)

Комментариев нет:

Отправить комментарий