Regions or no regions – this is the question

Die dnc12 ist gerade vorbei und wir k├Ânnten uns schon wieder zusammensetzen ­čÖé
Heute gab es auf Twitter eine kurz angerissene Diskussion, die das Zeug dazu hatte, die Gem├╝ter aufzuheizen: soll man oder soll man nicht #regions nutzen?

 

Nach dem kurzen Tweet-Austausch wurde es klar, dass es viele Entwickler gibt, die Regions gerne nutzen. Ich habe zwar den ganzen Abend nachgedacht, habe allerdings keine Gr├╝nde gefunden, sie selbst verwenden zu wollen.

 

Ich meine, warum sollte man Code NICHT sehen wollen?

  • Geht es vielleicht um eine oder mehrere Methoden, die man ausblenden will? Das w├╝rde aber entweder bedeuten, dass man
    – die Funktionalit├Ąt der ganzen Klasse ausblendet, aber dann wozu, man ├Âffnet einfach die Klasse nicht ­čśë
    – nur ein Teil der Funktionen ausblendet, und dann stellt sich die Frage, warum manche Funktionen viel ├Âfter angeschaut werden als andere? Das hat f├╝r mich irgendwie ein CodeSmell
  • Geht es vielleicht um ein Teil einer einzigen Methode, und zwar einer ganz gro├čen, sonst w├╝rde man sie nicht teilweise ausblenden wollen? Zusammengeklappt w├╝rde man dann eine Art Kommentar sehen, was mich sofort an Martin Fowlers Hinweis bez├╝glich Kommentare erinnert hat: Kommentare sind ideale Namensgeber. Wenn man im Code einen Kommentar braucht, dann ist das meistens ein Smell f├╝r ein Extract Method (genauso wie die Tatsache, dass die Methode wahrscheinlich zu lang ist)

    You have a code fragment that can be grouped together.

    Turn the fragment into a method whose name explains the purpose of the method.

  • Geht es vielleicht um Regionen um Methoden, Events, Fields, Properties, also nach Sichtbarkeit und Rolle? Daf├╝r k├Ânnte ich einen einzigen Grund vorstellen, und zwar den, dass man auf Anhieb die ├Âffentliche Methoden und Eigenschaften sehen will. Das w├Ąre allerdings die Aufgabe eines Interfaces, oder? Dazu kommt auch noch meine – pers├Ânliche – Vorliebe, Code wie ein Buch zu lesen, von oben nach unten, also von einer ├Âffentlichen Methode weiter in die Details, also zu den privaten Methoden (ganz nach CCD – Single Level of Abstraction (SLA))

    Hilfreich als Analogie ist der Blick auf Artikel in der Tageszeitung: dort steht zu oberst das Allerwichtigste, die ├ťberschrift. Aus ihr sollte in groben Z├╝gen hervorgehen, wovon der Artikel handelt. Im ersten Satz des Artikels wird dies auf einem hohen Abstraktionsniveau beschrieben. Je weiter man im Artikel fortschreitet, desto mehr Details tauchen auf. So k├Ânnen wir auch unseren Code strukturieren. Der Name der Klasse ist die ├ťberschrift. Dann folgen die ├Âffentlichen Methoden auf hohem Abstraktionsniveau. Diese rufen m├Âglicherweise Methoden auf niedrigerem Niveau auf, bis zuletzt die “Bitpfriemelmethoden” ├╝brig bleiben. Durch diese Einteilung kann ich als Leser der Klasse entscheiden, welchen Detaillierungsgrad ich mir ansehen m├Âchte. Interessiert mich nur grob, wie die Klasse arbeitet, brauche ich mir nur die ├Âffentlichen Methoden anzuschauen. In ihnen wird die Funktionalit├Ąt auf einem hohen Abstraktionsniveau gel├Âst. Interessieren mich weitere Details, kann ich tiefer einsteigen und mir die privaten Methoden ansehen.

Was meint ihr, ├╝bersehe ich da was? (Notiz an mich: bei #nossued das Gespr├Ąch fortsetzen!)

Sind Unit Tests wirtschaftlich untragbar?

Immer wieder h├Âre ich die Aussagen “Unit Tests sind sch├Ân und gut, wir haben nur keine Zeit daf├╝r.” Oder “Klar, man kann Tests machen, hauptsache, es nimmt nicht zu viel Zeit von der Arbeit weg” ??!!

 

Nur um Missverst├Ąndnisse zu vermeiden: Tests macht man nicht zum Spa├č oder aus Langeweile, die Tests stellen die Essenz, die abstraktester Form der L├Âsung dar!

 

Ich habe bis heute Schwierigkeiten damit, meinem Gegen├╝ber zu erkl├Ąren, dass er sich irrt. Ich WEI├č es einfach aus Erfahrung, dass dies eine Milchm├Ądchenrechnung ist. Keine ernsthafte Argumente gegen Tests w├╝rden bei einer tieferen ├ťberpr├╝fung standhalten. Die Pros ├╝bertreffen klar die Kontras. Aber wie soll ich etwas – f├╝r mich – Offensichtliches in Worte fassen? Wie soll ich etwas in ein paar S├Ątzen erkl├Ąren, was ich in einem andauernden Prozess durch jeden NICHT (oder nicht richtig) geschriebenen Test gelernt habe? Oder durch jeden Aha-Effekt oder durch jeden stressfreien Release (kein Stress entsteht, wo kein Platz f├╝r Bugs existiert ­čśë )

 

Unit Tests sind f├╝r den Open Mind “selbsterkl├Ąrend”: wenn der Bug in einem ungetesteten Code steckt, dann wird das zu einer “blinden” Fehlersuche f├╝hren, die Tage dauern kann und auf jedem Fall Geld und Ruf kostet. Wie lange dauert es, den Fehler in einem getesteten Codebasis zu finden, wo die Eingrenzung innerhalb von Sekunden erfolgt? Wie oft kommt es ├╝berhaupt vor, dass dieses Problem entsteht? F├╝r mich schaut die Rechnung so aus:


Zeit_f├╝r_fehlersuche = Unproduktive_Zeit;
f(Unproduktive_Zeit) = Verschwendetes_Geld;

0->Zeit_f├╝r_fehlersuche(getesteter_Code)----------------------->Zeit_f├╝r_fehlersuche(ungetesteter_Code)---.....oo

Ok, ich glaube, ihr kennt jetzt meinen Standpunkt ­čśë Aber ich bin ja nicht die ultimative Ma├čstab daf├╝r, wie man arbeiten sollte. Deshalb habe ich ein paar Artikel und Statistiken von kl├╝geren Leuten zusammengesucht, bitte liest die auch.

 

Diese Infos habe ich bei stackoverflow gefunden:

Realizing quality improvement through test driven development: results and experiences of four industrial teams und hier eine Diskussion dar├╝ber.

The study and its results were published in a paper entitled Realizing quality improvement through test driven development: results and experiences of four industrial teams, by Nagappan and research colleagues E. Michael Maximilien of the IBM Almaden Research Center; Thirumalesh Bhat, principal software-development lead at Microsoft; and Laurie Williams of North Carolina State University. What the research team found was that the TDD teams produced code that was 60 to 90 percent better in terms of defect density than non-TDD teams. They also discovered that TDD teams took longer to complete their projectsÔÇö15 to 35 percent longer.

ÔÇťOver a development cycle of 12 months, 35 percent is another four months, which is huge,ÔÇŁ Nagappan says. ÔÇťHowever, the tradeoff is that you reduce post-release maintenance costs significantly, since code quality is so much better. Again, these are decisions that managers have to makeÔÇöwhere should they take the hit? But now, they actually have quantified data for making those decisions.ÔÇŁ

Es gab auch kleinere Experimente dazu wie z.B. Code Lab – TDD vs. Non-TDD

Over 3 iterations, average time taken to complete the kata without TDD was 28m 40s. Average time with TDD was 25m 27s. Without TDD, on average I made 5.7 passes (delivering into acceptance testing). With TDD, on average I made 1.3 passes (in two attempts, they passed first time, in one it took 2 passes.)

Now, this was a baby experiment, of course. And not exactly laboratory conditions. But I note a couple of interesting things, all the same.

Und weil ein Bild mehr als tausend Worte spricht: Die Kostenverteilung bei getesteten Code schaut ungef├Ąhr so aus
Testing Benefits

Tests schreiben ist einfach, der Ertrag ist riesig. Warum soll man also keine Tests schreiben? Sind wir wirklich unfehlbar, schreiben wir immer den perfekten Code? Seien wir mal ehrlich…Ich bin es sicher nicht und ihr auch nicht.

 

Und hier noch die obligatorische Buchempfehlung: The Art Of Unit Testing Das Buch ist wunderbar verst├Ąndlich geschrieben mit echten Beispielen und guten Argumenten, warum und wie man testen soll.

Webforms mit TDD entwickeln

Diese hier ist schon wieder eine wunderbare Idee aus Jimmy Nilssons Applying Domain-Driven Design and Patterns (das Buch scheint bis zur letzten Seite super Tipps zu liefern ­čśë ) und zwar von Ingemar Lundberg.

Was ist eigentlich die Aufgabe einer Webseite: irgendwelche Controls mit Text zu f├╝llen. Was dieser Text beinhaltet, dass wird von verschiedenen Funktionen entschieden. (Wie er ausgegeben wird, interessiert nicht.) Die Hauptaufgabe also bei der testgetriebenen Entwicklung von Webforms ist, diese Funktionalit├Ąten zu ermitteln und zu implementieren. So bekommt man eine Webanwendung, bei der die Hauptbereiche getestet sind und nur die eigentliche Html-Ausgabe ungetestet bleibt. Au├čerdem wird auf dieser Art sichergestellt, dass die View sonst nichts tut.

Nehmen wir ein einfaches Beispiel: das F├╝llen eines Warenkorbs. Es stehen 3 Produkte zur Auswahl und der K├Ąufer darf in seinen Warenkorb maximal 3 stellen. Um etwas Logik dabei zu haben, wird festgelegt, dass von ein Produkt nur maximal 2-mal gew├Ąhlt werden darf. Wenn diese Bedingung erf├╝llt ist, soll das Produkt nicht mehr ausw├Ąhlbar sein. Gleiches gilt, wenn im Korb bereits 3 Produkte sind, kein Produkt darf mehr ausw├Ąhlbar sein.

Shopping Cart

Das Ausw├Ąhlen eines Produktes passiert z.B. mit einem OnClick-Event auf dem Link. Aus der Sicht der Funktionalit├Ąt ist das nicht wichtig, hauptsache das Event wird ausgel├Âst.

Was tut also ein Modell um eine View zu steuern: nachdem es sichergestellt hat, dass alle Controls leer sind, l├Ądt es die Daten mit irgendeiner Repository (nennen wir sie IDeposit), gibt sie der View und veranlasst diese, die Daten zu rendern. Danach muss es die ├╝bermittelten Daten identifizieren k├Ânnen und, wenn es OK ist, muss es diese mit einer anderen Repository (die nennen wir IAcquisition) abspeichern. Mit diesem “ist OK” wird sichergestellt, dass die obigen Regeln eingehalten wurden, also dass nicht zu viele Produkte bzw. identische Produkte ausgew├Ąhlt wurden. Danach muss die View die Daten wieder rendern.

Mit diesen Informationen k├Ânnen wir bereits das Produkt und die 2 Interfaces definieren, die wir hier als Blackbox betrachten:

namespace WebformMVP.Tests
{
//Wegen der Bedingung "nicht mehr als zwei vom selben Typ" muss eine Product-Klasse geben. Sonst w├╝rde auch ein string reichen
public class Product
{
public string Name;
public int Type;
public Product(string name, int type)
{
Name = name;
Type = type;
}
}

public interface IDeposit
{
IList<Product> Load();
}

public interface IAcquisition
{
void Add(Product product);
}
}

Jetzt ist endlich Zeit f├╝r den ersten Test. Wie ich schon am Anfang geschrieben habe, eine View muss einfach nur Text darstellen. Um die View simulieren zu k├Ânnen, wird sie von einem Interface abgeleitet, genauso wie die Testklasse, unsere Fakeview. Diese bekommt als Felder strings anstelle von Controls, die allerdings korrekt gef├╝llt werden m├╝ssen. Wir tun so als ob, wir abstrahieren die View auf das Minimum:

namespace WebformMVP.Tests
{
public interface IShoppingView
{
void AddSourceItem(string text, bool available);
}

[TestFixture]
public class Tests : IShoppingView
{
string m_sourcePanel;
string m_shoppingCartPanel;

[Test]
public void FillSourcePanel()
{
m_model.Fill();
m_model.Render();

Assert.That(m_sourcePanel, Is.EqualTo("Product 1 available; Product 2 available; Product 3 available; "));
}
}
}

So wird die Anwendung nat├╝rlich nicht mal kompiliert :), dazu brauchen wir noch ein paar Schritte.

Dadurch, dass die Testklasse von diesem Interface ableitet, sind wir in der Lage, die Methoden entsprechend ├╝berschreiben zu k├Ânnen. Dieser Trick nennt sich Implement Interfaces Explicitly. Gleichzeitig lassen wir die Klasse auch von IDeposit ableiten, um auch dessen Methode zu ├╝berschreiben:

namespace WebformMVP.Tests
{
[TestFixture]
public class Tests : IShoppingView, IDeposit
{
string m_sourcePanel;
string m_shoppingCart;
IList<Product> m_sources= new List<Product>{new Product("Product 1", 1), new Product("Product 2", 2), new Product("Product 3", 3)};

void IShoppingView.AddSourceItem(string text, bool available)
{
m_sourcePanel += text + (available ? " available;": string.Empty) + " ";
}

IList<Product> IDeposit.Load()
{
return m_sources;
}

[Test]
public void FillSourcePanel()
{
m_model.Fill();
m_model.Render();

Assert.That(m_sourcePanel, Is.EqualTo("Product 1 available; Product 2 available; Product 3 available; "));
}
}
}

Es funktioniert immer noch nicht, wir brauchen ja noch ein Modell.

namespace WebformMVP.Tests
{
public class ShoppingModel
{
public void Fill()
{
throw new NotImplementedException();
}
public void Render()
{
throw new NotImplementedException();
}
}

[TestFixture]
public class Tests : IShoppingView, IDeposit
{
...
private ShoppingModel m_model;

//Es muss sichergestellt werden, dass beim Laden der View alle Felder leer sind.
[SetUp]
public void Setup()
{
m_sourcePanel = string.Empty;
m_shoppingCart = string.Empty;
m_model= new ShoppingModel();
}

[Test]
public void FillSourcePanel()
{
m_model.Fill();
m_model.Render();

Assert.That(m_sourcePanel, Is.EqualTo("Product 1 available; Product 2 available; Product 3 available; "));
}
}
}

Ok, es kompiliert endlich! Aber wir gehen ja nach TDD vor, der Test ist wie gew├╝nscht rot :D. Die 2 Methoden Fill und Render sind noch nicht implementiert.
Was sollen die Methoden tun? Fill() sollte eine lokale Liste mit Hilfe der Deposit-Repository f├╝llen und Render() soll diese Elemente in das SourcePanel-Feld der View schreiben. Also muss unser Modell eine Liste, das IDeposit-Interface und das IShoppingVew als neue Member bekommen. Letzteren werden nat├╝rlich injectet (s. Dependency Inversion):

public class ShoppingModel
{
IDeposit m_deposit;
IList<Product> m_products;
[NonSerialized]IShoppingView m_view;

public ShoppingModel(IDeposit deposit)
{
m_deposit = deposit;
}

public void SetView( IShoppingView view )
{
m_view = view;
}
public void Fill()
{
m_products = m_deposit.Load();
}
public void Render()
{
foreach( Product product in m_products )
{
m_view.AddSourceItem( product.Name, true );
}
}
}

[TestFixture]
public class ShoppingCartTests:IShoppingView,IDeposit
{
...
private ShoppingModel m_model;

[SetUp]
public void Setup()
{
m_model = new ShoppingModel(this);
m_model.SetView( this );
m_sourcePanel = string.Empty;
m_cartPanel = string.Empty;
}

[Test]
public void FillSourcePanel()
{
m_model.Fill();
m_model.Render();

Assert.That( m_sourcePanel, Is.EqualTo( "Product 1 available; Product 2 available; Product 3 available; " ) );
}
...
}

Der Test ist gr├╝n! Jetzt ist sicher klar wie es weitergeht und ich will den Artikel nicht noch l├Ąnger machen. Hier sind also die n├Ąchsten Tests und die Implementierung dazu:

[TestFixture]
public class ShoppingCartTests:IShoppingView,IDeposit
{
private string m_sourcePanel;
private string m_cartPanel;
private IList<Product> m_products = new List<Product> { new Product( "Product 1", 1 ), new Product( "Product 2", 2 ), new Product( "Product 3", 3 ) };
private ShoppingModel m_model;

[SetUp]
public void Setup()
{
m_model = new ShoppingModel(this, new Cart());
m_model.SetView( this );
m_sourcePanel = string.Empty;
m_cartPanel = string.Empty;
}
...
[Test]
public void AddAnItem()
{
m_model.Fill();
m_model.AddAt( 0 );
m_model.Render();

Assert.That( m_sourcePanel, Is.EqualTo( "Product 1 available; Product 2 available; Product 3 available; " ) );
Assert.That( m_cartPanel, Is.EqualTo( "Product 1 " ) );
}

[Test]
public void AddTwoItemsOfAKind()
{
m_model.Fill();
m_model.AddAt( 0 );
m_model.AddAt( 0 );
m_model.Render();

Assert.That( m_sourcePanel, Is.EqualTo( "Product 1 Product 2 available; Product 3 available; " ) );
Assert.That( m_cartPanel, Is.EqualTo( "Product 1 Product 1 " ) );
}

[Test]
public void AddThreeDifferentItems()
{
m_model.Fill();
m_model.AddAt( 0 );
m_model.AddAt( 2 );
m_model.AddAt( 1 );
m_model.Render();

Assert.That( m_sourcePanel, Is.EqualTo( "Product 1 Product 2 Product 3 " ) );
Assert.That( m_cartPanel, Is.EqualTo( "Product 1 Product 3 Product 2 " ) );
}
...
void IShoppingView.AddCartItem( string text )
{
m_cartPanel += text + " ";
}
}
public class ShoppingModel
{
IDeposit m_deposit;
IList<Product> m_products;
ICart m_cart;
[NonSerialized] IShoppingView m_view;

public ShoppingModel(IDeposit deposit, ICart cart)
{
m_deposit = deposit;
m_cart = cart;
m_view = view;
m_products = new List<Product>();
}
public void SetView( IShoppingView view )
{
m_view = view;
}

public void Fill()
{
m_products = m_deposit.Load();
}

public void Render()
{
foreach( Product product in m_products )
{
m_view.AddSourceItem( product.Name, m_cart.IsOkToAdd(product) );
}
foreach( Product product in m_cart.List )
{
m_view.AddCartItem( product.Name );
}
}

public void AddAt( int index )
{
var product = m_products[index];
m_cart.Add( product );
}
}

public interface ICart
{
bool IsOkToAdd( Product product );
void Add( Product product );
IList<Product> List { get; }
}

public class Cart :ICart{

private IList<Product> m_cartItems = new List<Product>();

public bool IsOkToAdd( Product product )
{
return m_cartItems.Where( a => a.Type == product.Type ).Count() < 2 && m_cartItems.Count < 3;
}

public void Add( Product product )
{
m_cartItems.Add( product );
}

public IList<Product> List
{
get { return m_cartItems; }
}
}

Die einzige gr├Â├čere ├änderung zum ersten Test ist das neue ICart-Objekt. Da es hier um mehr als es eine Liste geht (irgendwo muss ja die Logik der maximal 2 gleichen Produkte pro Warenkorb errechnet werden), habe ich daf├╝r das Interface und die Klasse definiert.

Jetzt sind wir fast fertig. Es muss lediglich die abstrahierte Umgebung in eine Webanwendung nachgebaut werden. Das hei├čt, wir implementieren die Methoden Page_Load(), Pre_Render() und AddAt_Click() und die Methoden des Interface IShoppingView. Um die Kontrolle zu behalten, l├Âschen wir den Designer und schalten den ViewState aus (deswegen mag ich diesen Ingemar so sehr ­čśë ).

//default.aspx
<%@ Page Language="C#" AutoEventWireup="true" EnableViewState="false" CodeBehind="Default.aspx.cs" Inherits="ShoppingCart.Web._Default" %>

<html xmlns="http://www.w3.org/1999/xhtml" >
<head runat="server">
<title>Shopping Cart</title>
</head>
<body>
<form id="form1" runat="server">
<div>
<asp:Panel runat="server" ID="srcPanel"></asp:Panel>
<asp:Panel runat="server" ID="cartPanel"></asp:Panel>
</div>
</form>
</body>
</html>

//default.aspx.cs
using System;
using System.Collections.Generic;
using System.Web.UI.HtmlControls;
using System.Web.UI.WebControls;

namespace ShoppingCart.Web
{
public class _Default : System.Web.UI.Page, IShoppingView
{
protected ShoppingModel model;
protected HtmlTable srcTable, cartTable;
protected Panel srcPanel, cartPanel;

protected void Page_Load( object sender, EventArgs e )
{
if( !IsPostBack )
{
model = new ShoppingModel(new FakeDeposit(),new Cart());
//Speichern, hier in Session aber sonst nat├╝rlich mit einer Repository
Session["ShoppingModel"] = model;
model.Fill();
}
else
{
model = (ShoppingModel)Session["ShoppingModel"];
}
model.SetView( this );
ModelRender();
}

protected void Page_PreRender()
{
srcPanel.Controls.Clear();
cartPanel.Controls.Clear();
ModelRender();
}

private void ModelRender()
{
srcTable = new HtmlTable();
srcPanel.Controls.Add( srcTable );
srcTable.Width = "50%";
srcTable.Border = 1;

cartTable = new HtmlTable();
cartPanel.Controls.Add( cartTable );
cartTable.Width = "50%";
cartTable.Border = 1;
cartTable.BgColor = "#cccccc";

model.Render();
}
public void AddSourceItem( string text, bool available )
{
int index = srcTable.Rows.Count;
HtmlTableRow tr = new HtmlTableRow();
HtmlTableCell tc = new HtmlTableCell { InnerText = text };
if( available )
{
LinkButton lb = new LinkButton();
tc.Controls.Add( lb );
lb.ID = index.ToString();
lb.Text = ">>";
lb.Click += AddAt_Click;
}
tr.Cells.Add( tc );
srcTable.Rows.Add( tr );
}
private void AddAt_Click( object sender, EventArgs e )
{
model.AddAt( Convert.ToInt32( ((LinkButton)sender).ID ) );
}
public void AddCartItem( string text )
{
HtmlTableCell tc = new HtmlTableCell { InnerText = text };
HtmlTableRow tr = new HtmlTableRow();
tr.Cells.Add( tc );
cartTable.Rows.Add( tr );
}
}

internal class FakeDeposit :IDeposit
{
public IList<Product> Load()
{
return new List<Product> { new Product( "Product 1", 1 ), new Product( "Product 2", 2 ), new Product( "Product 3", 3 ) };
}
}
}

Fertig. Ich muss eingestehen, als ich das Beispiel aus dem Buch nachprogrammiert habe, war ich wirklich ├╝berrascht, wie alles geklappt hat, obwohl ich w├Ąhrend des Testens keine Webseite angesprochen habe. Die Wahrheit ist, ich habe noch nie nach dem MVP-Pattern entwickelt, aber eine Webseite so aufzusetzen ist genial! Hoch lebe die Abstraktion!

Ich lade hier das Projekt hoch, vielleicht glaubt es mir jemand nicht ­čśë