Annotaties en Aspect georiënteerd Programmeren - deel 2

In een eerder artikel legde Che Schneider uit hoe goed annotaties en aspect georiënteerd programmeren combineren. In dit tweede artikel geeft hij een uitgebreid voorbeeld.

Voorbeeld: Applicatie eigenschappen

Voor dit voorbeeld gaan we een properties management systeem ontwikkelen dat ons op een transparante wijze laat werken met applicatie properties en ons toch de flexibiliteit geeft in hoe deze properties geïmplementeerd worden.
De makkelijkste manier voor een ontwerper om toegang te verkrijgen tot properties is om ze als class members te plaatsen in precies die class waarin hij ze gebruikt. Let op: ik zeg niet dat het de meest nette manier is, maar wel de eenvoudigste. Het is een bad practice om dit te doen en natuurlijk moet elke ontwikkelaar niet toegeven om het wel zo te doen voor de redenen die we allemaal kennen: uitbreidbaarheid, hergebruik, enz.
Echter, we willen het ons zo makkelijk mogelijk maken en aspects en annotaties staan ons toe om gebruik te maken van de voordelen van makkelijk bereikbare properties als static members terwijl we de gewenste flexibiliteit behouden.

Laten we de main class van ons voorbeeld bekijken.

package com.finalist.aprops;
 
public class Aprops {
  private String applicationName = “Windows 95”;
  private static String APPLICATION_NAME = “Windows 98”;
 
  public static final void main(String args[]) {
    Aprops aprops = new Aprops();
    aprops.printInstanceName();
 
    System.out.println("Static name is " + APPLICATION_NAME);
    System.out.println("Setting name to Ubuntu Linux");
    APPLICATION_NAME = "Ubuntu Linux";
    System.out.println("Static name is " + APPLICATION_NAME);
 
    aprops.printInstanceName();
  }
 
  public void printInstanceName() {
    System.out.println("Instance name is " + this.applicationName);
  }
}

Dit is allemaal recht-toe-recht-aan en zou niet te cryptisch moeten zijn. We definiëren een class member en een instance member en initialiseren deze met default waardes. In de main method creëren we een instance en laten het de waarde van zijn instance member afdrukken. Daarna drukken we de waarde van de class member af, zetten het op een nieuwe waarde en drukken het weer af. Naderhand laten we de instance de waarde van zijn member weer afdrukken.

De enige reden waarom we de member twee keer definiëren in dit voorbeeld is om te laten zien hoe de aspecten werken met zowel statische als dynamische members en om te laten zien hoe een aanpassing van één van de prpoperties invoed heeft op alle members.
Het resultaat van de code hierboven zal zijn:

Instance name is Windows 95
Static name is Windows 98
Setting name to Ubuntu Linux
Static name is Ubuntu Linux
Instance name is Windows 95

Zoals verwacht is de class member veranderd terwijl de instance member niet veranderd is.

Dynamic Property loading

Volgende stap: de property manager. Voor dit voorbeeld gebruiken we een eenvoudige memory based PropertyManager. In een werkelijke applicatie zullen de properties waarschijnlijk gelezen worden van en geschreven worden naar een file, database of iets dergelijks.

package com.finalist.aprops;
 
public class PropertyStore {
  private Map<String,String> properties = new HashMap<String,String>();
 
  public PropertyStore() {
    properties.add(“applicationName”, "Windows XP");
  }
 
  public String getProperty(String name) {
    return properties.get(name);
  }
 
  public void setProperty(String name, String property) {
    properties.set(name, property);
  }
}

Bovenstaande code definieert de “application name” property met de waarde “Windows XP”- dit wordt de naam van onze applicatie. Maar hoe krijgen we deze property in onze main class hierboven?

Dit is waar het allemaal samenkomt:: we zullen een annotatie schrijven dat aangeeft dat de properties verbonden moeten worden aan de members van de application class. Maar laten we, voordat we beginnen met de implementatie, eerst kijken hoe we onze annotatie gaan gebruiken.

Zo ziet de main class van eerder eruit met geannoteerde properties:

package com.finalist.aprops;
 
import com.finalist.aprops.annotation.Property;
 
public class Aprops {
  @Property(name = "applicationName", 
      description = "The name of the application")
  private String applicationName = “Windows 95”;
 
