Freitag, 31. Januar 2014

Mit dem Zustandsmuster beginnt die Umsetzung eines Requirements Engineering.

Ein System zur Anforderungsentwicklung in der Umsetzung.

Artikelübersicht
1. Teil Mit dem Zustandsmuster beginnt die Umsetzung eines Requirements Engineering.
2. Teil Die Attributierung der Objekte in der Umsetzung.


In mehreren theoretischen Posts habe ich meine Art und Weise dargestellt, ein Requirements Engineering durchzuführen. Bisher habe ich sechs Artikel veröffentlicht, und so wie ich es abschätze, wird es viele weitere davon geben. Da eine reine theoretische Behandlung des Themas mir zu trocken erschien, habe ich versucht in praktischen Posts das Thema näher zu erläutern. Jetzt möchte ich einen dritten Weg gehen und die von mir beschriebene Vorgehensweise programmtechnisch umsetzen. Dazu möchte ich erst einmal Elemente der Geschäftslogik entwickeln. Ich denke, es könnte fruchtbar sein, Konzepte und Vorgehensweisen zu diskutieren. Durch Ihr Feedback könnte ich in einem frühen Stadium Fehlentwicklungen korrigieren, und ich hoffe, das Interesse am Requirements Engineering so auch bei Programmieren zu wecken, die dem Requirements Engineering eher skeptisch gegenüber stehen. Im Folgenden möchte ich noch einmal die schon erschienenen Posts aufführen:

Theoretischen Posts
  1. Ein Objekt-Aufgaben-Modell für das Requirements Engineering.
  2. Der Ideenraum.
  3. Themenobjekte ordnen Ideen in eine Roadmap.
  4. Themen müssen einer Systemkontextanalyse unterzogen werden.
  5. Ein hierarchisches System von Kontextobjekten.
  6. Die Geschichten der Benutzer - User Stories erstellen.
Praktischen Beispiele
  1. Ideen, Themenobjekte und die Roadmap im praktischen Beispiel.
  2. Praktisches Beispiel zur Kontextuntersuchung.
Nach und nach werde ich in den folgenden Artikeln dieser Umsetzungsreihe dargestellte Konzepte aufgreifen und diskutieren. Heute möchte ich mit dem Basiskonzept, dem Objekt-Aufgaben-Modell, beginnen. Im Artikel Ein Objekt-Aufgaben-Modell für das Requirements Engineering. gibt es zwei Statusdiagramme und jeweils zwei erläuternde Tabellen zu jedem Diagramm, eine für die Status und eine für die Statusübergänge. Aufgrund dieser Information ist es für mich naheliegend, Zustandsentwurfsmuster (state pattern) für dieses Grundkonzept einzusetzten. Das Zustandsentwurfsmuster ist ein objektbasiertes Verhaltensmuster. Der Zweck wird im Klassiker der Entwurfsmuster [GOF11] wie folgt beschrieben: "Ermögliche es einem Objekt, sein Verhalten zu ändern, wenn sein interner Zustand sich ändert. Es wird so aussehen, als ob das Objekt seine Klasse gewechselt hat." [S.398, GOF11] Eine gut nachvollziehbare Erläuterung dieses Entwurfsmuster findet sich auch in "Entwurfsmuster von Kopf bis Fuß"[FREE06].

Für jedes Objekt, welches seinen Zustand wechseln kann, werden wir jetzt ein UML-Klassendiagramm entwickeln. Das Objekt-Aufgaben-Objekt besitzt zwei Basisobjekte, das Objekt und die Aufgabe. Da in Java, meiner verwendeten Programmiersprache, das Wort Object bereits vergeben ist und es sich bei dem zu lösenden Problem um Anforderungsermittlungen handelt, habe ich die beiden Objekte RequirementsObject (fürs Objekt) und RequirementsTask (für die Aufgabe) genannt. Im Sinne des Zustandsentwurfsmuster bilden diese beiden Objekte den Kontext. Die jeweiligen Zustände werden in eine Ableitungshierarchie ausgelagert. Ein RequirementsObject besitzt ein RequirementsObjectState. RequirementsObjectState ist ein Interface. Die einzelnen Zustände werden mit Hilfe dieses Interfaces implementiert. Dabei handelt es sich um die Zustände New, Prioritized, Deferred, Ordered und Implemented. Im Interface vereinbaren wir die folgenden Methoden: create, prioritize, defer, delete, represent, order und implement. Im Folgenden stelle ich das Klassendiagramm dar, welches sich aus den Ausführungen ergibt:



