Monday, November 29, 2010

Задача для архитектора (часть 2)

Хранение в БД

С таблицей рецептур в общем-то все понятно. Делаем таблицу Receipt с полям: ID (ключ), Title (название рецептуры), Description (краткое описание – заказчик не просил, но думаю не помешает), CreatedDate (дата создания, тоже пригодится).
А вот как хранить команды рецептуры это хороший вопрос. Команды разного типа. Общего у них: ID (ключ), ReceiptID (ID рецептуры), CommandType (тип команды). А различного у них – параметры команды. Для загрузки компонентов это ID компонента и вес в кг. Для паузы – величина задержки в сек. И т.д.
Вариант хранения всех команд в одной таблице мне не нравится – будет много пустых столбцов, а добавлять новые типы команд будет затруднительно. Вариант хранения каждого типа команд в отдельной таблице тоже не нравится – слишком сложно будет читать данные. Для такой просто задачи это перебор.
И тут срабатывает ограничитель лени J Он говорит, что задача слишком простая, база данных слишком локальная и вообще надо быть проще… Не могу с ней не согласиться.
Вариантов упрощения жизни у меня два. Первый – сделать отдельную таблицу команд ReceiptCommand со всеми перечисленными выше полями, но параметры хранить в столбце Params типа string в виде XML. Тогда все будет просто – все что нужно будет сделать, это прочитать данные из таблицы команд для нужного ReceiptID, создать нужные типы команд и отдать им их XML-параметры. А уже каждая команда для себя решит как ей понять свои параметры. Сохранение точно так же, только каждая команда сохраняет свои параметры в виде XML и мы записываем их в это поле.
Второй вариант еще проще, но с точки зрения архитектуры менее красивый – текстовый столбец Commands в таблице Receipt. Тип у него text. А хранить я в нем буду список команд в виде XML, т.е. даже не раскладывать команды по строкам. Никаких “лишних” таблиц, чтение и запись данных будет элементарная. Правда с преобразованием в список будет немного возни, но тут тоже не сложно – рецептура выбирается один раз при старте системы, т.е. достаточно редко, а список рецептур редактируется тоже редко и не требует постоянных преобразований. Так что все получится.
Важно! Этим примером я не призываю ломать типизацию, но хочу обратить внимание, что иногда условия задачи складываются так, что можно позволить себе упрощение архитектуры без ее ухудшения. Конечно, не нужно доводить эту идею до маразма – а то можно дойти до одной таблицы и одного поля, в которое нужно будет писать XML содержащий вообще все. Нет, конечно. Но конкретно для хранения параметров некоторых объектов, которые отличаются только этими параметрами – вполне можно.
В результате я выбрал второй вариант, совсем простой. Аргументы у меня были (если не считать лени) такие: при создании рецептуры и при чтении списка команд оба варианты примерно равноправны. А вот при редактировании будут различия. В первом варианте нужно будет либо удалять все строки, соответствующие ID рецептуры и потом сохранять их заново, либо, что значительно сложнее, пытаться вносить в них изменения. А если стирать и заносить заново, то какая разница между этим и вторым вариантом, когда я храню сразу XML со списком команд? Задачи получить нужную команду по ее номеру у меня нет - список команд не разделим на части и команда по отдельности смысла не имеет. Запросы "дай мне все рецептуры, где используется этот тип команды" тоже смысла не имеют. Получается что второй вариант проще. Но - только в данных конкретных условиях задачи.
Продолжение следует...

Saturday, November 27, 2010

Сообщайте информацию о коде с помощью исключений

В больших системах над кодом работают не один и часто даже не десять программистов, а значительно больше. То что знает про код один разработчик, должны знать все другие. Механизм исключений позволяет сообщить дополнительную информацию о способах использования кода и сократить время отладки.
Например, есть некий метод
public void DoAction(int param)
{
  ... некие действия...
}
Разработчик этого кода знает, что вызов этого метода с параметром -1 не допустим, т. к. он приведет к зацикливанию. Конечно, он может написать комментарий к этому методу, где он может указать эту особенность. Но где гарантия, что другой разработчик, который использует этот код, прочитает это сообщение? Вероятнее всего, в случае зацикливания, он будет отлаживать код, найдет причину зацикливания, найдет, что это происходит именно в этом методе и только потом прочитает комментарий. Время будет потеряно. А ведь ничего не стоило сделать так:
public void DoAction(int param)
{
  if (param == -1)
    throw new AgrumentException("Нельзя вызывать этот метод с -1");
  ... некие действия...
}
Теперь если кто-то попытается вызвать этот метод с параметром -1 он точно узнает, что делать этого нельзя. Две строчки кода и экономия нескольких часов времени!
Я рекомендую всегда проверять значения параметров всех public‑метдов. Это позволит избежать затрат времени ваших коллег. Вот еще несколько примеров таких проверок.
public void DoAction(object param)
{
  if (param == null)
    throw new AgrumentNullException("param");
  ... некие действия...
}
public void DoAction(string param)
{
  if  string.IsNullOrEmpty(param)
    throw new AgrumentNullException("param");
  ... некие действия...
}

