Pull to refresh
17
0
Джошуа Лайт @JoshuaLight

Software Developer

Send message
Ну, замечательно. Вы изобрели DiscountService/DiscountManager под именем Discounts.

Имя — это всё.


Что из них репозиторий, что из них сущность, что сервис — непонятно.

Это избыточные детали. Приложение-то по работе с заказами, а не с сервисами заказов.


Можно экономить на названиях

Но ведь это не экономия на названиях, а использование слов из предметной области.


Можно экономить на названиях, но через 3 года сам же откроешь свой код, и будешь вспоминать, почему IOrders — это репозиторий, а IDiscount — сервис с методом
void Apply(Customer customer, Order order);

Единственное, что можно подумать: IDiscount — это скидка, а IOrders — заказы.


Такой подход убивает абстракцию.

Боюсь, DiscountManager — это не абстракция...


Если у нас несколько каналов продаж, можно было бы сделать объект Discount, содержащий лишь информацию о значениях скидок, а их вычисление отдать сервисам, которые будут писать разные люди.

Это новое "если".


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

Если я верно вас понял, вы предлагаете переиспользовать "лишь информацию о скидке". А если в каждом из каналов скидки выражаются разными данными? А если где-то выдаётся только процентная скидка? Если она не всегда ограничена по времени?


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

И что такого дают методы расширения?

var a = b.As<A>();
// Не то же самое, что:
var a = Represent.As<A>(b);

Методы расширений позволяют строить такие предложения, где расширяемый объект является субъектом действия.


Как видите, я рассматриваю их не с точки зрения деталей реализации, а с точки зрения кода как текста.

… как мне кажется, вызывает ступор и InvalidOperationException в мозгу.

Открываю руководство по расстановке запятых в английском языке, вижу "Thus, happy and lively are coordinate adjectives in the example and should be separated by a comma.".


Вот ещё по теме.


На мой взгляд, тут нужен mind-shift с императивного мышления на декларативное, где нет инструкций, а есть описания.

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

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

Просто обманывать не надо

Вы используете слово выполнить без контекста, тогда как в действительности это будет выполнить на тредпуле. Выполнение чего-то на тредпуле имеет другую семантику, чем просто выполнение чего-то. Почему бы тогда не писать ThreadPool.FindFreeThreadAndExecuteUserWorkItem(action)? А то QueueUserWorkItem не сообщает о том, что выполнение действия будет совершено только тогда, когда в тредпуле есть свободный поток.


Если после завершения метода Run фактически выполнение не началось — это наименование врёт.

Не имеет значения, началось ли оно фактически. Это инкапсулировано. Наименование врало бы, если бы разработчик ожидал от него мгновенного выполнения, а он этого не ожидает.

После модификации Order этим методом теряется информация, сколько заказ стоил до скидки

Зависит от реализации.


Тут и вырисовывается DiscountManager как сервис, и Discount как сущности, с которыми он работает.

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


Кстати, вот ещё в голову пришёл вариант:


public interface IDiscounts
{
    IDiscount One(Customer for, Order and);
}

public interface IDiscount
{
    public int Value { get; }
    public DiscountType Type { get; }
    public DateTime WillBeAvailableTo { get; } 
}

// Вызываем так:
Order order = ...;
Customer customer = ...;
IDiscounts discounts = ...; // Инжектим и т.д.

var price = Price(of: order); // Как-то считается цена.
var discount = discounts.One(for: customer, and: order);

discount.Apply(ref price); // `Apply` или внутрь `IDiscount`, или методом расширений.

Даже такой скорый вариант сильнее всякого менеджера или хелпера хотя бы тем, что он не размазывает ответственности. Думаю, можно намного лучше, если иметь хотя бы какое-то представление о конкретной задаче и её особенностях.

Но ведь читаем мы С#.

Полагаю, в вопросах обновления операционной системы, выделение одного объекта в памяти — не самая большая проблема.


Более того, бюджет производительности приложения в 99% случаях позволяет выделять столько объектов, сколько нужно, чтобы читалось хорошо.

Потому что "вытащить из" — промежуточный этап, который не так важен, как его результат.


Когда я кладу элемент в коллекцию Put, мне жизненно необходимо понимать, что нечто случится. Действие — конечная цель.
Когда мне нужен элемент коллекции по ключу, то не имеет значения, достаётся он, получается, и т.д. Даже если он достаётся из базы, он нужен всё равно. Элемент — конечная цель.

Я не уверен, что проблема решается без создания нового языка программирования (который, естественно, имеет все шансы не взлететь, а также все шансы превратить проблему в "теперь у нас n+1 язык, который разработчику в наши времена всяко нужно знать"). С точки зрения естественного языка, например, одна из проблем — это то, что действия могут быть без субъекта и с субъектом. И первые логично бы записывать в префиксной нотации и называть в повелительном наклонении, а вторые — в постфиксной. "Чисто объектные" языки не позволяют одно, "чисто функциональные" — другое, а на смешанных почему-то оказывается написано очень много спагетти-кода. Также есть легаси — программисты банально привыкли, что вызов метода идёт через точку от объекта, хотя это, в общем-то, и необязательно.

