Obsługa procesów wieloetapowych

Czasem, podczas pracy, spotykamy się z problemem przetwarzania dużych ilości danych w procesie, który składa się z etapów/kroków. Dosyć typowym podejściem jest stworzenie menadżera procesu, który wywołuje kolejne akcje/kroki przekazując im kontekst.

Kontekst jest tu magicznym obiektem do wszystkiego, wymiany danych między poszczególnymi krokami, dostarczania loggera i innych współdzielonych zależności.

Moim zdaniem, takie podejście jest zdecydowanie błędne. Powiela problemy używania wzorca Singleton.

  • Obiekt „context” jest mutowalny, nie wiemy kto i kiedy go zmienia
  • Ponieważ, obiekty współdzielą dane poprzez kontekst, niemożliwe (bardzo utrudnione) jest ustalenia od czego (której części kontekstu) zależy dana akcja/krok
  • Kontekst wraz z upływem czasu rośnie i spełnia coraz więcej zadań, nikt nad tym nie panuje.

Chciałbym w tym miejscu zaproponować zupełnie inne rozwiązanie o poniższych cechach:

  • niezależne od siebie akcje
  • zarządzane przez uruchamiający je manager
  • nieposiadające kontekstu tylko zależności
  • współdzielące dane poprzez cache Dao
  • tworzone poprzez fabryki zapewniające współdzielenie wybranych zależności (np. logger)

Ponieważ, najlepszym opisem jest kod, poniżej listingi wraz z objaśnieniami.

Zaczynamy od interfejsu definiującego krok procesu:


	public interface IStep
	{
		void Run();
	}

oraz menedżera procesu:


	public interface IStepManager
	{
		void RunSteps();
	}

są one bardzo proste.

Na potrzeby wpisu przygotowałem 3 implementacje kroków,
2 współdzielące między sobą zasób i wymieniające przez niego dane:


	internal class StepWithSharedDependencies : IStep
	{
		private readonly ISharedDependency sharedDependency;
		private readonly IPresenter presenter;

		public StepWithSharedDependencies(ISharedDependency sharedDependency, IPresenter presenter)
		{
			this.sharedDependency = sharedDependency;
			this.presenter = presenter;
		}

		public void Run()
		{
			presenter.ShowMessage("StepWithSharedDependencies");
			sharedDependency.AddToData("data1");
			presenter.ShowMessage("data added");
		}
	}

	internal class StepWithSharedDependencies2 : IStep
	{
		private readonly ISharedDependency sharedDependency;
		private readonly IPresenter presenter;

		public StepWithSharedDependencies2(ISharedDependency sharedDependency, IPresenter presenter)
		{
			this.sharedDependency = sharedDependency;
			this.presenter = presenter;
		}

		public void Run()
		{
			presenter.ShowMessage("StepWithSharedDependencies2");
			foreach (var data in sharedDependency.GetData())
				presenter.ShowMessage(" - " + data);
		}
	}

oraz jeden zupełnie niezależny (dla dopełnienia przykładu):


	internal class StepWithoutSharedDependencies : IStep
	{
		private readonly IPresenter presenter;

		public StepWithoutSharedDependencies(IPresenter presenter)
		{
			this.presenter = presenter;
		}

		public void Run()
		{
			presenter.ShowMessage("StepWithoutSharedDependencies");
		}
	}

Każdy z kroków przekazuje do prezentera wiadomość zawierającą jego nazwę oraz dodatkowo pierwszy z nich dodaje do naszej zależności nowy wpis, a drugi wypisuje wszystkie wpisy.

Presenter jest w tym przykładzie najprostszą możliwą implementacją pozwalającą śledzić przebieg procesu:


	internal class ConsolePresenter : IPresenter
	{
		public void ShowMessage(string message)
		{
			Console.WriteLine(message);
		}
	}

Przyjrzyjmy się dokładniej naszej współdzielonej zależności:


	public interface ISharedDependency
	{
		IEnumerable<string> GetData();

		void AddToData(string s);
	}

Posiada ona 2 metody do zarządzania kolekcją. Chciałbym przy okazji tego wpisu pokazać w jaki sposób tworzyć cache (np. dla Dao) poprzez dekoratory. Dlatego podstawowa implementacja nie zawiera w ogóle kolekcji:


	internal class SharedDependency : ISharedDependency
	{
		public IEnumerable<string> GetData()
		{
			//Do nothing
			return new string[0];
		}

		public void AddToData(string s)
		{
			//Do nothing
		}
	}