Genau in der selben Weise kann man das Klassendiagramm für das Basiskonzept RequirementsTask entwickeln.



Nachdem ich die Grundkonzepte beschrieben habe, geht es nun ans Codieren. Dabei habe ich den Code durch JUnit-Tests abgesichert. Diese Tests testen einige erlaubte und einige unerlaubte Pfade durch die Zustände des Objekts. Im Folgenden sind einige Pfade des Tests für das RequirementsObject beschrieben.

 

public class RequirementsObjectTest {

    @Before
    public void setUp() throws Exception {
    }

    @After
    public void tearDown() throws Exception {
    }

    @Test
    public void normalPath() {
        RequirementsObject requirementsObject = new RequirementsObject();
        requirementsObject.create();
        requirementsObject.prioritize();
        requirementsObject.order();
        requirementsObject.implement();
        assertTrue(requirementsObject.getRequirementsObjectState() instanceof Implemented);
    }

    @Test
    public void deferPath() {
        RequirementsObject requirementsObject = new RequirementsObject();
        requirementsObject.create();
        requirementsObject.prioritize();
        requirementsObject.order();
        requirementsObject.defer();
        requirementsObject.represent();
        assertTrue(requirementsObject.getRequirementsObjectState() instanceof New);
    }

    @Test
    // siehe Änderung zum null object pattern
    public void deletePath() {
        RequirementsObject requirementsObject = new RequirementsObject();
        requirementsObject.create();
        requirementsObject.delete();
        assertEquals(null, requirementsObject.getRequirementsObjectState());
    }

    @Test (expected=IllegalStateException.class)
    public void wrongPath() {
        RequirementsObject requirementsObject = new RequirementsObject();
        requirementsObject.create();
        requirementsObject.order();
    }
}



Danach habe ich den Code entwickelt und dafür gesorgt, dass ein Test nach dem anderen grün wurde. Den Code möchte ich hier vollständig zeigen. In der weiteren Entwicklung werde ich die Klasse RequirementsObject zu einer abstrakten Klasse machen. Damit möchte ich nur noch konkrete Artefakte erlauben, wie Ideen, Themen, User Stories usw. Ob eine solche Unterteilung auch für Aufgaben nötig ist, möchte ich späteren Erfordernissen überlassen. Durch diese Änderung ist natürlich eine Anpassung der JUnit-Test erforderlich. Im Package com.ralf.easyrequirement.business.requirementsObject befindet sich der Code zum RequirementsObject..

 

public class RequirementsObject {
    RequirementsObjectState requirementsObjectState;

    public void create() {
        requirementsObjectState = RequirementsObjectState.create();
    }

    public void prioritize() {
        requirementsObjectState = requirementsObjectState.prioritize();
    }

    public void defer() {
        requirementsObjectState = requirementsObjectState.defer();
    }

    public void delete() {
        requirementsObjectState = requirementsObjectState.delete();
    }

    public void represent() {
        requirementsObjectState = requirementsObjectState.represent();
    }

    public void order() {
        requirementsObjectState = requirementsObjectState.order();
    }

    public void implement() {
        requirementsObjectState = requirementsObjectState.implement();
    }

    public RequirementsObjectState getRequirementsObjectState() {
        return requirementsObjectState;
    }

}

public abstract class RequirementsObjectState {

    public static RequirementsObjectState create() {
        return new New();
    }

    public RequirementsObjectState prioritize() {
        throw new IllegalStateException(this + ": prioritize is not implemented!");
    }

    public RequirementsObjectState order() {
        throw new IllegalStateException(this + ": prioritize is not implemented!");
    }

    public RequirementsObjectState implement() {
        throw new IllegalStateException(this + ": order is not implemented!");
    }

    public RequirementsObjectState defer() {
        throw new IllegalStateException(this + ": defer is not implemented!");
    }

    public RequirementsObjectState delete() {
        throw new IllegalStateException(this + ": delete is not implemented!");
    }

    public RequirementsObjectState represent() {
        throw new IllegalStateException(this + ": represent is not implemented!");
    }

}

public class New extends RequirementsObjectState {

    @Override
    // siehe Änderung zum null object pattern
    public RequirementsObjectState delete() {
        return null;
    }

