Recenzja książki „The Art of Unit Testing: with Examples in .NET”

Opublikowany 21 maja 2011 w C#, Recenzje książek

Ostatnio wpadła mi w ręce książka The Art of Unit Testing: with Examples in .NET autorstwa Roy’a Osherove’a. Muszę przyznać, że mam nieco mieszane uczucia po jej przeczytaniu.

Minusy:
Książka liczy około 280 stron, ale czytając ją miałem wrażenie, że ta liczba jest mocno naciągnięta – olbrzymie marginesy, bardzo szeroka czcionka, dużo niezagospodarowanego miejsca itp. Samym formatowaniem można by było zaoszczędzić przynajmniej kilkadziesiąt stron – ile drzew mogłoby wciąż rosnąć!

Pierwsze trzy rozdziały od biedy można polecić do przeczytania młodszym programistom – tym bardziej zaawansowanym wystarczy pobieżnie przerzucić te 140 stron wyłapując pojawiające się od czasu do czasu słowa-klucze.

W mojej opinii wybór Rhino Mocks też nie jest zbyt właściwy – może faktycznie jest on popularny, ale wydaje mi się, że zawdzięcza to temu, że jest chyba najdłużej na rynku. Niestety z tego samego powodu jego stosowanie jest bardzo uciążliwe. Dużo łatwiej jest mi wytłumaczyć pewne rzeczy juniorom na przykładzie młodszych i chyba przez to bardziej nowoczesnych frameworków.

Trochę bezsensowne moim zdaniem załączniki z długimi opisami różnych narzędzi – takie rzeczy szybko się dezaktualizują – dla mnie to kolejne 20 stron do „odciśnięcia”.

Plusy:
Pomimo tego, iż książka mówi o testowaniu, nie znajdziemy w niej nachalnej promocji TDD – są tam raptem 3 strony na ten temat, z których możemy dowiedzieć się jak TDD działa, jakie są plusy i minusy itp.

Autor wyjaśnia, co to znaczy pisać dobre testy jednostkowe (generalnie testy muszą być godne zaufania, łatwe w utrzymaniu oraz czytelne) oraz jak poznać te źle napisane. Z książki możemy dowiedzieć się również, że kod testów jest równie istotny, jak produkcyjny kod źródłowy. Opisane zostało także zarządzanie testami, wykorzystanie ciągłej integracji do automatycznego ich uruchamiania, ogromne znaczenie testów zwłaszcza podczas refaktoryzacji kodu itp.

Ostatnie rozdziały mówią o tym, jak wprowadzić pisanie testów do organizacji (autor porusza bardzo ciekawe tematy – np. jak radzić sobie z trudnymi ludźmi, jak stać się promotorem zmian, jak wybrać potencjalne miejsca, od których można zacząć itp.), a także jak pracować ze starym kodem.

Podsumowanie:
Widać, że autor ma olbrzymie doświadczenie i zna bardzo dobrze temat, o którym pisze. Jednak dla mnie ta książka powinna być przynajmniej o połowę cieńsza – jeżeli autor chciał mieć tyle stron, to powinien dodać kilka dodatkowych rozdziałów – np. rozwinąć myśl o wspomnianym tylko wykorzystaniu kontenerów IoC itp. Wydaje mi się, że warto tę książkę podsunąć do przeczytania młodszym programistom (oraz tym, którzy nie mieli do tej pory styczności z testami jednostkowymi) – dla nich ta książka będzie idealna. Dla tych, którzy już na co dzień piszą testy – dobrze jest ją przeczytać, aby uporządkować swoją wiedzę, ale wymienione wyżej minusy mogą w niektórych momentach spowodować lekką irytację podczas lektury.

Z mieszanymi uczuciami, ale jednak polecam dla każdego programisty C#.

Podziel się na:
  • RSS
  • Twitter
  • Facebook
  • dotnetomaniak.pl
  • develway.pl
  • Blip
  • Grono.net
  • Spinacz
  • LinkedIn
  • Technorati
  • del.icio.us
  • PDF
  • Drukuj
  • email
komentarze: 1 » tagi: , , ,

[EN] WeakDelegate for all types of delegates

Opublikowany 6 marca 2011 w C#

