Programmation orientée objet (C#)

C# est un langage de programmation orienté objet. Les quatre principes de base de la programmation orientée objet sont les suivants :

  • Abstraction Modélisation des attributs et interactions pertinents des entités en tant que classes, afin de définir une représentation abstraite d’un système.
  • Encapsulation Masquage de l’état interne et des fonctionnalités d’un objet et autorisation d’accès uniquement par le biais d’un ensemble public de fonctions.
  • Héritage Possibilité de créer des abstractions basées sur des abstractions existantes.
  • Polymorphisme Possibilité d’implémenter des propriétés ou des méthodes héritées de différentes manières parmi plusieurs abstractions.

Dans le didacticiel précédent, Introduction aux classes, vous avez vu à la fois l’abstraction et l’encapsulation. La classe BankAccount a fourni une abstraction pour le concept d’un compte bancaire. Vous pouvez modifier son implémentation sans affecter le code qui a utilisé la classe BankAccount. Les classes BankAccount et Transaction fournissent l’encapsulation des composants nécessaires pour décrire ces concepts dans le code.

Dans ce didacticiel, vous allez étendre cette application pour utiliser l’héritage et le polymorphisme, afin d’ajouter de nouvelles fonctionnalités. Vous allez également ajouter des fonctionnalités à la classe BankAccount, en tirant parti des techniques d’abstraction et d’encapsulation que vous avez apprises dans le didacticiel précédent.

Créer différents types de comptes

Après avoir généré ce programme, vous recevez des demandes d’ajout de fonctionnalités. Il fonctionne très bien dans la situation où il n’y a qu’un seul type de compte bancaire. Au fil du temps, les besoins changent et des types de comptes associés sont demandés :

  • Un compte générant des intérêts qui s’accumulent à la fin de chaque mois.
  • Une marge de crédit qui peut avoir un solde négatif, mais quand il y a un solde, il y a des frais d’intérêt chaque mois.
  • Un compte de carte cadeau prépayée qui commence par un seul versement et qui peut uniquement être remboursé. Il peut être rempli une fois au début de chaque mois.

Tous ces différents comptes sont similaires à la classe BankAccount définie dans le didacticiel précédent. Vous pouvez copier ce code, renommer les classes et apporter des modifications. Cette technique fonctionnerait à court terme, mais engendrerait davantage de travail au fil du temps. Toutes les modifications seraient copiées dans toutes les classes affectées.

Au lieu de cela, vous pouvez créer de nouveaux types de comptes bancaires qui héritent des méthodes et des données de la classe BankAccount créée dans le didacticiel précédent. Ces nouvelles classes peuvent élargir la classe BankAccount avec le comportement spécifique nécessaire pour chaque type :

public class InterestEarningAccount : BankAccount
{
}

public class LineOfCreditAccount : BankAccount
{
}

public class GiftCardAccount : BankAccount
{
}

Chacune de ces classes hérite du comportement partagé de sa classe de base partagée, la classe BankAccount. Écrivez les implémentations pour les fonctionnalités nouvelles et différentes dans chacune des classes dérivées. Ces classes dérivées possèdent déjà tout le comportement défini dans la classe BankAccount.

Il est recommandé de créer chaque nouvelle classe dans un fichier source différent. Dans Visual Studio, vous pouvez cliquer avec le bouton droit sur le projet, puis sélectionner Ajouter une classe pour ajouter une nouvelle classe dans un nouveau fichier. Dans Visual Studio Code, sélectionnez Fichier, puis Nouveau pour créer un nouveau fichier source. Dans l’un des deux outils, nommez le fichier pour qu’il corresponde à la classe : InterestEarningAccount.cs, LineOfCreditAccount.cs et GiftCardAccount.cs.

Lorsque vous créez les classes comme indiqué dans l’exemple précédent, vous constatez qu’aucune de vos classes dérivées ne se compile. Un constructeur est responsable de l’initialisation d’un objet. Un constructeur de classe dérivée doit initialiser la classe dérivée et fournir des instructions sur la manière d’initialiser l’objet de classe de base inclus dans la classe dérivée. L’initialisation appropriée se produit normalement sans code supplémentaire. La classe BankAccount déclare un constructeur public avec la signature suivante :

public BankAccount(string name, decimal initialBalance)

Le compilateur ne génère pas de constructeur par défaut lorsque vous définissez vous-même un constructeur. Cela signifie que chaque classe dérivée doit appeler explicitement ce constructeur. Vous déclarez un constructeur qui peut passer des arguments au constructeur de classe de base. Le code suivant montre le constructeur pour la InterestEarningAccount :

public InterestEarningAccount(string name, decimal initialBalance) : base(name, initialBalance)
{
}

Les paramètres de ce nouveau constructeur correspondent au type de paramètre et aux noms du constructeur de classe de base. Vous utilisez la syntaxe : base() pour indiquer un appel à un constructeur de classe de base. Certaines classes définissent plusieurs constructeurs, et cette syntaxe vous permet de choisir le constructeur de classe de base que vous appelez. Une fois que vous avez mis à jour les constructeurs, vous pouvez développer le code pour chacune des classes dérivées. Les exigences pour les nouvelles classes peuvent être indiquées comme suit :

  • Un compte porteur d’intérêt :
    • Obtiendra un crédit de 2 % du solde de fin de mois.
  • Une marge de crédit :
    • Peut avoir un solde négatif, mais ne pas être supérieur en valeur absolue à la limite de crédit.
    • Entraîne des frais d’intérêt chaque mois lorsque le solde de fin de mois n’est pas de 0.
    • Entraîne des frais sur chaque retrait qui dépasse la limite de crédit.
  • Un compte de carte cadeau :
    • Peut être rempli avec un montant spécifié une fois par mois, le dernier jour du mois.

Vous pouvez voir que ces trois types de compte ont une action qui a lieu à la fin de chaque mois. Toutefois, chaque type de compte effectue des tâches différentes. Vous utilisez le polymorphisme pour implémenter ce code. Créez une méthode unique virtual dans la classe BankAccount :

public virtual void PerformMonthEndTransactions() { }

Le code précédent montre comment vous utilisez le mot clé virtual pour déclarer une méthode dans la classe de base pour laquelle une classe dérivée peut fournir une implémentation différente. Une méthode virtual est une méthode dans laquelle n’importe quelle classe dérivée peut choisir d’implémenter de nouveau. Les classes dérivées utilisent le mot clé overridepour définir la nouvelle implémentation. En règle générale, on parle de « substitution de l’implémentation de la classe de base ». Le mot clé virtual spécifie que les classes dérivées peuvent remplacer le comportement. Vous pouvez également déclarer des méthodes abstract où les classes dérivées doivent remplacer le comportement. La classe de base ne fournit pas d’implémentation pour une méthode abstract. Ensuite, vous devez définir l’implémentation de deux des nouvelles classes que vous avez créées. Commencez par la InterestEarningAccount :

public override void PerformMonthEndTransactions()
{
    if (Balance > 500m)
    {
        decimal interest = Balance * 0.02m;
        MakeDeposit(interest, DateTime.Now, "apply monthly interest");
    }
}

Ajoutez le code suivant à LineOfCreditAccount. Le code annule le solde pour calculer une charge d’intérêt positive qui est retirée du compte :

public override void PerformMonthEndTransactions()
{
    if (Balance < 0)
    {
        // Negate the balance to get a positive interest charge:
        decimal interest = -Balance * 0.07m;
        MakeWithdrawal(interest, DateTime.Now, "Charge monthly interest");
    }
}

La classe GiftCardAccount a besoin de deux modifications pour implémenter sa fonctionnalité de fin de mois. Tout d’abord, modifiez le constructeur pour inclure un montant facultatif à ajouter chaque mois :

private readonly decimal _monthlyDeposit = 0m;

public GiftCardAccount(string name, decimal initialBalance, decimal monthlyDeposit = 0) : base(name, initialBalance)
    => _monthlyDeposit = monthlyDeposit;

Le constructeur fournit une valeur par défaut pour la valeur monthlyDeposit, afin que les appelants puissent omettre un 0 sans aucun versement mensuel. Ensuite, remplacez la méthode PerformMonthEndTransactions pour ajouter le versement mensuel, s’il a été défini sur une valeur différente de zéro dans le constructeur :

public override void PerformMonthEndTransactions()
{
    if (_monthlyDeposit != 0)
    {
        MakeDeposit(_monthlyDeposit, DateTime.Now, "Add monthly deposit");
    }
}

Le remplacement applique l’ensemble des versements mensuels dans le constructeur. Ajoutez le code suivant à la méthode Main pour tester ces modifications pour GiftCardAccount et InterestEarningAccount :

var giftCard = new GiftCardAccount("gift card", 100, 50);
giftCard.MakeWithdrawal(20, DateTime.Now, "get expensive coffee");
giftCard.MakeWithdrawal(50, DateTime.Now, "buy groceries");
giftCard.PerformMonthEndTransactions();
// can make additional deposits:
giftCard.MakeDeposit(27.50m, DateTime.Now, "add some additional spending money");
Console.WriteLine(giftCard.GetAccountHistory());

var savings = new InterestEarningAccount("savings account", 10000);
savings.MakeDeposit(750, DateTime.Now, "save some money");
savings.MakeDeposit(1250, DateTime.Now, "Add more savings");
savings.MakeWithdrawal(250, DateTime.Now, "Needed to pay monthly bills");
savings.PerformMonthEndTransactions();
Console.WriteLine(savings.GetAccountHistory());

Vérifier les résultats. À présent, ajoutez un ensemble similaire de code de test pour LineOfCreditAccount :

var lineOfCredit = new LineOfCreditAccount("line of credit", 0);
// How much is too much to borrow?
lineOfCredit.MakeWithdrawal(1000m, DateTime.Now, "Take out monthly advance");
lineOfCredit.MakeDeposit(50m, DateTime.Now, "Pay back small amount");
lineOfCredit.MakeWithdrawal(5000m, DateTime.Now, "Emergency funds for repairs");
lineOfCredit.MakeDeposit(150m, DateTime.Now, "Partial restoration on repairs");
lineOfCredit.PerformMonthEndTransactions();
Console.WriteLine(lineOfCredit.GetAccountHistory());

Lorsque vous ajoutez le code précédent et que vous exécutez le programme, vous voyez une erreur similaire à celle-ci :

Unhandled exception. System.ArgumentOutOfRangeException: Amount of deposit must be positive (Parameter 'amount')
   at OOProgramming.BankAccount.MakeDeposit(Decimal amount, DateTime date, String note) in BankAccount.cs:line 42
   at OOProgramming.BankAccount..ctor(String name, Decimal initialBalance) in BankAccount.cs:line 31
   at OOProgramming.LineOfCreditAccount..ctor(String name, Decimal initialBalance) in LineOfCreditAccount.cs:line 9
   at OOProgramming.Program.Main(String[] args) in Program.cs:line 29

Notes

La sortie réelle inclut le chemin d’accès complet vers le dossier avec le projet. Les noms de dossiers ont été omis par souci de brièveté. En outre, selon le format de votre code, les numéros de ligne peuvent être légèrement différents.

Ce code échoue, car le BankAccount suppose que le solde initial doit être supérieur à 0. Une autre hypothèse inscrite dans la classeBankAccountest que le solde ne peut pas passer au négatif. Au contraire, tout retrait qui met à découvert le compte est rejeté. Ces deux hypothèses doivent changer. Le compte de marge de crédit commence à 0 et a généralement un solde négatif. En outre, si un client emprunte trop d’argent, il encourt des frais. La transaction est acceptée, mais elle coûte plus cher. La première règle peut être implémentée en ajoutant un argument facultatif au constructeur BankAccount qui spécifie le solde minimal. Par défaut, il s’agit de 0. La deuxième règle nécessite un mécanisme qui permet aux classes dérivées de modifier l’algorithme par défaut. Dans un sens, la classe de base « demande » au type dérivé ce qui doit se produire en cas de découvert. Le comportement par défaut consiste à rejeter la transaction en levant une exception.

Commençons par ajouter un deuxième constructeur qui inclut un paramètre minimumBalance facultatif. Ce nouveau constructeur effectue toutes les actions effectuées par le constructeur existant. En outre, il définit la propriété d’équilibre minimal. Vous pouvez copier le corps du constructeur existant, mais cela signifie que deux emplacements doivent être modifiés à l’avenir. Au lieu de cela, vous pouvez utiliser le chaînage du constructeur pour qu’un constructeur en appelle un autre. Le code suivant montre les deux constructeurs et le nouveau champ supplémentaire :

private readonly decimal _minimumBalance;

public BankAccount(string name, decimal initialBalance) : this(name, initialBalance, 0) { }

public BankAccount(string name, decimal initialBalance, decimal minimumBalance)
{
    Number = s_accountNumberSeed.ToString();
    s_accountNumberSeed++;

    Owner = name;
    _minimumBalance = minimumBalance;
    if (initialBalance > 0)
        MakeDeposit(initialBalance, DateTime.Now, "Initial balance");
}

Le code précédent montre deux nouvelles techniques. Tout d’abord, le champ minimumBalance est marqué comme readonly. Cela signifie que la valeur ne peut pas être modifiée une fois l’objet construit. Une fois qu’un BankAccount est créé, le minimumBalance ne peut pas changer. Deuxièmement, le constructeur qui prend deux paramètres utilise : this(name, initialBalance, 0) { } comme implémentation. L’expression : this() appelle l’autre constructeur, celui avec trois paramètres. Cette technique vous permet d’avoir une implémentation unique pour initialiser un objet, même si le code client peut en choisir un parmi de nombreux constructeurs.

Cette implémentation appelle MakeDeposit uniquement si le solde initial est supérieur à 0. Cela maintient la règle selon laquelle les versements doivent être positifs, tout en permettant au compte de crédit d’ouvrir avec un solde 0.

Maintenant que la classe BankAccount a un champ en lecture seule pour le solde minimal, la dernière modification consiste à remplacer le code en dur 0 par minimumBalance dans la méthode MakeWithdrawal :

if (Balance - amount < _minimumBalance)

Après avoir étendu la classe BankAccount, vous pouvez modifier le constructeur LineOfCreditAccount pour appeler le nouveau constructeur de base, comme indiqué dans le code suivant :

public LineOfCreditAccount(string name, decimal initialBalance, decimal creditLimit) : base(name, initialBalance, -creditLimit)
{
}

Notez que le constructeur LineOfCreditAccount modifie le signe du paramètre creditLimit, afin qu’il corresponde à la signification du paramètre minimumBalance.

Différentes règles de découvert

La dernière fonctionnalité à ajouter permet àLineOfCreditAccount de facturer des frais en cas de dépassement de la limite de crédit, plutôt que de refuser la transaction.

Une technique consiste à définir une fonction virtuelle dans laquelle vous implémentez le comportement requis. La classe BankAccount refactorise la méthode MakeWithdrawal en deux méthodes. La nouvelle méthode effectue l’action spécifiée lorsque le retrait amène le solde sous le minimum. La méthode existante MakeWithdrawal a le code suivant :

public void MakeWithdrawal(decimal amount, DateTime date, string note)
{
    if (amount <= 0)
    {
        throw new ArgumentOutOfRangeException(nameof(amount), "Amount of withdrawal must be positive");
    }
    if (Balance - amount < _minimumBalance)
    {
        throw new InvalidOperationException("Not sufficient funds for this withdrawal");
    }
    var withdrawal = new Transaction(-amount, date, note);
    _allTransactions.Add(withdrawal);
}

Remplacez-le par le code suivant :

public void MakeWithdrawal(decimal amount, DateTime date, string note)
{
    if (amount <= 0)
    {
        throw new ArgumentOutOfRangeException(nameof(amount), "Amount of withdrawal must be positive");
    }
    Transaction? overdraftTransaction = CheckWithdrawalLimit(Balance - amount < _minimumBalance);
    Transaction? withdrawal = new(-amount, date, note);
    _allTransactions.Add(withdrawal);
    if (overdraftTransaction != null)
        _allTransactions.Add(overdraftTransaction);
}

protected virtual Transaction? CheckWithdrawalLimit(bool isOverdrawn)
{
    if (isOverdrawn)
    {
        throw new InvalidOperationException("Not sufficient funds for this withdrawal");
    }
    else
    {
        return default;
    }
}

La méthode ajoutée est protected, ce qui signifie qu’elle peut être appelée uniquement à partir de classes dérivées. Cette déclaration empêche d’autres clients d’appeler la méthode. Elle est également virtual, afin que les classes dérivées puissent modifier le comportement. Le type de retour est Transaction?. L’annotation ? indique que la méthode peut retourner null. Ajoutez l’implémentation suivante dans le LineOfCreditAccount pour facturer des frais lorsque la limite de retrait est dépassée :

protected override Transaction? CheckWithdrawalLimit(bool isOverdrawn) =>
    isOverdrawn
    ? new Transaction(-20, DateTime.Now, "Apply overdraft fee")
    : default;

Le remplacement retourne une transaction de frais lorsque le compte est à découvert. Si le retrait ne dépasse pas la limite, la méthode retourne une transaction null. Ceci indique qu’il n’y a pas de frais. Testez ces modifications en ajoutant le code suivant à votre méthode Main dans la classe Program :

var lineOfCredit = new LineOfCreditAccount("line of credit", 0, 2000);
// How much is too much to borrow?
lineOfCredit.MakeWithdrawal(1000m, DateTime.Now, "Take out monthly advance");
lineOfCredit.MakeDeposit(50m, DateTime.Now, "Pay back small amount");
lineOfCredit.MakeWithdrawal(5000m, DateTime.Now, "Emergency funds for repairs");
lineOfCredit.MakeDeposit(150m, DateTime.Now, "Partial restoration on repairs");
lineOfCredit.PerformMonthEndTransactions();
Console.WriteLine(lineOfCredit.GetAccountHistory());

Exécutez le programme et vérifiez les résultats.

Résumé

Si vous êtes bloqué, vous pouvez afficher la source de ce didacticiel dans notre référentiel GitHub.

Ce didacticiel a présenté un grand nombre des techniques utilisées dans la programmation orientée objet :

  • Vous avez utilisé l’abstraction lorsque vous avez défini des classes pour chacun des différents types de compte. Ces classes ont décrit le comportement de ce type de compte.
  • Vous avez utilisé l’encapsulation lorsque vous avez conservé de nombreux détails private dans chaque classe.
  • Vous avez utilisé l’héritage lorsque vous avez tiré parti de l’implémentation déjà créée dans la classe BankAccount pour enregistrer le code.
  • Vous avez utilisé le polymorphisme lorsque vous avez créé virtual des méthodes que les classes dérivées peuvent remplacer pour créer un comportement spécifique pour ce type de compte.