Użycie czystej architektury w web serwice’ach

W kilku poprzednich postach pisałem o tym jak powinna wyglądać czysta i przejrzysta architektura aplikacji. Kluczową rzeczą w takiej architekturze jest jednokierunkowy przepływ wywołań od kontrolera do prezentera. Kłopot z taką architekturą pojawia się, gdy mamy narzucaną architekturę w której naszym punktem wejścia do obsługi zdarzenia jest metoda, która musi zwrócić wynik poprzez return.
Nie byłbym sobą gdybym nie rozwiązał tego problemu przy pomocy kontenera i architektury.

Załóżmy, że mamy proces biznesowy realizujący wywołanie z Web Service. Jego rolą jest zarejestrować w systemie Bon o zadanym okresie ważności, nadanie mu numeru i zwrócenie go w odpowiedzi.

public class RegisterNewCouponData
{
	public DateTime? ValidFromDate { get; set; }
	public DateTime? ValidToDate { get; set; }
}

[DataContract(Namespace = "")]
public class ServiceResponse<T> where T : ServiceResponse<T>, new()
{
	[DataMember]
	public StatusEnum Status { get; set; }

	public enum StatusEnum
	{
		Ok,
		Failed
	}
}

[DataContract(Namespace = "")]
public class CouponServiceResponse<T> : ServiceResponse<T> where T : CouponServiceResponse<T>, new()
{
}

[DataContract(Namespace = "")]
public class RegisterNewCouponResponse : CouponServiceResponse<RegisterNewCouponResponse>
{
	[DataMember]
	public string Number { get; set; }

	[DataMember]
	public DateTime? ValidFromDate { get; set; }

	[DataMember]
	public DateTime? ValidToDate { get; set; }

	public static RegisterNewCouponResponse Ok(string number, DateTime? validFromDate, DateTime? validToDate)
	{
		return new RegisterNewCouponResponse
			{
				Number = number,
				ValidFromDate = validFromDate,
				ValidToDate = validToDate,
				Status = StatusMessage.Ok
			};
	}
}

public interface IRegisterNewCoupon
{
	void Run(RegisterNewCouponData couponData);
}

public class CouponData
{
	public string Number { get; set; }
	public DateTime? ValidFromDate { get; set; }
	public DateTime? ValidToDate { get; set; }
}

internal class RegisterNewCoupon : IRegisterNewCoupon
{
	private readonly IRegisterNewCouponPresenter presenter;

	public RegisterNewCoupon(
		IRegisterNewCouponPresenter presenter)
	{
		this.presenter = presenter;
	}

	public void Run(RegisterNewCouponData couponData)
	{
		var couponNumber = GenerateCouponNumber();
		ShowCouponData(couponNumber);
	}

	private void ShowCouponData(string couponNumber)
	{
		presenter.ShowCouponData(new CouponData
		{
			Number = couponNumber,
			ValidFromDate = DateTime.Now,
			ValidToDate = DateTime.Now
		});
	}

	private static string GenerateCouponNumber()
	{
		return "xxx";
	}
}

Aby móc wywołać taki proces z Web Service potrzebujemy implementację kontrolera oraz prezentera w warstwie web service:
Z kontrolerem i wywołaniem sprawa jest prosta:

[DataContract(Namespace = "")]
public class RegisterNewCouponRequest
{
	[DataMember]
	public DateTime? ValidFromDate { get; set; }

	[DataMember]
	public DateTime? ValidToDate { get; set; }
}

public interface IRegisterNewCouponWebServiceController
{
	public void Run(RegisterNewCouponRequest requestData)
}

internal class RegisterNewCouponWebServiceController : IRegisterNewCouponWebServiceController
{
	private readonly IRegisterNewCoupon registerNewCoupon;

	public RegisterNewCouponWebServiceController(
		IRegisterNewCoupon registerNewCoupon)
	{
		this.registerNewCoupon = registerNewCoupon;
	}

