Deutsch

Абасс... обсудите рахитекурту

1143  1 2 3 4 5 6 7 8 все
alex445 патриот18.05.24 07:37
18.05.24 07:37 
Последний раз изменено 18.05.24 08:23 (alex445)

улыб

Драма в действиях - с прологом, эпилогом и интермедиями. Задача выделена жирным.


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

#1 
alex445 патриот18.05.24 08:16
NEW 18.05.24 08:16 
в ответ alex445 18.05.24 07:37, Последний раз изменено 18.05.24 08:25 (alex445)

Кстати, пошарился по МСДНу и Стековерфлоу - разбираются всякие переопределения, сокрытия свойств, но нигде нет примера переопределениям свойства с его полем - нужно ли поле тоже "переопределять". Да, поля нельзя переопределять, но как их использовать в переопределённом свойстве? Все примеры переопределения - лишь для автосвойств. Например. Или здесь темку как-то заболтали, т.к. автор был неконкретен и сразу скатился в "или... или... или...". Или все статьи про полиморфизм, про переопределение, про использование переопределения или сокрытия - там везде разбираются лишь простейшие примеры, типа возврата константы или переопределения свойства вообще без поля и кода, или лишь примеры с методами, а не свойствами. И ни одного смешанного примера, где бы возвращалось поле поддержки (backing field). То, что для свойств с полем в базовом классе, чтобы использовать это поле в классе-потомке, нужно вызвать базовую версию геттера или сеттера, которая работает с базовым полем (а не заводить второе поле для класса-потомка) - нигде не показывается. А я вот в своём примере выше это разобрал. А вот если хочешь скрыть свойство, тогда да - лучше завести второе поле для класса-потомка.


Но это так, дополнение к моему первому посту. Попробуйте какого-нибудь сеньёра поспрашивать про не очень стандартный пример не из букваря - посыпится наверняка. ))

Если привык к самым стандартным конструкциям и годами только их и встречал, а потом встретил чуть более не такое - будет сидеть и репу чесать, хотя "10+ лет опыта, ценный специалист и заслуженный сеньёр". ))

#2 
alex445 патриот18.05.24 08:30
NEW 18.05.24 08:30 
в ответ alex445 18.05.24 08:16, Последний раз изменено 18.05.24 08:31 (alex445)

ИЧСХ, вроде спрашиваешь поисковик конкретный и довольно короткий вопрос, типа

"C# override property with the same backing field"

или

"C# override property example"

И тебе выдаёт либо всякую муть, либо простейшие примеры переопределения без использования поля поддержки базового класса. Гугл, Майкрософт, мать вашу, вы своих долбаных ИИ чему обучаете, если они ни ответа дать не могут (что их поисковики, что их ИИ чат боты), ни маршрут нормальный в навигаторе построить - самые насущные вопросы. Зато всякую херню делать годами и десятилениями, инклюзивность и прочая смузёво-скриптизёрская параша - это хлебом не корми.

#3 
Срыв покровов патриот18.05.24 13:19
NEW 18.05.24 13:19 
в ответ alex445 18.05.24 08:30

как выглядит функция ValidateMaxValue?


Последний пример наверное будет работать, пока у класса Param только один наследник.
Если ты присвоишь новое значение MaxValue во втором наследнике, то первый же об этом ничего не узнает?

#4 
alex445 патриот18.05.24 13:39
NEW 18.05.24 13:39 
в ответ Срыв покровов 18.05.24 13:19, Последний раз изменено 18.05.24 13:41 (alex445)
как выглядит функция ValidateMaxValue?

В общем виде так

double ValidateMaxValue(double incomingValue)


А в реальности сложнее - у класса Param есть ещё свойство ParamType, которое тоже передаётся в функцию валидации, и в зависимости от него валидируется. Но это детали моей реализации для моего случая. В общем же виде я выше написал.


Последний пример наверное будет работать, пока у класса Param только один наследник.
Если ты присвоишь новое значение MaxValue во втором наследнике, то первый же об этом ничего не узнает?

А зачем двум разных наследникам - т.е. двум разным параллельным классам - знать, что происходит в параллельном классе? Это же будут разные объекты, у которых из общего лишь базовый класс. Или какой пример наследования вы имели ввиду - уже не от Param, а от ParamVolatile?


Пока у меня действительно лишь один наследник от Param, и всё вроде работает.

#5 
alex445 патриот18.05.24 13:45
NEW 18.05.24 13:45 
в ответ alex445 18.05.24 13:39, Последний раз изменено 18.05.24 13:47 (alex445)

