Рефакторинг шаблона команд

Я реализовал шаблон команды для SDK, который создаю на C#, но хочу предотвратить его неправильное использование. То, что у меня сейчас есть (1), это. Обратите внимание, что метод Execute запроса можно вызвать напрямую, минуя логику метода Execute SDK:

 public partial class Sdk
 {
     public async Task<TOut> Execute1<TOut>(ICommandRequest1<TOut> commandRequest)
         where TOut : ICommandResponse1
     {
         // validation logic and other logic goes here
         return await commandRequest.Execute1(this);
     }
 }

 public interface IRequest1 { }
 public interface IResponse1 { }
 public interface ICommandResponse1 : IResponse1 { }

 public interface ICommandRequest1<TOut> : IRequest1
     where TOut : ICommandResponse1
 {
     Task<TOut> Execute1(Sdk sdk);
 }

 public class DeleteOrderResponse1 : ICommandResponse1
 {
     public int OrderNum { get; set; }
 }

 // there are many request objects. this one is for deleting orders.
 public class DeleteOrderRequest1 : ICommandRequest1<DeleteOrderResponse1>
 {
     public int OrderNum { get; set; }

     public async Task<DeleteOrderResponse1> Execute1(Sdk sdk)
     {
         // logic goes here

         return await Task.FromResult(new DeleteOrderResponse1()); // example response
     }
 }

 public class TestItOut1
 {
     public async Task Test()
     {
         var sdk = new Sdk();
         var request = new DeleteOrderRequest1 { OrderNum = 1 };
         var response = await sdk.Execute1(request); // correct usage

         // I do not want to allow this, because it bypasses all the logic in the
         // sdk.Execute1() method
         var response2 = request.Execute1(sdk);
     }
 }

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

 public partial class Sdk
 {
     public async Task<TOut> Execute2<TOut>(ICommandRequest2<TOut> commandRequest)
         where TOut : class, ICommandResponse2
     {
         // validation logic and other logic goes here
         return await new CommandExecutor2<TOut>().Execute2(commandRequest, this);
     }
 }

 public interface IRequest2 { }
 public interface IResponse2 { }
 public interface ICommandResponse2 : IResponse2 { }

 public interface ICommandExecutor2<TOut> : IRequest2
    where TOut : ICommandResponse2
 {
     Task<TOut> Execute2(ICommandRequest2<TOut> request, Sdk sdk);
 }

 public interface ICommandRequest2<TOut> : IRequest2
     where TOut : ICommandResponse2
 {
     //note: no longer contains the Execute method - good
 }

 public class DeleteOrderResponse2 : ICommandResponse2
 {
     public int OrderNum { get; set; }
 }
 public class DeleteOrderRequest2 : ICommandRequest2<DeleteOrderResponse2>
 {
     public int OrderNum { get; set; }
 }

 // there are many command executors. this one is for deleting orders.
 // note: internal - so clients outside the SDK cannot access this
 internal class CommandExecutor2<TOut> : ICommandRequest2<DeleteOrderResponse2>, ICommandExecutor2<TOut>
     where TOut : class, ICommandResponse2
 {
     public async Task<TOut> Execute2(ICommandRequest2<TOut> inputRequest, Sdk sdk)
     {
         // I don't like having to do a cast here
         DeleteOrderRequest2 request = inputRequest as DeleteOrderRequest2;

         // logic goes here

         // I also don't like having to do a cast here
         return (await Task.FromResult(new DeleteOrderResponse2())) as TOut; // example response
     }
 }

 public class TestItOut2
 {
     public async Task Test()
     {
         var sdk = new Sdk();
         var request = new DeleteOrderRequest2 { OrderNum = 1 };
         DeleteOrderResponse2 response = await sdk.Execute2(request);

         // this no longer works - good - clients will not be able to bypass the validation logic
         // var response2 = request.Execute2(sdk);
     }
 }

Есть ли у кого-нибудь лучшие идеи для решения проблемы? Я не хочу использовать операторы отражения или if/switch. В идеале я хотел бы использовать CommandExecutor2 конкретные типы DeleteOrderRequest2 и DeleteOrderResponse2, но пока мне с этим не повезло.

Возможно, ваш вопрос будет более успешным на сайте codereview.stackexchange.com

Jeanot Zubler 26.07.2024 08:36
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
0
1
75
3
Перейти к ответу Данный вопрос помечен как решенный

Ответы 3

Это ближе к тому, что я хочу, и проще. Модифицировано из здесь.

Однако он по-прежнему не возвращает правильный тип во время сборки — мне нужен DeleteOrderResponse вместо IResponse. Клиенту раздражает то, что ему приходится выполнять это самому.