As I promised in one of my previous posts I’m showing you corrected WeakDelegate<TDelegate> which is working with all types of delegates. The implementation was less challenging than I expected. Fortunately everything could be done with Expression Trees and usage of dynamic was not necessary.
And now it’s time for some source code:

public class WeakDelegate<TDelegate>
	where TDelegate : class {
	private readonly WeakReference _weakTarget;
	private readonly TDelegate _proxyDelegate;
	private Action<TDelegate> _cleanUp;

	public WeakDelegate( TDelegate @delegate, Action<TDelegate> cleanUp ) {
		var d = @delegate as Delegate;

		if( d == null )
			throw new ArgumentNullException( "delegate" );

		if( d.Target == null || d.Method == null || d.Method.IsStatic )
			throw new ArgumentException(
				"Can't create weak delegate to static method." );

		this._weakTarget = new WeakReference( d.Target );
		this._cleanUp = cleanUp;

		var targetType = d.Target.GetType();
		var returnType = d.Method.ReturnType;

		var lambdaArgs = d.Method.GetParameters()
			.Select( ( p, i ) =>
				Expression.Parameter( p.ParameterType, "a" + i ) )
			.ToArray();

		var targetVar = Expression.Variable( targetType, "target" );

		var getTargetCall = Expression.Invoke( (Expression)
			this.GetType()
			.GetMethod( "GetTargetBoundExpression",
				BindingFlags.Instance | BindingFlags.NonPublic )
			.MakeGenericMethod( targetType )
			.Invoke( this, new object[ 0 ] ) );

		var returnLabel = Expression.Label( returnType );
		var defaultReturn = Expression.Default( returnType );

		var lambda = Expression.Lambda<TDelegate>( Expression.Block(
			new[] { targetVar },
			new Expression[] {
				Expression.Assign( targetVar, getTargetCall ),
					Expression.IfThenElse(
						Expression.Equal( targetVar, Expression.Constant( null ) ),
						Expression.Return( returnLabel, defaultReturn ),
						Expression.Return( returnLabel,
							Expression.Call( targetVar, d.Method, lambdaArgs ) ) ),
					Expression.Label( returnLabel, defaultReturn )
				} ), lambdaArgs );

		this._proxyDelegate = lambda.Compile();
	}

	public static implicit operator TDelegate(
		WeakDelegate<TDelegate> weakDelegate ) {
		return weakDelegate != null ? weakDelegate._proxyDelegate : null;
	}

	private Expression GetTargetBoundExpression<TTarget>() {
		return (Expression<Func<TTarget>>) ( () => this.GetTarget<TTarget>() );
	}

	private TTarget GetTarget<TTarget>() {
		var target = (TTarget) this._weakTarget.Target;
		if( target != null )
			return target;

		if( this._cleanUp != null ) {
			this._cleanUp( this._proxyDelegate );
			this._cleanUp = null;
		}

		return target;
	}
}

In lines from 23 to 50 using types read from original delegate and expression trees I’m creating proxy TDelegate simmilar to the following metacode:

this._proxyDelegate = (a0...aN) => {
	var target = this.GetTarget<TTarget>();
	if( target == null )
		return default( TDelegateReturnType );
	else
		return target.OriginalDelegateMethod( a0...aN );
}

The implementation is sill quite fast – only 6-9 times slower than direct delegate invocation. It has a lower performance than WeakEventHandler<TEventHandler> described in one of my previous posts which is only 4-6 times slower.
There are also few drawbacks of my solution:

  • after the target is garbage collected, the proxy delegate will return default value of the TDelegate‘s return type (I think that this can be solved somehow using for example cleanUp callback)
  • cleaning up is not thread safe etc.

The source code with examples is here.

Podziel się na:
  • RSS
  • Twitter
  • Facebook
  • dotnetomaniak.pl
  • develway.pl
  • Blip
  • Grono.net
  • Spinacz
  • LinkedIn
  • Technorati
  • del.icio.us
  • PDF
  • Drukuj
  • email
komentarze: 0 » tagi: ,

WeakEventHandler – praktyczne wykorzystanie WeakReference

Opublikowany 27 lutego 2011 w C#

Trochę teoretycznego wstępu

