Класс-обертка для зажима минимального/максимального значения и валидации

Класс-обертка для зажима минимального/максимального значения и валидации
Класс-обертка для зажима минимального/максимального значения и валидации - houseklaus @ Unsplash

У меня есть много классов, которые имеют числовые значения, которые настраиваются клиентом во время выполнения и должны быть зажаты между минимальным и максимальным значением. Изменения значений также должны регистрироваться. Пример этого приведен ниже:

public class MyClass 
{
    public double AMin { get; set; }
    public double AMax { get; set; }
    public double A { get; private set; }
    public double BMin { get; set; }
    public double BMax { get; set; }
    public double B { get; private set; }
    public double CMin { get; set; }
    public double CMax { get; set; }
    public double C { get; private set; }

    public bool SetA(double value) 
    {
        double clamped = Clamp(value, AMin, AMax);
        if (clamped != value) 
        {
            LogError("New value: {value} for A is out of range.");
            return false;
        }
        else 
        {
            LogError("Set A to: {value}.");
            return true;
        }
    }

    public bool SetB(double value) 
    {
        double clamped = Clamp(value, BMin, BMax);
        if (clamped != value) 
        {
            LogError("New value: {value} for B is out of range.");
            return false;
        }
        else 
        {
            LogError("Set B to: {value}.");
            return true;
        }
    }

    public bool SetC(double value) 
    {
        double clamped = Clamp(value, CMin, CMax);
        if (clamped != value) 
        {
            LogError("New value: {value} for C is out of range.");
            return false;
        }
        else 
        {
            LogError("Set C to: {value}.");
            return true;
        }
    }

    public static double Clamp(double value, double min, double max) 
    {
        if (value <= min) 
        {
            _value = min;
            return;
        }

        if (value >= max) 
        {
            _value = max;
            return;
        }

        _value = value;
    }
}

Поскольку количество значений растет, вы можете видеть, что это заставляет меня писать шаблонный код и легко делать ошибки копирования/вставки. Я мог бы сделать для этого сниппет в Visual Studio, но мне интересно, будет ли хорошим решением, если я рефакторингую это повторение в класс, чтобы он выглядел как показано ниже:

public class MyClass 
{
    public Parameter A { get; init; } = new Parameter("A");
    public Parameter B { get; init; } = new Parameter("B");
    public Parameter C { get; init; } = new Parameter("C"); 
    ...
}

public class Parameter 
{
    public string Name { get; set; } = "";

    public double Min { get; set; } = double.MinValue;
    public double Max { get; set; } = double.MaxValue;

    public double Value { get => _value; }

    private double _value = 0.0;

    public Parameter(string name) 
    {
        Name = name;
    }

    public double GetValue() 
    {
        return _value;
    }

    // returns true on success
    public bool SetValue(double value) 
    {
        if (value <= Min) 
        {
            _value = Min;
            Log.Error($"New value: {value} for {Name} too low.");
            return false;
        }

        if (value >= Max) 
        {
            _value = Max;
            Log.Error($"New value: {value} for {Name} too high.");
            return false;
        }

        Log.Info($"Set {Name} to: {value}.");

        _value = value;
    }    
}

Хотя это заставит меня постоянно писать MyInstance.MyParameter.Value вместо MyInstance.MyParameter, и это "нарушает" закон Деметры, я думаю, что в конечном итоге это сэкономит мне много времени, и я думаю, что это будет более удобным в обслуживании.

Это обычная практика, чтобы делать что-то подобное, или есть большой недостаток, который я пока не вижу?

Я работаю в области промышленной автоматизации, а точнее с ПЛК, поэтому валидация с использованием исключений для меня не вариант. У меня есть опыт работы с C#, поэтому пример приведен на этом языке.Side-note:

Возможность повторного использования

По мере роста количества значений, вы можете увидеть, что это заставляет меня писать шаблонный код и легко делать ошибки копирования/вставки. Я мог бы сделать для этого сниппет в Visual Studio, но мне интересно, будет ли хорошим решением, если я рефакторую это повторение в класс,

.

на 100% правильное решение. Повторение должно быть преобразовано в возможность повторного использования.


Закон Деметры?

Хотя это заставило бы меня постоянно писать MyInstance.MyParameter.Value вместо MyInstance.MyParameter, и это "нарушает" закон Деметры

Просто для ясности, потому что я подозреваю, что это лежит в основе вашего вывода, закон Деметры - это , а не упражнение по подсчету точек .

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

MyInstance.MyParameter.SetValue() имеет наибольший смысл в данном случае именно потому, что раскрывает зажатый характер значения и позволяет потребителю получить булево значение, чтобы указать, произошло ли зажатие или нет.

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

public static implicit operator double(Parameter p)  
{  
   return p.Value;  
}

Это позволит вам делать такие вещи, как:

var myInstance = new MyClass();
bool wasClamped = myInstance.A.SetValue(1.23);
double myValue = myInstance.A;

Обратите внимание, что в третьей строке вы можете пропустить шаг .Value благодаря неявному преобразованию, но вы не можете избежать вызова .SetValue() во второй строке, поскольку он нужен для получения булева значения.

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


Обзор кода в краткой форме

Это не относится к вашему вопросу, но в любом случае кажется полезным упомянуть:

public double GetValue() 
{
    return _value;
}

Этот метод можно удалить, поскольку Value служит той же цели.

public double Value { get => _value; }

private double _value = 0.0;

Это можно сжать в public double Value { get; private set }. Вам не нужно явно устанавливать его на 0.0, так как это значение по умолчанию в любом случае.

public Parameter A { get; init; } = new Parameter("A");

Возможно, было бы лучше использовать nameof(A) в конструкторе, чтобы сделать код более удобным для рефакторинга.

Более того, я подозреваю, что вам не нужен init здесь, потому что вы действительно хотите, чтобы потребитель мог переопределить экземпляр параметра?

Кроме того, мне кажется странным, что вы делаете свои границы min/max публично устанавливаемыми. Я инстинктивно ожидал, что они будут принудительно устанавливаться в конструкторе и затем сохраняться неизменными, специально для того, чтобы ваш родитель MyClass мог настроить параметр и сохранить его таким, а потребители MyClass не могли изменить эту конфигурацию.

Это также влияет на значение по умолчанию: что, если значение по умолчанию 0.0 не находится в определенном диапазоне? Это конкретная проблема для вашего текущего кода, поскольку я могу установить значение, а затем изменить границы. Я не думаю, что это желательное поведение.

Я бы также склонялся к тому, чтобы позволить конструктору опционально передавать начальное значение.

Еще один момент: этот Parameter класс может очень явно выиграть от того, чтобы быть общим, как способ максимизировать повторное использование.

Принимая во внимание все это, я бы больше склонялся к тому, чтобы сделать его неизменяемым:

public class MyClass 
{
    // Initial value unspecified, will try to be 0 but within clamping rules
    public Parameter<double> A { get; } = new Parameter<double>(nameof(A), 1, 10);

    // Initial value explicitly defined, but will still be clamped!
    public Parameter<double> B { get; } = new Parameter<double>(nameof(B), 1, 10, 5);
}

public class Parameter<T>
{
    public string Name { get; }
    public T Min { get; }
    public T Max { get; }

    public Parameter(string name, T min, T max, T initialValue)
    {
        this.Name = name;
        this.Min = min;
        this.Max = max;

        this.SetValue(initialValue);
    }

    public Parameter(string name, T min, T max)
      : this(name, min, max, default) 
    {}

    // ...
}

LetsCodeIt, 17 февраля 2023 г., 11:48