Шаблон проектирования Единицы измерения

/ Просмотров: 569

Сегодня я попытаюсь преподнести читателям решение одной задачи, которую мне довелось воплощать. Я разрабатывал программу на C#, в которой нужно было реализовать расчет параметров воздухонагревателя Калугина с использованием разных единиц измерения, которые выбирает пользователь. Как мне представляется, способ, который я выбрал для решения задачи, весьма неплох. К тому же, в сети я не видел других опубликованных методов (ткните носом, если ошибаюсь). Поэтому я решил опубликовать своё решение и оформить его в виде шаблона проектирования. В статье рассмотрен пример реализации шаблона на C#, но при желании можно портировать его и на другие языки.

Название: Единицы измерения.

Альтернативное название: нет.

Когда следует применять: когда требуется реализовать расчёт с использованием различных единиц измерения.

В основе шаблона лежат два интерфейса: IUnit и IDimension. Интерфейс IDimension описывает способ управления измеряемой величиной, а IUnit представляет абстрактную единицу измерения. При переводе используется базовая единица измерения (например, градус Цельсия) и вспомогательные. В частях программы, не имеющих отношения к данному шаблону, расчет ведётся в базовой единице измерения. На пользовательском интерфейсе может осуществляться ввод в любой единице измерения. После ввода цифры должны преобразовываться в базовую единицу измерения. Далее следует выполнять расчет, после которого можно перевести результат из базовой единицы в пользовательскую и вывести на графический интерфейс. Диаграмма шаблона представлена на рисунке ниже.

/uploads/_pages/21/uml_png_19227.png

Рассмотрим интерфейс IUnit. Метод ThisToBase должен принимать единственный числовой параметр и возвращать числовое значение, переведённое в базовую единицу измерения. Метод BaseToThis должен принимать единственный числовой параметр и возвращать числовое значение, переведённое из базовой единицы в ту, которая соответствует реализации интерфейса. Строковое поле Symbol должно содержать обозначение данной единицы измерения. Например: Па, мм.рт.ст, кГц и др.

Теперь рассмотрим интерфейс IDimension, который представляет измеряемую величину. Поле Units должно содержать массив (список, IEnumerable) элементов типа IUnit. В него должны помещаться все единицы измерения, относящиеся к данной величине. Например, если реализовываем температуру, то можно поместить в этот массив градусы Цельсия, Кельвина и Фаренгейта. Поле Selected типа IUnit должно содержать рабочую единицу измерения (то есть пользовательскую, а не базовую, хотя они могут совпадать). В этой единице значения будут читаться либо выводиться на пользовательский интерфейс. Метод ToBase должен переводить переданное значение в пользовательской единице в базовую. Для этого лучше использовать метод Selected.ThisToBase. Метод ToSelected должен переводить значение, переданное в базовой единице измерения, в пользовательскую. Событие OnUpdate генерируется, когда изменилось значение свойства Selected. Например, пользователь выбрал новую единицу измерения давления, программа изменила свойство Selected, и в обработчике события выполнила перерасчет.

Теперь рассмотрим программную реализацию шаблона на C#. Интерфейсы выглядят следующим образом:

public interface IUnit
{
    double ThisToBase(double value);
    double BaseToThis(double value);
    string Symbol { get; }
}

public interface IDimension
{
    List<IUnit> Units {get;}
    IUnit Selected { get; set; }
    double ToBase(double value);
    double ToSelected(double value);
    string SelectedSymbol { get ;}
    event EventHandler OnUpdate;
}

Теперь реализуем эти интерфейсы на примере температуры. Для этого создадим классы Celsius, Kelvin, Farenheit. В качестве базовой единицы измерения выбран градус Цельсия.

public struct Celsius : IUnit, IFormattable 
{ 
    public double ThisToBase(double value) 
    {
        return value; //Преобразование не требуется, так как это и есть базовая единица 
    }

    public double BaseToThis(double value)
    {
        return value;
    }

    public override string ToString()
    {
        //Так удобно выводить, скажем, в ComboBox
        return "Градус Цельсия";
    }

    public string ToString(string format, IFormatProvider formatProvider)
    {
        return ToString();
    }

    public string Symbol
    {
        get
        {
            return "°C";
        }
    }
}

public struct Kelvin : IUnit, IFormattable
{
    public double ThisToBase(double value)
    {
        //Переводим в базовую единицу
        return Temperature.KelvinToCelsius(value);
    }

    public double BaseToThis(double value)
    {
        //Переводим в градусы Кельвина
        return Temperature.CelsiusToKelvin(value);
    }

    public override string ToString()
    {
        return "Градус Кельвина";
    }

    public string ToString(string format, IFormatProvider formatProvider)
    {
        return ToString();
    }

    public string Symbol
    {
        get
        {
            return "°K";
        }
    }
}