	public void Run(RegisterNewCouponRequest requestData)
	{
		registerNewCoupon.Run(new RegisterNewCouponData
			{
				ValidFromDate = requestData.ValidFromDate,
				ValidToDate = requestData.ValidToDate
			});
	}
}

I już możemy zrobić Web Service:

[ServiceContract]
public interface ICouponService
{
	[OperationContract]
	[WebInvoke(UriTemplate = "/Register", Method = "POST", ResponseFormat = WebMessageFormat.Xml, BodyStyle = WebMessageBodyStyle.Bare, RequestFormat = WebMessageFormat.Xml)]
	[XmlSerializerFormat(Style = OperationFormatStyle.Document, Use = OperationFormatUse.Literal)]
	RegisterNewCouponResponse RegisterNewCoupon(RegisterNewCouponRequest request);
}

[ServiceBehavior(AddressFilterMode = AddressFilterMode.Any)]
internal class CouponService : ICouponService
{
	public CouponService(IRegisterNewCouponWebServiceController registerNewCouponController)
	{
		this.registerNewCouponController = registerNewCouponController;
	}

	public RegisterNewCouponResponse RegisterNewCoupon(RegisterNewCouponRequest requestData)
	{
		registerNewCouponController.Run(requestData);
		return [...]
	}

	private readonly IRegisterNewCouponWebServiceController registerNewCouponController;
}

I tu pojawia się sedno problemu. Skąd wziąć dane odpowiedzi. W tym celu napisałem prezenter:

public class EventArgs<TEventData> : EventArgs
{
	public EventArgs(TEventData eventData)
	{
		EventData = eventData;
	}

	public TEventData EventData { get; private set; }
}

internal class WcfWebServicePresenter<TResponseData> : IWcfWebServicePresenter<TResponseData>
{
	public void SetResponse(TResponseData responseData)
	{
		OnResponseSet(new EventArgs<TResponseData>(responseData));
	}

	public event EventHandler<EventArgs<TResponseData>> ResponseSet;

	protected virtual void OnResponseSet(EventArgs<TResponseData> e)
	{
		EventHandler<EventArgs<TResponseData>> handler = ResponseSet;
		if (handler != null)
			handler(this, e);
	}
}

Pozwala on na “złapanie” momentu kiedy logika biznesowa ustawiła odpowiedź i zwrócenie sterowania do metody CouponService.RegisterNewCoupon.
Trzeba jeszcze było utworzyć adapter prezentera dla logiki biznesowej:

public class RegisterNewCouponPresenter : IRegisterNewCouponPresenter
{
	private readonly IWcfWebServicePresenter<RegisterNewCouponResponse> wcfWebServicePresenter;

	public RegisterNewCouponPresenter(IWcfWebServicePresenter<RegisterNewCouponResponse> wcfWebServicePresenter)
	{
		this.wcfWebServicePresenter = wcfWebServicePresenter;
	}

	public void ShowCouponData(CouponData couponData)
	{
		wcfWebServicePresenter.SetResponse(RegisterNewCouponResponse.Ok(
			couponData.Number,
			couponData.ValidFromDate,
			couponData.ValidToDate
		));
	}
}

Aby nie musieć w każdym wywołaniu zapisywać/wypisywać się ręcznie na zdarzenie prezentera postanowiłem opakować całość wrapperem tworzącym szablon wywołania.
W tym celu wydzieliłem z interfejsu IRegisterNewCouponWebServiceController interfejs generyczny:

 public interface IWcfWebServiceController<in TRequestData>
{
	void Run(TRequestData requestData);
}

public interface IRegisterNewCouponWebServiceController : IWcfWebServiceController<RegisterNewCouponRequest>
{
}

Pozwoliło to na napisanie wrappera generycznego:

public class WcfInteractorCallWrapper<TRequestData, TResponseData>
{
	private readonly IWcfWebServiceController<TRequestData> controller;
	private readonly IWcfWebServicePresenter<TResponseData> presenter;

