CodeGym /Cours /Kotlin SELF /Le primary constructor comme API de classe : paramètres, ...

Le primary constructor comme API de classe : paramètres, propriétés et valeurs par défaut

Kotlin SELF
Niveau 31, Leçon 0
Disponible

1. Primary constructor : la « porte d’entrée » de la classe

Quand on commence à écrire des classes, on a envie de se dire : « Bon, le constructeur, c’est juste une façon de mettre des valeurs à l’intérieur ». Mais en réalité, le constructeur est l’entrée publique de votre type, c’est-à-dire une partie de l’API de la classe. C’est comme la porte d’entrée d’un bureau : on peut la rendre pratique et bien indiquée, ou bien installer 12 portes identiques sans panneau — et ensuite s’étonner que les employés finissent dans la salle des serveurs.

En Kotlin, la façon principale de créer un objet, c’est le primary constructor, qui s’écrit directement dans l’en-tête de la classe. Il est considéré comme « la route principale de création », et le reste du code s’organise généralement autour de lui. Dans la documentation Kotlin, c’est décrit explicitement : le primary constructor est déclaré dans l’en-tête, et ses paramètres peuvent devenir des propriétés via val/var.

Un exemple simple « une classe comme fonction de création d’objet » :

class User(val name: String, var age: Int)

fun main() {
    val u = User(name = "Ann", age = 20)
    println(u.name) // Ann
    u.age += 1
    println(u.age)  // 21
}

Notez un autre « bonus agréable de Kotlin » : lors de la création d’un objet, on n’a pas besoin de new — vous appelez la classe comme une fonction.
C’est un détail, mais il rend le code plus compact : moins de « mots rituels », plus de sens.

2. Paramètre de constructeur vs propriété : ce qui devient l’état

À ce stade, presque tous les débutants marchent sur le même râteau. Ça paraît logique : « J’ai écrit class X(a: Int) — donc l’objet a a ». Mais non : un paramètre de constructeur sans val/var, c’est juste une valeur d’entrée, comme le paramètre d’une fonction. Il n’est pas obligé de devenir une partie de l’état de l’objet.

Kotlin sait faire une chose élégante : si le paramètre est déclaré avec val ou var, il est à la fois paramètre du constructeur et propriété de l’objet. C’est un des éléments clés du « style Kotlin » — moins de boilerplate.

Comparons deux variantes.

Variante A : le paramètre est une propriété

class Label(val text: String)

fun main() {
    val l = Label("Hi")
    println(l.text) // Hi
}

Variante B : le paramètre n’est pas une propriété

class Label(text: String) {
    val text: String = text
}

fun main() {
    val l = Label("Hi")
    println(l.text) // Hi
}

Les deux variantes fonctionnent, mais leur sens est différent. Dans la variante B, vous dites en quelque sorte : « À la création on m’a donné text, mais moi je stockerai text (le mien) ». Dans la documentation Kotlin, il est souligné séparément que les paramètres sans val/var ne sont pas conservés comme propriétés et ne sont accessibles que dans le corps de la classe, tant que vous ne les copiez pas dans une propriété.

Pour ne pas garder ça « au feeling », il est utile de fixer les choses ainsi :

Écriture dans l’en-tête Est-ce une propriété de l’objet ? Peut-on y accéder depuis l’extérieur (obj.x) ? Sens typique
class A(x: Int)
non non valeur d’entrée « au moment de la création »
class A(val x: Int)
oui (read-only) oui partie de l’état, non modifiable
class A(var x: Int)
oui (mutable) oui partie de l’état, modifiable

3. Concevoir Expense : un type agréable à instancier

Voici une idée pratique importante : un bon constructeur est celui qu’on a plaisir à lire à l’endroit où l’objet est créé. Parce que la vraie douleur n’est pas d’écrire la classe, mais d’ouvrir le code un mois plus tard et de comprendre ce que signifie Expense(1, "Taxi", 900, "transport") — est-ce un taxi à 9 USDT ou à 900 USDT, et d’ailleurs, c’est quoi ce « 1 » ?

Imaginons que dans notre tracker de dépenses en console, une entrée correspond à une dépense : elle a un identifiant, un nom, un montant (en unités entières pour éviter de souffrir avec Double), et une catégorie sous forme de chaîne (pour l’instant en chaîne : le enum viendra plus tard).

