Notion de fonction

Principe de base

Une fonction est un bloc nommé paramétrable, réutilisable, centré sur une responsabilité, et une seule. Voir à ce sujet le pattern Principe de responsabilité unique (qui s’applique également au concept de classe)

Listing 1. Exemple de définition
fun sum(a: Int, b: Int) : Int = a + b

Pour utiliser cette fonction, nous devrons lui fournir 2 entiers.

En retour, cette fonction nous retournera un entier

Le type abstrait de la fonction est donc :Int X Int → Int (se lit Int croix Int dans Int)

Int X Int représente le produit cartésien de deux entiers, soit l’ensemble possible de tous les couples d’entiers (Int, Int). Dans le cadre d’une fonction simple (pas une méthode), l’ensemble des paramètres constitue le domaine de départ de la fonction. Ainsi une fonction se caratérise par son domaine de départ et son domaine d’arrivé (le type de la valeur de retour de la fonction)

Int X Int est le domaine de départ et Int le domaine d’arrivé.

C’est le contrat d’utilisation que devra respecter toute instruction qui ferait appel à cette fonction.

Dans le cas de fonctions simples, le domaine de définition est directement déduit des paramètres de la fonction (comme dans cet exemple)
Listing 2. Exemple d’appel (utilisation)
 val x: Int = sum(40, 2)
 println(x)   // 42

Nous avons bien donné une paire d’entier (40, 2) et en retour nous avons reçu un entier (42), valeur stockée ici "dans" la variable x

Chaque fois que vous aurez à définir une fonction, prendre bien soin à définir son type abstrait (domaine de départ et domaine d’arrivée), et de respecter le principe de Single Responsability - faire une chose, et le faire bien

Quelques exemples

println: String → Unit

Remarque 1 : La fonction ne retourne aucune valeur. En Kotlin, nous utiliserons pour cela le type Unit.

Remarque 2 : une fonction qui ne rend rien et qui ne fait rien est rare ! Par exemple, println écrit sur la sortie standard. Cette fonction modifie le système : après son appel, la sortie standard à été modifiée et peut impacter des processus qui sont en écoute de la sortie standard (voir le concept d' effet de bord)

Listing 3. Exemple d’appel
 println("Hello world !")

emptyList: T → List<T>

Le paramètre est ici un type (nommé T). En retour, on obtient une référence à un objet de type List<T>, vide.

Listing 4. Exemple d’appel
val produits = emptyList<Produit>()  // liste vide en lecture seule

val mutableList = mutableListOf<Produit>() // liste vide au départ

readLine: → String?

Sans paramètre. En retour on obtient une référence à un objet de type String ou null.

Listing 5. Exemple d’appel
 var s1: String? = readLine()

 if (s1 == null) { s1 = "" }

// ou plus simplement et plus sûrement :

 val s2: String = readLine()  ?: ""

transfert: Compte X Compte x Double → Unit

La méthode transfert reçoit 2 références à des comptes et un montant. En retour, elle ne retourne rien, mais réalise 2 effets de bords : les objets reçus en paramètre ont été modifiés par la méthode !

Listing 6. Exemple de déclaration
fun transfert(source: Compte, dest: Compte, montant: Double) {
   if (source.isOperationDebiterPossible(montant)) {
     dest.crediter(montant)
     source.debiter(montant)
   }
}
Listing 7. Exemple d’appel
    @Test
    fun testTransfertFonction() {
        val source = Compte("Mezigue", 100.0)
        val destinataire = Compte("Tezigue", 12.0)

        transfert(source, destinataire, 30.0)

        assertEquals(70.0, source.solde)
        assertEquals(42.0, destinataire.solde)
    }

Cas des méthodes

Une méthode est une fonction déclarée dans le contexte d’une classe, sa portée est d’instance par défaut (comprendre que son appel devra toujours se faire à partir d’une instance).

Listing 8. Exemple de définition
class Compte constructor(
    val nomClient: String,
    solde: Double = 0.0,
    var plafontDecouvert: Int = 0
) {
    var solde: Double = solde
        private set

    [...]

    fun transfert(destinataire: Compte, montant: Double) {
        if (this.isOperationDebiterPossible(montant)) {
            this.debiter(montant)
            dest.crediter(montant)
        }
    }
}

Du coup son domaine de départ contiendra toujours la classe dans laquelle la méthode est déclarée. Ce qui fait, que même déclaré dans une classe,

Exemple

transfert: Compte X Compte X Double → Unit

La méthode transfert reçoit 1 référence à un compte. En retour, elle ne retourne rien, mais réalise 2 effets de bords : la référence à l’instance de compte à partir de laquelle la fonction a été appelée (référencé par this dans le corps de méthode) ainsi que son paramètre ont été modifiés par la méthode, comme le montre le test ci-après!

Listing 9. Exemple d’appel
    @Test
    fun testTransfertFonction() {
        val source = Compte("Mezigue", 100.0)
        val destinataire = Compte("Tezigue", 12.0)

        source.transfert(destinataire, 30.0) (1)

        assertEquals(70.0, source.solde)
        assertEquals(42.0, destinataire.solde)
    }
1 Un seul argument de type Compte en paramètre (le premier argument de la version précédente a été placé en préfixe de l’appel)

Le contrat de la fonction

Connu aussi sous la dénomination documentation de l’API

Le contrat d’utilisation précise le rôle des éléments du domaine de départ et celui d’arrivé. Dans les langages courants, le contrat est représenté par un commentaire technique structuré (JavaDoc, PhpDoc, KDoc, etc.)

La documentation technique démarre par une courte description du rôle de la fonction. On peut y placer des exemples d’utilisation.

Puis, paramètre(s) et retour de fonction sont commentés, ainsi que les exceptions éventuelles et leurs conditions de déclenchement.

Listing 10. Exemple de déclaration de contrat
/**
* Réalise le transfert d'une somme donnée du compte à un autre compte
*
* @param destinataire celui qui bénéficie du transmfert
* @param montant le montant à transférer
* @throws IllegalOperationException si crédit de this insuffisant
*/
@Throws(IllegalOperationException)
fun transfert(destinataire: Int, montant: Double) {
   if (debitPossible(montant) == false)
       throw IllegalOperationException("solde insuffisant pour le tranfert")
   // sinon on réalise les opérations demandées
   // ...
}

Tester le contrat

Le contrat est de bonne inspiration pour la définition des tests unitaires.

Listing 11. Exemple d’un test unitaire qui contrôle le bon déclenchement d’une exception
  @Test
    fun testTransfertImpossible() {
        val source = Compte("Mezigue", 10.0)
        val destinataire = Compte("Tezigue", 12.0)

        try {
           // test un transfert interdit
           source.transfert(destinataire, 42.0)
           // si on arrive ici, c'est que l'exception ne s'est pas déclenchée !
           fail("Exception attendue !")
        } catch (e: IllegalOperationException) {
           // ok
        }
        // les comptes n'ont pas bougés
        assertEquals(10.0, source.solde)
        assertEquals(12.0, destinataire.solde)
    }
Dorénavant, dans vos projet, veuillez inscrire le contrat d’utilisation de toutes vos fonctions et méthodes.