	public WcfInteractorCallWrapper(
		IWcfWebServiceController<TRequestData> controller,
		IWcfWebServicePresenter<TResponseData> presenter)
	{
		this.controller = controller;
		this.presenter = presenter;
	}

	public TResponseData Call(TRequestData requestData)
	{
		using (var caller = new InteractorCaller<TResponseData, TRequestData>(controller, requestData, presenter))
			return caller.Call();
	}

	private class InteractorCaller<TResponse, TRequest> : IDisposable
	{
		private readonly IWcfWebServiceController<TRequest> controller;
		private readonly TRequest requestData;
		private readonly IWcfWebServicePresenter<TResponse> presenter;
		private readonly AutoResetEvent resetEvent = new AutoResetEvent(false);

		private TResponse response;

		public InteractorCaller(
			IWcfWebServiceController<TRequest> controller,
			TRequest requestData,
			IWcfWebServicePresenter<TResponse> presenter)
		{
			this.controller = controller;
			this.requestData = requestData;
			this.presenter = presenter;
			this.presenter.ResponseSet += PresenterOnResponseSet;
		}

		private void PresenterOnResponseSet(object sender, EventArgs<TResponse> eventArgs)
		{
			response = eventArgs.ResponseData;
			resetEvent.Set();
		}

		public TResponse Call()
		{
			controller.Run(requestData);
			if (!resetEvent.WaitOne(TimeSpan.FromSeconds(10)))
				throw new CallProcessingException();

				return response;
		}

		public void Dispose()
		{
			presenter.ResponseSet -= PresenterOnResponseSet;
		}
	}
}

Wrapper ten nie tylko pilnuje zapisania/wypisania się ze zdarzenia, ale także pilnuje, żeby zawsze zaczekać aż logika biznesowa się wykona nie zwrócić wyniku przedwcześnie (np. w przypadku uruchamiania nowych wątków przez logikę biznesową) poprzez użycie AutoResetEvent.

Teraz klasa webService będzie wyglądać następująco:

[ServiceBehavior(AddressFilterMode = AddressFilterMode.Any)]
internal class CouponService : ICouponService
{
	public CouponService(
		WcfInteractorCallWrapper<RegisterNewCouponRequest, RegisterNewCouponResponse> registerNewCouponCallWrapper)
	{
		this.registerNewCouponCallWrapper = registerNewCouponCallWrapper;
	}

	public RegisterNewCouponResponse RegisterNewCoupon(RegisterNewCouponRequest requestData)
	{
		return registerNewCouponCallWrapper.Call(requestData);
	}

	private readonly WcfInteractorCallWrapper<RegisterNewCouponRequest, RegisterNewCouponResponse> registerNewCouponCallWrapper;
}

Pozostaje jeszcze zapewnić, że otrzymamy jedną instancję wcfPresenter per Request. Postanowiłęm zrobić to przy pomocy kontenera.
Zaimplementowałem klasę pomocniczą Scoped<T>

public class Scoped<T>
{
	private readonly ILifetimeScope scope;

	public Scoped(ILifetimeScope scope)
	{
		this.scope = scope;
	}

	public void Action(Action<T> action)
	{
		using (var s = scope.BeginLifetimeScope())
			action(s.Resolve<T>());
	}

	public TResult Func<TResult>(Func<T, TResult> func)
	{
		using (var s = scope.BeginLifetimeScope("s1"))
			return func(s.Resolve<T>());
	}
}

i zmienić webService:

[ServiceBehavior(AddressFilterMode = AddressFilterMode.Any)]
internal class CouponService : ICouponService
{
	public CouponService(
		Scoped<WcfInteractorCallWrapper<RegisterNewCouponRequest, RegisterNewCouponResponse>> registerNewCouponCallWrapper)
	{
		this.registerNewCouponCallWrapper = registerNewCouponCallWrapper;
	}

	public RegisterNewCouponResponse RegisterNewCoupon(RegisterNewCouponRequest requestData)
	{
		return registerNewCouponCallWrapper.Func(w => w.Call(requestData));
	}