    @Override
    public RequirementsObjectState prioritize() {
        return new Prioritized();
    }

}

public class Prioritized extends RequirementsObjectState {

    @Override
    public RequirementsObjectState defer() {
        return new Deferred();
    }

    @Override
    public RequirementsObjectState order() {
        return new Ordered();
    }

}

public class Ordered extends RequirementsObjectState {

    @Override
    public RequirementsObjectState defer() {
        return new Deferred();
    }

    @Override
    public RequirementsObjectState implement() {
        return new Implemented();
    }

}

public class Implemented extends RequirementsObjectState {

}

public class Deferred extends RequirementsObjectState {

    @Override
    // siehe Änderung zum null object pattern
    public RequirementsObjectState delete() {
        return null;
    }

    @Override
    public RequirementsObjectState represent() {
        return new New();
    }

}



Im Package com.ralf.easyrequirement.business.requirementsTask befindet sich der Code zum RequirementsTask.

 

public class RequirementsTask {
    RequirementsTaskState requirementsTaskState;

    public void create() {
        requirementsTaskState = RequirementsTaskState.create();
    }

    public void implement() {
        requirementsTaskState = requirementsTaskState.implement();
    }

    public void positiveReview() {
        requirementsTaskState = requirementsTaskState.positiveReview();
    }

    public void negativeReview() {
        requirementsTaskState = requirementsTaskState.negativeReview();
    }

    public void acceptance() {
        requirementsTaskState = requirementsTaskState.acceptance();
    }

    public void negativeAcceptance() {
        requirementsTaskState = requirementsTaskState.negativeAcceptance();
    }

    public void abort() {
        requirementsTaskState = requirementsTaskState.abort();
    }

    public RequirementsTaskState getRequirementsTaskState() {
        return requirementsTaskState;
    }

}

public class RequirementsTaskState {

    public static RequirementsTaskState create() {
        return new Open();
    }

    public RequirementsTaskState implement() {
        throw new IllegalStateException(this + ": implement is not implemented!");
    }

    public RequirementsTaskState positiveReview() {
        throw new IllegalStateException(this + ": positiveReview is not implemented!");
    }

    public RequirementsTaskState negativeReview() {
        throw new IllegalStateException(this + ": negativeReview is not implemented!");
    }

    public RequirementsTaskState acceptance() {
        throw new IllegalStateException(this + ": acceptance is not implemented!");
    }

    public RequirementsTaskState negativeAcceptance() {
        throw new IllegalStateException(this + ": negativeAcceptance is not implemented!");
    }

    public RequirementsTaskState abort() {
        throw new IllegalStateException(this + ": abort is not implemented!");
    }

}

public class Open extends RequirementsTaskState {

    @Override
    public RequirementsTaskState implement() {
        return new Implemented();
    }

    @Override
    // siehe Änderung zum null object pattern
    public RequirementsTaskState abort() {
        return null;
    }

}

public class Implemented extends RequirementsTaskState {

    @Override
    public RequirementsTaskState positiveReview() {
        return new Reviewed();
    }

    @Override
    public RequirementsTaskState negativeReview() {
        return new Open();
    }

    @Override
    // siehe Änderung zum null object pattern
    public RequirementsTaskState abort() {
        return null;
    }

}

public class Reviewed extends RequirementsTaskState {

    @Override
    // siehe Änderung zum null object pattern
    public RequirementsTaskState acceptance() {
        return null;
    }

    @Override
    public RequirementsTaskState negativeAcceptance() {
        return new Implemented();
    }

    @Override
    // siehe Änderung zum null object pattern
    public RequirementsTaskState abort() {
        return null;
    }

}



Den weiteren Verlauf dieser Umsetzungsreihe möchte ich auch ein bisschen dem Feedback überlassen. Damit möchte ich zeigen, dass ich mich durch Argumente beeinflussen lasse und den Willen habe, aus Ihren Erfahrungen zu lernen.

  • [FREE06]: Eric Freeman, Elisabeth Freeman: "Entwurfsmuster von Kopf bis Fuß", 1. Auflage, O'Reilly Verlag, Köln, 2006
  • [GOF11]: Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides: "Entwurfsmuster", deutsche Übersetzung, Addison-Wesley Verlag, Münschen, 2011


Zusatz vom 3.2.2014: Änderung der Rückgabe von null in den Methoden durch das null object pattern.

