Pratiquons le Design Pattern Delegate (ou Façade) : Démos avancées

 

Le design pattern delegate ( ou façade ) est un pattern très utilisé et facile à expliquer.

Deux démos, une simple et une seconde très avancée, vous permettent de pratiquer sereinement ce design sans difficulté.

Ainsi, les ingrédients de ce blog sont divisés en deux parties:

  • La première partie, contenant une démo simple, n'exige aucun prérequis (mis à part un peu de java 8).
  • La seconde partie, contenant une démo très avancée, nécessite de connaître un peu spring-batch et en particulier son FlatFileItemReader (retrouvez un article sur le sujet ici).

Enfin, notez que le framework Lombok est utilisé à divers endroits. Pensez donc à rajouter cette dépendance:

	<dependency>
			<groupId>org.projectlombok</groupId>
			<artifactId>lombok</artifactId>
	 <version>1.16.16</version>	

</dependency> 

PREMIÈRE PARTIE

Partons de l'interface fonctionnelle IReader ayant une (seule) méthode :

public interface IReader<T> {
	T read();	
}

 

Soit une classe du modèle nommée Entreprise ayant trois attributs:

public class Entreprise{

	private String raisonSociale;
	private String id;	
	private LocalDate dateCreation;
	
}

 

C'est ici la partie intéressante de ce billet, nous allons introduire le pattern delegate via ce code:

public class Reader implements IReader<Entreprise>{
	
	IReader<Entreprise> delegate;

	@Override
	public Entreprise read() {
		return delegate.read();
	}
}

Ici, l'implémentation Reader (implémentant IReader) délègue l'implémentation du contrat au delegate;

Ce que confirme la ligne: return delegate.read();

En résumé, le Reader délègue la responsabilité du comportement read() à l'attribut (objet externe) nommé ici delegate.

PS: L'implémentation Reader ne sera pas utilisée dans la première partie puisque les lambdas fournissent le nécessaire.

Question: Mais pourquoi le faire ainsi?

On peut penser que l'objectif final est de s'appuyer sur une implémentation externe sûre de la méthode read() ou encore de ne pas se préoccuper des détails de son implémentation. Ou encore que le Reader veux varier les implémentations de la méthode read en fonction du besoin/contexte.

Dans tous les cas, le Reader délègue l'implémentation du contrat à un autre composant (externe) via l'attribut delegate.

Delegate ressemble un peu au pattern proxy.

A noter aussi que le pattern delegate peut être considéré comme une solution alternative à l'héritage mais ce point ne sera pas abordé ici.

 

Test JUnit

 

Écrivons le premier test Junit qui permet d'illustrer encore mieux notre propos:

import org.junit.Assert;
import org.junit.Test;

public class DemoDelegateForReaderTest {

 @Test public void testReaderDelegate() {
	final IReader<Entreprise> delegate = ()-> new Entreprise(); 
	IReader<Entreprise> reader = () -> delegate.read();
	Entreprise e = reader.read();
	Assert.assertNull(e.getRaisonSociale());		
 //another reader delegate
 final IReader<Entreprise> delegat2 = ()-> new Entreprise("rs",null,null);
	 reader= () -> delegat2.read();
	e = reader.read();
	Assert.assertEquals("rs",e.getRaisonSociale());
	}
}

 

SECONDE PARTIE

Cette seconde partie avancée exige de connaître un peu spring-batch en particulier le reader FlatFileItemReader.

Cette démo permet réellement d'étudier un cas intéressant, très pratique et pratiqué.

Ici on délègue la responsabilité de lire un fichier (IO) à FlatFileItemReader ce qui nous laisse le loisir de l'adapter (légèrement ici) à notre contexte projet.

Pour rappel rapide, FlatFileItemReader est une implémentation du contrat de spring-batch ItemReader<T>:

	/**Reads a piece of input data and advance to the next one. Implementations
	 * must return <code>null</code> at the end of the input
	 * data set. In a transactional setting, caller might get the same item
	 * twice from successive calls (or otherwise), if the first call was in a
	 * transaction that rolled back.
	 * @throws ParseException if there is a problem parsing the current record
	 * (but the next one may still be valid)
	 * @throws NonTransientResourceException if there is a fatal exception in
	 * the underlying resource. After throwing this exception implementations
	 * should endeavour to return null from subsequent calls to read.
	 * @throws UnexpectedInputException if there is an uncategorised problem
	 * with the input data. Assume potentially transient, so subsequent calls to
	 * read might succeed.
	 * @throws Exception if an there is a non-specific error. @return T the item to be processed*/

public interface ItemReader<T> {
	T read() throws Exception, UnexpectedInputException, ParseException, NonTransientResourceException;
}

 

Donc la seule méthode du contrat est read().

Comme nous ne souhaitons jamais réinventer la roue, nous nous appuyons sur l'implémentation FlatFileItemReader pour notre propre implémentation (adaptée) au contexte projet. Ici c'est MyFlatFileItemReader:

import org.springframework.batch.item.ItemReader;
import org.springframework.batch.item.file.FlatFileItemReader;
import org.springframework.batch.item.file.transform.FieldSet;

import lombok.AllArgsConstructor;

@AllArgsConstructor
public  class MyFlatFileItemReaderImpl implements ItemReader<FieldSet> {
	private FlatFileItemReader<FieldSet> delegate;
	
