Showing posts with label Архитектура. Show all posts
Showing posts with label Архитектура. Show all posts

Saturday, February 26, 2011

Подклассы или типы

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

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

public Color Foreground()
{
   swith (type)
   {
     case StoneType.One: return Colors.Red;
   ............
}

Либо вообще вынести эти параметры в конструктор и типы не заводить:

public Stone(Color foreground, int countStrike)
{
  .....
}

А вот если отличий будет очень много, то класс будет замусорен кучей if или switch. Вот тогда имеет смысл делать подклассы и разносить логику и код. Разумеется, если какой-то из "типов" имеет дополнительные свойства, то это однозначно нужно выностить в подкласс.

Wednesday, December 1, 2010

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

С классами теперь все просто.
Receiptкласс рецептуры. Атрибуты над полями нужны для загрузки данных из БД, про это как-нибудь в другой раз.
public class Receipt : Entity
{
    /// <summary>
    /// Дата создания
    /// </summary>
    [FieldName("CreatedDate")]
    public DateTime CreatedDate { get; set; }

    /// <summary>
    /// Название
    /// </summary>
    [FieldName("Title")]
    public string Title { get; set; }

    /// <summary>
    /// Описание
    /// </summary>
    [FieldName("Description")]
    public string Description { get; set; }

    /// <summary>
    /// XML описание команд
    /// </summary>
    [FieldName("Commands")]
    public string Commands { get; set; }
}

Для команд я ввел перечисление, хранящее типы команд:
public enum ReceiptCommandType
{
    [ViewableEnumMember("Загрузка компонента")]
    Loading = 1,
}

Почему описание вынесено в атрибуты я уже писал.
Теперь надо разобраться с классом команды. Очевидно, что нужно сделать базовый класс для команд:
public abstract class BaseReceiptCommand
{
    private ReceiptCommandType commandType;
    private string commandTypeName;

    public BaseReceiptCommand(ReceiptCommandType commandType, XElement element)
    {
        Init(commandType);

        if (element != null)
            LoadFromXML(element);
    }

    public BaseReceiptCommand(ReceiptCommandType commandType) :
        this(commandType, null)
    {
    }

    public abstract void LoadFromXML(XElement element);

    private void Init(ReceiptCommandType commandType)
    {
        this.commandType = commandType;
        this.commandTypeName = EnumHelper<ReceiptCommandType>.GetName(commandType);
    }

    public ReceiptCommandType CommandType
    {
        get
        {
            return commandType;
        }
    }

    public string CommandTypeName
    {
        get
        {
            return commandTypeName;
        }
    }

    public virtual XElement ToXml()
    {
        return new XElement("command", new XAttribute("type", CommandType));
    }

    public static ReceiptCommandType GetTypeFromXML(XElement element)
    {
        return (ReceiptCommandType)Enum.Parse(typeof(ReceiptCommandType), element.Attribute("type").Value);
    }

    public override string ToString()
    {
        return "Команда";
    }

    public abstract string ToFullString();
}

По идее, тип команды определяется самим классом команды и нужды в отдельном поле нет, но мне не хотелось завязывать имена классов на типы команд именно таким образом, поэтому я добавил поле типа в сам класс команды. Хотя, конечно, связать их нужно и я сделал это с помощью фабрики:
public static class ReceiptCommandFactory
{
    public static BaseReceiptCommand GetReceiptCommand(ReceiptCommandType commandType)
    {
        return GetReceiptCommand(commandType, null);
    }

    public static BaseReceiptCommand GetReceiptCommand(ReceiptCommandType commandType, XElement element)
    {
        switch (commandType)
        {
            case ReceiptCommandType.Loading:
                return new LoadReceiptCommand(element);
            default:
                throw new ApplicationException("ReceiptCommandFactory::GetReceiptCommand");
        }
    }

}

Конструкторов у класса команды получилось два – один создает пустую (новую) команду, второй – загружает ее из XML. Обратите внимание на абстрактный метод LoadFromXML. Он заставляет все классы, наследующие от базового, реализовывать собственный метод загрузки параметров команды (об этом я потом расскажу отдельно). Метод абстрактный, т.к. каждая команда сама решает, как ей загрузить себя из XML.
Кроме поля типа команды мне потребовалось поле имени (названия) этой команды. Это легко было сделать, получив атрибут соответствующей команды. Поменять тип команды на лету нельзя, поэтому вполне достаточно прочитать ее имя в конструкторе.
Ну и еще, при чтении XML мне потребовалось заранее знать тип команды, поэтому нужен был отдельный метод для чтения типа.
Вот как выглядит чтение и сохранение команд в XML:
public static class ReceiptXMLHelper
{
    /// <summary>
    /// Сохранение списка команд в XML
    /// </summary>
    public static XDocument ReceiptCommandListToXml(IList commandList)
    {
        XDocument document = new XDocument();
        XElement root = new XElement("root");
        document.Add(root);

        foreach (BaseReceiptCommand command in commandList)
        {
            root.Add(command.ToXml());
        }

        return document;
    }

    /// <summary>
    /// Получение списка команд
    /// </summary>
    public static List<BaseReceiptCommand> GetReceiptCommandList(Receipt receipt)
    {
        List<BaseReceiptCommand> result = new List<BaseReceiptCommand>();

        if (receipt != null)
        {
            if (!string.IsNullOrEmpty(receipt.Commands))
            {
                XDocument document = XDocument.Parse(receipt.Commands);
                return GetReceiptCommandListFromXml(document);
            }
        }

        return result;
    }

    /// <summary>
    /// Восстановление списка команд из XML
    /// </summary>
    public static List<BaseReceiptCommand> GetReceiptCommandListFromXml(XDocument document)
    {
        List<BaseReceiptCommand> result = new List<BaseReceiptCommand>();
           
        foreach (var command in document.Root.Elements())
        {
            ReceiptCommandType type = BaseReceiptCommand.GetTypeFromXML(command);
            result.Add(ReceiptCommandFactory.GetReceiptCommand(type, command));
        }

        return result;
    }
}

Ничего сложного тут нет. При сохранении в XML мы бежим по списку команд и каждой команде “говорим” сохранить себя в XML. При чтении – обратная операция, только сначала нам нужно определиться с типом создаваемой команды, запросить этот тип у фабрики и затем передать команде XML, чтобы она прочитала из него свои параметры.
А вот как выглядит команда "загрузка компонента":
public class LoadReceiptCommand : BaseReceiptCommand
{
    static CultureInfo cultureEnUS = new CultureInfo("en-US");

    public LoadReceiptCommand() : base (ReceiptCommandType.Loading)
    {
    }

    public LoadReceiptCommand(XElement element)
        : base(ReceiptCommandType.Loading, element)
    {
    }

    public Int64 ComponentId
    {
        get
        {
            if (componentRecord != null)
                return componentRecord.Id;
            else
                return -1;
        }
        set
        {
            if ((componentRecord == null) || (componentRecord.Id != value))
            {
                componentRecord = MainBLL.Instance.GetComponent(value);
            }
        }
    }
       
    public double Weight
    {
        get; set;
    }

    private Entities.Component componentRecord = null;

    public string ComponentName
    {
        get
        {
            if (componentRecord == null)
                return "(не задан)";
            return componentRecord.ComponentName;
        }
    }

    public override void LoadFromXML(XElement element)
    {
        ComponentId = -1;

        XAttribute componentAttr = element.Attribute("componentId");
        if (componentAttr != null)
        {
            ComponentId = Int64.Parse(componentAttr.Value);
        }

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

    public override XElement ToXml()
    {
        XElement result = base.ToXml();
        result.Add(new XAttribute("componentId", ComponentId));
        result.Add(new XAttribute("weight", Weight));
        return result;
    }

    public override string ToString()
    {
        return "Загрузка компонента " + ComponentName;
    }

    public override string ToFullString()
    {
        return string.Format("Загрузка [{0}]: {1} кг", ComponentName, Weight);
    }

}

Команда загрузки компонента имеет два дополнительных поля: ComponentId (Id загружаемого компонента), Weight (сколько нужно загружать). Причем, первое поле на самом деле загружает собственно запись компонента, полученную из таблицы компонентов. При отображении нам потребуется не Id, а поле ComponentName, возвращающее название компонента.
Сохранение и чтение из XML просто записывает и читает все нужные поля из XML.
В следующей части я расскажу, как просто и быстро сделать редактирование списка команд.

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

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

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

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


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

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


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

Вторая часть