Temps : 45, 20, 30, 15 min.
Diffiulté : ***
Cette partie aborde les fondamentaux de la POO avec le langage Kotlin, selon les points suivants :
En programmation, les packages servent à garder le code organisé.
agua
En Kotlin, une classe est défini avec le mot clé class
.
Note : Le nom d'une classe commence par une majuscule.
agua
Aquarium
Aquarium
, définissez et initialisez les variables width, height, length
package agua
class Aquarium {
var width: Int = 20
var height: Int = 40
var length: Int = 100
}
Note : Il n'y a pas besoin d'écrire les getters et setters,
ils sont implicites. De plus, par défaut tout est publique, d'où l'absence
du mot clé public
.
En programmation, la fonction main()
est le point d'entré d'un programme.
En Kotlin, une fonction est déclaré avec le mot clé fun
.
agua
Main.kt
buildAquarium()
, elle créé une instance de Aquarium
package agua
fun buildAquarium() {
val myAquarium = Aquarium()
}
Note : Pour créer une instance, référencez la classe comme s'il s'agissait d'une fonction (Aquarium()
)
Cela appelle le constructeur de la classe et crée une instance de cette dernière,
similaire à l'utilisation du mot clé new dans d'autres langages.main()
, elle appelle buildAquarium()
package agua
...
fun main() {
buildAquarium()
}
main()
(Control + Shift + R)Une fonction membre, member function, ou méthode est une fonction pouvant s'appliquer sur un objet spécifique.
Aquarium
, ajoutez une méthode d'affichage :
fun printSize() {
println("Width: $width cm " +
"Length: $length cm " +
"Height: $height cm ")
}
Main.kt
, depuis buildAquarium()
,
appelez la méthode printSize()
sur myAquarium
fun buildAquarium() {
val myAquarium = Aquarium()
myAquarium.printSize()
}
buildAquarium()
, modifiez un des attributs de myAquarium
,
puis affichez le changement
fun buildAquarium() {
val myAquarium = Aquarium()
myAquarium.printSize
// modification de la hauteur
myAquarium.height = 60
myAquarium.printSize()
}
En Kotlin, il y a deux types de constructeurs :
constructor
Aquarium
,
sans constructeur, avec des propriétés par défaut. Aquarium
, changez l'entête de classe
pour inclure un constructeur primaire avec des valeurs par défaut,
modifiez les propriétés en conséquence :
class Aquarium(length: Int = 100, width: Int = 20, height: Int = 40) {
// Dimensions en cm
var length: Int = length
var width: Int = width
var height: Int = height
...
}
class Aquarium(var length: Int = 100, var width: Int = 20, var height: Int = 40) {
...
}
Main.kt
,
buildAquarium()
, en créant différentes instances d'aquarium :
val aquarium1 = Aquarium()
aquarium1.printSize()
// default height and length
val aquarium2 = Aquarium(width = 25)
aquarium2.printSize()
// default width
val aquarium3 = Aquarium(height = 35, length = 110)
aquarium3.printSize()
// everything custom
val aquarium4 = Aquarium(width = 25, height = 35, length = 110)
aquarium4.printSize()
main()
(Control + Shift + R),
observez le résultat attenduEn Kotlin, il existe le bloc d'initialisation, init
.
Il permet de placer du code d'initialisation lorsque le constructeur en a besoin.
Aquarium
, placez un bloc init
:
class Aquarium(var length: Int = 100, var width: Int = 20, var height: Int = 40) {
init {
println("aquarium initializing")
}
}
En Kotlin, il est possible de déclarer les deux types de constructeur
primaire et secondaire dans la même classe. Cela permet de faire de la surcharge
de constructeur, constructor overloading, avec des arguments différents.
Lorsqu'il y a plusieurs constructeur, il s'appelle les uns des autres avec
le mot clé this
(et des arguments null
,
cf. Challenge : Créer une vue personnalisée).
Note : L'ordre d'exécution des constructeurs et bloc d'initialisation est
Aquarium
, ajoutez un constructeur secondaire :
constructor(numberOfFish: Int) : this() {
// tank: le réservoir d'eau, la cuve
// 2,000 cm^3 par poisson + espace sup. pour éviter les éclaboussures
val tank = numberOfFish * 2000 * 1.1
}
// la largueur de l'aquarium est fixe, la hauteur est flexible,
// il s'agit de calculer la hauteur nécessaire pour le bien être des poissons
height = (tank / (length * width)).toInt()
Main.kt
, appellez le constructeur ainsi créé
depuis buildAquarium()
:
fun buildAquarium() {
val aquarium6 = Aquarium(numberOfFish = 29)
aquarium6.printSize()
println("Volume: ${aquarium6.width * aquarium6.length * aquarium6.height / 1000} liters")
}
En Kotlin, l'accesseur et le mutateur d'une propriété est automatiquement créés. Cependant, il est possible de créer un mutateur, ou un accesseur, personnalisé pour chaque propriété.
Aquarium
, définissez une propriété personnalisée :
// le volume se calcule à partir des autres propriétés hauteur et largueur
val volume: Int
get() = width * height * length / 1000 // 1000 cm^3 = 1 liter
init
affichant le volumebuildAquarium()
printSize()
, ajoutez l'affichage du volume
fun printSize() {
println("Width: $width cm " +
"Length: $length cm " +
"Height: $height cm "
)
// 1 liter = 1000 cm^3
println("Volume: $volume liters")
}
Aquarium
, changez la propriété volume immuable en mutable var
// le volume se calcule à partir des autres propriétés hauteur et largueur
val volume: Int
get() = width * height * length / 1000 // 1000 cm^3 = 1 liter
set(value) {
height = (value * 1000) / (width * length)
}
buildAquarium()
, ajoutez la modification du volume
fun buildAquarium() {
val aquarium6 = Aquarium(numberOfFish = 29)
aquarium6.printSize()
aquarium6.volume = 70
aquarium6.printSize()
}
En Kotlin, les classes, les objets, les interface, les constructeurs,
les fonctions, les propriétés et leurs accesseurs peuvent avoir des modificateurs de visibilité.
Par défaut, tout est publique, public
.
public |
visible au dehors de la classe |
private |
seulement visible à l'intérieur de la classe |
protected |
visible également par les sous classes |
internal |
visible dans le module (c'est un ensemble de classe ou fichier compiler ensemble) |
En Kotlin, l'héritage n'est pas automatique, une classe mère doit être
déclaré open
, de façon a lui permettre d'être une super classe.
De même les propriétés et membre doivent être déclaré open
.
open
L'objectif est de transformer l'aquarium en une classe mère.
Aquarium
, changez la signature de façon a déclaré la classe open
open
open class Aquarium (open var length: Int = 100, open var width: Int = 20, open var height: Int = 40) {
open var volume: Int
get() = width * height * length / 1000
set(value) {
height = (value * 1000) / (width * length)
}
open val shape = "rectangle"
open var water: Double = 0.0
get() = volume * 0.9
fun printSize() {
println(shape)
println("Width: $width cm " +
"Length: $length cm " +
"Height: $height cm ")
// 1 l = 1000 cm^3
println("Volume: $volume liters Water: $water liters (${water / volume * 100.0}% full)")
}
buildAquarium()
, créez une instance d'aquarium comme suit :
fun buildAquarium() {
val aquarium6 = Aquarium(length = 25, width = 25, height = 40)
aquarium6.printSize()
}
Il s'agit de créer une sous classe ou classe fille. Contexte : L'aquarium est rectangle, nous souhaitons à présent créer un aquarium cyclindrique.
Aquarium
,
à la suite de la classe Aquarium
, déclarez une classe TowerTank
class TowerTank (override var height: Int, var diameter: Int): Aquarium(height = height, width = diameter, length = diameter) {
override var volume: Int
// ellipse area = π * r1 * r2
get() = (width/2 * length/2 * height / 1000 * PI).toInt()
set(value) {
height = ((value * 1000 / PI) / (width/2 * length/2)).toInt()
}
override var water = volume * 0.8
override val shape = "cylinder"
package agua
import java.lang.Math.PI
... // Classe Aquarium existante
class TowerTank (override var height: Int, var diameter: Int): Aquarium(height = height, width = diameter, length = diameter) {
override var volume: Int
// ellipse area = π * r1 * r2
get() = (width/2 * length/2 * height / 1000 * PI).toInt()
set(value) {
height = ((value * 1000 / PI) / (width/2 * length/2)).toInt()
}
override var water = volume * 0.8
override val shape = "cylinder"
}
buildAquarium()
, créez une instance d'aquarium cyclindrique :
fun buildAquarium() {
val myAquarium = Aquarium(width = 25, length = 25, height = 40)
myAquarium.printSize()
val myTower = TowerTank(diameter = 25, height = 40)
myTower.printSize()
}
Dans certain cas, nous souhaitons définir des comportements identiques pour
des propriétés ou des classes.
Par exemple, nous allons créer :
Une classe abstraite est partiellement définie. C'est de la responsabilité
de la sous classe de définir les méthodes et propriétés.
Par défaut une classe abstract
est open
, il n'y a pas
besoin de le spécifier.
Elle peut avoir des propriétés et des méthodes abstraite, dans ce cas la sous
classe est en charge de les définir.
Elle peut aussi définir un constructeur commun pour toute les sous classes.
agua
, créez un nouveau fichier AquariumFish.kt
AquariumFish
déclarez là comme abstraite :
package agua
abstract class AquariumFish
abstract val color: String
AquariumFish
: Shark
et Plecostomus
color
est abstraite, Shark
et Plecostomus
doivent la définir
class Shark: AquariumFish() {
override val color = "grey"
}
class Plecostomus: AquariumFish() {
override val color = "gold"
}
Main.kt
, créez une fonction makeFish()
afin d'y instancier des poissons
fun makeFish() {
val shark = Shark()
val pleco = Plecostomus()
println("Shark: ${shark.color}")
println("Plecostomus: ${pleco.color}")
}
makeFish()
dans le main
,
exécutez le programme et observez le résultat
AquariumFish.kt
, créez une interface avec une méthode
interface FishAction {
fun eat()
}
FishAction
aux deux sous classeseat()
:
class Shark: AquariumFish(), FishAction {
override val color = "grey"
override fun eat() {
println("hunt and eat fish")
}
}
class Plecostomus: AquariumFish(), FishAction {
override val color = "gold"
override fun eat() {
println("eat algae")
}
}
makeFish()
, faites manger les poissons
fun makeFish() {
val shark = Shark()
val pleco = Plecostomus()
println("Shark: ${shark.color}")
shark.eat()
println("Plecostomus: ${pleco.color}")
pleco.eat()
}
L'astuce est d'utiliser une classe abstraite tant qu'il n'est pas possible de compléter la classe.
AquariumFish.kt
, modifiez la classe abstraite
pour générer une implémentation générale pour le comportement du poisson
abstract class AquariumFish : FishAction {
abstract val color: String
override fun eat() = println("yum")
}
class RedFish : AquariumFish() {
override val color = "red"
}
makeFish()
testez le poisson rouge
fun makeFish() {
val fish = RedFish()
println("Fish: ${fish.color}")
fish.eat()
}
En Kotlin, les qualificatifs de classe les plus connus sont :
data
: représente une donnéenested
: une classe dans la classe (static class
in Java)inner
: visibilité des membres élargie (ajoute une référence à une classe extérieur)enum
: représente une énumérationsealed
: restreint la hiérarchie de la classe (le nombre de sous classe est fixe, elles sont
placées dans le même fichier)La classe de donnée, ou data class
, est une bonne pratique afin d'indiquer la simplicité
de l'objet.
Déclarée avec le mot clé data
, la classe a accès a des fonctions
générées automatiquement, tel que la fonction equals()
, hascode()
,
toString()
...
agua
, nommez-le deco
Decoration
:
data class Decoration(val rocks: String)
Decoration.kt
, à l'extérieur de la classe,
ajoutez une fonction makeDecorations()
pour afficher des instances de la classe de donnée
fun makeDecorations() {
val decoration1 = Decoration("granite")
println(decoration1)
}
main()
pour tester, exécutezmakeDecorations()
fun makeDecorations() {
val decoration1 = Decoration("granite")
val d2 = Decoration("crystal")
val d3 = Decoration("crystal")
println("$decoration1 $d2 $d3")
}
makeDecorations()
, testez l'égalité des instances :
println("deco 1 et deco 2 : ${decoration1.equals(d2)}")
println("deco 2 et deco 3 (equals): ${d2.equals(d3)}")
println("deco 2 et deco 3 (==): ${d2 == d3}")
println("deco 2 et deco 3 (===): ${d2 === d3}")
Note : ==
et equals
c'est pareil (operator overloading),
===
correspond à l'égalité sur la référence de l'objet
La classe d'énumération, ou enum class
, est une bonne pratique
afin d'énumerer des valeurs.
Cela dit, il est plus courant d'utiliser la classe salée.
Decoration.kt
, déclarez une classe d'énumération :
enum class Direction(val degrees: Int) {
NORTH(0), SOUTH(180), EAST(90), WEST(270)
}
main
testez :
println(Direction.EAST.name)
println(Direction.EAST.ordinal)
println(Direction.EAST.degrees)
Finalement, Kotlin est un langage de programmation fonctionnelle et orientée objet par rapport à :
class
, open
, abstract
, interface
, data
, enum
constructor
et le bloc init