Commençons simplement :

class Expense(
    val id: Int,
    val title: String,
    val amount: Int,
    val category: String
)

fun main() {
    val e = Expense(1, "Coffee", 350, "food")
    println(e.title)   // Coffee
    println(e.amount)  // 350
}

Déjà pas mal : c’est un seul objet, tout est regroupé, et on ne recolle plus des données « par index » dans des listes différentes.

Mais soyons honnêtes : la ligne Expense(1, "Coffee", 350, "food") se lit moyennement. Avec quatre paramètres, ça passe encore, mais dès qu’un cinquième apparaît — on commence à « deviner ».

C’est là que le constructeur en tant qu’API se révèle particulièrement : on ne choisit pas seulement des types, on conçoit aussi la lisibilité de l’appel.

4. Valeurs par défaut : moins de chaos à la création

À un moment, vous aurez envie de dire : « La catégorie peut être optionnelle — si elle n’est pas fournie, on considère "other" ». Et là, deux chemins existent. Le premier : bricoler plusieurs variantes de création (spoiler : les secondary constructors viendront plus tard). Le second : les valeurs par défaut.

Le primary constructor supporte les default values comme les fonctions. C’est décrit officiellement : on peut définir des valeurs par défaut directement dans les paramètres du constructeur, et si un argument n’est pas fourni — la valeur par défaut sera utilisée.

Faisons ainsi :

class Expense(
    val id: Int,
    val title: String,
    val amount: Int,
    val category: String = "other"
)

fun main() {
    val a = Expense(1, "Coffee", 350)
    val b = Expense(2, "Taxi", 900, "transport")

    println(a.category) // other
    println(b.category) // transport
}

Effet important : maintenant, notre classe a moins de « modes de création », et c’est une bonne chose. Le cerveau du lecteur n’a pas à se souvenir que « si la catégorie n’est pas passée, il faut créer l’objet autrement ». La classe sait elle-même « démarrer » correctement.

En pratique, les défauts apportent le plus souvent deux avantages. D’abord, ils réduisent la quantité de données obligatoires : on ne rend obligatoire que ce sans quoi l’objet n’a pas de sens. Ensuite, ils diminuent le nombre d’erreurs à la création : moins d’endroits où il faut « ne rien oublier ».

5. Arguments nommés : la lisibilité côté appel

Maintenant, faisons ce qui transforme « quatre paramètres — acceptable » en « six paramètres — toujours lisible » : les arguments nommés.

L’idée clé est simple : les noms des paramètres du constructeur ne sont pas juste « comment vous les avez nommés chez vous ». Ils font partie de l’interface externe : les gens les verront dans les appels, surtout s’ils utilisent les named arguments. Donc, un mauvais nom de paramètre dans un constructeur, c’est comme une mauvaise étiquette sur un bouton dans une application.

Voyons à quel point c’est plus clair :

class Expense(
    val id: Int,
    val title: String,
    val amount: Int,
    val category: String = "other"
)

fun main() {
    val e = Expense(
        id = 10,
        title = "Lunch",
        amount = 550,
        category = "food"
    )

    println(e.title)    // Lunch
    println(e.amount)   // 550
}

Oui, c’est un peu plus long. Mais ici, la « longueur » agit comme une assurance contre les erreurs. Surtout quand des paramètres d’un même type se répètent. Deux lignes String à la suite, c’est la source classique du « j’ai inversé et je ne l’ai pas vu ».

Pour voir à quel point il est facile de se tromper sans arguments nommés, imaginez cette classe :

class Client(val firstName: String, val lastName: String)

fun main() {
    val c = Client("Ivan", "Petrov")
    println("${c.firstName} ${c.lastName}") // Ivan Petrov
}

Si vous les inversez par accident, le compilateur ne vous sauvera pas : les types sont identiques. Avec des named arguments, le risque de confusion est beaucoup plus faible.

6. val ou var : « modifiable » fait partie de l’API

Voici un moment où le développeur grandit et commence à dire des choses intimidantes du genre « la mutabilité doit se mériter ». Mais sans fanatisme : var n’est pas le mal, ça ouvre simplement la porte aux changements — et les changements doivent être contrôlés.