По-моему, у меня как-то запутана валидация - она раздроблена между свойствами: вот эта запись это по сути тоже валидация


set => currentValue = Math.Clamp(value, 0, MaxValue);


А у меня видно же, что свойства связанные - одно от другого зависит. По идее, связанные свойства должны валидироваться одной функцией валидации, которая учитывает значение сразу всех связанных свойств. Логично? Этот описанный мной вариант масштабируется на сколько угодно свойств - просто вызываешь одну функцию валидации на любое изменение любого свойства из группы зависимых. А вариант в моём первом посте масштабировать трудно - нужно переписать много кода и не ошибиться в заимных зависимостях.

#6 
AlexNek патриот18.05.24 13:57
AlexNek
NEW 18.05.24 13:57 
в ответ alex445 18.05.24 07:37

ну, для начала слишком много букафф смущ.

И опять что то из мира извращений.

У нас есть логика которая требует одновременного знания/изменения 2 значений. Сначала разносим это по разным углам, а после пытаемся как то косо-криво связать.

Отчего не сделать всё требуемую логику в одном объекте, а после пользовать его где надо?

#7 
AlexNek патриот18.05.24 14:05
AlexNek
NEW 18.05.24 14:05 
в ответ alex445 18.05.24 07:37
CurrentValue = CurrentValue; // чтобы принудительно вызвать валидацию CurrentValue

Сразу не проходит review смущ нужна валидация, значит делаем, а не вызываем side эффект с комментарием.


#8 
alex445 патриот18.05.24 14:07
NEW 18.05.24 14:07 
в ответ AlexNek 18.05.24 13:57, Последний раз изменено 18.05.24 14:08 (alex445)

Это я ещё до фабрики фабрик не добрался - так, на уровне букваря ковыряю. )))

Просто писал, как придумывал такую архитектуру, по шагам.


Что значит "вся логика в одном объекте"? Есть базовый класс, которого достаточно для многих объектов. И есть наследник, который расширяет базовый. У каждого своя валидация, но в наследнике использовать валидацию базового класса было бы неплохо.


Как вариант, я могу сделать свойство MaxValue в наследнике не override, а new, со своим собственным полем поддержки. И валидацию базового класса для этого свойства вызвать методом ValidateMaxValue, а не через вызов родительского варианта сеттера base.MaxValue = value;

#9 
alex445 патриот18.05.24 14:15
NEW 18.05.24 14:15 
в ответ alex445 18.05.24 14:07, Последний раз изменено 18.05.24 14:16 (alex445)

Сделал с new и отдельным вызовом валидирующего метода ValidateMaxValue для потомка. И ещё вынес валидацию CurrentValue тоже в отдельный метод. Вроде, так лучше


public class Param
{
    double maxValue;
    public double MaxValue
    {
        get => maxValue;
        set => maxValue = ValidateMaxValue(value);
    }

    protected double ValidateMaxValue(double value) => ...
}

public class ParamVolatile : Param
{
    double maxValue;
    public new double MaxValue
    {
        get => maxValue;
        set
        {
            maxValue = ValidateMaxValue(value);
            currentValue = ValidateCurrentValue(CurrentValue, 0, maxValue);
        }
    }

    double currentValue;
    public double CurrentValue
    {
        get => currentValue;
        set => currentValue = ValidateCurrentValue(CurrentValue, 0, MaxValue);
    }

    ValidateCurrentValue(double value, double min, double max) => Math.Clamp(value, min, max);
}


#10 
AlexNek патриот18.05.24 14:15
AlexNek
NEW 18.05.24 14:15 
в ответ alex445 18.05.24 14:07
как придумывал такую архитектуру,

вообще то называть это архитектурой у меня язык не поворачивается. Слишком мелко.

Типа в озере на доске катался, а после говорить как на море тренировался.


Что значит "вся логика в одном объекте"?

Ну вот же описан объект в нём и должно быть всё


public double CurrentValue { get; set; }
public double MaxValue { get; set; }
#11 
alex445 патриот18.05.24 14:19
NEW 18.05.24 14:19 
в ответ AlexNek 18.05.24 14:15, Последний раз изменено 18.05.24 14:29 (alex445)
Что значит "вся логика в одном объекте"?
Ну вот же описан объект в нём и должно быть всё



public double CurrentValue { get; set; }
public double MaxValue { get; set; }

