Абасс... обсудите рахитекурту
Драма в действиях - с прологом, эпилогом и интермедиями. Задача выделена жирным.
Всё просто - в классе есть свойства CurrentValue и MaxValue - они отвечают соответственно за два значения одного параметра. Сначала для простоты я написал так
public double CurrentValue { get; set; }
public double MaxValue { get; set; }
Теперь добавим условие для CurrentValue - оно не должно быть больше MaxValue, но не меньше 0. Для этого добавлю валидацию для CurrentValue, расписав его геттер и сеттер:
double currentValue;
public double CurrentValue
{
get => currentValue;
// если value вне диапазона, то вернуть ту или другую границу диапазона - смотря в какую сторону выходит
set => currentValue = Math.Clamp(value, 0, MaxValue);
}
Далее усложняем. Хоть MaxValue и называется "максимальным", но оно тоже может изменяться, только менее часто, чем CurrentValue. Т.е. каждый раз, когда меняется MaxValue, CurrentValue должно быть пересчитано. Пришлось отказаться от автосвойств и расписать геттер и сеттер и для MaxValue:
double maxValue;
public double MaxValue
{
get => maxValue;
set
{
maxValue = value;
CurrentValue = CurrentValue; // чтобы принудительно вызвать валидацию CurrentValue
}
}
Т.к. maxValue уже установлено, то при валидации CurrentValue внутри сеттера MaxValue в этом месте, геттер MaxValue будет возвращать уже новое значение maxValue - т.е. тут всё законно.
Далее ещё усложняем. У меня CurrentValue и MaxValue на самом деле находятся в двух разных классах - один потомок другого. Один класс имеет лишь просто значения CurrentValue, а другой - добавляет MaxValue. Это потому, что есть параметры, которые меняются часто между минимумом и максимумом, а есть, которые просто меняются, и при этом нечасто. Где просто меняются, достаточно одного значения параметра - я назвал его не текущим, а максимальным (MaxValue ), а где часто - там нужно ещё и текущий - CurrentValue. Т.е. пока я просто разнёс их в разные классы:
public class Param
{
double maxValue;
public double MaxValue
{
get => maxValue;
set
{
maxValue = value;
CurrentValue = CurrentValue;
}
}
}
public class ParamVolatile : Param
{
double currentValue;
public double CurrentValue
{
get => currentValue;
set => currentValue = Math.Clamp(value, 0, MaxValue);
}
}
Тут загвоздка - класс Param не знает о CurrentValue из ParamVolatile. На помощь пришло переопределение свойства - мы оставим MaxValue в классе Param девственно чистым, не знающим о существовании всяких ветренных и непостоянных ParamVolatile, а ParamVolatile будет иметь своё собственное "видение" для MaxValue - с блекджеком и так далее:
public class Param
{
double maxValue;
public virtual double MaxValue
{
get => maxValue;
set => maxValue = value;
}
}
public class ParamVolatile : Param
{
double maxValue;
public override double MaxValue
{
get => maxValue;
set
{
maxValue = value;
CurrentValue = CurrentValue;
}
}
double currentValue;
public double CurrentValue
{
get => currentValue;
set => currentValue = Math.Clamp(value, 0, MaxValue);
}
}
Но зачем нам тогда наследование, если у нас ParamVolatile имеет своё собственное своство MaxValue? А это для заморочек с приведениями типов. Надо, короче. Но всё же мы можем кое-что упростить. Например, зачем нам два приватных поля maxValue в двух разных классах? Это же по сути два разных значения, никак не связанных. А нам по идее нужно лишь одно. Давайте уберём это поле из класса ParamVolatile и заставим использовать этот класс поле из Param - через свойство MaxValue класса Param. Код изменённого класса ParamVolatile - поменялось лишь свойство MaxValue и мы убрали дублирующее поле maxValue
public class ParamVolatile : Param
{
public override double MaxValue
{
get => base.MaxValue;
set
{
base.MaxValue = value;
CurrentValue = CurrentValue;
}
}
double currentValue;
public double CurrentValue
{
get => currentValue;
set => currentValue = Math.Clamp(value, 0, MaxValue);
}
}
Ну и добавим валидацию и для MaxValue базового класса - а то чё это оно у нас без валидации, зря чтоли от автосвойства отказывался. Просто обозначу валидирующую функцию и приведу окончательный код обоих классов:
public class Param
{
double maxValue;
public virtual double MaxValue
{
get => maxValue;
set => maxValue = ValidateMaxValue(value);
}
}
public class ParamVolatile : Param
{
public override double MaxValue
{
get => base.MaxValue;
set
{
base.MaxValue = value;
CurrentValue = CurrentValue;
}
}
double currentValue;
public double CurrentValue
{
get => currentValue;
set => currentValue = Math.Clamp(value, 0, MaxValue);
}
}
Теперь у нас по идее есть валидация в базовом классе для свойства MaxValue, которая та же самая используется и в производном классе для этого же переопределённого свойства, и плюс специальная механика для свойства CurrentValue в производном классе, завязанная на изменение свойства MaxValue базового класса. Потестил - вроде работает.
Можно ли сделать лучше для описанной задачи (напомню - выделено жирным)? Может что с интерфейсами намутить? Кода не так уж много, и вроде вылизан, а интерфейсы лишь добавят лапшы, нет? Меня здесь смущает в основном запись CurrentValue = CurrentValue для принудительной валидации CurrentValue. В комментах к коду я написал, почему так делаю, но вроде можно вместо такой записи (присваивание самому себе) и повторить код сеттера CurrentValue:
currentValue = Math.Clamp(value, 0, MaxValue);
Но это же повтор кода! Страшное нарушение какого-то там принципа каких-то там шибко умных старпёров. Короче, табу. Поэтому решил, что просто присвоить свойство самому себе выйдет дешевле. Комментиком сверху пригладил, зачем так сделал, и зашибись.
Сразу говорю, что не юзал разные валидирующие атрибуты типа RangeAttribute, т.к. они ничего не валидируют, а лишь задают валидирующие значения и сообщения об ошибках - для них надо писать свою функцию валидации, которая вытаскивает из этих атрибутов их значения (через рефлексию, ага) и производит собственно валидацию. Для некоторых фреймворков типа ASP.NET MVC это уже написано и работает под капотом, но для своего кода надо писать самому. Зачем так сложно, если эти значения у меня уже в готовых свойствах, как и валидация.