Задача для архитектора (часть 1)

Система управления производством. Рецептура, по которой осуществляется управление, системой состоит последовательных загрузок компонентов. Рецептура хранится в БД, причем, эта база локальная, т.е. обращение к таблицам локальное.
Рецептура должна редактироваться пользователем, с рецептурой должно быть просто и удобно работать из кода. Как это сделать просто и быстро? Попробую описать по шагам решение.

Шаг 1. Где хранить данные?


Для хранения я выбрал MS SQL CE 3.5. Почему? Во-первых, она бесплатная. Во-вторых, хотя мне и сказали, что работать по сети это не должно, почему не заложиться на будущее – MS SQL CE совместим с полным MS SQL. Если вдруг что – не сложно будет перенести это в сетевой вариант. Причем MS SQL Express все еще бесплатна (ограничения в 4 Гб для такой маленькой системы явно хватит).

Шаг 2. Список команд


Пока я знаю только про одну команду – “загрузить xxx кг компонента xxx”. Но из опыта я подозреваю, что этим дело не закончится. Как минимум будет “пауза на xxx сек”, "подождать сигнал на канале xxx" и может еще что-то. Поэтому добавлю в систему немного гибкости – рецептура будет состоять из команд. А уже каждая команда будет определенного типа и со своими параметрами.
Тут главное не переборщить в гибкости. Если просили написать программу поиска корней квадратного уравнения, то нет нужны писать программу поиска корней уравнения n-й степени. А потом “ну только сконфигурить”… Типовая ошибка архитектора - ненужное усложнение системы. Но в моем случае лучше заложить немного гибкости в систему. Ровно настолько насколько нужно.
Теперь нужно придумать, как это хранить в таблицах и как загружать, сохранять, редактировать и работать с этими данными. Об этом в следующих частях.

Вторая часть

Thursday, November 25, 2010

XDocument и Double

Будьте внимательны - между двумя строчками кода

Weight = 22.44;
new XAttribute("weight", Weight.ToString())
new XAttribute("weight", Weight)


есть большая разница в случае русской культуры. В первом случае в XML будет записано 22,44 тогда как во втором 22.44 (через точку) и при попытке прочитать это значение будет ошибка.

Но ведь XML должен быть корректным не зависимо от используемой культуры (может быть он будет прочитан совсем на другом компьютере, чем записан), поэтому всегда используйте второй вариант, а при чтении устанавливайте культуру в en-US:

static CultureInfo cultureEnUS = new CultureInfo("en-US");

XAttribute weightAttr = element.Attribute("weight");
if (weightAttr != null)
{
    Weight = double.Parse(weightAttr.Value, cultureEnUS);
}

Wednesday, November 24, 2010

Почему XML

Нам нужно сохранить данные в файл. Нет ничего проще – взяли да и записали через запятую все данные, если их много – можно по-строчно. Проблем нет, пока… Пока не окажется что внутри данных тоже может быть запятая, а значит нужно данные ограничивать кавычками, как в формате CSV. Потом оказывается к данным нужно дописывать комментарии, типы данных, имена полей (иначе как те кому файл предназначен поймут где тут что?)… Да и хорошо бы сделать так, что новые форматы работали бы со старым кодом и наоборот, т.е. обновления не мешали друг другу…

Любое изменение формата записываемых данных ведет к изменению парсера, который их читает. Конечно, делать парсер под каждый случай  - это лишние затраты времени и сил. Да и лень просто…

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

Единый формат записи данных особенно актуален для Интернета, когда передающая и принимающая стороны могут и не знать о существовании друг друга.

Прочитать о формате XML можно в Википедии.
Что еще позволяет формат XML:
·         искать данные с помощью XPath, делать запросы как к базе данных
·         хранить иерархические и древовидные данные
·         проверять формат файла на соответствие его определенной схеме
·         преобразовывать XML в другие форматы
·         отображать данные в браузере, с помощью CSS-стилей
·         легко и просто редактировать данные с помощью XML-редакторов
·         встроенная поддержка XML в библиотеке .NET
·         возможность автоматического сохранения данных в XML формат с помощью сериализации
XML формат поддерживается почти везде –от баз данных до встроенных систем.

