[Программирование, C#, ООП, Промышленное программирование] Методы без аргументов — зло для неизменяемых объектов, и вот как его полечить
Автор
Сообщение
news_bot ®
Стаж: 6 лет 9 месяцев
Сообщений: 27286
Привет!
Идея в том, что бы использовать ленивые кешируемые свойства везде, где в обычном случае мы бы использовали процессорно тяжелые методы без аргументов. А статья — как это задизайнить и и зачем.
Несмотря на то, что я должен сделать оговорку,
Дисклеймер
SPL
Этот подход не подойдет в случаях:
1) Если вы пишете что-нибудь сверхбыстрое, и красивый код — последнее, о чем думаете
2) Если ваши объекты никогда не используются дважды (например, беспрекословно соблюдается SRP)
3) Если вы настолько ненавидете свойства, что код их содержащий в ваших глазах покрывается блюром
мне очень нравится подход, которым я хочу поделиться в этой заметке, и я считаю, что такой дизайн удобен как авторам кода, так и его пользователям.
TL;DR в самом низу.
Почему зло?
Приведу утрированный пример. Предположим, у нас есть неизменяемый рекорд Integer, определенный следующим образом:
public sealed record Integer(int Value);
У него есть одно свойство Value типа int. Теперь, нам понадобился следующий метод:
public sealed record Integer(int Value)
{
public Integer Triple() => new Integer(Value * 3);
}
Каждый раз при необходимости утроить инстанс нашего числа, придется вызывать этот метод, и брать на себя ответственность за кеширование. Например, придется писать
public int SomeMethod(Integer number)
{
var tripled = number.Triple();
if (tripled.Value > 5)
return tripled.Value;
else
return 1;
}
Вместо того, что бы писать
public int SomeMethod(Integer number)
=> number.Tripled > 5 ? number.Tripled.Value : 1;
Красивее, короче, читабельнее, безопаснее. Потенциально, оно также быстрее, если у нас к одному и тому же Tripled происходит обращение не только здесь.
Что нам хочется?
- Удобный дизайн кода для его пользователя. Например, я не хочу думать о кешировании при обращении к объекту, я просто хочу от него данные.
- Бесплатность обращения к свойству. Время я плачу только за первое обращение, и это никогда не хуже, чем вызов метода (обычно — почти как обращение к полю по стоимости).
- Удобный дизайн кода для его разработчика. Разрабатывая новый immutable object, я не хочу оверрайдить конструктор, Equals и GetHashCode рекорда просто потому, что я добавил какое-то приватное поле для кеша, которое внезапно ломает мне все сравнения.
Я уже привел пример того, насколько удобнее свойства чем методы, в очень простом случае. А как разработчик объекта, я хочу писать так:
public sealed record Number(int Value)
{
public int Number Tripled => tripled.GetValue(@this => new Number(@this.Value * 3), @this);
private FieldCache<Number> tripled;
}
А вот в жааве
SPL
Можно было лучше, и насколько мне известно, в джаве это решается аттрибутом Cacheable. В шарпе недавно добавленные source-генераторы код изменять не могут, а значит такой же красоты мы по-любому не получим. Поэтому этот сэмпл — лучшее, к чему я смог прийти.
А вот как пишут обычно:
Подход 1 (да зачем нам кеш?):
public sealed record Number(int Value)
{
public int Number Tripled => new Number(@this.Value * 3);
}
(очень дорогой по очевидным причинам)
Подход 2 (используем Lazy<T>):
public sealed record Number : IEquatable<Number>
{
public int Value { get; init; } // приходится оверлоадить конструктор, поэтому выносим сюда
public int Number Tripled => tripled.Value;
private Lazy<Number> tripled;
public Number(int value)
{
Value = value;
tripled = new(() => value * 3); // мы не можем это сделать в конструкторе поля, потому что на тот момент this-а еще не существует
}
// потому что Equals, который генерируется для рекордов, генерируется на основе полей, и поэтому наш Lazy<T> все сломает
public bool Equals(Number number) => Value == number.Value;
// то же самое с GetHashCode
public override int GetHashCode() => Value.GetHashCode();
}
Как мы видим, очень сложно и неадекватно становится дизайнить наш объект. А что если там не одно кешируемое свойство, а несколько? За всем придется следить, включая все оверрайды.
Более того, у нас перестанет работать with, который клонирует все ваши поля, кроме указанного(-ых). Ведь он скопирует и ваше поле с Lazy, в котором будет лежать уже неверный кеш.
Подход 3 (используем ConditionalWeakTable):
public sealed record Number
{
public Number Tripled => tripled.GetValue(this, @this => new Integer(@this.Value * 3));
private static ConditionalWeakTable<Number, Number> tripled = new();
}
Наиболее адекватное решение среди прочих. Но для него придется писать обертку над ValueType так как ConditionalWeakTable принимает только референс-тип. Поэтому такая штука существенно медленнее, чем что-то подобное без оверхеда (по моему бенчмарку получается разница в, по меньшей мере, 6 раз, по сравнению с типом, о котором я расскажу).
Подход 4 (сразу посчитать):
public sealed record Number
{
public int Value { get; init; }
public Number Tripled { get; }
public Number(int value)
{
Value = value;
Tripled = new Number { Value = value * 3 };
}
}
В конкретно этом случае это вообще даст stackoverflow, но даже если нам повезло, и кешируемый тип не совпадает с "холдером" — нам гарантированно придется заплатить временем за эту инициализацию, которая нам может и не понадобиться.
Решение
- Итак, начнем с того, что наш ленивый контейнер будет структурой. Зачем лишний раз кучить мучу мучить кучу?
- Equals и GetHashCode всегда будут возвращать true и 0 соответственно. Это убивает смысл этих методов, но этот контейнер нам нужен только ради кеша, а значит сам по себе не должен влиять на результаты сравнения двух рекордов или получения хеша. Таким образом, мы не обязаны оверрайдить Equals и GetHashCode для каждого рекорда, пусть об этом думает Рослин.
- Допустим любой тип в качестве кешируемого. Лочить будем по холдеру, то есть тому, в ком объявлен наш кеш.
- Фабрика передается не в конструкторе, а в методе GetValue, по тому же принципу, как у ConditionalWeakTable. Тогда не придется создавать конструктор и писать спаггети-код, как мы это делали с Lazy<T>.
- Чтобы не сломать замечательную операцию with, вместо переменной initialized мы будем сравнивать holder, и в случае изменения референса — запускаем фабрику снова.
Коду!
Для начала, так у нас выглядят поля и оверрайденные методы:
public struct FieldCache<T> : IEquatable<FieldCache<T>>
{
private T value;
private object holder; // от этой штуки нам нужен ТОЛЬКО референс, смысла делать его generic нет
// как я уже говорил, сделано, чтобы рослиновский Equals не сломался от приватного поля
public bool Equals(FieldCache<T> _) => true;
public override int GetHashCode() => 0;
}
И примитивная имплементация GetValue выглядит так:
public struct FieldCache<T> : IEquatable<FieldCache<T>>
{
public T GetValue<TThis>(Func<TThis, T> factory, TThis @this) where TThis : class // record - это тоже класс. А ограничение нужно, чтобы тип был референсным
{
// если холдер изменился ИЛИ еще не записывался (например, если он - null)
if (!ReferenceEquals(@this, holder))
lock (@this)
{
if (!ReferenceEquals(@this, holder))
{
// мы передаем в фабрику, потому что наш FieldCache нужен для случаев, когда какие-то кешируемые проперти зависят ТОЛЬКО от полей нашего самого холдера. Можно, конечно, и захватить в передаваемой лямбде, но тогда будет реаллокация каждый раз
value = factory(@this);
holder = @this;
}
}
return value;
}
}
Таким образом, мы можем себе позволить такой дизайн:
public sealed record Number(int Value)
{
public int Number Tripled => tripled.GetValue(@this => new Number(@this.Value * 3), @this);
private FieldCache<Number> tripled;
}
Код очень короткий, и его можно найти на гитхабе.
Производительность
Единственное, что быстрее, чем наш наивный FieldCache — это встроенный Lazy<T>.
Method
Mean
BenchFunction
4,599.1638 ns
Lazy
0.6717 ns
FieldCache
3.6674 ns
ConditionalWeakTable
25.0521 ns
BenchFunction — это какие-то сложные страшные вычисления, которые производились бы каждый раз при обращении к методу, поэтому мы хотим его кешировать. Другие три строчки занимают три разных подхода. Как видим, FieldCache<T> немного помедленнее, чем Lazy<T>.
Я считаю, что так как он все равно занимает не очень много времени, во многих местах адекватный дизайн будет лучше, чем пару сэкономленных наносекунд.
Кратый TL;DR или выводы
Хотелка: ленивые кешируемые свойства неизменяемых объектов, зависящие от первичных свойств данных объектов.
И известные существующие подходы, по всей видимости, не дают это красиво сделать, поэтому приходится писать свое.
===========
Источник:
habr.com
===========
Похожие новости:
- [Python, Программирование] Как сделать ваш код на Python быстрым и асинхронным с Sanic (перевод)
- [Промышленное программирование] Модели и алгоритмы построения цифровой платформы CNCIOT для сбора данных с оборудования
- [PHP, Программирование, Java] Финальные классы в PHP, Java и других языках (перевод)
- [Open source, .NET, XML, C#] Конвертируем doc в docx и xml на C#
- [Python, Программирование] Itertools в Python (перевод)
- [Программирование, Go] JSON с опциональными полями в Go (перевод)
- [Программирование, Разработка под iOS, Облачные сервисы] Интеграция CI/CD для нескольких сред с Jenkins и Fastlane. Часть 3 (перевод)
- [Ненормальное программирование, Разработка веб-сайтов, Python, Программирование] iPad для разработчика
- [Программирование, .NET, Разработка под MacOS] Поддержка процессоров Apple M1 в .NET
- [Программирование, Java, Apache, Промышленное программирование] Spring Boot app with Apache Kafka in Docker container (перевод)
Теги для поиска: #_programmirovanie (Программирование), #_c#, #_oop (ООП), #_promyshlennoe_programmirovanie (Промышленное программирование), #_c#, #_dizajn_koda (дизайн кода), #_patterny_programmirovanija (паттерны программирования), #_programmirovanie (
Программирование
), #_c#, #_oop (
ООП
), #_promyshlennoe_programmirovanie (
Промышленное программирование
)
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 18:51
Часовой пояс: UTC + 5
Автор | Сообщение |
---|---|
news_bot ®
Стаж: 6 лет 9 месяцев |
|
Привет! Идея в том, что бы использовать ленивые кешируемые свойства везде, где в обычном случае мы бы использовали процессорно тяжелые методы без аргументов. А статья — как это задизайнить и и зачем. Несмотря на то, что я должен сделать оговорку, ДисклеймерSPLЭтот подход не подойдет в случаях:
1) Если вы пишете что-нибудь сверхбыстрое, и красивый код — последнее, о чем думаете 2) Если ваши объекты никогда не используются дважды (например, беспрекословно соблюдается SRP) 3) Если вы настолько ненавидете свойства, что код их содержащий в ваших глазах покрывается блюром мне очень нравится подход, которым я хочу поделиться в этой заметке, и я считаю, что такой дизайн удобен как авторам кода, так и его пользователям. TL;DR в самом низу. Почему зло? Приведу утрированный пример. Предположим, у нас есть неизменяемый рекорд Integer, определенный следующим образом: public sealed record Integer(int Value);
У него есть одно свойство Value типа int. Теперь, нам понадобился следующий метод: public sealed record Integer(int Value)
{ public Integer Triple() => new Integer(Value * 3); } Каждый раз при необходимости утроить инстанс нашего числа, придется вызывать этот метод, и брать на себя ответственность за кеширование. Например, придется писать public int SomeMethod(Integer number)
{ var tripled = number.Triple(); if (tripled.Value > 5) return tripled.Value; else return 1; } Вместо того, что бы писать public int SomeMethod(Integer number)
=> number.Tripled > 5 ? number.Tripled.Value : 1; Красивее, короче, читабельнее, безопаснее. Потенциально, оно также быстрее, если у нас к одному и тому же Tripled происходит обращение не только здесь. Что нам хочется?
Я уже привел пример того, насколько удобнее свойства чем методы, в очень простом случае. А как разработчик объекта, я хочу писать так: public sealed record Number(int Value)
{ public int Number Tripled => tripled.GetValue(@this => new Number(@this.Value * 3), @this); private FieldCache<Number> tripled; } А вот в жаавеSPLМожно было лучше, и насколько мне известно, в джаве это решается аттрибутом Cacheable. В шарпе недавно добавленные source-генераторы код изменять не могут, а значит такой же красоты мы по-любому не получим. Поэтому этот сэмпл — лучшее, к чему я смог прийти.
А вот как пишут обычно: Подход 1 (да зачем нам кеш?): public sealed record Number(int Value)
{ public int Number Tripled => new Number(@this.Value * 3); } (очень дорогой по очевидным причинам) Подход 2 (используем Lazy<T>): public sealed record Number : IEquatable<Number>
{ public int Value { get; init; } // приходится оверлоадить конструктор, поэтому выносим сюда public int Number Tripled => tripled.Value; private Lazy<Number> tripled; public Number(int value) { Value = value; tripled = new(() => value * 3); // мы не можем это сделать в конструкторе поля, потому что на тот момент this-а еще не существует } // потому что Equals, который генерируется для рекордов, генерируется на основе полей, и поэтому наш Lazy<T> все сломает public bool Equals(Number number) => Value == number.Value; // то же самое с GetHashCode public override int GetHashCode() => Value.GetHashCode(); } Как мы видим, очень сложно и неадекватно становится дизайнить наш объект. А что если там не одно кешируемое свойство, а несколько? За всем придется следить, включая все оверрайды. Более того, у нас перестанет работать with, который клонирует все ваши поля, кроме указанного(-ых). Ведь он скопирует и ваше поле с Lazy, в котором будет лежать уже неверный кеш. Подход 3 (используем ConditionalWeakTable): public sealed record Number
{ public Number Tripled => tripled.GetValue(this, @this => new Integer(@this.Value * 3)); private static ConditionalWeakTable<Number, Number> tripled = new(); } Наиболее адекватное решение среди прочих. Но для него придется писать обертку над ValueType так как ConditionalWeakTable принимает только референс-тип. Поэтому такая штука существенно медленнее, чем что-то подобное без оверхеда (по моему бенчмарку получается разница в, по меньшей мере, 6 раз, по сравнению с типом, о котором я расскажу). Подход 4 (сразу посчитать): public sealed record Number
{ public int Value { get; init; } public Number Tripled { get; } public Number(int value) { Value = value; Tripled = new Number { Value = value * 3 }; } } В конкретно этом случае это вообще даст stackoverflow, но даже если нам повезло, и кешируемый тип не совпадает с "холдером" — нам гарантированно придется заплатить временем за эту инициализацию, которая нам может и не понадобиться. Решение
Коду! Для начала, так у нас выглядят поля и оверрайденные методы: public struct FieldCache<T> : IEquatable<FieldCache<T>>
{ private T value; private object holder; // от этой штуки нам нужен ТОЛЬКО референс, смысла делать его generic нет // как я уже говорил, сделано, чтобы рослиновский Equals не сломался от приватного поля public bool Equals(FieldCache<T> _) => true; public override int GetHashCode() => 0; } И примитивная имплементация GetValue выглядит так: public struct FieldCache<T> : IEquatable<FieldCache<T>>
{ public T GetValue<TThis>(Func<TThis, T> factory, TThis @this) where TThis : class // record - это тоже класс. А ограничение нужно, чтобы тип был референсным { // если холдер изменился ИЛИ еще не записывался (например, если он - null) if (!ReferenceEquals(@this, holder)) lock (@this) { if (!ReferenceEquals(@this, holder)) { // мы передаем в фабрику, потому что наш FieldCache нужен для случаев, когда какие-то кешируемые проперти зависят ТОЛЬКО от полей нашего самого холдера. Можно, конечно, и захватить в передаваемой лямбде, но тогда будет реаллокация каждый раз value = factory(@this); holder = @this; } } return value; } } Таким образом, мы можем себе позволить такой дизайн: public sealed record Number(int Value)
{ public int Number Tripled => tripled.GetValue(@this => new Number(@this.Value * 3), @this); private FieldCache<Number> tripled; } Код очень короткий, и его можно найти на гитхабе. Производительность Единственное, что быстрее, чем наш наивный FieldCache — это встроенный Lazy<T>. Method Mean BenchFunction 4,599.1638 ns Lazy 0.6717 ns FieldCache 3.6674 ns ConditionalWeakTable 25.0521 ns BenchFunction — это какие-то сложные страшные вычисления, которые производились бы каждый раз при обращении к методу, поэтому мы хотим его кешировать. Другие три строчки занимают три разных подхода. Как видим, FieldCache<T> немного помедленнее, чем Lazy<T>. Я считаю, что так как он все равно занимает не очень много времени, во многих местах адекватный дизайн будет лучше, чем пару сэкономленных наносекунд. Кратый TL;DR или выводы Хотелка: ленивые кешируемые свойства неизменяемых объектов, зависящие от первичных свойств данных объектов. И известные существующие подходы, по всей видимости, не дают это красиво сделать, поэтому приходится писать свое. =========== Источник: habr.com =========== Похожие новости:
Программирование ), #_c#, #_oop ( ООП ), #_promyshlennoe_programmirovanie ( Промышленное программирование ) |
|
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 18:51
Часовой пояс: UTC + 5