  @Property(name = "applicationName", 
      description = "Also the name of the application")
  private static String APPLICATION_NAME = “Windows 98”;
 
  public static final void main(String args[]) {
    ... as above ...
  }
 
  public void printInstanceName() {
    ... as above ...
  }
}

Zoals je ziet, is de code identiek aan de code hiervoor behalve de annotaties van beide members in de class. Aangezien we denken dat beide members dezelfde property vertegenwoordigen, hebben beide annotaties dezelfde naam en een vergelijkbare omschrijving.

Dit is de code van onze Property annotatie:

package com.finalist.aprops.annotation;
 
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
 
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface Property {
  String name();
  String description() default "";
}

Wanneer we een aspect een annotatie op runtime willen evalueren, moet de retention policy (retention betekend ‘ vasthouden’ [red]) van de annotatie op “RUNTIME” gezet worden, anders zal de annotatie onzichtbaar zijn voor ons aspect tijdens uitvoering van de applicatie. Tevens specificeren we, voor dit voorbeeld, “FIELD” als de enig geldige target van onze annotatie. Dit betekend dat het alleen kan worden toegepast op class members en instance members.
Verder, om iets meer informatie te verstrekken aan de degene die de annotaties leest,, staan we toe dat een property naam en een beschrijving wordt doorgegeven aan de annotatie. We gebruiken de property naam als een unieke identificatie zodat we meerdere variabelen kunnen hebben die refereren aan dezelfde applicatie property terwijl de beschrijving een mogelijke lezer een hint geeft over wat wij danken dat deze member representeert.

Ontwikkelen van het advice

Tot nu toe zal het weer laten draaien van de applicatie hetzelfde reslultaat geven als eerder. Omdat we niets hebben veranderd in de draaiende code, zal het gedrag van de applicatie onveranderd zijn.
Het is tijd om ons aspect te schrijven om onze PropertyManager werkelijk toe te passen op de geannoteerde members in onze main class.

package com.finalist.aprops;
 
import com.finalist.aprops.annotation.Property;
 
public aspect PropertyAspect {
  private static PropertyManager propertyManager = new PropertyManager();
 
  pointcut propertyRead(Property property) :
    get(@Property * *) &&
    @annotation(property) &&
    !within(PropertyAspect) &&
    !cflow(adviceexecution());
 
  pointcut propertyWrite(Property property, String newValue) :
    set(@Property * *) &&
    @annotation(property) &&
    args(newValue) &&
    !cflow(staticinitialization(*)) &&
    !cflow(initialization(*.new(..))) &&
    !within(PropertyAspect) &&
    !cflow(adviceexecution());
 
  String around(Property property) : 
    propertyRead(property)
  {
    return propertyManager.getProperty(property.name());
  }
 
  before(Property property, String newValue) : 
    propertyWrite(property, newValue)
  {
    propertyManager.setProperty(newValue);
  }  
}

Dit is een behoorlijk lastig om mee te beginnen. Maar laten we eens kijken in kleine stukjes.

pointcut propertyRead(Property property) :
  get(@Property * *) &&
  @annotation(property) &&
  !within(PropertyAspect) &&
  !cflow(adviceexecution());

Wat we hier doen is het definiëren van een pointcut genaamd “propertyRead” dat reageert op het event dat een member geannoteerd met @Property wordt benadert met een lees operatie. Je kan voorlopig de laatste twee regels van de pointcut veilig negeren: zij vertellen eenvoudigweg de pointcut om de waardes van de annotatie te exporteren voor gebruik in de aspect code en om geen triggers te eavalueren die plaatsvinden in de flow van het aspect zelf.

pointcut propertyWrite(Property property, String newValue) :
  set(@Property * *) &&
  @annotation(property) &&
  !cflow(staticinitialization(*)) &&
  !cflow(initialization(*.new(..))) &&
 
  args(newValue) &&
  !within(PropertyAspect) &&
  !cflow(adviceexecution());

Hier definiëren we een pointcut genaamd “property Write” dat reageert op het event dat een member geannoteerd met @Property wordt benadert met een schrijf operatie en we niet werkenbinnen de operationele flow van static of instance initialisatie.
Nogmaals, negeer voorlopig de laatste vijf regels, zij geven hetzelfde gedrag als hierboven en exporteert bovendien de waarde waarop de member wordt gezet voor gebruik in de aspect code.

