Technologies : Java 17+, Spring Boot 3.x, Spring Data JPA, H2, Lombok, JUnit 5
Vous héritez d’un projet existant en très mauvais état : la startup CleanBooks vous confie le backend de sa librairie en ligne. Le code actuel fonctionne… mais il est illisible, non testé, et impossible à maintenir.
Votre mission : refactoriser intégralement le code “sale” fourni en appliquant les principes du Clean Code vus en formation, puis ajouter de nouvelles fonctionnalités proprement.
Voici le code existant que vous devez nettoyer. Il est intentionnellement mauvais.
// À REFACTORISER @Entity public class b { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) public Long id; public String t; // title public String i; // isbn public double p; // price public String a; // author name public boolean av; // available public int q; // quantity public String cat; // category public java.time.LocalDateTime dt; // creation date // Pas de constructeur, pas de méthodes, pas d'encapsulation }
// À REFACTORISER — fait tout : validation, logique métier, accès BDD @RestController public class BC { @Autowired org.springframework.data.jpa.repository.JpaRepository repo; @PostMapping("/b") public Object add(@RequestBody Map m) { // Validation mélangée avec la logique if(m.get("t")==null||m.get("t").toString().trim().equals("")) return "err: no title"; if(m.get("p")==null) return "err: no price"; double pp = Double.parseDouble(m.get("p").toString()); if(pp<0||pp>99999) return "err: bad price"; if(m.get("i")==null) return "err: no isbn"; // Logique métier dans le controller b book = new b(); book.t = m.get("t").toString(); book.p = pp; book.i = m.get("i").toString(); book.a = m.get("a") != null ? m.get("a").toString() : "Unknown"; book.av = true; book.q = m.get("q")!=null ? Integer.parseInt(m.get("q").toString()) : 0; book.dt = java.time.LocalDateTime.now(); repo.save(book); return book; } @GetMapping("/b/{id}") public Object get(@PathVariable Long id) { // Pas de gestion d'erreur return repo.findById(id).get(); // Peut lancer NoSuchElementException ! } @GetMapping("/bs") public Object getAll() { // Retourne les entités directement (expose les détails internes) return repo.findAll(); } @DeleteMapping("/b/{id}") public Object del(@PathVariable Long id) { // Aucune vérification que le livre existe repo.deleteById(id); return "ok"; } // Méthode géante qui fait tout : recherche, filtre, calcul, formatage @GetMapping("/stats") public Object stats() { List all = repo.findAll(); int total = all.size(); double sum = 0; int av = 0; Map cats = new HashMap(); for(Object o : all) { b bk = (b) o; sum += bk.p; if(bk.av) av++; cats.put(bk.cat, cats.containsKey(bk.cat) ? (int)cats.get(bk.cat)+1 : 1); } Map res = new HashMap(); res.put("total", total); res.put("avg_price", total > 0 ? sum/total : 0); res.put("available", av); res.put("by_category", cats); return res; } }
// À REFACTORISER public class BookUtil { // Méthode qui fait 5 choses différentes ! public static String process(b book, int qty, boolean flag, String code) { // 1. Vérification stock if(book.q < qty) return "no stock"; // 2. Application remise (effet de bord !) if(flag && code != null && code.equals("PROMO10")) { book.p = book.p * 0.9; } // 3. Mise à jour stock (effet de bord !) book.q -= qty; if(book.q == 0) book.av = false; // 4. Calcul total double total = book.p * qty; // 5. Formatage résultat return String.format("OK:%.2f", total); } // Nombre magiques partout public static boolean isok(b book) { return book.p > 0 && book.p < 99999 && book.q >= 0 && book.q <= 10000 && book.t != null && book.t.length() > 0 && book.t.length() <= 255 && book.i != null && book.i.length() == 13; } }
Avant de coder, listez tous les problèmes que vous voyez dans le code fourni. Identifiez pour chaque problème :
Rédigez cette analyse dans un fichier ANALYSE.md.
ANALYSE.md
Créez une entité Book propre qui respecte :
Book
L’entité doit avoir au minimum :
id
title
isbn
price
author
category
availableQuantity
createdAt
isAvailable()
availableQuantity > 0
sell(int quantity)
restock(int quantity)
Créez les classes suivantes en respectant le Clean Code :
CreateBookRequest — DTO d’entrée avec Bean Validation :
CreateBookRequest
BookCategory
initialQuantity
BookResponse — DTO de sortie (ce qu’on expose via l’API) :
BookResponse
available
BookMapper — conversion entre Book et DTOs
BookMapper
BookCategory — enum : FICTION, NON_FICTION, SCIENCE, HISTORY, TECHNOLOGY, CHILDREN, OTHER
FICTION
NON_FICTION
SCIENCE
HISTORY
TECHNOLOGY
CHILDREN
OTHER
Créez une hiérarchie d’exceptions propres :
AppException (RuntimeException) ├── ResourceNotFoundException → 404 ├── BusinessRuleException → 422 (avec un ruleCode) └── InsufficientStockException → 409 (avec availableQty et requestedQty)
Chaque exception doit :
Implémentez BookService en appliquant les principes Clean Code :
BookService
public interface BookService { BookResponse createBook(CreateBookRequest request); BookResponse findById(Long id); Page<BookResponse> findAll(Pageable pageable); Page<BookResponse> searchByTitle(String title, Pageable pageable); List<BookResponse> findByCategory(BookCategory category); BookResponse sell(Long bookId, int quantity); BookResponse restock(Long bookId, int quantity); void delete(Long id); BookStats getStats(); }
Règles à respecter obligatoirement :
return null
BookStats doit contenir :
BookStats
totalBooks
totalAvailableBooks
averagePrice
booksByCategory
Map<BookCategory, Long>
Implémentez BookController en respectant les principes :
BookController
POST /api/books
GET /api/books/{id}
GET /api/books
GET /api/books/search?title=...
GET /api/books/category/{category}
POST /api/books/{id}/sell
POST /api/books/{id}/restock
DELETE /api/books/{id}
GET /api/books/stats
Règles :
GlobalExceptionHandler
Écrivez au moins 8 tests unitaires pour BookService couvrant :
BusinessRuleException
ResourceNotFoundException
InsufficientStockException
Chaque test doit :
should_when_
Ajoutez un DataInitializer (CommandLineRunner) qui charge 10 livres de démo au démarrage, couvrant au moins 4 catégories différentes.
DataInitializer
Ajoutez un endpoint GET /api/books?category=TECHNOLOGY&minPrice=10&maxPrice=50&sort=price,asc avec filtres multiples.
GET /api/books?category=TECHNOLOGY&minPrice=10&maxPrice=50&sort=price,asc
Ajoutez une entité SaleRecord qui enregistre chaque vente (bookId, quantity, totalPrice, date). Ajoutez un endpoint GET /api/books/{id}/sales pour consulter l’historique.
SaleRecord
GET /api/books/{id}/sales
Dans votre ANALYSE.md, documentez chaque refactoring effectué : avant → après, et le principe Clean Code appliqué.
cleanbooks/ ├── src/main/java/com/cleanbooks/ │ ├── CleanBooksApplication.java │ ├── config/ │ │ ├── DataInitializer.java │ │ └── OpenApiConfig.java (bonus) │ ├── domain/ │ │ ├── Book.java ← Entité riche │ │ └── BookCategory.java ← Enum │ ├── dto/ │ │ ├── CreateBookRequest.java │ │ ├── SellRequest.java │ │ ├── RestockRequest.java │ │ ├── BookResponse.java │ │ └── BookStats.java │ ├── exception/ │ │ ├── AppException.java │ │ ├── ResourceNotFoundException.java │ │ ├── BusinessRuleException.java │ │ └── InsufficientStockException.java │ ├── mapper/ │ │ └── BookMapper.java │ ├── repository/ │ │ └── BookRepository.java │ ├── service/ │ │ ├── BookService.java ← Interface │ │ └── BookServiceImpl.java ← Implémentation │ └── controller/ │ ├── BookController.java │ └── GlobalExceptionHandler.java └── src/test/java/com/cleanbooks/ └── service/ └── BookServiceTest.java
# Créer un livre POST http://localhost:8080/api/books { "title": "Clean Code", "isbn": "9780132350884", "price": 35.90, "author": "Robert C. Martin", "category": "TECHNOLOGY", "initialQuantity": 10 } # Récupérer un livre http://localhost:8080/api/books/1 # Lister avec pagination http://localhost:8080/api/books?page=0&size=5&sort=title,asc # Rechercher par titre http://localhost:8080/api/books/search?title=clean # Vendre des exemplaires POST http://localhost:8080/api/books/1/sell { "quantity": 2 } # Statistiques http://localhost:8080/api/books/stats # Test erreur : livre inexistant http://localhost:8080/api/books/999 # Test erreur : stock insuffisant POST http://localhost:8080/api/books/1/sell \ { "quantity": 9999 }
Rappel Clean Code : Avant de soumettre votre travail, relisez votre propre code comme si c’était du code que vous lisiez pour la première fois. Comprenez-vous immédiatement ce que chaque méthode fait ? Si non, renommez ou extrayez !