И мне также не нужен дополнительный шаг по созданию DeleteOrderCommand для клиента. SDK должен позаботиться об этом.

public class TestItOut
{
    public static void Test()
    {
        Sdk sdk = new Sdk();

        Request request = new Request { OrderNum = "1234" };
        AbstractCommand command = new DeleteOrderCommand(request);

        var response = (DeleteOrderResponse)sdk.ExecuteSdkCommand(command);
    }
}

public abstract class AbstractCommand
{
    protected Request request;

    // Constructor

    public AbstractCommand(Request request)
    {
        this.request = request;
    }

    public abstract IResponse Execute();
}

public interface IResponse { }
public class DeleteOrderResponse : IResponse
{
}

public class CreateOrderResponse : IResponse
{
}

/// <summary>
/// The 'ConcreteCommand' class
/// </summary>
public class DeleteOrderCommand : AbstractCommand
{
    public DeleteOrderCommand(Request request) : base(request)
    {
    }

    public override DeleteOrderResponse Execute()
    {
        string orderNum = request.OrderNum;
        // do logic here
        return new DeleteOrderResponse();
    }
}
public class CreateOrderCommand : AbstractCommand
{
    public CreateOrderCommand(Request request) : base(request)
    {
    }

    public override CreateOrderResponse Execute()
    {
        string orderNum = request.OrderNum;
        // do logic here
        return new CreateOrderResponse();
    }
}

public class Request
{
    public string OrderNum { get; set; }
}

public class Sdk
{
    public IResponse ExecuteSdkCommand(AbstractCommand command)
    {
        return command.Execute();
    }
}

Я обдумал это и нашел решение, которое работает так, как я хочу.

Несколько ключевых моментов:

  • Тип ответа заключен в интерфейсе запроса. Таким образом, клиенту не нужно приводить ответ из интерфейса к конкретному типу, когда он получает его обратно.
  • Требования заключались в том, что я не хотел, чтобы клиент мог сам выполнить команду. Для этого объекты запроса не могут содержать никаких ссылок на класс команды, поскольку объектом запроса является public. Поэтому я использовал internal class для команд и перегрузил широко известный метод входным аргументом типов запроса. Может быть есть другие способы сделать это, которые лучше? Было бы неплохо не использовать dynamic, чтобы IRequest<TOut> распознавал либо DeleteOrderRequest, либо CreateOrderRequest. В любом случае, это работает.
    using Microsoft.VisualStudio.TestTools.UnitTesting;
    
    namespace Hack6;
    
    [TestClass]
    public class Tests
    {
        private Sdk sdk;
        public Tests()
        {
            this.sdk = new Sdk();
        }
    
        [TestMethod]
        public void CanCreateOrder()
        {
            var createOrderRequest = new CreateOrderRequest();
            var createOrderResponse = this.sdk.ExecuteSdk(createOrderRequest);
            Assert.IsInstanceOfType(createOrderResponse, typeof(CreateOrderResponse));
            Assert.AreEqual(createOrderResponse.OrderId, "1234");
            Assert.AreEqual(createOrderResponse.Status, "success");
        }
        [TestMethod]
        public void CanDeleteOrder()
        {
            var sdk = new Sdk();
            var deleteOrderRequest = new DeleteOrderRequest { OrderNum = "1234" };
            var deleteOrderResponse = this.sdk.ExecuteSdk(deleteOrderRequest);
            Assert.IsInstanceOfType(deleteOrderResponse, typeof(DeleteOrderResponse));
            Assert.AreEqual(deleteOrderResponse.Status, "deleted");
        }
    }
    
    public interface IResponse { }
    
    public class DeleteOrderResponse : IResponse
    {
        public string Status { get; set; }
    }
    
    public class CreateOrderResponse : IResponse
    {
        public string OrderId { get; set; }
        public string Status { get; set; }
    }
    
    public interface IRequest<TOut> where TOut : IResponse
    {
    }
    
    public class DeleteOrderRequest : IRequest<DeleteOrderResponse>
    {
        public string OrderNum { get; set; }
    }
    
    public class CreateOrderRequest : IRequest<CreateOrderResponse>
    {
    }
    
    public class Sdk
    {
        public TOut ExecuteSdk<TOut>(IRequest<TOut> request)
            where TOut : IResponse
        {
            var commands = new Commands();
            var response = commands.ExecuteCommand((dynamic)request);
            return response;
        }
    }
    
    internal class Commands
    {
        public DeleteOrderResponse ExecuteCommand(DeleteOrderRequest request)
        {
            // logic goes here
            return new DeleteOrderResponse { Status = "deleted" };
        }
    
        public CreateOrderResponse ExecuteCommand(CreateOrderRequest request)
        {
            // logic goes here
            return new CreateOrderResponse
            {
                OrderId = "1234",
                Status = "success"
            };
        }
    }

Ответ принят как подходящий

В конце концов решение оказалось довольно простым — просто сделайте команды членами двух интерфейсов — одного публичного, не содержащего метода ExecuteCommand(), и одного внутреннего, содержащего метод ExecuteCommand(). Затем SDK примет общедоступный интерфейс в своем методе ExecuteSdk() и немедленно преобразует этот объект во внутренний интерфейс для внутреннего использования метода ExecuteCommand().

Сборка 1 — клиент:

    [TestClass]
    public class Tests
    {
        [TestMethod]
        public async Task CanCreateOrder()
        {
            var sdk = new Sdk();
            var createOrderRequest = new CreateOrderRequest();
    
            // note: createOrderRequest.Execute() does not exist in this assembly - good
            // this means the client cannot bypass sdk.ExecuteSdk()
            // createOrderRequest.Execute();
    
            // but sdk.ExecuteSdk does exist in this assembly - exactly as I want
            var createOrderResponse = await sdk.ExecuteSdk(createOrderRequest);
    
            Assert.IsInstanceOfType(createOrderResponse, typeof(CreateOrderResponse));
            Assert.AreEqual(createOrderResponse.OrderId, 1234);
            Assert.AreEqual(createOrderResponse.Status, "success");
        }
    }

Сборка 2 – SDK:

public class Sdk
{
    public async Task<TOut> ExecuteSdk<TOut>(ICommandRequest<TOut> commandRequest)
        where TOut : ICommandResponse
    {
        // validation logic and other logic goes here
        return await ((IInternalCommandRequest<TOut>)commandRequest).ExecuteCommand(this);
    }
}

public interface IResponse { }
public interface ICommandResponse : IResponse { }

public interface ICommandRequest<TOut>
    where TOut : ICommandResponse
{
    // note: do not expose the ExecuteCommand() method here, otherwise it will be public
    // and can be called directly by users of this SDK, bypassing the validations in the
    // Sdk.ExecuteSdk() methods
}

internal interface IInternalCommandRequest<TOut>
    where TOut : ICommandResponse
{
    // note: do define the ExecuteCommand() method here, only to be used internally by
    // the SDK
    Task<TOut> ExecuteCommand(Sdk sdk);
}

public class CreateOrderResponse : ICommandResponse
{
    public int OrderId { get; set; }
    public string Status { get; set; }
}

// there are many request objects. this one is for creating orders.
public class CreateOrderRequest : IInternalCommandRequest<CreateOrderResponse>, ICommandRequest<CreateOrderResponse>
{
    async Task<CreateOrderResponse> IInternalCommandRequest<CreateOrderResponse>.ExecuteCommand(Sdk sdk)
    {
        // logic goes here
        var createOrderResponse = new CreateOrderResponse
        {
            OrderId = 1234,
            Status = "success"
        };
        return await Task.FromResult(createOrderResponse); // example response
    }
}

public class DeleteOrderResponse : ICommandResponse
{
    public string Status { get; set; }
}

public class DeleteOrderRequest : IInternalCommandRequest<DeleteOrderResponse>, ICommandRequest<DeleteOrderResponse>
{
    async Task<DeleteOrderResponse> IInternalCommandRequest<DeleteOrderResponse>.ExecuteCommand(Sdk sdk)
    {
        // logic goes here
        return await Task.FromResult(new DeleteOrderResponse { Status = "failed"}); // example response
    }
}

Другие вопросы по теме

Похожие вопросы

Быстрый целочисленный sqrt с использованием Math.Sqrt
Консоль неправильно выводит следующую строку
Как заставить мою функцию правильно использовать обратный вызов после завершения асинхронных функций?
После перемещения файла в другое место и последующего создания файла с тем же именем в исходном месте время создания неверно
Как отправить команды в приложение WPF для отображения всплывающих подсказок и значков на панели задач с помощью именованных каналов IPC в С#?
Почему C# DateTime.Now/DateTime.UtcNow опережает SYSUTCDATETIME()/SYSDATETIME() SQL Server, хотя код C# выполняется до SQL-запроса
Как я могу использовать шаблон объявления вне условия if?
Получить документы/файлы с диска Share Point
Получение «System.IO.FileNotFoundException: не удалось загрузить файл или сборку Azure.Core, версия = 1.38.0.0» в приложении-функции Azure
Как динамически устанавливать дополнительные свойства в моделях с несколькими запросами в ASP.NET Core Web Api