String around(Property property) : 
    propertyRead(property) 
{
  return propertyManager.getProperty(property.name());
}

Nu wordt het interessant. Dit stukje code definieert ons eerste advice, de code die loopt wanneer een pointcut wordt getriggerd.
Het “around” keyword vertelt AspectJ de code uit te voeren rond de invocatie van de lees operatie. Binnen de advice code spreken we de getter aan van onze property manager voor de waarde die het opgeslagen heeft voor de naam van hetproperty. En dit is de waarde die we terug sturen in plaats van de werkelijke waarde van het member.
Dit is correct: met dank aan het “around” gedeelte van het advice kunnen we inderdaad de waarde veranderen die terug komt van de lees operatie van een member. Merk op dat de originele code niet uitgevoerd wordt- alhoewel we dat makkelijk zouden kunnen, in plaats daarvan “overschrijven” we de originele code.

before(Property property, String newValue) : 
  propertyWrite(property, newValue) 
{
  propertyManager.setProperty(newValue);
}

Vergeleken met wat we deden met een lees operatie, is dit bijna een beetje minder mooi. We definiëren een advice dat de waarde leest dat de applicatie probeert op te slaan in de member die ook door de pointcut is geïdentificeerd. Deze waarde geven we door aan onze property manager: vanaf nu zal de property deze waarde hebben wanneer wij deze aanspreken in de applicatie. Wederom, we voeren niet de originele code uit maar alleen de code in dit advice.

Het resultaat

Wanneer we dit aspect toe passen op de applicatie en het opnieuw starten, zal de uitkomt als volgt zijn.

Instance name is Windows XP
Static name is Windows XP
Setting name to Ubuntu Linux
Static name is Ubuntu Linux
Instance name is Ubuntu Linux

Het ziet er nu compleet anders uit dan wat we eerder zagen.
En dit is wat er is gebeurd:
Alhoewel we “Windows 95” gedefinieerd hebben als de default applicatie naam voor de instance member, onderschept het aspect het opvragen hiervan en vervangt het met een aanroep naar onze property manager. Hetzelfde gebeurt voor de static member.
Bovendien, aangezien we meta-informatie hebben toegevoegd aan beide members die hetzelfde property vertegenwoordigen, zodra we de static member hebben veranderd naar een andere waarde wordt de instance member ook aangepast: de verandering is doorgevoerd, door de hele applicatie!

Conclusie

Wat we bereikt hebben met deze excursie in AOP en annotaties is dat we een startpunt hebben gecreëerd voor een framework waarin we simpelweg application properties kunnen toevoegen aan een class door zijn members te annoteren.

Maar wat zijn de pluspunten boven traditionele programmeer technieken?

Elke ontwikkelaar die binnen een applicatie werkt kan erg eenvoudig zijn eigen application properties specificeren als members in zijn class. Door de members eenvoudigweg correct te annoteren, kunnen zij dynamisch geladen en applicatie breed beheerde properties worden.
Hij kan gemakkelijk de members initialiseren met waardes die geldig zijn voor zijn omgeving en werken met de applicatie zonder ooit aspects toe te voegen aan zijn code. Alleen wanneer de applicatie moet draaien in andere omgevingen zal het aspect moeten worden toegevoegd en de properties overeenkomstig worden veranderd.
Echter, aangezien de ontwikkelaar de members annoteert, is hij zich ervan bewust dat zij kunnen worden gemodificeerd. Daarom zal er geen vinger wijzen zijn in de trend van “Als ik had geweten dat je mijn variabelen veranderd met jouw aspecten…”.
Bovendien is het gegarandeerd dat de operationale code werkt zoals het deed voordat enige members geannoteerd werden als properties. Er zullen geen refactoring wensen of compilatie fouten zijn doordat de API van de properties manager is veranderd.
Door zijn code te annoteren, levert de ontwikkelaar een contract dat – indien in ere gehouden door de aspect ontwikkelaar – het mogelijk maakt een extreem flexibel en uitgebreid applicatie ontwerp te maken en eenvoudig en intuïtief een oplossing biedt voor traditionele programmeer problemen terwijl een onderhoudbare en schone code behouden blijft.

Vertaling: Wilma Noordam

—————————————————————————————
Meer weten over Java-specialist Finalist IT Group?


Reageer

RSS feed for comments on this post · TrackBack URI