Jak uprościć kod

0

Macie pomysły, jak uprościć ten kod? Muszę wykonać kilka operacji, które zwracają Result, a następnie, jeśli wszystko jest OK, przekazać wartości z tych resultów do pewnej metody. Jakbym miał kilka operacji do wykonania po sobie, z których każda by zwracała Result, to bym sobie zrobił łańcuszek z jakimś FlatMap, ale w sytuacji takiej jak poniżej nie widzę, jak to zrobić.

Result<Discount> discountResult = Discount.Of(request.Discount);

if (!discountResult.HasValue)
  return NotValid("Discount not valid.");

Result<CustomerId> customerIdResult = CustomerId.Of(request.CustomerId);

if (!customerIdResult.HasValue)
  return NotValid("Customer ID not valid.");

Result<PriceListId> priceListIdResult = PriceListId.Of(request.PriceListId);

if (!priceListIdResult.HasValue)
  return NotValid("Price list ID not valid.");

Result<Order> orderResult = Order.Create(discount.Value, customerId.Value, priceListId.Value);

if (!orderResult.HasValue)
{
  return Result.Failure(orderResult.Code, orderResult.Message);
}

_orderRepository.Add(orderResult.Value);
return Result.Ok();

I tak w ogóle się zastanawiam, czy na poziomie warstwy aplikacji powinienem używać API zwracającego resulty przy tworzeniu value objectów. Z jednej strony, jesteśmy już po wstępnej walidacji danych w modelu (requeście), więc utworzenie value objectu musi się powieść (jak się nie powiedzie, to jest sytuacja wyjątkowa). Z drugiej strony, ktoś może wywołać tę metodę z pominięciem walidacji (no, handlerów raczej nikt nie wykona z pominięciem walidacji, jeśli walidacja będzie w pipeline, ale jakby to był jakiś serwis aplikacyjny, a nie handler, to taka sytuacja byłaby możliwa). Z trzeciej strony, takie wywołanie w ogóle nie powinno mieć miejsca, jeśli każdy use case ma własny niezależny handler/metodę serwisową.

1

Ale co tu upraszczać – kod jest prosty. Jak zaczniesz „upraszczać” to będzie to bardziej skomplikowane.

1

czasem może i trzeba zaakceptować to, że kod nawet z monadami może być po prostu brzydki, ale działa i ciężko o coś lepszego bez budowania $company_framework

0
Result<Discount> discountResult = Discount.Of(request.Discount);

if (!discountResult.HasValue)
  return NotValid("Discount not valid.");