Как мне кажется, C# в этом смысле пока сильнее многих других языков: он позволяет создавать и субъектные выражения с помощью методов расширений, и безсубъектные с помощью using static. Кроме этого, он разрешает именованные параметры, а это также многократно улучшает читаемость и fluent-составляющую многих вызовов. Но мы всё ещё на этапе хелперов.


Другое решение — разрешить программисту лепить ad hoc конструкции и делать свои DSL для отдельных частей кодовой базы. Но тут из примеров только Лиспы, где с помощью макросов можно "исправить" практически любую "неудачную" конструкцию. Нужно, например, много работать с массивами, а (aref array index) писать всё время долго — не проблема. Оборачиваем код в макрос и можем вместо этого писать просто (array index). Но из не-лиспов я такого сорта макросы видел только в Julia, и там "человеческая" запись выражений только мешает их писать.

Как раз недавно искал какие-то новые языки, которые переосмысливают всю ту императивную кашу, которая осталась ещё с доисторических времён. Понравился Nim. Там кроме элегантного синтаксиса, есть шаблоны и макросы, позволяющие настроить язык под себя, как вы и сказали.

Вот хоть убейте, Separate должен возвращать нечто разделенное

Если вызывать его на чём-то целом, да. Но когда у вас уже есть нечто разделённое, Separated(with: ", "), как мне кажется, вполне однозначно говорит, что произойдёт. Не ожидаю же я, что мы разделим уже разделённое?


Кстати, именованный параметр with обязателен. Сделать его таковым язык не позволяет, но без него конструкция неполна.

Вообще, в целом я с Вами согласен (особенно надеюсь на статью код-как-английский-язык)

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


но! ИМХО Вы слишком категоричны

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


рассмотрите варианты типа threadPool.QueueWorkItem(() => {… }) — вполне возможно что внутри скобочек не
будет слова action => неискушённому взгляду не будет понятно что там ставят в очередь…

И всё же, давайте представим сценарий. Разработчик, имеющий представление о потоках и о том, как они управляются в пуле (а это почти обязательное требование, чтобы им пользоваться), смотрит на запись ThreadPool.Run( () => { ... } ) и такой думает: "Не, бред какой-то. Что тут происходит?". Потом встречает ThreadPool.QueueUserWorkItem, и тотчас восклицает: "А, так это ж UserWorkItem! Теперь ясно!".


Боюсь, разработчики просто привыкают к QueueUserWorkItem, и для них это просто alias выражения "Вызвать метод на тредпуле".

что этот подход нельзя перенести на остальные подсистемы

Зачем это может быть нужно? Каждая подсистема имеет свой ограниченный контекст. Жертвовать читаемостью кода из-за того, что он не масштабируется на разработку операционных систем для ракетостроения, — не самое лучшее решение.

Попробуйте поискать FindUsages такого AdditionalValue — найдутся обращения к Item2 всех кортежей.

И вот мы начинаем обсуждать, хороши ли именованные кортежи, как часто их приходится рефакторить, искать использования полей...


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


Кстати, есть ещё вариант:


public interface IDiscount
{
    void Apply(Customer customer, Order order);
}

Но и тут можно придраться к тому, менять Order нельзя. Как и к тому, что скидка с несколькими полями — вещь сомнительная. Разумеется, Apply можно переделать на Apply(ref price, Customer customer, Order order), но и тут можно найти минусы.


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

Мысль не закончена. Какой из этого вывод?

Что методы расширений не являются "теми же самыми хелперами, просто с другим синтаксисом".


А вот это
@"C:\Windows".UpgradeOS();


выглядит как какая-то пышь-пышь магия ))

Всё верно, действительно, магия. В таких случаях, конечно, лучше привлекать чуть больше предметных сущностей, чем просто строка.


Например:


var windows = Windows.Of(path: @"C:\Windows");

windows.Upgrade();

Разве это хуже, чем:


WindowsUtils.UpgradeFromPath(@"C:\Windows");
Главное достоинство — программист пишет так, как ему удобно думать. Главный недостаток — его коллегам удобно думать по-другому.

Поэтому, как мне видится, в качестве судьи нужно использовать язык. Например, string.Join(", ", numbers) можно прочитать разве что как "присоединить к строке запятую и числа", но разве так можно сказать? Да и зачем, если уже есть выражение "separate with a comma"?


Разумеется, о том, какие предложения языка считать более понятными, а какие менее — можно отдельно спорить. Но хорошо бы хотя бы начать вести такие споры. Ведь суть того же примера со string.Join не в самом string.Join, а в том, что над словом нужно думать, искать, подбирать (что также демонстрирует пример с ExceptWith). А то подчас простой по содержанию код похож на философский трактат с его интенциями, трансцендентностями и прочим.

Это находится за пределами темы статьи и обсуждения.

Загрузка объекта из хранилища — тоже действие, притом дорогое (по сравнению с работой с объектами в памяти).

Загрузка — это Load, а не Get.


Кстати, даже тут можно уйти от Load. По контексту хранилища обычно понятно, будет это дорогостоящая операция или нет. Например, database.User(id) — мы видим, что обращение User происходит на объекте базы, поэтому понимаем: операция дорогостоящая.


Кроме этого, если контракт (интерфейс) обобщает способ получения и, вообще, конкретные детали, то уточнение стоимости получения — нарушение инкапсуляции.

Information

Rating
Does not participate
Location
Харьков, Харьковская обл., Украина
Date of birth
Registered
Activity