Нет, базовый класс тоже вполне самодостаточный - можно создавать его объекты отдельно. Но часть его функциональности я бы хотел иметь в потомке. Гляньте мой последний вариант. Мне в нём не нравится только то, что свойство MaxValue в потомке почти полностью повторяет родителя. Может, вернуться к virtual-override и вызову базового сеттера, оставив лишь валидирующие функции?


public class Param
{
    double maxValue;
    public virtual double MaxValue
    {
        get => maxValue;
        set => maxValue = ValidateMaxValue(value);
    }

    double ValidateMaxValue(double value) => ...
}

public class ParamVolatile : Param
{
    public override double MaxValue
    {
        get => base.MaxValue;
        set
        {
            base.MaxValue = value;
            currentValue = ValidateCurrentValue(CurrentValue, 0, MaxValue);
        }
    }

    double currentValue;
    public double CurrentValue
    {
        get => currentValue;
        set => currentValue = ValidateCurrentValue(CurrentValue, 0, MaxValue);
    }

    ValidateCurrentValue(double value, double min, double max) => Math.Clamp(value, min, max);
}
#12 
Срыв покровов патриот18.05.24 14:29
NEW 18.05.24 14:29 
в ответ alex445 18.05.24 13:39
А зачем двум разных наследникам - т.е. двум разным параллельным классам - знать, что происходит в параллельном классе?

Да, это я что-то затупил.

#13 
Срыв покровов патриот18.05.24 14:30
NEW 18.05.24 14:30 
в ответ alex445 18.05.24 14:19
Гляньте мой последний вариант. Мне в нём не нравится только то, что свойство MaxValue в потомке почти полностью повторяет родителя

там же свойство базового класса вообще не используется.

#14 
alex445 патриот18.05.24 14:35
NEW 18.05.24 14:35 
в ответ Срыв покровов 18.05.24 14:30, Последний раз изменено 18.05.24 14:42 (alex445)

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


#15 
AlexNek патриот18.05.24 14:43
AlexNek
NEW 18.05.24 14:43 
в ответ alex445 18.05.24 14:19
Нет, базовый класс тоже вполне самодостаточный

ну тогда описание должно быть другим. Мы же видим только маленький описанный кусочек. А что там еще должно быть, знает только автор. Много раз на личном примере убеждался, что не просто описать описать всё сразу.

Я бы попытался бы вначале сформулировать что хочется. Иногда помогает себе же.


оставив лишь валидирующие функции?

Это кстати не совсем валидирующие функции. Валидирующие функции имеют право возвращать результат: валидный / невалидный объект

#16 
alex445 патриот18.05.24 14:46
NEW 18.05.24 14:46 
в ответ AlexNek 18.05.24 14:43, Последний раз изменено 18.05.24 14:47 (alex445)
оставив лишь валидирующие функции?
Это кстати не совсем валидирующие функции. Валидирующие функции имеют право возвращать результат: валидный / невалидный объект

У меня они больше корректирующие - принудительно корректируют значение. Дело в том, что это не валидация пользователя, которому сообщают, что значение неправильное и он должен его исправить. Тут значение приходит из другого объекта, и попросить его исправить не получится. Поэтому эти фукнции исправляют его сами.

#17 
alex445 патриот18.05.24 14:49
NEW 18.05.24 14:49 
в ответ alex445 18.05.24 14:35, Последний раз изменено 18.05.24 14:52 (alex445)

Мне в последнем варианте (с зелёным) не нравится, что я два раза вызываю один и тот же код для currentValue: currentValue = ValidateCurrentValue(...). И всё лишь ради того, чтобы не было "странного" CurrentValue = CurrentValue, которое смотрится компактнее и лаконичнее. Но если вся валидация будет внутри метода ValidateCurrentValue, без добавок снаружи него, то вроде нормально - не придётся вносить одинаковые изменения в двух разных местах.

#18 
AlexNek патриот18.05.24 14:51
AlexNek
NEW 18.05.24 14:51 
в ответ alex445 18.05.24 14:46
У меня они больше корректирующие - принудительно корректируют значение

ну вот именно, поэтому название валидация сбивает с толку.

#19 
alex445 патриот18.05.24 14:54
NEW 18.05.24 14:54 
в ответ AlexNek 18.05.24 14:51, Последний раз изменено 18.05.24 14:56 (alex445)


У меня они больше корректирующие - принудительно корректируют значение
ну вот именно, поэтому название валидация сбивает с толку.

Просто вы привыкли, что валидация, это когда юзеру сообщаешь, а он исправляет. А более общее определение валидации - это убедиться, что данные правильные. А как это будет - через юзера или принудительно - неважно.


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

#20 
1 2 3 4 5 6 7 8 все