Środowisko .NET charakteryzuje się tym, że samo zarządza cyklem życia obiektów. Garbage collector potrafi wykryć, które obiekty nie są już potrzebne i usunąć je z pamięci. Oczywiście jest to bardzo duże uproszczenie całego mechanizmu – szczegóły jego działania są bardzo dobrze wyjaśnione w opisywanej przeze mnie wcześniej książce "CLR via C#, 3rd edition" w rozdziale 21 – Automatic Memory Management (Garbage Collection).

Oprócz automatycznego zarządzania pamięcią CLR umożliwia także ręczne monitorowanie oraz kontrolę cyklu życia obiektów za pomocą GC handle table. Dodawanie oraz usuwanie wpisów do tej tabeli umożliwiają metody struktury GCHandle. Mechanizm ten jest szczególnie przydatny, kiedy komunikujemy się lub współpracujemy w jakiś sposób z kodem niezarządzalnym, ale część właściwości można także wykorzystać, gdy pracujemy tylko z kodem zarządzalnym – konkretnie chodzi mi tutaj o słabe referencje.

W dużej ogólności obiekt A musi posiadać referencję do obiektu B, aby mógł na nim wołać metody:

Diagram 1.

Diagram 1. Mocna referencja z obiektu A do obiektu B.


Sytuacja taka uniemożliwia jednak automatyczne usunięcie obiektu B z pamięci, kiedy nie jest on już potrzebny, a jedyną referencją, jaka go jeszcze trzyma przy życiu, jest referencja posiadana przez obiekt A. Czasami takie zachowanie nie jest pożądane i aby temu zaradzić, możemy posłużyć się klasą WeakReference, która opakowuje strukturę GCHandle umożliwiając łatwiejsze jej wykorzystanie w kodzie.

W praktyce wygląda to następująco. Zamiast takiego kodu:

	class ClassB {
		public void Foo() { }
	}

	class ClassA
	{
		private ClassB _objectB;

		public ClassA( ClassB objectB ) {
			// mocna referencja z A do B
			_objectB = objectB;
		}

		public void CallB() {
			_objectB.Foo();
		}
	}

kod klasy A możemy napisać następująco z wykorzystaniem klasy WeakReference i jej właściwości Target:

	class ClassA {
		private WeakReference _weakObjectB;

		public ClassA( ClassB objectB ) {
			// słaba referencja z A do B
			_weakObjectB = new WeakReference( objectB );
		}

		public void CallB() {
			// pobierz referencję do obiektu B
			var objectB = _weakObjectB.Target as ClassB;
			if( objectB != null ) { // jeżeli obiekt B jest jeszcze w pamięci
				objectB.Foo(); // to możemy zawołać na nim metodę Foo
			}
		}
	}

I praktyczne zastosowanie

Opisany powyżej mechanizm można w praktyczny sposób wykorzystać podczas pracy z eventami. Spójrzmy na uproszczony model:

	class Producer {
		public event EventHandler<EventArgs> Event;

		public void Produce() {
			Console.WriteLine( "Producer.Produce - raising event" );
			if( this.Event != null ) {
				this.Event( this, EventArgs.Empty );
			}
		}
	}

	class Consumer {
		public Consumer( Producer producer ) {
			producer.Event += new EventHandler<EventArgs>( this.Consume );
		}

		private void Consume( object sender, EventArgs e ) {
			Console.WriteLine( "Consumer.Consume - consuming event" );
		}
	}

	static class SimpleWeakReferenceExample {
		public static void Run() {
			var producer = new Producer();
			var consumer = new Consumer( producer );

			producer.Produce();
			consumer = null; // konsument nie jest już nam potrzebny
			GC.Collect();
			// consumer i tak przeżyje,
			// ponieważ pośrednio referencję do niego trzyma consumer
			producer.Produce();
		}
	}

Obiekt klasy Producer jest producentem zdarzeń, na które zapisuje się obiekt klasy Consumer. Tworzy się nam taka oto struktura:

Diagram 2.

Diagram 2. Diagram obiektów dla przykładu Producent-Konsument.


