Абасс... обсудите рахитекурту
Драма в действиях - с прологом, эпилогом и интермедиями. Задача выделена жирным.
Всё просто - в классе есть свойства 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 это уже написано и работает под капотом, но для своего кода надо писать самому. Зачем так сложно, если эти значения у меня уже в готовых свойствах, как и валидация.