	@Override
	public FieldSet read() throws Exception {
		FieldSet fs = delegate.read();
		
		return fs;
	}
}

 

Nous avons porté la responsabilité de lire un fichier à notre delegate l'attribut de notre classe MyFlatFileItemReader.

Il nous reste à écrire un test JUnit pour mettre en évidence le concept.

Or là, nous avons un peu de code ou de configuration à préparer pour pouvoir réellement lire le fichier, nommé entreprises.csv, et transformer ses lignes en objet java Entreprise.

Ce fichier src/test/resources/entreprises.csv contient deux ligne pour l'instant:

raisonSociale, id, dateCreation
"rs","1",2017-05-12

Revenons au code nécessaire pour l'ouverture de flux du reader.

Préalablement, l'objet de spring FlatFileItemReader doit être open.

Pour le faire, nous utilisons un mock fourni par spring-batch MetaDataInstanceFactory permettant de mocker un contexte spring-batch:

MetaDataInstanceFactory.createStepExecution().getExecutionContext()

 

Ainsi, on peut écrire ceci:

final FlatFileItemReader<FieldSet> delegate = new FlatFileItemReader<FieldSet>();

delegate.open(MetaDataInstanceFactory.createStepExecution().getExecutionContext());

 

Le reste du code à préparer, qui ne sera pas détaillé ici, sert justement à définir:

  • un DelimitedLineTokenizer (séparateur virgule) pour lire un csv,
  • puis un DefaultLineMapper (avec skip de la première ligne).

Toute cette partie de code (de configuration du FlatFileItemReader) est regroupée à part dans une méthode privée.

Voici donc le code test JUnit:

import java.text.SimpleDateFormat;
import java.time.LocalDate;
import org.junit.*;
import org.springframework.batch.item.ItemReader;
import org.springframework.batch.item.file.FlatFileItemReader;
import org.springframework.batch.item.file.mapping.*;
import org.springframework.batch.item.file.transform.*;
import org.springframework.batch.test.MetaDataInstanceFactory;
import org.springframework.core.io.FileSystemResource;
import fr.netapsy.abdou.patterndelegate.Entreprise;

public class DemoAvanceDelegateForReaderTest {
  @Test public void testFlatFileReaderDeleg() throws Exception {
 final FlatFileItemReader<FieldSet> deleg= 
             new FlatFileItemReader<FieldSet>();
 
	configureDelegateReader(deleg);
        deleg.open( 
             MetaDataInstanceFactory.
              createStepExecution().
              getExecutionContext()
      );

     ItemReader<FieldSet> reader = ()->  deleg.read();

	FieldSet fs = reader.read();
	Assert.assertNotNull(fs);
	Entreprise ent= mapFieldSetToEntrep(fs);
		 
      assertEquals("rs",ent.getRaisonSociale());
      assertEquals("1",ent.getId());
      assertEquals(LocalDate.of(2017, 5, 12),ent.getDateCreation());
    
    deleg.close();
}

private Entreprise mapFieldSetToEntrep(FieldSet fs) {
		if(fs==null) return null;
		SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
		LocalDate localDate = LocalDate.parse( sdf.format(fs.readDate("dateCreation")) ) ;
		return new Entreprise( fs.readString("raisonSociale"), fs.readString("id"),localDate , null);	
	}
	private void configureDelegateReader(FlatFileItemReader<FieldSet> delegate) {
	final String IN_FILE = "src/test/resources/entreprises.csv";
        DelimitedLineTokenizer lineTokenizer = new DelimitedLineTokenizer();
        DefaultLineMapper<FieldSet> lineMapper = new DefaultLineMapper<FieldSet>();
        lineTokenizer.setNames(new String[]{"raisonSociale","id","dateCreation"});
        lineMapper.setLineTokenizer(lineTokenizer);
        lineMapper.setFieldSetMapper(new PassThroughFieldSetMapper());
        delegate.setLinesToSkip(1);
        delegate.setLineMapper(lineMapper);
        delegate.setResource(new FileSystemResource(IN_FILE));
	}

 

Il vous reste à lancer mvn test et constater que les assertions valident l'opération de lecture (de la première ligne) retournant bien l'objet attendu.

Enfin, voici le pom.xml du projet spring-boot que j'ai utilisé pour la seconde démo:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<groupId>fr.netapsys.abdou</groupId>
	<artifactId>demoPaternDelegate</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<packaging>jar</packaging>
	<parent>
<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-parent</artifactId>
		<version>1.5.2.RELEASE</version>
		<relativePath/> 
	</parent>
	<properties>
	<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
	<java.version>1.8</java.version>	<lombok.version>1.16.16</lombok.version>
</properties>

<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter</artifactId>
		</dependency>

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>		
		<dependency>
	<groupId>org.projectlombok</groupId>
			<artifactId>lombok</artifactId>
			<!-- <version>${lombok.version}</version> -->
		</dependency> 
	
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-batch</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.batch</groupId>
			<artifactId>spring-batch-test</artifactId>
		</dependency>
</dependencies>

<build>
		<plugins>
			<plugin>				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
			</plugin>
		</plugins>
</build>
</project>

 

Dans un prochain billet, nous allons plus loin dans cette démo en personnalisant notre implémentation MyFlatFileItemReader, en ne gardant que les lignes conformes à certains critères métier.

A bientôt

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Captcha *