"Das steht da nirgends drin", "Das ist viel zu ungenau", "ich kann da nicht raus lesen, dass da so etwas gefordert wird"... Userstories. In einer Userstory beschreib ein User was er mit dem Produkt machen will oder wie er das Produkt bedienen will. Jeden Falls kommt die Definition von Userstory oft so rüber. Aber jeder der mal mit einem Anwender zusammen Prozesse aufgenommen hat und sich dabei beschreiben lies, was der Benutzer eigentlich immer macht um zum Ziel zu kommen, weiß dass Benutzer nicht immer die zuverlässigste Quelle für korrekte oder vollständige Informationen ist. Was da manchmal raus kam.. damit will niemand einen Sprint planen. Ich halte Userstories für eine wichtige Sache, aber würde nie eine Userstory 1:1 in Tasks transferieren.
Mir gefällt diese Interpretation der Userstory als Hypothese sehr gut, weil mehr kann eine Userstory nicht sein. Eine Userstory ist der Lösungsweg einer Interpretation eines Problems oder Ziels. Niemand kann bei einer Userstory sicher sein, dass das Ziel richtig verstanden und seine Position im übergreifenden Gesamtbild richtig verstanden worden ist. Ist der Weg im gesamten Prozessablauf sinnvoll und schlank umgesetzt?
Wenn ich eine Userstory direkt von einem Benutzer bekomme, muss diese einfach nochmal gegen geprüft werden. Es gibt immer ein Problem, das zu lösen ist oder ein Ziel, das zu erreichen ist. Gerade wenn es um die Verbesserung eines vorhandenen Produkts geht, wo mehr zu tun ist, als ein paar GUI-Elemente neu zu ordnen, ist bis zur wirklichen Verwendung durch die Benutzer nie klar, ob die erhoffte Verbesserung wirklich eintritt. Oft ist nicht einmal klar, ob es nicht auch ungewollte Nebeneffekte gibt, die nicht bedacht wurden (die aber eingeplant werden müssen).
Man kennt es ja sicher, dass eine Abteilung mit neuen Anforderungen kommt, die alles besser machen sollen und man Tätigkeiten, die man gar nicht bräuchte, entfernt hat. Oft gucken, die dann sehr doof, wenn man denen erklärt, dass diese Tätigkeiten, aber für die nach gelagerten Prozesse in einer anderen Abteilung echt wichtige Daten liefern, die auch nur da her kommen können. Die Userstory war also eine Hypothese der Abteilung, die sich als falsch heraus stellte. Zum Glück noch bevor etwas umgesetzt wurde und es sich im Test oder gar im Produktivbetrieb herausstellen konnte.
Die im Artikel beschriebene Impact-Map bringt genau diese Übersicht mit und zwängt die Userstories in einen direkten Kontext in dem sie viel besser zu beurteilen und zu interpretieren sind sind, als wenn diese allein stehen würden.
- Welches Ziel soll insgesamt erreicht werden?
- Wer ist von diesen Änderungen betroffen? (Abteilungen, Kunden, etc)
- Welche einzelnen Änderungen sollen zum Gesamtziel führen? (Wo gibt es also momentan Probleme und Verbesserungspotential)
Erst dann kommen Epics und Userstories.
So steht über jeder Userstory ein Problem, dass zu lösen ist. Wenn eine Userstory eindeutig, dass Problem nicht löst, setzt man diese nicht um. Ähnliches gilt für Dinge die eindeutig fehlen um ein Problem zu lösen. "Da steht aber nirgends, dass man eine Benutzerverwaltung mit Rollen und Hierarchie will!".. aber es steht da, dass die Bereichsleiter Reports über die Tätigkeiten ihrer Abteilungen sehen wollen. Da würde einfach der wieder der Blick auf die Übergreifenden Prozesse vergessen und nur die Prozesse in den Abteilungen betrachtet. Also fängt man damit an eine Umgebung zu schaffen, bei der man sich Anmelden kann und ein Benutzer-Objekt mit Berechtigungen erhält, bevor man anfängt sensible Ansichten zu bauen, die diese Benutzer-Daten brauchen, um die richtigen Daten anzeigen zu können (und nicht zu viele Daten, die der Benutzer nicht sehen dürfte).
Und am Ende darf man nie vergessen, zu prüfen, ob eine Userstory auch wenn sie logisch klang, den gewünschten Effekt hatte. Dabei muss man schnell prüfen und messen, damit man schnell reagieren kann, um dann wieder rum schnell nachbessern zu können.
Userstories helfen also dabei den Kontext zu verstehen, aber nehmen einen nicht die Hauptaufgabe ab, nämlich als Entwickler eine Lösung für ein Problem zu entwickeln. Der Begriff Softwareentwickler beschreibt nicht was man entwickelt, sondern mit welchen Werkzeug man entwickelt!
Gerade im Zend Framework 2 kommt man immer mehr in Bereiche wo man auf altbekannte Konzept aus der Enterprise Welt und dort auch auf viel bekanntes aus Java (EE) trifft. Neben Depenency-Injection und Factories, findet man immer wieder gute Möglichkeiten Konzept wie DAO oder
auch POPOs als PHP-Variante der POJOs anzuwenden (wobei ich hier POJO und Bean gleichsetzen würde).
Auch Getter und Setter gehören hier genau so zum Alltag wie in Java.
Ich möchte hier ein paar Beispiele geben, wie man eine einfache Umsetzung in einem ZF2-Modul realsieren kann. Ich gehe einfach mal davon aus, dass man in der Lage ist ein einfaches Module in ZF2 zu erstellen und auch sich mit Controllern ViewModel und TableGateway zu mindest einmal beschäftigt hat. Der Einstieg ist nicht besonders einfach, wenn man keine lauffähige Anwendung hat, in die man sich einarbeiten kann.
Die hier umgesetzten Ansätze, sollen die Entwicklung möglichst beschleunigen und viel Code sparen, der in vielen Beispielen auch mit drin ist und alles sehr unübersichtlich macht. Auch wenn hier einige Ableitungen vorkommen halte ich das exesive Ableiten und Vererben für einen falschen Ansatz. Factories und Dependency-Incetion halte ich für einen besseren Weg eine Klasse mit bestimmten Eigenschaften auszustaten.
Ich leite auch vom TableGateway ab, wobei die bessere Methode es wohl wäre in der Factory mit Hilfe des ServiceManagers ein TableGateway zuerstellen und dieses an den entsprechenden Constructor zu übergeben.
Aber fanden wir einfach mal mit dem Model bzw der Entität an.
public function setName($name){
$this->_name=$name;
}
public function getValue(){
return $this->_value;
}
public function setValue($value){
$this->_value=$value;
}
}
Die voran gestellen _ entsprechen dem Zend Code-Style und sollen private und protected Attribute der Klasse deutlicher hervor haben.
Eigentlich sollte ja jedes Attribute der Klasse private oder protected sein. Nur wer die PHP eigenen JSON Funktionen oder Klassen verwendet, könnte man in die Verlegenheit kommen etwas public zu machen, aber auch hier gibt es bessere Lösungen.
Ich halte wenig davon alle Attribute grundsätzlich auf protected zu setzen, nur weil die Möglichkeit besteht mal eine Klasse ableiten zu wollen.
In den meisten Fällen, sollte man dann überlegen, ob es wirklich eine andere Entität ist oder man die vorhandene erweitern oder anpassen könnte so, dass keine weitere sehr ähnliche Entität nötig wird. Ableiten von einer BasicEntity oder einem BasicModel ist natürlich sinnvoll. Aber konkrete fachliche
Entitäten abzuleiten ist meistens weniger sinnvoll.
Nun kommen wir zu einem Punkt, der in der ZF2-Welt oft anders gesehen wird. Ich leite das Model nicht direkt vom TableGatway ab. Das Problem ist hier, das man für das TableGateway einen DBAdapter benötigt den man mit Hilfe des ServiceManagers bekommt. Also muss man beim Erzeugen des Model-Objekts immer diesen übergeben und das TableGateway instanzieren. Das kann man natürlich super mit den Factories des ZF2 erledigen. ABER.. wenn ich eine Liste des Models aus der DB lade und in einer Schleife mir für jede Row aus der DB eine Instanz der Klasse erzeuge ist das Vorgehen jede Instanz über diesen Factory bauen zu lassen, alles anderes als elegant oder schnell.
Also bleiben wir bei unserem einfachen POPO und erstellen für die Zugriffe auf die Datenbank nutzen wir eine weitere Klasse. Für jedes Model/Entity erzeugen wir uns eine DAO-Klasse. Diese enthält alle Methoden zum Laden, Speichern und Löschen sowie Listen laden des Models. Wie für fast jedes DAO benötigen wir hier eine Factory um die Einstellungen des Systems (DB-Verbindung) dem DAO bekannt zu machen.
Die Factory tragen wir in der module.config.php ein, die wir in der Methode getConfig der Module.php laden.
$sm ist der ServiceManager, den wir hier an den Constructor des DAOs übergeben. Der Constructor nutzt diesen um den DBAdapter zu bekommen und an das TableGateway weiterzugeben. Wie schon weiter oben geschrieben, hätte man dies auch in der Factory machen können und das TableGateway hier an das DAO übergeben können. Könnte einen etwas Code sparen es so zu machen.
Nun möchte man den DAO in dieser Art und Weise gerne verwenden:
Dafür schreiben wir uns jetzt einen eifnachen DAO, der erstmal nur die load-Methode enthält:
class XWTestDAO extends TableGateway{
$this->_sm=null;
public function __construct($serviceManager){
$this->_sm=$serviceManager;
parent::__construct(
$this->_tableName,
$serviceManager->get('dbAdapter')
);
}
//eine einfache Methode zum Kopieren der Daten aus dem Array in das Objekts
private function _copyData($entity,$array){
$dbm=new XWDBMapper();
return $dbm->setValuesFromArray($entity,$array);
}
public function loadTest($id){
$entity=new XWTest();
if($id!=null && $id>0){
$rsetItem=$this->select(
array('test_id' => $id)
)->current();
Ideal wäre es hier, wie man deutlich sehen kann 'test_id' auch später mal durch Annotationen zu ersetzen. Dafür wäre eine Annotation wie @tableid dann geeignet nach der man sucht und sich dann den @dbcolumn-Eintrag aus dem selben DocComment-Block heraus liest.
Aber so weit ist es ja schon mal ganz schön. Wie brauchten kein Array zu erzeugen und sonst haben wir wenig Code schreiben müssen.
Das wichtigste fehlt hier aber natürlich noch. Wie mappe ich die Annotationen auf das Array und umgekehrt?
Dieser Mapper ist ansich auch nicht wirklich kompliziert und basiert hauptsächlich darauf mit der RefelcetionClass zu arbeiten und mit deren Hilfe die ReflectionProperties zu bekommen.
class XWDBMapper{
public function getColumnNameFromDoc($doc){
$result=null;
if(preg_match("/@dbcolumn=[a-zA-Z0-9_]+/",$doc)){
$result=preg_replace("/^.+@dbcolumn=([a-zA-Z0-9_]+)\s.+$/Uis","$1",trim($doc));
}
return $result;
}
Sehr wenig Code und erleichtert einen das Leben sehr. Ist auch relativ schnell. Das händische Schreiben bzw Mappen der Arrays war doch immer sehr fehleranfällig und wenn man mal eine Column zur Datenbanktabelle hinzugefügt hat, müsste man an vielen stellen diese nachpflegen und hoffen, dass man keine Stelle vergessen hat. Jetzt muss man nur noch eine Annotation einbauen und es wird immer richtig gemappt.
Nachdem wir also nun das Laden einmal durchgegangen sind, kommt hier nur kurz die Methode für den Mapper, wie man ein Array aus einem Objekt erhält (die meisten werden schon wissen wie man die Methode da oben umdrehen kann). Deswegen brauche ich hier wohl auch nicht viel dazu schreiben.
Um jetzt alles noch schneller zu machen könnte man sich einen Cache für die Propert-Liste schreiben, dass diese nicht immer und immer wieder erzeugt werden muss. Beim Laden von Listen wäre das ein großer Vorteil. Aber selbst so ist alles schon sehr schnell. Jeden Falls habe ich zwischen den beiden Varianten bis jetzt in der Benutzung der Anwendungen keinen gravierenden Unterschied feststellen können, was die Performance betrifft. Mit XDebug durch gemessen habe ich noch nicht.
So haben wir mit nur eine paar kleinen Zeilen Code uns viel Arbeit erspart.. vielleicht habe ich hiermit noch anderen deren Arbeit vereinfacht.
Möchtest Du AdSense-Werbung erlauben und mir damit helfen die laufenden Kosten des Blogs tragen zu können?