Jest to celowy zabieg, ponieważ dane przechowamy w cache’u, którym udekorujemy naszą zależność podczas rejestracji komponentów w kontenerze.
Cache:


	internal class DependencyCache : ISharedDependency
	{
		private readonly ISharedDependency sharedDependency;
		private readonly List<string> cache = new List<string>();

		public DependencyCache(ISharedDependency sharedDependency)
		{
			this.sharedDependency = sharedDependency;
		}

		public IEnumerable<string> GetData()
		{
			//Always return from cache, sharedDependency is fake DB
			return cache;
		}

		public void AddToData(string s)
		{
			cache.Add(s);
			sharedDependency.AddToData(s);
		}
	}

Pozostaje, już tylko implementacja menadżera procesu:


	internal class StepManager : IStepManager
	{
		private readonly IEnumerable<IStep> steps;

		public StepManager(IEnumerable<IStep> steps)
		{
			this.steps = steps;
		}

		public void RunSteps()
		{
			foreach (var step in steps)
				step.Run();
		}
	}

oraz fabryki, która go utworzy:


	internal class StepManagerFactory
	{
		private readonly StepWithSharedDependencies stepWithSharedDependencies;
		private readonly StepWithSharedDependencies2 stepWithSharedDependencies2;
		private readonly StepWithoutSharedDependencies stepWithoutSharedDependencies;

		public StepManagerFactory(
			Func<ISharedDependency, StepWithSharedDependencies> stepWithSharedDependenciesProvider,
			Func<ISharedDependency, StepWithSharedDependencies2> stepWithSharedDependencies2Provider,
			StepWithoutSharedDependencies stepWithoutSharedDependencies,
			ISharedDependency sharedDependency)
		{
			stepWithSharedDependencies = stepWithSharedDependenciesProvider(sharedDependency);
			stepWithSharedDependencies2 = stepWithSharedDependencies2Provider(sharedDependency);
			this.stepWithoutSharedDependencies = stepWithoutSharedDependencies;
		}

		public StepManager Create()
		{
			return new StepManager(
				new IStep[]
					{
						stepWithSharedDependencies,
						stepWithoutSharedDependencies,
						stepWithSharedDependencies2
					});
		}
	}

 

Istotną częścią naszej fabryki jest wykorzystanie umiejętności Autofac do tworzenia automatycznych „fabryk” zależności, które pozwalają ręcznie przekazać zależność, podczas użycia fabryki (dokumentacja Autofac).
Chodzi o linie:

Func<ISharedDependency, StepWithSharedDependencies> stepWithSharedDependenciesProvider,
Func<ISharedDependency, StepWithSharedDependencies2> stepWithSharedDependencies2Provider,

W ten sposób Autofac utworzy i przekaże do konstruktora naszej fabryki, funktory tworzące implementacje StepWithSharedDependencies oraz StepWithSharedDependencies2, które przyjmują parametr ISharedDependency. Paremetr ten również otrzymujemy z kontenera. Zapewniamy sobie w ten sposób, że niezależnie od sposobu rejestracji ISharedDependency w kontenerze (może być InstancePerDependency) wszystkie kroki naszego procesu otrzymają tę samą instancję ISharedDependency.

Ostatnią potrzebną nam rzeczą jest zbudowanie kontenera zależności:


	public class AppBuilder
	{
		public IContainer Build()
		{
			var builder = new ContainerBuilder();
			builder.RegisterType<ConsolePresenter>().As<IPresenter>().SingleInstance();

			builder.RegisterType<StepManagerFactory>().SingleInstance();
			builder.Register<IStepManager>(c => c.Resolve<StepManagerFactory>().Create()).SingleInstance();

			builder.RegisterType<StepWithSharedDependencies2>().SingleInstance();
			builder.RegisterType<StepWithoutSharedDependencies>().SingleInstance();
			builder.RegisterType<StepWithSharedDependencies>().SingleInstance();

			builder.RegisterType<SharedDependency>().Named<ISharedDependency>("dep1").SingleInstance();
			builder.RegisterDecorator<ISharedDependency>((c, inner) => new DependencyCache(inner), fromKey: "dep1").SingleInstance();

			return builder.Build();
		}
	}

oraz uruchomienie procesu:


	internal class Program
	{
		private static void Main(string[] args)
		{
			var appBuilder = new AppBuilder();
			var appContainer = appBuilder.Build();

			var stepManager = appContainer.Resolve<IStepManager>();
			stepManager.RunSteps();

			Console.ReadLine();
		}
	}

Wynikiem działania aplikacji będzie:

StepWithSharedDependencies
data added
StepWithoutSharedDependencies
StepWithSharedDependencies2
- data1

Możemy zaobserwować, że pomimo iż poszczególne kroki nie przekazują sobie żadnych danych poprzez „kontekst” lub „singletone’a”, to dzięki zastosowaniu cache Dao, krok 3 w wydajny sposób może wykorzystać dane zmodyfikowane w kroku pierwszym.

W załączonym pliku zip, znajduje się działający przykład.
Przykładowa aplikacja

Comments:0

Leave a Reply

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