Pomimo tego, że konsument nie jest już potrzebny (linikja 28, czerwona strzałka na diagramie 2), to pośrednio poprzez EventHandler jest powiązany z producentem i będzie żył tak długo, jak i on. Aby temu zaradzić, można np. zaimplementować w klasie Consumer interfejs IDisposable i wołać metodę Dispose, w której konsument wypisuje się ze zdarzenia Event producenta. Niestety czasami w bardziej skomplikowanych przypadkach, nie jest to takie łatwe – kilkukrotnie spotkałem się z takimi sytuacjami. Pomóc może nam poprzednio opisany mechanizm słabych referencji.

Błędny WeakEventHandler z książki "CLR via C#, 3rd edition"

W książce "CLR via C#, 3rd edition" znajduje się przykładowa implementacja klasy WeakEventHandler bazująca na bardziej ogólnej klasie WeakDelegate<TDelegate>, niestety posiada ona bardzo poważny błąd logiczny, którego – o dziwo – nikt do tej pory nie zgłosił. Problem opisałem w poprzednim wpisie i zgłosiłem go do autora.

Podstawowe założenia do implementacji WeakEventHandler<TEventHandler>

Klasa ma umożliwiać tworzenie słabych event handlerów dla trzech typów delegatów:

  • podstawowego, generycznego EventHandler<TEventArgs>
  • niegenerycznego EventHandler
  • dowolnego delegata, którego sygnatura jest zgodna z jednym z dwóch powyższych (ten scenariusz dodałem po dyskusji z Jeffrey’iem Richterem – ma zapewnić wsparcie dla starszego typu eventów obecnych w dużych ilościach zwłaszcza w WinForms)

Nasza struktura obiektów będzie wyglądać następująco:

Diagram 3.

Diagram 3. Wymagane połączenia obiektów.


Przerywaną linią jest zaznaczona słaba referencja. Musimy także zająć się mocną referencją od producenta do naszego niebieskiego delegata – po usunięciu konsumenta, kiedy delegat już nie jest potrzeby, powinien sam wyrejestrować się ze zdarzenia.

Implementacja WeakEventHandler<TEventHandler>

Sporo eksperymentowałem z różnymi implementacjami, aż w końcu doszedłem do czegoś takiego:

public sealed class WeakEventHandler<TEventHandler>
	where TEventHandler : class
{
	private delegate void OpenEventHandler<TTarget, TEventArgs>
		( TTarget target, object sender, TEventArgs eventArgs )
		where TTarget : class
		where TEventArgs : EventArgs;

	private readonly WeakReference _weakTarget;
	private readonly Delegate _openEventHandler;
	private readonly Action<TEventHandler> _cleanUp;
	private readonly TEventHandler _proxyHandler;

	public WeakEventHandler(
		TEventHandler eventHandler, Action<TEventHandler> cleanUp ) {
		var d = eventHandler as Delegate;
		if( d == null || d.Target == null )
			throw new ArgumentException(
				"Can't make weak event handler to static method.", "d" );

		if( cleanUp == null )
			throw new ArgumentNullException( "cleanUp" );

		// store event handler target as a WeakReference
		_weakTarget = new WeakReference( d.Target );
		_cleanUp = cleanUp;

		// extract the types from the event handler
		var eventHandlerType = typeof( TEventHandler );
		var targetType = d.Target.GetType();
		var eventArgsType = eventHandlerType.IsGenericType
			? eventHandlerType.GetGenericArguments()[ 0 ]
			: eventHandlerType.GetMethod( "Invoke" )
				.GetParameters()[ 1 ].ParameterType;

		// create open event handler for fast calling of the real event handler
		Type openEventHandlerType = typeof( OpenEventHandler<,> )
			.MakeGenericType( eventHandlerType, targetType, eventArgsType );
		_openEventHandler = Delegate.CreateDelegate(
			openEventHandlerType, null, d.Method );

		// create proxy handler that points to our proxy method
		var proxyHandlerMethod =
			typeof( WeakEventHandler<TEventHandler> )
			.GetMethod(
				"ProxyHandler",
				BindingFlags.Instance | BindingFlags.NonPublic )
			.MakeGenericMethod( targetType, eventArgsType );
		_proxyHandler =
			Delegate.CreateDelegate( eventHandlerType, this, proxyHandlerMethod )
			as TEventHandler;
	}

	private void ProxyHandler<TTarget, TEventArgs>(
		object sender, TEventArgs e )
		where TTarget : class
		where TEventArgs : EventArgs
	{
		var target = _weakTarget.Target as TTarget;
		var openEventHandler = this._openEventHandler
			as OpenEventHandler<TTarget, TEventArgs>;

		// if target is still alive
		if( target != null || openEventHandler == null )
			openEventHandler( target, sender, e ); // call it
		else
			_cleanUp( _proxyHandler ); // else clean up
	}

	public static implicit operator TEventHandler(
		WeakEventHandler<TEventHandler> weakEventHandler ) {
		return weakEventHandler != null
			? weakEventHandler._proxyHandler
			: null;
	}
}

