Posts Under TDD Category
EDIT: Hop. Enfin la release 1.9.0 est dispo en téléchargement.
Après pas mal de travail avec des périodes plus ou moins intenses – bref les vicissitudes du développement Open Source – le projet sort une nouvelle version 1.9.0 en Release Candidate, avec des bugfixes et bien sûr des nouvelles features. Il y a un changelog mais dans les faits le billet suivant liste brièvement ce qui est nouveau. Ah oui la version est téléchargeable ici et bientôt disponible sur le central maven.
- Pour être plus fluent et expressif, l’API introduit les alias then et will pour les réponses personnalisées (Answer). Ainsi que d’autres petits tweak de l’API:
@Test public void engine_should_only_work_with_diesel() { given(engine.start()).will(throwExceptionIfEssenceInsteadOfDiesel()); // ... } private Answer throwExceptionIfEssenceInsteadOfDiesel() { return new Answer<EngineStatus>() { public EngineStatus answer(InvocationOnMock invocation) { // answer code } }; } - Les mocks peuvent maintenant être déclaré dans la configuration du stub, sur une ligne.
DieselEngine de = given(mock(DieselEngine.class).start()).willThrow(TankIsEmpty.class).getMock();
- On peut maintenant renvoyer la classe d’une exception plutôt que son instance.
given(someMock).willThrow(IllegalArgumentException.class, SomethingIsWrongException.class);
- Si jamais vous avez besoin de debugguer un bout de code ou les interactions sont non prédictibles, il est maintenant possible de loguer les invocations du mock ou de l’espion. Attention, bien qu’utile à l’occasion avec du code legacy, quand même si jamais ce besoin s’en fait sentir sur un nouveau développement c’est que ce code devient trop complexe.
List mockedList = mock(List.class, withSettings().verboseLogging()); mockedList.get(0);
On pourra également ajouter des callbacks sur chaque interaction du mock.
Observer observer = mock(Observer.class, withSettings().invocationListeners(listener1, listener2)); willThrow(IllegalArgumentException.class).given(observer.update(observable, "what has changed"));
- Pas mal de travail a été fait sur les annotations. Maintenant il n’est plus nécéssaire d’initialiser un champ annoté par @Spy s’il existe dans la classe un constructeur sans argument.
@RunWith(MockitoJUnitRunner.class) public class SomeTest { // pas besoin d'initialiser le champs @Spy private ArrayList spiedArrayList; @Test public void verify_some_interactions() { spiedArrayList.iterator(); verify(spiedArrayList, once()).iterator(); } } - Et pour la fin mais pas des moindres, le mécanisme d’injection de mockito supporte maintenant l’injection par constructeur. A l’heure actuelle, seul les mocks et spies déclaré dans le test en tant que champs pourront être injecté dans le constructeur du champs annoté par @InjectMocks.
@RunWith(MockitoJUnitRunner.class) public class EngineTest { @Mock Diesel diesel; @InjectMocks Engine engine; @Test public void engine_should_consume_Diesel() { engine.start(); } }Ou Engine a un constructeur avec le paramètre Diesel.
public class Engine { Diesel diesel; public Engine(Diesel diesel) { this.diesel = diesel; } public boolean start() { checkNotEmpty(diesel); // ... } // ... }
Pour l’instant en RC, cette release permettra d’adoucir les angles si nous en avons loupé certains éléments. N’hésitez pas à nous poser des questions sur la mailing list ou stackoverflow.
Hello, j’en avais un peu marre d’écrire régulièrement voire répétitivement dans mes tests les constructions mockito.
Pour ça je me suis créé dans mon IDE favori, IntelliJ, ce qu’on appelle des Live Template. Ces templates permettent à partir d’une abréviation d’insérer des fragments de code. Ainsi par exemple :
Taper iter dans votre éditeur puis de faire Ctrl+J (sous OSX) va développer cette abréviation dans le bout de code ci-dessous (suivant le contexte bien entendu)
for (TypeInIterable type : someIterable) {
}
Taper sur Ctrl+J (sous OSX) vous permet de lister les abréviations disponible dans le contexte courant.
Les Live Template pour Mockito
Bien qu’imparafaite pour des raisons de limite technique d’IntelliJ, elles sauvent un minimum de temps, multiplié par le nombre de test. Malheureusement il n’y a pas non plus d’import export uniquement pour les live template, il faut donc se taper la configuration de intellij à la main. Cela dit il est possible de contourner partiellement ce problème avec la sauvegarde de la configuration personnelle sur les serveurs intellij, ou encore d’exporter la configuration pour les live templates, les file templates, et encore autre chose.
J’ai défini toutes ces annotations dans un nouveau groupe ‘test’, et j’ai activé pour toutes le contexte Java, avec reformatage et simplification du nom qualifié.
- Description : Creates a field with the @Mock annotation
Abbréviation : ‘am’
Template text :@org.mockito.Mock private $TYPE$ $MOCK_FIELD$
Les variables du templates sont :
Name Expression Default value Skip if defined TYPE variableOfType(“Object”) MOCK_FIELD suggestVariableName() - Description : Creates a field with the @Spy annotation
Abbréviation : ‘as’
Template text :@org.mockito.Spy private $TYPE$ $MOCK_FIELD$
Les variables du templates sont :
Name Expression Default value Skip if defined TYPE variableOfType(“Object”) MOCK_FIELD suggestVariableName() - Description : Creates a field with the @InjectMocks annotation
Abbréviation : ‘aim’
Template text :@org.mockito.InjectMocks private $TYPE$ $MOCK_FIELD$
Les variables du templates sont :
Name Expression Default value Skip if defined TYPE variableOfType(“Object”) MOCK_FIELD suggestVariableName() - Description : Add @RunWith(MockitoJUnitRunner.class)
Abbréviation : ‘rwm’
Template text :@org.junit.runner.RunWith(org.mockito.runners.MockitoJUnitRunner.class)
- Description : BDD Stub mock with given(…).willReturn(…) style
Abbréviation : ‘gw’
Template text :given($MOCK$).willReturn($ARGS$)$END$
Les variables du templates sont :
Name Expression Default value Skip if defined MOCK variableOfType(“Object”) ARGS - Description : BDD Stub spy/mock with willReturn(…).given(…) style
Abbréviation : ‘wg’
Template text :org.mockito.BDDMockito.willReturn($RETURNED$).given($MOCK$).$CALL$ $END$
Les variables du templates sont :
Name Expression Default value Skip if defined RETURNED complete() MOCK variableOfType(“Object”) CALL complete() - Description : Inserts a verify(…) statement
Abbréviation :‘verif’
Template text :org.mockito.Mockito.verify($MOCK$).$CALL$
Les variables du templates sont :
Name Expression Default value Skip if defined MOCK variableOfType(“Object”) CALL complete() - Description : Inserts Mockito.inOrder(mocks) followed by inOrder.verify(…) statement
Abbréviation : ‘ioverif’
Template text :org.mockito.InOrder $inOrderVar$ = org.mockito.Mockito.inOrder($MOCKS$); $IN_ORDER_VAR$.verify($MOCK$).$CALL$;
Les variables du templates sont :
Name Expression Default value Skip if defined IN_ORDER_VAR suggestVariableName() MOCKS variableOfType(“Object”) MOCK variableOfType(“Object”) CALL complete() - Description :Inserts a verify(…) statement
Abbréviation :‘verif’
Template text :$IN_ORDER_VAR$.verify($MOCK$).$CALL$;
Les variables du templates sont :
Name Expression Default value Skip if defined IN_ORDER_VAR variableOfType(“org.mockito.InOrder”) MOCK variableOfType(“Object”) CALL complete()
Voilà donc les templates que je me suis créé pour IntelliJ, il manque certainement des cas d’utilisation, mais je trouvais plus judicieux de mettre ces cas là au moins. Pour nos amis Eclipse oou Netbeans, il y a des fonctionnalités comparables plus ou moins évoluées (de mémoire le système d’Eclipse est plutôt pas mal).
Références
Vous devez écrire du code qui fait appel à JMX, en bon citoyen et bon développeur vous voulez tester ce code.
Première approche; vous enregistrez vos MBean sur un MBeanServer, disons celui de la plateforme (avec Java 6 : ManagementFactory.getPlatformMBeanServer()).
mBeanServer.registerMBean(theMBean, theMBean.getObjectName());
Étant donné que MBeanServer étends MBeanServerConnection il est possible d’exécuter des querys, de faire des invocations sur les MBean etc. Si le code est suffisamment isolé des aspects techniques de connexion à JMX, vous passerez le MBeanServer en lieu et place de la MBeanServerConnection.
Supposons le code suivant.
public class OperateOnJMXConnection implements JMXOperation {
public void perform(MBeanServerConnection connection) {
// doing some stuff there
}
public Result getResult() { return result; }
}
Pour tester ce code il faudrait alors écrire :
@Test
public void do_not_fail() {
operateOnJMXConnection.perform(mbeanServer);
assertThat(result).satisfies(someCondition);
}
Mais voilà, vous restez en local, et par exemple si vous avez merdé sur la sérialisation de vos beans, vous ne verrez pas d’échec dans vos test et vous aurez une surprise en prod, ou avant si votre projet a un processus qualité décent.
Évidement il y a une solution, l’idée c’est de pouvoir se connecter au mBeanServer local à votre processus (typiquement dans maven 3, l’exécution de vos tests peuvent être forkée).
Alors j’ai essayé de récupérer les informations pour récupérer les informations de la VM qui tourne, mais bon on tombe dans des classes sun, j’ai préféré ne pas continuer sur ce chemin semé d’embûches, sans compter sur la faiblesse de cette approche.
Bref en relisant les articles de Khanh sur JMX, j’ai vu quelque chose d’intéressant JMXConnectorServerFactory. Cette classe permet donc de créer un JMXConnectorServer avec l’URL qu’on lui spécifie et d’un MBeanServer. A noter que cette URL doit respecter un certain formalisme tel que la javadoc l’indique : service:jmx:protocol:remainder.
Le protocole ne peut pas être n’importe quoi, il faut qu’il y ait le bon service enregistré pour qu’il soit géré. Dans notre cas RMI est standard, c’est donc le protocole que je prendrai. Pour le remainder, il s’agit plus d’une partie d’une URL, je vous laisse voir la Javadoc de JMXServiceUrl à ce sujet, mais dans les grandes lignes la forme doit être la suivante : //[host[:port]][url-path]
JMXConnectorServer connectorServer = JMXConnectorServerFactory.newJMXConnectorServer(
new JMXServiceURL("service:jmx:rmi://"),
null,
mBeanServer
);
connectorServer.start();
Hop dans le code précédent, on a créé puis démarrer notre JMXConnectorServer. Il n’y a plus qu’à se connecter dessus de manière standard :
Je vais utiliser connectorServer.getJMXServer() pour récupérer l’URL du service, il y a une raison à cela, c’est que comme l’indique la javadoc, l’URL passée pour la création du JMXConnectorServer peut être légèrement modifiée par celui-ci, il faut donc récupérer la nouvelle URL.
JMXConnector jmxConnetor = JMXConnectorFactory.connect(connectorServer.getJMXServiceUrl()); MBeanServerConnection connection = jmx.getgetMBeanServerConnection();
Et voilà vous avez accès à une MBeanServerConnection, qui vit dans la JVM locale, mais qui utilise RMI pour communiquer avec le MBeanServer, du coup vous êtes nettement plus proches des conditions du code de production et c’est ce qui nous intéresse dans cet article.
Pour référence les articles de Khanh, et en français s’il vous plait
:
- Partie 1 : http://jetoile.blogspot.com/2010/10/jmx-pour-les-nuls-les-concepts-partie-1.html
- Partie 2 : http://jetoile.blogspot.com/2010/11/jmx-pour-les-nuls-les-differents-mbeans.html
- Partie 3 : http://jetoile.blogspot.com/2010/11/jmx-pour-les-nuls-les-agents-jmx-partie.html
- Partie 4 : http://jetoile.blogspot.com/2010/11/jmx-pour-les-nuls-les-classes-de-base.html
- Partie 5 : http://jetoile.blogspot.com/2010/11/jmx-pour-les-nuls-le-mbean-server.html
- Partie 6 : http://jetoile.blogspot.com/2010/12/jmx-pour-les-nuls-chargement-dynamique.html
- Partie 7 : http://jetoile.blogspot.com/2010/12/jmx-pour-les-nuls-les-services-jmx.html
- Partie 8 : http://jetoile.blogspot.com/2010/12/jmx-pour-les-nuls-les-connecteurs.html
Quelques liens javadoc :
Enfin je me suis créé une petite classe de commodité qui permet de créé facilement un loopback pour les TU :
Prologue
A l’école nos professeurs nous apprenaient ce qu’était la programmation orientée objet; en particulier l’encapsulation. En effet avoir un accès public aux variables internes d’un objet n’est pas particulièrement recommandé, pourtant nous avons connaissons tous la convention JavaBean :
class Bean {
/** constructeur sans argument, optionnel si c'est le seul constructeur de la classe */
public Bean() { }
public void setBeanName(String name) {
beanName = name;
}
public String getBeanName() {
return beanName;
}
}
Manque de bol, cette convention qui a pourtant son utilité -voire sa nécéssité- peut dans certains contextes briser l’encapsulation, et plus dangereux pour votre code, elle permet à vos objets d’être mutable, c’est à dire de pouvoir modifier l’état d’un objet après sa création. Bien que dans certains cas le design ou le rôle de la classe demande cette caractéristique, dans beaucoup d’autres situations la mutabilité peut poser problème.
D’ailleurs historiquement les JavaBeans ont été pensé pour être utilisé par des applications graphiques afin d’être construit itérativement et finalement pour être facilement dé/sérialisés [1]. Mais ces objets exposent publiquement leurs états, du coup :
- Il y a de l’adhérence à des propriétés internes d’un objet, s’il y a beaucoup de code qui utilise ces propriétés internes, l’évolutivité et la maintenance de ce code peut très vite devenir difficile et donc couteuse.
- Ce n’est plus vraiment de la programmation orientée objet. C’est en quelque sorte des variables globales, ça fait plus de 30 ans qu’on sait que les variables globales c’est mal! Demandez à Barbara Liskov [2].
- Avec cette possibilité de muter les objets, il peut y avoir des problèmes au runtime, et croyez moi avec l’arrivée de la parallélisation en plus dans vos applications il va y avoir des surprises.
Bon revenons au design, et aux problèmes rencontrés.
Illustration des problèmes de design du code
hashCode et equals
Donc pour commencer, on va juste faire quelques tests sur un objet sans les méthodes hashCode() et equals(). Prenons les test suivants, je créé 4 instances de beans, obj1 et obj3 puis obj2 et obj4 ont les mêmes propriétés.
Ce test montre les problèmes quand on oublie les méthodes equals et hashCode.
public class MutabilityCanBeBadTest {
private AJavaBean obj1;
private AJavaBean obj2;
private AJavaBean obj3;
private AJavaBean obj4;
@Before
public void initTheBeans() {
obj1 = new AJavaBean();
obj1.setName("paraboot");
obj1.setSellingDate(new GregorianCalendar(2010, 03, 30).getTime());
obj2 = new AJavaBean();
obj2.setName("ethnies");
obj2.setSellingDate(new GregorianCalendar(2010, 10, 30).getTime());
obj3 = new AJavaBean();
obj3.setName("paraboot");
obj3.setSellingDate(new GregorianCalendar(2010, 03, 30).getTime());
obj4 = new AJavaBean();
obj4.setName("ethnies");
obj4.setSellingDate(new GregorianCalendar(2010, 10, 30).getTime());
}
@Test
public void objectsShouldBeEquals() throws Exception {
assertEquals(obj2, obj4); // fail
assertEquals(obj1, obj3); // fail
}
@Test
public void hashCodeShouldBeEquals() throws Exception {
assertEquals(obj1.hashCode(), obj3.hashCode()); // fail
assertEquals(obj2.hashCode(), obj4.hashCode()); // fail
}
@Test
public void addAndRemoveToHashBasedCollection() throws Exception {
Set<AJavaBean> set = new HashSet<AJavaBean>();
assertTrue(set.add(obj1));
assertTrue(set.add(obj2));
assertFalse(set.add(obj3)); // fail
assertFalse(set.add(obj4)); // fail
assertEquals(2, set.size()); // fail
assertTrue(set.remove(obj1));
assertTrue(set.remove(obj2));
assertFalse(set.remove(obj3)); // fail
assertFalse(set.remove(obj4)); // fail
}
}
Si l’implémentation de AJavaBean oublie donc le hashCode et le equals, la plus part des assertions ne marchent plus.
public class AJavaBean {
private String name;
private Date sellingDate;
public String getName() {
return name;
}
public void setName(final String name) {
this.name = name;
}
public Date getSellingDate() {
return sellingDate;
}
public void setSellingDate(final Date uid) {
this.sellingDate = uid;
}
@Override
public String toString() {
return "AJavaBean [name=" + name + ", sellingDate=" + sellingDate + "]";
}
}
Que s’est-il passé? S’il n’y a pas de hashCode et de equals, ce sont les méthodes de la super classe qui sont utilisées, dans le code listé plus haut ce sont les méthodes de Object qui seront utilisées pour tester l’égalité et le hashCode.
- Donc pour l’égalité Object.equals(Object) vérifie uniquement si l’instance est la même. Ce qui explique que les tests d’égalité échouent plus haut.
- Pour le hashCode, c’est la JVM qui le génère, bref autant dire que le hashcode est différent pour chaque instance. Ceci explique que les instances obj1 et obj2 sont ajoutées au HashSet, si le hashcode avait été le même alors les opérations d’ajout et de suppression auraient renvoyé false (n’oublions pas qu’il s’agit d’un HashSet).
Et donc pour le code mutable
Ok, bon maintenant qu’on a vu ça, notre bean implémente les méthodes equals et hashCode de manière idoine, c’est à dire dans notre cas que le code se base sur les attributs name et sellingDate. Pas de mystère, on peut utiliser l’outils de génération de l’IDE.
Eclipse génère ça:
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((name == null) ? 0 : name.hashCode());
result = prime * result + ((sellingDate == null) ? 0 : sellingDate.hashCode());
return result;
}
@Override</div>
public boolean equals(final Object obj) {
if (this == obj) { return true; }
if (obj == null) { return false; }
if (getClass() != obj.getClass()) { return false; }
AJavaBean other = (AJavaBean) obj;
if (name == null) {
if (other.name != null) { return false; }
} else if (!name.equals(other.name)) { return false; }
if (sellingDate == null) {
if (other.sellingDate != null) { return false; }
} else if (!sellingDate.equals(other.sellingDate)) { return false; }
return true;
}
Bon à priori on se dit que notre code est safe puisqu’on a nos méthodes equals et hashcode, mais on se fourvoie ; notre objet est mutable!
Exemple :
@Test
public void playWithMutabilityWithABeanInHashBasedCollection() throws Exception {
Set<AJavaBean> set = new HashSet<AJavaBean>();
assertTrue(set.add(obj1));
assertTrue(set.add(obj2));
obj2.setSellingDate(new GregorianCalendar(2010, 05, 30).getTime()); // valeur précédente : 2010-10-30
assertTrue(set.remove(obj2)); // owned
assertEquals(2, set.size()); // owned
assertFalse(set.add(obj2)); // owned
}
Alors on sait que les méthodes equals et hashCode utilisent les deux propriétés name et sellingDate, donc quand on ajoute un objet dans le HashSet le hashCode correspondra au calcul fait partir des valeurs des ces attributs. Mais voilà le hashcode de l’objet n’est calculé qu’une fois, au moment de l’interaction dans la Map (ajout, suppression, contains, etc…).
Donc ce qu’il se passe c’est qu’on a fait muter l’état de notre objet, du coup le hashcode est différent, mais la collection conserve la référence de l’objet qu’elle contiens et ne recalcule pas son hashcode! C’est aussi avec avec ce genre de code que vous pouvez avoir des fuites mémoires. Et on est même pas dans un contexte multithreadé, alors imaginez si la collection est partagée entre plusieurs thread!
Attention aux collections ou aux dates du JDK
Par ignorance puis par laxisme, j’avoue que j’ai écris du code qui ressemble à ça (et j’ai honte de le dire) :
public class AnotherSupposedImmutableClass {
private final String name;
private final Date aDate;
private final Map<String, Integer> aMap;
public AnotherSupposedImmutableClass(final String name, final Date aDate, final Map<String, Integer> aMap) {
super();
this.name = name;
this.aDate = aDate;
this.aMap = aMap;
}
public String getName() {
return name;
}
public Date getADate() {
return aDate;
}
public Map<String, Integer> getAMap() {
return aMap;
}
}
Et forcement il y a des hics! A priori notre classe n’est pas mutable. Mais cela ne vous aura pas échappé, les propriétés aDate et aMap sont mutable!
@Test
public void playWithInternalMutability() throws Exception {
Map<String, Integer> map = new HashMap<String, Integer>();
AnotherSupposedImmutableClass supposedImmutableClass = new AnotherSupposedImmutableClass(
"name",
new GregorianCalendar(2010, 05, 30).getTime(),
map
);
supposedImmutableBean.getADate().setTime(123456789l); // oups
supposedImmutableBean.getAMap().put("trente quatre", Integer.valueOf(34)); // oups
supposedImmutableBean.getAMap().clear(); // oups, again
}
Et là, vous vous retrouverez les mêmes surprises que celles vu plus haut, ou évidement pire si vous êtes dans une application multithreadée. A ce sujet j’ai vu des ConcurrentModificationException parceque levé par du code à priori immutable, une optimisation d’un vieux code multithreadé avait déplacé une section qui modifiait une Map.
Je vous conseille vivement d’utiliser des objets immutables pour vos property, les librairies Joda-Time [3] et Google-Collections [4] fournissent des objets immutables.
Le pattern Builder de Joshua Bloch
Pour Joshua Bloch, c’est un peu une référence en Java, je pense qu’on peut lui faire confiance. Il est l’auteur du fameux livre Effective Java [5].
Alors pourquoi le pattern Builder de Joshua Bloch et non le pattern Builder du GoF ? En fait ce design vient d’une constatation au sujet de la construction d’objet complexes et pour s’affranchir des inconvénients des accesseurs.
En gros un objet du genre agrégat pourrait être construit avec un constructeur avec un paquet d’argument ou itérativement avec une foule de setter. Mais, un les gros constructeur ce n’est pas très pratique, puis deux les setters ça peux vite être lourd et ça rends votre objet mutable (ce qui n’est donc pas souhaité dans tous les cas).
Cette déclinaison du builder permet de construire un objet itérativement sans forcer la mutabilité.
Exemple les collections google :
public abstract class ImmutableMap<K, V> implements Map<K, V>, Serializable {
// ...
public static <K, V> Builder<K, V> builder() {
return new Builder<K, V>();
}
// ...
public static class Builder<K, V> {
final List<Entry<K, V>> entries = Lists.newArrayList();
public Builder() {}
public Builder<K, V> put(K key, V value) {
entries.add(entryOf(key, value));
return this;
}
// ...
public ImmutableMap<K, V> build() {
return fromEntryList(entries);
}
}
private static <K, V> ImmutableMap<K, V> fromEntryList(List<Entry<K, V>> entries) {
// ...
}
// ...
}
Ou encore avec une classe de notre domaine :
public class ACoolImmutableClass {
private final String name;
private final DateTime sometime;
private final List<String> listOfStuff;
// many other fields
public String getName() {
return name;
}
public DateTime getSometime() {
return sometime;
}
public static class Builder {
private String name;
private DateTime sometime;
private List<String> listOfStuff = new ArrayList<String>();
public Builder withName(String name) {
this.name = name;
return this;
}
public Builder at(DateTime moment) {
this.sometime = moment;
return this;
}
public Builder addThisThing(String thing) {
this.listOfStuff.add(thing);
return this;
}
public ACoolImmutableClass build() {
return new ACoolImmutableClass(this);
}
}
private ACoolImmutableClass(Builder builder) {
this.name = builder.name;
this.sometime = builder.sometime;
this.listOfStuff = ImmutableList.copyOf(builder.listOfStuff);
}
}
A noter que cette classe utilise des objets immutables pour ces attributs (DateTime, et ImmutableList).
Un des avantages, c’est qu’il est possible de valider les propriétés avant la création effective de l’objet. Avec les setters c’est faisable mais ça peut être délicat dans certaines situations.
Il y a un plugin Eclipse, qui permet de générer ces Builder, celà dit il est loin d’être super user friendly.
http://code.google.com/p/bpep/
Quoiqu’il en soit en aucun cas ce pattern n’est un remplacement du pattern Builder du GoF, il s’agit plus d’un pattern à appliquer dans un contexte ou il faut des objets immutable. Et encore ce n’est pas la seule solution, JodaTime typiquement n’utilise pas de builders.
Comment gérer la modification de l’objet
Si un comportement qui fait partit du domaine de l’objet et doit modifier l’état, alors il faut peut-être créer une nouvelle instance. La bibliothèque Joda-Time fait typiquement ça lorsqu’il y a modification d’un champs.
DateTime instance1 = new DateTime("2009-04-01");
DateTime instance2 = instance1.withYear(2010);
Je ne m’étends pas sur le sujet, mais ce genre de choses dépends de votre contexte, du rôle et du besoin. Un objet devrait être par défaut immutable, sauf si vraiment votre domaine identifie un cas ou l’état doit bouger et alors vous aurez des méthodes documentées qui appliqueront cette modification.
Conclusion
Mieux vaut des objets bien pensés et immutables que d’introduire la possibilité de changer l’état d’un objet et avoir des surprises. Et puis aussi :
- Il y a un risque fort d’avoir des problèmes au runtime, d’autant plus 10 ans après lorsqu’il y a une évolution à apporter et que plus personne ne sait qu’à tel endroit dans le code il y a le truc qui fout tout en l’air. Et les problèmes au runtime ca peut vite couter cher à analyser.
- Si vos objets ne peuvent pas être modifié alors vous n’aurez pas à vous soucier des problèmes de concurrences, c’est manifestement un gain de temps au développement et en maintenance. (Et donc un gain d’argent sur le long terme.)
- Bon ces objets sont bien cool, mais voilà il y a encore plein de framework (à tord ou à raison) qui se basent sur la convention JavaBean, je pense notamment aux objets marshallés en XML et consort.
- Ce code basé sur les builders est propre, mais il faut passer un petit peut plus de temps pour le faire. Il y a bien un plugin pour Eclipse, mais quid des autres IDE.
Quoi qu’il en soit, ces solutions sont toujours à appliquer avec du recul et toujours en fonction du contexte de votre domaine.
D’ailleurs cette entrée parle des problèmes rencontrés avec les collections du JDK, mais le problème pourrait se manifester différemment si une collection ou un de vos objets fonctionne autrement.
Encore une fois les remarques sont les bienvenues, ça fait plus de 40 ans que l’Homme fait du logiciel, et mafois on se plante encore assez souvent.
Références
- http://www.javaworld.com/javaworld/jw-09-2003/jw-0905-toolbox.html
- http://www.infoq.com/presentations/liskov-power-of-abstraction
- http://dutheil.brice.online.fr/blog/index.php/2010/02/09/a-propos-de-joda-time/
- http://dutheil.brice.online.fr/blog/index.php/2010/02/16/les-collections-par-google-comment-sy-retrouver/
- http://www.amazon.fr/Effective-Java-Joshua-Bloch/dp/0321356683/ref=sr_1_1?ie=UTF8&s=english-books&qid=1269958692&sr=8-1
- http://rwhansen.blogspot.com/2007/07/theres-builder-pattern-that-joshua.html





Follow