public struct Farenheit : IUnit, IFormattable
{
    public double ThisToBase(double value)
    {
        return Temperature.FarenheitToCelsius(value);
    }

    public double BaseToThis(double value)
    {
        return Temperature.CelsiusToFarenheit(value);
    }

    public override string ToString()
    {
        return "Градус Фаренгейта";
    }

    public string ToString(string format, IFormatProvider formatProvider)
    {
        return ToString();
    }

    public string Symbol
    {
        get
        {
            return "°F";
        }
    }
}

Теперь определим измеряемую величину "Температура". Для этого реализуем интерфейс IDimension. В своей программе я использовал WPF, поэтому в классе реализуются не только поля и методы IDimension, но и DependencyProperty. Также реализуется интерфейс IEnumerable для упрощённого доступа к единицам измерения.

public class TemperatureDimension : DependencyObject, IDimension, IEnumerable, IFormattable
{
    private List<IUnit> _Units;

    //Сюда складываются все единицы измерения данной величины. Это поле можно сделать источником данных для ComboBox. При этом элементом Item будет являться IUnit, а на экран будет выводиться результат выполнения функции ToString(), которая переписана в классах Celsius, Kelvin, Farenheit.
    public List<IUnit> Units
    {
        get
        {
            return _Units;
        }
    }

    //С помощью этого свойства удобно взаимодействовать с пользовательским интерфейсом. При выборе новой единицы измерения её обозначение будет автоматически обновлено на всех элементах пользовательского интерфейса
    //Это свойство является дополнительным и не имеет прямого отношения к шаблону, но показывает особенности его применения.
    public static readonly DependencyProperty SelectedSymbolProperty = DependencyProperty.Register("SelectedSymbol", typeof(string), typeof(TemperatureDimension), new PropertyMetadata(""));

    private IUnit _Selected;

    //Пользовательская единица измерения
    public IUnit Selected
    {
        get
        {
            return _Selected;
        }
        set
        {
            _Selected = value;
            SetValue(SelectedSymbolProperty, SelectedSymbol);
            if (OnUpdate != null) OnUpdate(this, new EventArgs());
        }
    }

    public event EventHandler OnUpdate;

    public string SelectedSymbol
    {
        get
        {
            return Selected.Symbol;
        }
    }

    //Здесь осуществляется перевод выбранной единицы измерения в базовую
    public double ToBase(double value)
    {
        return Selected.ThisToBase(value);
    }

    //Перевод базовой единицы измерения в выбранную
    public double ToSelected(double value)
    {
        return Selected.BaseToThis(value);
    }

    public TemperatureDimension()
    {
        //Создаём все единицы измерения данной величины
        _Units = new List{
            new Celsius(),
            new Kelvin(),
            new Farenheit()
        };
        Selected = _Units[0];
    }

    //Это тоже дополнительный код, не имеющий прямого отношения к шаблону проектирования.
    public IEnumerator GetEnumerator()
    {
        return _Units.GetEnumerator();
    }

    System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
    {
        return GetEnumerator();
    }

    public override string ToString()
    {
        return Selected.ToString();
    }

    public string ToString(string format, IFormatProvider formatProvider)
    {
        return ToString();
    }
}

Как я уже упоминал, в проекте, где этот шаблон разрабатывался и применялся, я использовал WPF. Это даёт дополнительное удобство в использовании данного шаблона: можно обеспечить преобразование единиц измерения очень прозрачно и незаметно. Для этого следует лишь реализовать IValueConverter, который будет преобразовывать цифры из полей пользовательского интерфейса в базовую единицу и передавать их в DependencyProperty.

//Этот код не имеет прямого отношения к шаблону, а показывает способ его применения
//Этот класс является прослойкой между пользовательским интерфейсом и системой хранения данных. Элемент пользовательского интерфейса связан с помощью Binding с каким-то DependencyProperty. При передаче значения из пользовательского интерфейса в структуру данных вызывается функция ConvertBack, при передача из структур данных в пользовательский интерфейс вызывается функция Convert. Свойство Dimension содержит информацию о измеряемой величине и единице измерения.
[ValueConversion(typeof(string), typeof(string))]
public class UnitsConverter : DependencyObject, IValueConverter
{
    public static readonly DependencyProperty DimensionProperty = DependencyProperty.Register("Dimension", typeof(IDimension), typeof(UnitsConverter));
    public IDimension Dimension
    {
        get { return (IDimension)GetValue(DimensionProperty);}
        set { SetValue(DimensionProperty, value);}
    }
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        return Dimension.ToSelected(System.Convert.ToDouble(value));
    }

    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        return Dimension.ToBase(System.Convert.ToDouble(value)).ToString(Thread.CurrentThread.CurrentCulture);
    }
}

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

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

Комментарий будет опубликован после проверки

Вы можете войти под своим логином или зарегистрироваться на сайте.

(обязательно)