La différence est simple. val dans le constructeur : l’objet est créé, et cette propriété ne peut pas être réassignée (même si l’on stocke à l’intérieur un objet mutable, c’est une autre histoire, mais on n’y va pas aujourd’hui). var : on peut changer la valeur de la propriété après la création.

Par exemple, pour une dépense il n’y a généralement aucun sens à modifier id après la création, et souvent aucun sens à modifier amount (une dépense est un fait). Donc, dans le modèle, il est logique de garder cela en val. En revanche, si l’on veut autoriser l’édition de l’entrée (par exemple, corriger une faute dans le titre) — on peut faire title en var. Mais il est important que ce choix soit conscient, et pas « je mets var partout pour ne pas réfléchir ».

Exemple de « mutabilité consciente » :

class Expense(
    val id: Int,
    var title: String,
    val amount: Int,
    val category: String = "other"
)

fun main() {
    val e = Expense(id = 1, title = "Cofee", amount = 350) // faute de frappe :)
    e.title = "Coffee"
    println(e.title) // Coffee
}

Ici, var title se lit comme une promesse de la classe : « Le titre peut être corrigé ». Et val amount se lit comme une promesse : « On ne touche pas au montant, c’est un fait ». Et c’est exactement ce que signifie « le constructeur fait partie de l’API » : via val/var, vous communiquez les règles d’utilisation de votre type.

7. Quand un paramètre ne doit pas être une propriété

Parfois, on a envie de passer au constructeur quelque chose de temporaire : par exemple, du « texte brut » nécessaire uniquement pour calculer une valeur normalisée. Ou bien, disons, vous recevez la chaîne " Food " et vous voulez stocker "food".

Aujourd’hui, on ne fait pas encore init (ce sera la prochaine leçon de la journée), mais il est important de comprendre l’idée : parfois, un paramètre de constructeur n’est qu’une matière première à partir de laquelle vous fabriquez l’état, et il n’est pas nécessaire de stocker cette matière brute.

Voici un micro-exemple sur un sujet parlant :

class Category(raw: String) {
    val name: String = raw.trim().lowercase()
}

fun main() {
    val c = Category("  FoOd  ")
    println(c.name) // food
}

Pourquoi raw n’est-il pas val raw ? Parce qu’on n’a pas besoin de stocker une « entrée sale ». On veut stocker une valeur déjà préparée, exploitable par la logique.

Cette approche se combine bien avec l’idée de la documentation : un paramètre sans val/var n’est pas une propriété et, en soi, ne « vit » pas à l’intérieur de l’objet.

8. Pratique et nuances utiles

Quand constructor est nécessaire, et quand il ne l’est pas

Parfois, vous verrez deux variantes d’écriture :

class Person constructor(name: String)

et

class Person(name: String)

La différence n’est pas le sens, mais la syntaxe. Kotlin permet d’omettre le mot-clé constructor s’il n’y a ni annotations ni modificateurs de visibilité sur le constructeur. C’est une pratique normale : la plupart des classes s’écrivent sans constructor.

Dans notre cours, dans 99% des cas, nous écrirons de manière compacte :

class Expense(val id: Int, val title: String, val amount: Int)

Et le mot constructor sera utile plus tard, quand on abordera des cas plus « adultes » (par exemple, des modificateurs de visibilité du constructeur), mais ce n’est pas le sujet de cette leçon.

Schéma : le primary constructor comme contrat

Il est utile de visualiser mentalement la création d’objet comme un mini-flux de données : le monde extérieur transmet des valeurs → la classe les accepte selon un contrat → l’objet apparaît dans le programme.

Voici un schéma simple :