Mir wurde geraten in der Methode delete nicht return null; zurückzugeben. Dabei kann es zu null pointer exceptions kommen. Statt dessen soll das null object pattern verwendet werden. Dieses Objekt weist keine Aktion auf, wir verweisen jedoch jederzeit auf eine gültige Referenz. Im obigen Code habe ich die entsprechend zu ändernden Methoden mit einem Kommentar gekennzeichnet. Im Folgenden möchte ich die notwendigen Änderungen zeigen.

Änderungen beim Test des RequirementsObject.

 

public class RequirementsObjectTest {

    //...

    @Test
    public void deletePath() {
        RequirementsObject requirementsObject = new RequirementsObject();
        requirementsObject.create();
        requirementsObject.delete();
        assertTrue(requirementsObject.getRequirementsObjectState() instanceof NullRequirementsObject);
    }
    
    //...
    
}



Änderungen im Package com.ralf.easyrequirement.business.requirementsObject.

 

public class New extends RequirementsObjectState {

    @Override
    public RequirementsObjectState delete() {
        return new NullRequirementsObject();
    }

    // ...

}

public class Deferred extends RequirementsObjectState {

    @Override
    public RequirementsObjectState delete() {
        return new NullRequirementsObject();
    }

    // ...

}

public class NullRequirementsObject extends RequirementsObjectState {

}



Änderungen im Package com.ralf.easyrequirement.business.requirementsTask.

 

public class Open extends RequirementsTaskState {

    // ...

    @Override
    public RequirementsTaskState abort() {
        return new NullAbortTask();
    }

}

public class Implemented extends RequirementsTaskState {

    // ...

    @Override
    public RequirementsTaskState abort() {
        return new NullAbortTask();
    }

}

public class Reviewed extends RequirementsTaskState {

    @Override
    public RequirementsTaskState acceptance() {
        return new NullRequirementsTask();
    }

    // ...

    @Override
    public RequirementsTaskState abort() {
        return new NullAbortTask();
    }

}

public class NullRequirementsTask extends RequirementsTaskState {

}

public class NullAbortTask extends RequirementsTaskState {

}



folgender Post dieses Themas


Print Friendly Version of this page Print Get a PDF version of this webpage PDF

Kommentare:

  1. Bei delete() darfst du nicht null zurückliefern. Sonnst null-pointert es und der Sinn des State-Musters geht verloren (wir wollen ja nicht prüfen in welchem State das Objekt ist bevor wir den Aufruf machen).

    AntwortenLöschen
  2. In der letzten Ausgabe der IX habe ich gelesen, die GOF-Muster wären vor allem dazu da, die Schwächen von Java auszugleichen. Wie sehen Sie das?

    AntwortenLöschen
    Antworten
    1. Nein, das glaube ich nicht.

      Das Design Pattern Buch von der Gang of Four ist 1995 erschienen und die darin beschriebenen Pattern sind mit C++ Codebeispielen. Das ist nur zufälligerweise auch das Erscheinungsjahr von Java.

      Entwurfsmuster werden ge-funden und nicht er-funden. Die GoF-Leute haben sich einfach erfolgreiche Softwaresysteme angeschaut, die vor allem besonders gut evolvierbar waren, und die Muster im Code gefunden. Im Buch werden diese Projekte bei den jeweiligen Pattern auch genannt.

      Löschen
  3. Das kann nicht sein, das GOF Buch ist vor Java erschienen, im Jahr 1994. Damals war C++ die Sprache des Mainstreams, aber auch Smalltalk hat einen Einfluss auf das Werk gehabt. Viele der Muster gehen von einem statischem Typsystem aus und werden in dynamischen Sprachen anders umgesetz. Trotzdem gelten die Muster auch heute noch in den meisten OO-Sprachen, auch wenn sie dynamischer Natur sind. Ich kenne z.B. keine Sprache die eine State-Machine oder einen Proxy als Sprachkonstrukt eingebaut hat.

    AntwortenLöschen
  4. GOF Design Patterns sind nicht wegen Schwächen von Java da. Sie sind dazu da die Schwächen der populären OO Sprachen allgemein auszugleichen. Sie sind eine Architektur-Design Convention und in aller erster linie sind sie ein Glossar. Sie geben gebräuchlichen Design mustern Namen.

    AntwortenLöschen