Минусы XML формата напрямую связаны с тем, что это текстовый формат:
·         Объем XML-файла существенно больше, чем просто бинарный файл.
·         Данные не типизированы, т.е. все записываются в текстовом виде.

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

В .NET для работы с XML используются классы XDocument (начиная с версии 4) или XmlDocument.

Tuesday, November 23, 2010

Подключение Dictionary к ComboBox

Простой способ подключить словарь к выпадающему списку без циклов и т.д. Все просто:

cbCommand.DataSource = new BindingSource(myDictionary);"Value";"Key";
cbCommand.DisplayMember =
cbCommand.ValueMember =

Замечу, что подключить его напрямую нельзя, т.к. Dictionary не реализует IList.

Monday, November 22, 2010

Свойства свойств

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

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

public class User
{
  private UserProfile profile;
  public UserProfile Profile
  {
    get
    {
      if (profile == null)
         profile = LoadProfile(userId);
      return profile;
    }
  }
}

Если ленивая инициализация по какой-то причине не годится, то стоит закрыть тайное знание соответствующим сообщением:

public UserProfile Profile
{
  get
  {
     if (profile == null)
       throw new ApplicationException("Call Init method before!");
      return profile;
  }
}
Вариант, когда свойство падает без указания причин, просто потому что не вызван метод Init, про который знает только разработчик класса, это плохой стиль.

Ссылочные свойства должны быть только для чтения (если, конечно, логикой программы не предусмотрено другое поведение, но такое бывает редко):

private ArrayList userList = new ArrayList();
public ArrayList UserList
{
  get
  {
    return userList;
  }
}

Сейчас предоставляется доступ только к содержимому списка, т.е. к его элементами. Если же здесь предоставить метод set, то вполне можно полностью поломать список, записав в него новое значение.

Методы или свойства

Проще всего применять такую концепцию: методы определяют действия, а свойства определяют данные. Желательно, чтобы при чтении свойств не происходило дополнительных действий, тем более “тяжеловесных”.

Не определяйте свойств “только для записи”. Если свойство извне класса должно только устанавливаться, то сделайте метод. Например, класс пользователей

public class User
{
  public Int64 Id {get; set;}
  public string Name {get; set;}
  public string Email {get; set;}
}

Пусть в базе данных есть еще поле IsDeleted, определяющее удалена запись или нет. В классе User эта запись нам, в общем-то, не нужна – удаленных пользователей мы просто не читаем из базы. В этом случае делать свойство смысла нет и правильный вариант – реализовать метод SetDeletedFlag(Id) или просто метод DeleteUser(Id).
Если же удаленные записи пользователей мы показываем, то класс будет такой:
public class User
{
  public Int64 Id {get; set;}
  public string Name {get; set;}
  public string Email {get; set;}
  public bool IsDeleted {get; set;}
}

Вопрос только как будет реализована логика удаления, да и вообще обновления записей пользователей. Возможно, это будет специальный класс UserDAL, работающий с такими записями и имеющий метод DeleteUser(User) или DeleteUser(Id). Но, в любом варианте, свойства всего лишь устанавливают соответствующие поля класса, но не выполняют действий.

Цитата

9 декабря 1708 года Пётр I выпустил указ о том, как надо относиться к начальству: «Подчинённый перед лицом начальствующим должен иметь вид лихой и придурковатый, дабы разумением своим не смущать начальства».

В чем разница между typeof и GetType

Разницу можно увидеть с помощью такого кода:

    class Base { }
    class Derived : Base { }
    class Program
    {
        static void Main()
        {
            ShowType( new Derived() );
        }
        static void ShowType( Base b )
        {
            Console.WriteLine(typeof(Base));
            Console.WriteLine(b.GetType());
        }
    }

Будет напечатано:

    Base
    Derived

Проверка символа

Если нужно проверить не равен ли символ каким-то конкретным значениям, то не пишите код:

       if  ((ch=='a') || (ch=='b') || (ch=='c')...)

Есть более простой и красивый путь сделать это:

       if ("abc".IndexOf(ch) > 0)

Получение имени файла из полного пути

Иногда в коде я вижу странные методы для получения имени файла из полного пути. Например так:

   string[] folders = fileUpload.FileName.Split(new char[] {'/', '\\'});
   fileName = folders[folders.Length - 1];

На самом деле все гораздо проще - можно просто написать
   Path.GetFileName(fileUpload.FileName).

Разбор URL

Для разбора URL на части использовать методы Replace, IndexOf и т.д. - не правильный подход. Например, удаление префикса:

url.Replace("http://", "") // так не правильно!

Почему? Как минимум, это не будет работать на https-протоколе.

Правильный путь - использовать класс UriBuilder:

namespace UriParse
{
      class Class1
      {
            [STAThread]
            static void Main(string[] args)
            {

                  UriBuilder parser = new UriBuilder("http://microsoft.com:80/default.aspx?id=55");
                  Console.WriteLine(parser.Host);    // microsoft.com
                  Console.WriteLine(parser.Scheme);  // http
                  Console.WriteLine(parser.Uri);     //
http://microsoft.com/default.aspx?id=55
                  Console.WriteLine(parser.Path);    // /default.aspx
                  Console.WriteLine(parser.Port);    // 80
                  Console.WriteLine(parser.Query);   // ?id=55
            }
      }
}

Saturday, November 20, 2010

Не распыляйте логику или информация в Enum

Для перечисления типов отчетов нам нужно было где-то сохранить название отчета и имя файла, хранящего его разметку.
public enum ReportTypes
{
    ComponentList = 1,
    ComponentAudit = 2,
}

Самый простой вариант – добавить специальные методы, возвращающие по типу соответствующую информацию. Внутри методов, разумеется, торчит switch:
public static string GetReportTitle(ReportTypes type)
{
    switch (type)
    {
        case ReportTypes.ComponentList:
            return "Список компонентов";
        case ReportTypes.ComponentAudit:
            return "Аудит компонентов";
    }

    return string.Empty;
}

public static string GetReportFileName(ReportTypes type)
{
    switch (type)
    {
        case ReportTypes.ComponentList:
            return "ComponentList.frx";
        case ReportTypes.ComponentAudit:
            return "ComponentAudit.frx";
    }

    return string.Empty;
}

Первая очевидная проблема в этом коде – если вдруг мы передадим неверный тип отчета, то код об этом не скажет ничего. Исправить это не сложно:
public static string GetReportTitle(ReportTypes type)
{
    switch (type)
    {
        case ReportTypes.ComponentList:
            return "Список компонентов";
        case ReportTypes.ComponentAudit:
            return "Аудит компонентов";
        default:
            throw new ArgumentException("ReportTypes");
    }
}

public static string GetReportFileName(ReportTypes type)
{
    switch (type)
    {
        case ReportTypes.ComponentList:
            return "ComponentList.frx";
        case ReportTypes.ComponentAudit:
            return "ComponentAudit.frx";
        default:
            throw new ArgumentException("ReportTypes");
    }
}

Стало получше, но логика раскидана в трех местах – собственно в перечислении и в двух методах.
Можно попробовать собрать информацию с помощью класса, возвращающего все поля сразу:
public class ReportInfo
{
    public string Title { get; set; }
    public string FileName { get; set; }
}

public static ReportInfo GetReportInfo(ReportTypes type)
{
...
}

Теперь логика раскидана всего в двух местах, но мне не нравится и это. При добавлении нового типа отчета в перечисление нужно вспоминать, где же лежит метод, возвращающий информацию.
В .NET если очень мощный механизм, позволяющий решить этот вопрос – атрибуты. Создаем атрибут, хранящий информацию об отчете:
    [AttributeUsage(AttributeTargets.Field, AllowMultiple = false)]
    public class ReportAttribute : Attribute
    {
        public ReportAttribute(string title, string fileName)
        {
            this.ReportTitle = title;
            this.ReportFile = fileName;
        }

        public string ReportTitle
        {
            get; protected set;
        }

        public string ReportFile
        {
            get; protected set;
        }
    }

Добавляем информацию прямо в перечисление:
public enum ReportTypes
{
    [ReportAttribute("Список компонентов", "ComponentList.frx")]
    ComponentList = 1,
    [ReportAttribute("Аудит компонентов", "ComponentAudit.frx")]
    ComponentAudit = 2,
}

Делаем класс, работающий с этими атрибутами:
public static class ReportTypesHelper
{
    private static Type type = typeof(ReportTypes);

    /// <summary>
    /// Атрибут отчета для конкретного типа отчета
    /// </summary>
    public static ReportAttribute GetReportAttribute(ReportTypes reportType)
    {
        FieldInfo propInfo = type.GetField(reportType.ToString());
        if (propInfo != null)
        {
            var attribs = propInfo.GetCustomAttributes(typeof(ReportAttribute), false) as ReportAttribute[];

            if (attribs != null && attribs.Length > 0)
            {
                return attribs[0];
            }
        }

        return null;
    }

    /// <summary>
    /// Список отчетов
    /// </summary>
    /// <returns></returns>
    public static IEnumerable<ReportAttribute> GetReportList()
    {
        var obj = Enum.GetValues(type);

        foreach (ReportTypes item in obj)
        {
            yield return GetReportAttribute(item);
        }
    }
}

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