Facile de répondre au besoin du comment avec du code. Je te rejoins sur le pourquoi et pour ce faire :
1) Relire Clean Code de Robert C. Martin.
2) Relire Object Thinking de David West.
3) Coder en FOP (function-object-programming).
4) N'avoir besoin de documenter que les interfaces afin de répondre au pourquoi.
5) Enjoy.
A lot of programmers make the mistake of thinking the way you make code flexible is by predicting as many future uses as possible, but this paradoxically leads to less flexible code. The only way to achieve flexibility is to make things as simple and easy to change as you can.
A random guy on Twitter
Avant toute chose, SMACSS n'est pas un framework CSS, c'est un ensemble de pratiques CSS/SCSS permettant d'écrire des feuilles de styles clean code.
Rien que sur les deux premières pages, j'y ai choppé des astuces.
@Lenny @Doudou @Chlouchloutte, c'est pour vous !
Edit :
Pour faire gagner du temps à tout le monde, je tente ici d'établir un résumé des concepts.
1. Les CSS se divisent en 5 catégories :
-
Base : ce sont les CSS des éléments HTML. Typiquement, le body, le html, la couleur par défaut des liens, des inputs. Bref, les balises HTML, sans classes ni ID.
-
Layout : votre page se découpe en général par grands blocs (d'ailleurs presque toujours les mêmes lorsque l'on respecte les canons du milieu). En général, ils portent un ID, se trouvent directement dans le
<body>
et vous y trouverez les sectionsheader
,footer
,content
,side-content
,article
,side-bar
... -
Module : il s'agit de la même chose que les Layouts, à la différence qu'ils se trouvent soit dans un Layout, soit dans un Module eux-mêmes. Ils sont conçus pour être auto-porteurs et nous pouvons les comparer aux JPanels de Swing (ça me fait mal d'écrire ça). En général, ils ne sont pas forcément unique, ne portent pas d'ID, et leurs contenus sont skinés via une classe héritée, typiquement :
.mon-module span {...}
.mon-module a {...}
- **State :** ce sont les CSS qui vont changer l'interface en fonction des actions de l’utilisateur. Ils sont forcément liés à du JS de part leur aspect dynamique. Par exemple, vous cliquez sur un bouton et une zone passe de _is-visible_ à _is-hidden_. En général, ils commencent par `is-...` ou `has-...` et ne sont constitués que de classes.
- **Theme :** il s'agit de tout ce qui touche à l'apparence mais pas l'orientation et la forme cf. (modules) ni la structure de la page (cf. layouts). Ils comprennent donc les couleurs (texte & background), l'épaisseur des traits et bordures, la police de caractères, les images de fond, les logos, etc.
### 2. L'usage de SCSS ou SASS
Pour thématiser des CSS, il faut les variabiliser et pour ce faire nous avons deux outils :
- SASS (et sa variante que je lui préfère SCSS car plus proche de CSS pour les néophytes).
- Les variables CSS.
À cela s'ajoute un autre point, il faut générer plusieurs fichiers CSS (un principale contenant les 4 premières catégories de CSS, et un par thème afin de pouvoir changer dynamiquement de thème pendant l'exécution).
### 3. Les bonnes pratiques
J'en utilisais déjà pas mal sans y avoir forcément réfléchi, mais de façon formelle :
- Limiter la profondeur d'imbrication des balises HTML quand vous le pouvez.
- Limiter la profondeur d'imbrication des CSS (mieux vaut préférer l'imbriquation du HTML à l'imbriquation des CSS).
- Utiliser le moins d'ID possible puisqu'ils sont spécifiques à votre PWA et rendront difficile le portage de votre design vers une autre PWA.
- Changer de thème ne consiste pas à modifier toutes les balises HTML, non. Il faut supprimer du DOM le CSS contenant le fichier de l'ancien thème pour y ajouter un `<link>` vers celui du nouveau thème.
- Si vous n'utilisez pas des polices comme Fontisto ou Fontawesome, utilisez des sprites CSS avec des images, vous aurez moins de requêtes HTTP et moins de data à transférer.
- Éviter de faire du balisage HTML spécifique à votre contenu.
@Chlouchloutte m'a faite découvrir ce diagramme il y a quelques minutes.
J'avoue que je ne sais pas trop quoi en penser dans le sens où il m'apparaît comme très procédurale. Typiquement en OOP (Object Oriented Programming), nous avons une encapsulation des données et les traitements qui s'appliqueront sur elles au sein d'une même classe. Or, le découpage en couche implique une séparation des données dans des structures et l'application d'une logique applicative, répartie en couche. Les structures / entités traversant toutes les couches.
Ce n'est pas objet pour un sou. Et ce sera même quasiment impossible de coder proprement en objet avec de telles architectures puisque chaque couche insistera pour séparer les entités des traitements. De facto, nous nous retrouverons avec une armada de développeurs arguant que l'objet "sainul" puisqu'ils ne pourront tout simplement jamais exprimer facilement en objet. Ainsi, les classes issues de l'OOP se résumeront à être de simple modules (au sens Functional-Programming du terme, c'est-à-dire un namespace de fonctions) et les instances ne seront plus que des singletons, qu'ils soient gérés manuellement ou via Spring.
Pour faire de l'objet, il faut que votre architecture soit orientée objet (je simplifie). Pour coder en fonctionnel, il faut que vos données soit accessibles dans des monades (je simplifie aussi). Pour tirer le meilleur des deux mondes, il vous suffit d'avoir une architecture orientée objet, des algorithmes codés via une approche fonctionnelle. Mais visiblement, mes contemporains préfèrent une lutte de religion radicale que de découvrir les avantages de l'autre camp.
@Chlouchloutte j'aurai quand même besoin de te reposer des questions.
L'objectif de ce guide à la programmation est de vous apprendre à écrire des lignes de code de manière claire et concise.
Pour @Doudou qui me l'avait demandé.
Plein de bonnes pratiques PHP adaptablent à d'autres langages. #Kotlin 😍
Si vous devez améliorer votre code, cette courte lecture peut vous aider.
Préférez éliminer vos contraintes au plus tôt dans le code.
Aparté
Après plusieurs jours de recherche et quelques semaines de remise en question, je peux enfin mettre à plat ce que j'ai compris du clean code et surtout de ce que devrait être des TU écrits en TDD pour un service RESTful. Pour vous faire saisir l'idée dernière cela, je vais considérer que nous écrivons un serveur RESTful en Sparkjava (je n'ai pas encore migré totalement vers Javalin).
Principe de base
Selon Martin Fowler, Robert Martin et Kent Bent, nous n'avons pas forcément le lien MaClasse => MaClasseTest. En réalité, nous avons une problématique à laquelle répond une interface derrière laquelle se trouve une implémentation pouvant avoir un groupe de classes. Notre classe de test va donc tester ce petit groupe de classes à travers des TU.
Pourquoi ne pas conserver le pattern UneClasse <=> UneClasseTest ?
Simplement parce qu'il corrèle nos tests à la structure et l'organisation de notre code (ie. noms et packages). De manière formelle, on dit que le code des classes des tests et le code des sources sont covariants ; c'est-à-dire que changer l'architecture de l'un, induit inévitablement un changement chez l'autre.
Alors qu'en réalité, les TU écrits en TDD s'attachent à ce qui doit être fait et non comment cela est fait. De facto, tester l'implémentation à travers une interface décroit le couplage et rend le code de nos tests contra-variant avec le code de nos sources.
Débutons par un exemple
Considérons un micro-service RESTful qui gère un carnet d'adresse. La première question à se poser est : quelle est la liste des fonctionalités qu'expose ce service ?
Nous allons en considérer que deux :
- Ajouter un contact
- Supprimer un contact
=> Pas de recherche ni modification pour cet exemple.
En BDD, nous testerions ce service REST en lui envoyant du JSON ou carrément depuis l'interface graphique via du Selenium ou du Protractor où nos uses cases auraient été rédigés en Gherkin. Dans notre exemple en TDD, nous allons tester directement les méthodes des routes Sparkjava en mockant les paramètres spark.Request et spark.Response pour qu'ils retournent les valeurs qui nous arrangent bien.
Architecture applicative
L'achitecture demeure en couche, même si une encapsulation forte (au sens Yegor Bugayenko du terme) est omniprésente.
__________________
| |
| ROUTES |
| ^ |
|--------|---------
| v |
| PERSISTENCE |
| ^ |
|--------|--------|
| v |
| BASE DE DONNÉES |
|_________________|
Tests unitaires & TDD
Avant d'écrire la route AddPerson, nous allons commencer par écrire un test qui échoue :
/*
* COPYRIGHT © ITAMETIS - TOUS DROITS RÉSERVÉS
* Pour plus d'information veuillez contacter : copyright@itametis.com
*
* -------------------------------------------------------------------
*
* Cet extrait de code est tiré des dojos de nos amis chez ITAMETIS.
* Merci de m'avoir permise de m'en service ici pour étayer mon
* propos.
*/
import org.testng.Test
import org.assertj.Assertions.assertThat
import org.mockito.Mock.mock
import org.mockito.Mock.when
import spark.Request
import spark.Response
class AddPersonTest {
private val route:Route = AddPerson()
@Test
fun `AddPerson route should be able to create a regular user in base`() {
// Given
val json = "{'name':'Wayne', 'firstName':'Bruce', 'mobile':'+33012345678' }"
val request = mock(Request::class.java)
val response = mock(Response::class.java)
when(request.body()).thenReturn(json)
// When
val result = route.handle(request, response)
// Then
assertThat(result).contains("id")
.contains("'name':'Wayne'")
.contains("'firstName':'Bruce'")
.contains("'mobile':'+33012345678'")
}
}
Implémentation de la route AddPerson
Les routes sont des objets dédiés à une préoccupation. Typiquement la route AddPerson s'écrirait de la façon suivante :
/*
* COPYRIGHT © ITAMETIS - TOUS DROITS RÉSERVÉS
* Pour plus d'information veuillez contacter : copyright@itametis.com
*
* -------------------------------------------------------------------
*
* Cet extrait de code est tiré des dojos de nos amis chez ITAMETIS.
* Merci de m'avoir permise de m'en service ici pour étayer mon
* propos.
*/
import com.jsoniter.JsonIterator
import com.jsoniter.JsonStream
import spark.Request
import spark.Response
import com.itametis.sample.dto.PersonDto
import com.itametis.sample.entity.People
import com.itametis.sample.entity.People.Person
class AddPerson:Route {
companion object {
private const val CONTENT_TYPE = "Content-Type"
private const val RESPONSE_TYPE = "application/json"
}
data class PersonDto @JvmOverload construtor(
val id:Long? = null,
val name:String = "",
val firstName:String = "",
val mobile:String = "",
)
override fun handle(request:Request, response:Response):Any {
// Récupération du DTO
val dto:PersonDto = JsonIterator.deserialize(request.body(), Person::class.java)
// Enregistrement de la personne en base
val person:Person = People.createIt(
"name", dto.getName(),
"firstName", dto.getFirstName(),
"phone", dto.getPhone()
)
// Conversion en JSON de la personne fraîchement créée en base (avec son ID)
response.header(CONTENT_TYPE, RESPONSE_TYPE)
return JsonStream.serialize(person)
}
}
Quant à la déclaration de cette route dans Sparkjava, imaginons qu'elle soit accessible depuis deux URL :
- POST:/api/person/contact
- POST:/api/contact
Nous la déclarerions de la sorte :
/*
* COPYRIGHT © ITAMETIS - TOUS DROITS RÉSERVÉS
* Pour plus d'information veuillez contacter : copyright@itametis.com
*
* -------------------------------------------------------------------
*
* Cet extrait de code est tiré des dojos de nos amis chez ITAMETIS.
* Merci de m'avoir permise de m'en service ici pour étayer mon
* propos.
*/
class Main {
companion object {
@JvmStatic
fun main(vararg params:String) {
val addRoute:Route = AddPerson()
Sparkjava.post("/api/person/contact", addRoute)
Sparkjava.post("/api/contact", addRoute)
}
}
}
Que remarque-t-on ?
Avec cette architecture nous remarquons plusieurs choses :
- Que nos routes sont adhérentes à Sparkjava et que nous pourrions encapuler les objets Request et Response dans un objet à nous afin de se découpler de Sparkjava.
- Que la couche de service a été supprimée. Le code est intégralement remonté au niveau de la route. Cela fait sens puisque le contrôleur
RESTful se substitue aux Services de JEE dès que l'on a une SPA. - Qu'il est possible de factoriser les routes en leur affectant plusieurs URL.
- Que la route est porteuse de son DTO => Autre route => Autre contrat d'interfaçage => Autre DTO. On peut tout de même s'autoriser à
faire hériter nos routes d'une route abstraite.
Améliorations possibles
On peut améliorer les routes en les décorants par d'autres routes implémentant la même interface :
/*
* COPYRIGHT © ITAMETIS - TOUS DROITS RÉSERVÉS
* Pour plus d'information veuillez contacter : copyright@itametis.com
*
* -------------------------------------------------------------------
*
* Cet extrait de code est tiré des dojos de nos amis chez ITAMETIS.
* Merci de m'avoir permise de m'en service ici pour étayer mon
* propos.
*/
class Main {
companion object {
@JvmStatic
fun main(vararg params:String) {
val addRoute:Route = LogRouteInvocation( // On log la requête quoi qu'il arrive
AuthorizationChecker( // Est-on autorisé à exécuter la route
DtoToJsonConverter( // Évite le 'return JsonStream.serialize(...)' et le 'response.header(...)'
AddPerson() // La route d'origine
)
)
)
Sparkjava.post("/api/person/contact", addRoute)
Sparkjava.post("/api/contact", addRoute)
}
}
}
Je m'étais déjà faite la remarque : quand je refactor mon code, je casse toujours mes tests et je recolle les bouts à la fin.
Mes TU sont donc fortement couplés à mon code et ça c'est moisi.
Le blog de Robert Martin.
A practical demonstration of how to apply the Single Responsibility Principle
Pour toi Lenny
Tout est dans le titre. Ce genre d'articles est bon apprendre.
How to write good code and clean code ?
Un set de best-pratices de Stack Overflow.
Martin Fowler toujours aussi efficace dans ses explications.
Une vision plus théorique des techniques de refactoring mais toujours bon à avoir.