Как стать автором
Обновить

Комментарии 43

Элегантно. Имплементация намного проще чем я подумал вначале статьи. Мне бы это очень пригодилось на прошлом проекте с CQRS

Да ни черта не элегантно! fluent потому и fluent, что позволяет гибко управлять составом опций, применять их в произвольном порядке. А тут надо расписать полное дерево вариантов, это ж экспоненциальный взрыв. А если я захочу навесить ещё до 5 опциональных параметров — мне придётся дерево вариантов умножить в 32 раза, к другим опциям, что уже есть.

А внутренности всех вариантов копипастить, если логика чуть сложнее, чем одной заглушки ExecuteAsync…

А как эти инстанцированные типы будут выглядеть в отладчике, логах, стек-трейсах? Идейка хитроумная, как головоломка. В прод я бы такое не пустил.

Альтернатива в виде отдельных классов ещё хуже. Ну и 5 дженерик параметров - это в любом случае сигнал о архитектурной проблеме

По сути, мы и заставляем разработчика библиотеки написать и вручную(!) поддерживать тысячу классов, но чтобы юзер не запутался, какой класс ему нужен, делаем для него дорожку к нужному классу, предлагая ответить на 10 вопросов.
Проблема ручной поддержки всей 1000 классов никуда не уходит.
SourceGenerators?
Возможно. Но тогда нужен какой-то мета-язык для описания, какой класс мы хотим построить при «применении» fluent-операторов к типу. Чтобы в генераторе не говнокодить
if (feature1) {
   WriteLine("public abstract Task<ActionResult> ExecuteAsync(");
} else {
   WriteLine(...); }

Как вариант, для упрощения можно все эти статические классы сделать абстрактными, в них содержать общую логику (дабы не копипастить), а их наследовать вложенными классами

Статические классы абстрактными?

Подожди еще пару лет, и они там и до абстрактных статических классов дойдут. Потому что дефолтную реализацию в интерфейсы уже ввели, над абстрактными членами интерфейсов еще думают.. а значит и абстрактные статики не за горами.

Абстрактные статики в интерфейсах уже в .net 6 под флагом появились.

В net7 уже будут стабильные

А где информация по поводу того, что в .net 7 уже будут стабильными?

Есть у меня какая то чуйка, что если к этому добавить Source Generator, то будет что то стоющее..

Очень интересная идея, надо бы попробовать что-то такое в каком то из своих петпроектов

А потом появляется второй метод, и... Ой.

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

Более того, если мы представим расширение функционала библиотеки
(например, добавление  не асинхронных обработчиков), то станет очевидно,
что такая архитектура плохо масштабируется.

Затем, в своём решении, он героически НЕ решает озвученную проблему.

Выгладит отвратительно честно говоря. Не надо в сройный язык тянуть абсолютно чуждые концепциии. Во-первых и главных оно выглядит (читается) очень плохо. Оно вводит некий "язык" описания дженериков, которого не было до того. А какую проблему при этом решает? Немного чего-то-там переиспользовать? А зачем? Только чтоб переиспользовать. Чем тут плохи просто тупо уникальные классы?
Это абсолютно та-же гадость что тянется из DI контейнетов - вызовы методов через точку. Плохо тем, что приходится учить совершенно искусственный синтаксис, который нигде больше не используется. В LINQ вызов через точку хоть оправдан - там цепь вызовов и типы возврящаемого значения отличаются. А тут зачем?

Выгладит отвратительно

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

Ключевое как мне видится это вот:

var result = new Command("git")     
    .WithArguments("pull")     
    .WithWorkingDirectory("/my/repository")  

Какое возвращаемое значение у WithArguments и какой вход у WithWorkingDirectory? В случае с DI контейнером мне вываливается сотня-другая абслолютно бесполезных подсказок, среди которых еще и нет того, что я ищу (потому, что забыт using). Или как тут - допустим я помню, что надо вызвать WithArguments, но не помю у кого. Что мне теперь поможет?

В случае с дженериками из статьи еще и не понятно зачем оно вообще? Так часто что-ли ендпоинты с одним методом делаются? Да никогда практически. Ну и какую проблему оно решает? А если часто, то чем оно лучше Endpoint<SomeRequest, SomeResponce>?

А если еще дальше смотреть, то накой мне вообще этот public abstract метод?

Для решения этих проблем есть обязательные и необязательные параметры, которые могут быть указаны при вызове по имени.

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

Вот и я про то-же. По-моему вот так оно выглядит лучше и читается проще:

var result = new Command("git")     
    {
       Arguments = {"pull"},
       WorkingDirectory = "/my/repository"
    };

Да, тоже об этом подумал, когда читал

Слишком душно.

Какое возвращаемое значение у WithArguments и какой вход у WithWorkingDirectory?

Мое мнение, это придирка ради придирки. У вас есть тысячи мест, где тип возвращаемого значения неизвестен исходя из названия потому что нейминг не всегда может решить эту проблему. Тем более что в конкретно этом месте некаких проблем нет, это концептуально производное от стандартного Builder паттерна, использование которого вы каждый день видите:

await Host.CreateDefaultBuilder(args)
    .ConfigureServices(services =>
    {
        services.AddHostedService<SampleHostedService>();
    })
    .Build()
    .RunAsync();

Именно! Builder - бельмо в глазу. И исполльзуется он ровно в одном месте - инициализация ASP.NET (Microsoft DI на самом деле) и более нигде! Это в Джаве он распространет, а в шарпе нет.


Как я сказал второе место - это LINQб но там реально новый язык введен. Со своими правилами оформления и очень ограниченым набором методов, которые к тому-же применимы к любым коллекциям, а не являются позиционными для определенных типов объектов.

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

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

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

Так часто что-ли ендпоинты с одним методом делаются?

При использовании этой библиотеки - да, часто (всегда). Для неё собственно этот подход и придуман.
https://github.com/ardalis/ApiEndpoints

А если часто, то чем оно лучше Endpoint<SomeRequest, SomeResponce>

Тем что у тебя может быть Endpoint с пустым Request или с пустым Response.
В предлагаемом подходе тебе не надо вспоминать, как называется класс, который "ендпоинт с пустым ответом" - у тебя есть единая точка входа "Endpoint", от которой ты уже идёшь WithRequest или WithEmptyRequest

А если еще дальше смотреть, то накой мне вообще этот public abstract метод?

А что не так с public abstract?

PS: Проблема пустого запроса/ответа отлично решается при помощи Unit, как это показывает, например Mediatr или любой функциональный ЯП.

Проблемы пустого ответа просто нет! Тут решается абсолютно искусственная, надуманая проблема. Вот смотрите: в статье постулируется как данность что 1) эндпоинты с одним методом распространены на столько, что вообще заслуживают внимания 2) что для эндпоинтов нужен базовый класс/интерфейс. Оба утверждения на мой взгляд неверны. Эндпоинтов с одним методом исчезающе мало и ежели таковые все-же распространены в приложении для них вообще не надо ничего базового, как и для всех остальных. Чего плохого в стандартном, рекомендуемом методе (просто отрастить от ControllerBase)? Помнить как называется класс для пустого возвращаемого значения надо только при использовании данной библиотеки. Т.е. проблема искусственно создана и успешно решается.

Кроме того даже если принять положения статьи - однометодовые эндпоинты реализуются сильно по-разному. Возвращаемое значение может быть как сам класс так и Task<Result>, ActionResult, ActionResult<Result> и т.д. Даже если библиотека все это поддерживает, то полезность ее как-бы размывается.

До кучи. У методов эндпоинта есть атрибуты. Библиотека тут не поможет (ну и не помешает конечно). А именно в атрибутах все веселье - авторизация, методы, собственно путь...

А что не так с public abstract?

Не так с ним то, что он вообще есть. В чем его смысл? Дать по рукам джуну, чтоб писал как все? Ну а как ему надо видео стрим вернуть или еще чего сподвыподвертом?


Не знаю... мне кажется в данном случае лучше быть проще и вообще не использовать тулы типа предлагаемого.

Увидел скриншот. Подумал "а нафига это нужно если есть MediatR?". Пробежал глазами пост. Подумал то же самое еще раз.

При том, что все что описал автор статьи уже примерно воплощено в интерфейсе IRequestHandler<TRequest, TResponse>. И fluent ни разу не делает код проще, а наоборот усложняет, и непонятно, ради какой выгоды.