Tak naprawdę ten Result<T> to powinien być Result<Error,T> i ta wiadomość Discount not valid powinna iść z metody Of (wydaje się BTW że Javoviec to pisał bo tam of(something) jest bardziej popularne, a w .NET'cie raczej From(Something)).

Więc jak by był porządny Either (czyli Result<Error,T>) to mógłbyś pojechać monadycznie:

var result = from discount in Discount.Of(...)
                   from customerId in CustomerId.Of(...)
                   from ....
                   from order in createOrder(discount, customerId, ...)
                   select addToRepo(order);
return result;

Przykład: https://codewithstyle.info/understand-monads-linq/ (LOL pracowałem kiedyś z tym gościem; tj. autorem)
Ten jest jeszcze lepszy: https://mikehadlow.blogspot.com/2011/01/monads-in-c-4-linq-loves-monads.html

Alternatywne podejście bez magii, to wydzielenie mappera wtedy będzie:

Order order = OrderMapper.FromDTO(request); // rzuca wyjątkami
repo.save(order);

Lub możesz połączyć tego mappera z FP i zrobić tak żeby zwracał Either/Result.

Generalnie C# nie ma składni dla monad (poza reużyciem LINQ) więc nie da się tego ładnie zapisać...

0
0xmarcin napisał(a):

Tak naprawdę ten Result<T> to powinien być Result<Error,T> i ta wiadomość Discount not valid powinna iść z metody Of (wydaje się BTW że Javoviec to pisał bo tam of(something) jest bardziej popularne, a w .NET'cie raczej From(Something)).

Ten Result ma w sobie właściwość Error (NotValid, NotFound itd.). To Of wziąłem stąd : https://github.com/itlibrium/DDD-starter-dotnet/blob/83ce8a40a4f7ccdd707dc2a047f3c162a76c5f03/Sources/Sales/Sales.DeepModel/Commons/Money.cs#L19

Skoro już została wspomniana Java, to ciekaw jestem, jakby to zrobili ludzie używający vavra (zakładając używanie vavrowych typów ofc): @jarekr000000, @Aleksander32. Problem ten sam: kilka metod zwracających Option\Either. Chcemy zwrócić wiadomość o błędzie, jeśli którakolwiek z tych metod zwróci błąd. Jeśli wszystko OK, to chcemy przekazać wszystkie wartości do jakiejś zbiorczej metody.

3

@nobody01:
w przypadku vavr jest tu metoda : sequenceRight - czyli z listy eitherów robimy Either listy
https://www.javadoc.io/doc/io.vavr/vavr/0.10.0/io/vavr/control/Either.html#sequenceRight-java.lang.Iterable-

Jest kilka podobnych metod (niestety, przez braki w systemie typów javy nie da się tej metody/mtedod napisać raz a dobrze i trzeba powtarzać dla każdej monady).

EDIT:
ale to ma zastosowanie jak mamy listę - jak poszczególne eithery sa różnych typów to jedyne co znam to For - yield w vavrze (ale dla mnie średnio użyteczne).

0

Czyli tak by było?

Either<Error, Discount> discountEither = Discount.Of(request.Discount);

Either<Error, CustomerId> customerIdEither = CustomerId.Of(request.CustomerId);

Either<Error, PriceListId> priceListIdEither = PriceListId.Of(request.PriceListId);

Either<Error, (Discount, CustomerId, PriceListId)> eithers = Either.SequenceRight(discountEither, customerIdEither, priceListIdEither)

return eithers
  .FlatMap(x => Order.Create(x.Discount, x.CustomerId, x.PriceListId))
  .Tap(order => _orderRepository.Save(order));

EDIT: To wyżej nie wiem, jak zrobić, ale takie coś już mi się kompiluje:

public static class EitherExtensions
{
  public static Either<TError, (T1, T2, T3)> SequenceRight<TError, T1, T2, T3>(
      Either<TError, T1> either1,
      Either<TError, T2> either2,
      Either<TError, T3> either3)
  {
      if (either1.IsLeft) return either1.Error;

      if (either2.IsLeft) return either2.Error;

      if (either3.IsLeft) return either3.Error;

      return (either1.Value, either2.Value, either3.Value);
  }
}

var eithers = EitherExtensions.SequenceRight<Discount, CustomerId, PriceListId>(discountEither, customerIdEither, priceListIdEither);

return eithers
  .FlatMap(((Discount Discount, CustomerId CustomerId, PriceListId PriceListId) x) => Order.Create(x.Discount, x.CustomerId, x.PriceListId))
  .Tap(order => _orderRepository.Save(order));
0
jarekr000000 napisał(a):

EDIT:
ale to ma zastosowanie jak mamy listę - jak poszczególne eithery sa różnych typów to jedyne co znam to For - yield w vavrze (ale dla mnie średnio użyteczne).

Czemu For-yield jest średnio użyteczny? Wygląda całkiem ok dla przypadku który próbuje rozwiązać @nobody01:

        Either<Integer, String> either1 = Either.right("either1");
        Either<Integer, Integer> either2 = Either.left(1);
        Either<Integer, Boolean> either3 = Either.left(2);
        Either<Integer, String> either4 = Either.right("either4");

        For(either1, either2, either3, either4).yield(API::Tuple);
0

@DisQ:
Zauważ, że to gubi informację o błędzie. Po yield dojstajemy Iterable - odpowiednik Option.

1

@jarekr000000: Ah, sprawdzałem na wersji 1.0.0-alpha-3 i tam już jest "normalnie":

Either<Integer, Tuple4<String, Integer, Boolean, String>> yield = For(either1, either2, either3, either4).yield(API::Tuple);
1

Niestety nie wiem o co chodzi, bo jak widzę atrybut DddValueObject a potem readonly struct to już dalej nie czytam i zamykam się w sobie.

Natomiast, żeby uzyskać taki efekt:

public Result<OrderId> ProcessOrder(OrderRequest request)
{
    return _customerService.CheckCustomer(order.Customer)
                  .Then(x => _paymentService.MakePayment(order.Payment))
                  .Then(x => _warehauseService.PackOrder(order))
                  .Then(x => _invoicingService.IssueInvoice(order))
}

To używam po prostu takiego swojego Resulta: Zwracanie odpowiedzi serwisu
Napisałem sobie sam, bo w Microsofcie nie potrafią (są widocznie zbyt zajęci łataniem upośledzonego ORMa przy użyciu play-doh), a innych Resultów nie rozumiem i nie chce mi się uczyć. Ani też opierać mojego kodu o jakieś kołczingowe biblioteki, które wymagają, aby programista wreszcie uwierzył w bycie zwycięzcą, i w to, że C# to taki piękny funkcyjny język pod brudnymi piórkami tego, czym jest.

0

Ja próbowałem pisać kod w pełni używajacy monad, ale tak praktycznie się nie da przy dużych projektach, bo zazwyczaj ludzie nie rozumia co się dzieje w kodzie i doszedłem do praktycznie takiego samego rozwiazania jak kolega @somekind
Jakoś to działa, ludzie nie płacza (tal bardzo), wielbiciele drabinek if na 20 wcięć maja koszmary po Code Review, a kod czytelniejszy.

1 użytkowników online, w tym zalogowanych: 0, gości: 1