	private readonly Scoped<WcfInteractorCallWrapper<RegisterNewCouponRequest, RegisterNewCouponResponse>> registerNewCouponCallWrapper;
}

Ostatnią rzeczą są rejestracje w kontenerze “PerLifeTimeScope”:

builder.RegisterType<RegisterNewCoupon>().As<IRegisterNewCoupon>().InstancePerLifetimeScope();
builder.RegisterType<RegisterNewCouponPresenter>().As<IRegisterNewCouponPresenter>().InstancePerLifetimeScope();

//WebService
builder.RegisterType<CouponService>().As<ICouponService>().InstancePerLifetimeScope();
builder.RegisterType<RegisterNewCouponWebServiceController>()
	.As<IRegisterNewCouponWebServiceController>()
	.As<IWcfWebServiceController<RegisterNewCouponRequest>>()
	.InstancePerLifetimeScope();

builder.RegisterGeneric(typeof(WcfWebServicePresenter<>)).AsImplementedInterfaces().InstancePerLifetimeScope();
builder.RegisterGeneric(typeof(WcfInteractorCallWrapper<,>));
builder.RegisterGeneric(typeof(Scoped<>));

Viola!

Comments:6

  1. Czy istnieje jakas zaleta, by zamiast uzycia returna, kozystac z watkow, synchronizacji oraz eventow? Wydaje mi sie ze zaciemnia to przeplyw w kodzie.

    1. Dla mnie podstawowa jest właśnie czytelność. Oczywiście return jest prostszy, ale tylko w przypadku prostych funkcji. W przypadku bardziej złożonych wersji, konieczne staje się tworzenie return code’ów lub rzucania wyjątkami. Dodatkowo struktura zwracanej encji jest jedna. Jeżeli użyjemy prezentera to w zależności od kierunku w jakim pójdzie nasz kod możemy przekazać do niego adekwatną strukturę danych.

      W przypadku webservice i tak konieczne jest sprowadzenie wyniku do takiej postaci (return), ale zysk pojawia się gdy klasa webservice nie jest jedyny użytkownikiem logiki biznesowej.

      1. Gdy pojawia sie webservice to powinno uzyc sie async ktory, faktycznie bedzie wykonywal cos podobnego ale pewnie zrobi to efektywniej.

        Ciage wydaje mi sie ze podstawa czystej architektory powinna byc klarownosc operacji jakie sie dzieja. Wydaje mi sie, ze poprzez zwracanie wartosci z wywolania service, utrudniamy debugowanie, wprowadzamy dodatkowa wiedze o systemie jaka dev musza miec, by zrozumiec co sie dzieje.

        1. Czysta architektura polega przede wszystkim na skoncentrowaniu się wokół logiki biznesowej. To logika biznesowa mówi co potrafi, jak się komunikuje itd. Ta sama logika może być przecież użyta do aplikacji Web, WinForms, WCF, REST. W takim kontekście WebService jest tylko sposobem prezentacji wyniku (wyników) i w żadnym wypadku nie powinien determinować implementacji logiki biznesowej.

  2. Przy zabawie z eventami i klasą AutoResetEvent zawsze istnieje ryzyko że dostaniemy nie swoją odpowiedź zwłaszcza gdy kod jest nie trywialny a nasz kontener IoC ma zarejestrowane niektóre zależności jako Singletony. Lepszymi rozwiązaniami jak dla mnie są:
    promise, callback, task które naturalnie są podnoszone dokładnie raz i utrzymują kontekst
    Metoda typu void i kolejka po stronie serwera oraz przerzucenie na stronę klienta większej odpowiedzialności.

    1. Dość ciekawa koncepcja, nadal jednak pozostaje kwestia separacji logiki biznesowej. WcfInteractorCallWrapper musiałby wtedy otrzymywać callback jako parametr i przekazywać do prezentera. Zyskujemy tyle, że nie trzeba synchronizować wątków.

Leave a Reply

Your email address will not be published. Required fields are marked *