Пример с request/response - просто пример. Статья немного о другом.

Причина тому неоднозначность между Endpoint<TReq> и Endpoint<TRes>, поскольку нет возможности определить означает типовой параметр запрос или ответ.

А могли бы определить один единственный тип Endpoint<TReq, TRes> и подставлять на место позиционных параметров фиктивный тип Unit тогда, когда запроса или ответа не ожидается.

Подобный подход использует MediatR, например, как правильно выше заметили, с его IRequestHandler.

Hidden text

P.S. в функциональщине unit-типы вообще куда натуральнее смотрятся чем отсутствие типа или void. Посмотрите как изящно в том же F# там сделаны нативные делегаты. Вместо 100500 типов а-ля

Action, Action<T1>, ..Action<T1, ..T7>, Func<R>, ..Func<T1,..T7, R>

там один единственный

FSharpFunc<T, R>

и прекраснопричём работает.

Впервые увидел этот подход вот тут (ardalis/ApiEndpoints). С одной стороны, возникает бОльшая типизация и разделение вместо микса пачки методов в одном контроллере, а с другой - изобретение велосипеда, который вместо паттерна через пару лет может стать техническим долгом, который непонятно как переводить на рельсы новой версии .NET, и который нужно будет объяснять каждому новому разработчику

Ну в случае с ApiEndpoints никакой проблемы с переходом на новые рельсы нет, так как фундаментально это самые обычные контроллеры с атрибутами (никто даже не запрещает формально тебе напихать новые методы в класс ендпоинта и разметить их атрибутами)

Подход забавный, но код, который приводится как пример не решает никакой полезной задачи. Вместо предлагаемого автором кода вида:

public class SignInEndpoint : Endpoint
    .WithRequest<SignInRequest>
    .WithResponse<SignInResponse>
{
    [HttpPost("auth/signin")]
    public override async Task<ActionResult<SignInResponse>> ExecuteAsync(
        SignInRequest request,
        CancellationToken cancellationToken = default)
    {
        // ...
    }
}

я могу спокойно написать:

public class SignInEndpoint
{
    [HttpPost("auth/signin")]
    public override async Task<ActionResult<SignInResponse>> ExecuteAsync(
        SignInRequest request,
        CancellationToken cancellationToken = default)
    {
        // ...
    }
}

Поэтому и сижу, думаю: зачем?! Не в плане покритиковать, а в плане придумать, где это может быть оправдано.

Здесь ругнётся на слове override (нет виртуальной ф-ции, которую перекрываем), а если его убрать, можно будет написать свою ф-цию вообще с любой сигнатурой, например
сделать опечатку и напутать в названиях входных/выходных классов:
ExecuteAsync(SignOutRequest request, ...)

Чисто теоретически, базовый Endpoint<TReq, TRes> может как-то хорошо завязываться на типы TReq/TRes, например, куда-то чего-то логируя, или предоставляя вспомогательные удобные ф-ции, заточенные под TReq/TRes, для вызова из ExecuteAsync.

Но да, нам не раскрыли весь потенциал этой идеи.

Ну да. override не почистил, базовый класс не указал. Спасибо за замечание.

код оставлю в старом виде - чтобы было понятно, какой был оригинал )

Я бы пофантазировал на тему:

class FireBall : Spell
  .WithSphere<Fire>
  .WithTarget<LandPoint>
  .WithArea<SphereVolume>
  .WithEffect<AreaDamage>
{
  public override Cast(/* ... */)
  {
    //...
  }
}

class Heal : Spell
  .WithSphere<Light>
  .WithSelfTarget
  .WithEffect<RestoreHealth>
{
  public override Cast(/* ... */)
  {
    //...
  }
}

Но это не точно!

Так себе идея, потому что если я захочу сделать
Spell
.WithEffect<Sound<Screaming>>
.WithEffect<AreaDamage>
.WithEffect<AreaLight>

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

Это было бы веселее в TS, потому что справа от extends может быть выражение, но там дженериков в рантайме нет :( Можно конечно "решить" с помощью clazz и/или символов, но снова костыли...

Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации

Истории