flowchart LR
    A["Code externe
Expense(...)"] --> B["Primary constructor
(parametres)"] B --> C["Proprietes de l’objet
(val/var)"] C --> D["Objet pret a etre utilise"]

Dans cette image, le primary constructor est l’endroit où vous définissez le contrat : quelles données sont nécessaires, lesquelles sont optionnelles (valeurs par défaut), lesquelles deviennent une partie de l’état (val/var), et lesquelles ne sont qu’une entrée.

Mini-étape : une liste de dépenses sous forme d’objets

Pour ne pas tout laisser dans la « théorie sur l’API », faisons une mini-étape dans notre application : dans main(), créons plusieurs dépenses et affichons-les. Pas de commandes compliquées : juste ressentir que le modèle est devenu lisible.

class Expense(
    val id: Int,
    val title: String,
    val amount: Int,
    val category: String = "other"
)

fun main() {
    val expenses = mutableListOf<Expense>()

    expenses.add(Expense(id = 1, title = "Coffee", amount = 350, category = "food"))
    expenses.add(Expense(id = 2, title = "Taxi", amount = 900, category = "transport"))
    expenses.add(Expense(id = 3, title = "Notebook", amount = 120)) // categorie par defaut

    for (e in expenses) {
        println("#${e.id}: ${e.title} — ${e.amount} (${e.category})")
        // #1: Coffee — 350 (food)
        // #2: Taxi — 900 (transport)
        // #3: Notebook — 120 (other)
    }
}

Ici, on voit immédiatement que le primary constructor « fonctionne vraiment comme une API » : il dicte quels champs existent dans une dépense, comment ils s’appellent, et quelles valeurs peuvent ne pas être fournies. Et les arguments nommés transforment la création en un code presque auto-documenté.

9. Erreurs typiques

Erreur n°1 : confondre un paramètre de constructeur avec une propriété et oublier val/var.
Ça ressemble à ceci : vous écrivez class Expense(id: Int, title: String), puis vous essayez d’accéder à e.title, et le compilateur répond « ça n’existe pas ». En réalité, tout est cohérent : title n’était qu’un paramètre d’entrée, pas une partie de l’état. Si une valeur doit vivre dans l’objet — mettez val title: String directement dans le constructeur, ou assignez-la à une propriété à l’intérieur du corps de la classe.

Erreur n°2 : rendre toutes les propriétés var, parce que « on ne sait jamais ».
C’est une des habitudes les plus coûteuses. Quand vous mettez var, c’est comme si vous laissiez une porte ouverte : « ça peut changer à tout moment ». Ensuite, on découvre que quelque part une valeur a changé, la logique a déraillé, et vous cherchez le coupable comme un détective avec une loupe. Commencez avec val et ne passez à var que ce qui doit réellement changer par le sens.

Erreur n°3 : trop de paramètres dans le primary constructor et des positions d’arguments « magiques ».
Si le constructeur exige 6–8 paramètres et que vous l’appelez en positionnel (par exemple, A(1, "x", "y", 10, 20, true)), le code ressemble à une incantation qu’on n’a pas le droit de toucher, sinon on invoque un démon. Au niveau de cette leçon, le salut le plus simple est d’utiliser plus activement les arguments nommés et les valeurs par défaut, pour que seuls les éléments vraiment nécessaires restent obligatoires.

Erreur n°4 : de mauvais noms de paramètres dans le constructeur.
Le constructeur fait partie de l’interface publique. Si vous appelez les paramètres a, b, x1, s2, vous forcez littéralement le lecteur à deviner quoi passer. C’est particulièrement douloureux quand vous utilisez des arguments nommés : Expense(a = 1, b = "Coffee", x1 = 350). Les noms de paramètres doivent refléter le sens, parce qu’ils vivront dans le code de création d’objets.

Erreur n°5 : ne pas utiliser de default values là où elles sont naturelles, et complexifier inutilement.
Quand un paramètre prend souvent la même valeur (par exemple, la catégorie "other" ou le flag isActive = true), il est plus pratique de lui donner une valeur par défaut dans le primary constructor que de forcer chaque appel à répéter la même chose. Kotlin le supporte directement : les défauts dans le primary constructor fonctionnent comme les défauts dans les fonctions.

1
Mission
Kotlin SELF, niveau 31, leçon 0
Bloqué
Badge du bénévole
Badge du bénévole
1
Mission
Kotlin SELF, niveau 31, leçon 0
Bloqué
Paramètres de connexion
Paramètres de connexion
1
Mission
Kotlin SELF, niveau 31, leçon 0
Bloqué
Fiche client
Fiche client
1
Mission
Kotlin SELF, niveau 31, leçon 0
Bloqué
Étiquette de catégorie
Étiquette de catégorie
Commentaires
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION