Я реализовал шаблон команды для 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
, но пока мне с этим не повезло.
Это ближе к тому, что я хочу, и проще. Модифицировано из здесь.
Однако он по-прежнему не возвращает правильный тип во время сборки — мне нужен 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
}
}
Возможно, ваш вопрос будет более успешным на сайте codereview.stackexchange.com