Implementacja spełnia postawione wcześniej założenia, co ważne – jest relatywnie szybka – około 4-6 razy wolniejsza, niż bezpośrednie wywołanie delegata. Musimy także podać delegat cleanUp, który umożliwi wyrejestrowanie się ze zdarzenia i posprzątanie, kiedy _target zostanie już usunięty. Wszystkie niezbędne dane są wydobywane z oryginalnego eventHandlera, następnie tworzone są: tzw. open event handler – delegat, który umożliwia późniejsze szybkie wołanie metody na konsumencie oraz proxy event handler – delegat, który wskazuje na generyczną metodę ProxyHandler.

Rozszerzenia

Aby ułatwić używanie ww. implementacji, można skorzystać z dodatkowych extension methods:

public static class WeakEventHandlerExtensions {
	// usage:
	// p.SomeEvent += new SomeEventHandler( c.HandleEvent )
	//                 .AsWeak( eh => p.SomeEvent -= c.HandleEvent );
	public static TEventHandler AsWeak<TEventHandler>(
		this TEventHandler eventHandler, Action<TEventHandler> cleanUp )
		where TEventHandler : class {
		return new WeakEventHandler<TEventHandler>( eventHandler, cleanUp );
	}

	// usage:
	// p.SomeEvent += new EventHandler( c.HandleEvent )
	//                 .AsWeak( eh => p.SomeEvent -= c.HandleEvent );
	public static EventHandler AsWeak(
		this EventHandler eventHandler, Action<EventHandler> cleanUp ) {
		return new WeakEventHandler<EventHandler>( eventHandler, cleanUp );
	}

	// usage:
	// p.SomeEvent += new EventHandler<SomeEventArgs>( c.HandleEvent )
	//                 .AsWeak( eh => p.SomeEvent -= c.HandleEvent );
	public static EventHandler<TEventArgs> AsWeak<TEventArgs>(
		this EventHandler<TEventArgs> eventHandler,
		Action<EventHandler<TEventArgs>> cleanUp )
		where TEventArgs : EventArgs {
		return new WeakEventHandler<EventHandler<TEventArgs>>(
			eventHandler, cleanUp );
	}
}

Użycie

// decision on event consumer side:
producer.Event += new EventHandler( this.HandleEvent )
	.AsWeak( eh => producer.Event -= eh );

// decision on event producer side
// caution, with this construction it's not possible
// to deregister from the event
public class EventProvider {
	private EventHandler<EventArgs> _event;
	public event EventHandler<EventArgs> Event {
		add {
			this._event += value.AsWeak( eh => this._event -= eh );
		}
		remove {
			this._event -= value;
		}
	}

	public void Raise() {
		this._event( this, EventArgs.Empty );
	}
}

Jak widać z załączonych przykładów, użycie klasy WeakEventHandler poprzez extension methods AsWeak jest bardzo proste. Ciekawostką jest możliwość wykorzystania rozszerzeń także po stronie producenta i stworzenie słabego eventa – Jeffrey Richter dostrzegł w tym kodzie błąd – a mianowicie nie jest możliwe wyrejestrowanie konsument z eventa. Aby tego dokonać, należałoby zmienić lekko implementację części remove oraz dodać metody pozwalające na porównanie obiektów WeakEventHandler<TEventHandler< z obiektami TEventHandler.

Implementację klasy wraz z rozszerzeniami oraz opisywanymi przykładami można pobrać tutaj.

Podziel się na:
  • RSS
  • Twitter
  • Facebook
  • dotnetomaniak.pl
  • develway.pl
  • Blip
  • Grono.net
  • Spinacz
  • LinkedIn
  • Technorati
  • del.icio.us
  • PDF
  • Drukuj
  • email
komentarze: 0 » tagi: ,