L'objectif est d'écrire du code que les autres comprendront dans un an. Les conventions de nommage sont l'orthographe commune de l'équipe. Le polymorphisme, c'est pouvoir demander « calcule ton aire » à n'importe quelle forme sans savoir si c'est un cercle ou un carré : chaque forme sait le faire à sa manière.
Les bases indispensables Junior
Avant les normes et les patterns, quatre notions reviennent dans tout le code .NET et dans tous les chapitres de ce guide.
Classe abstraite ou interface ?
| Interface | Classe abstraite | |
|---|---|---|
| Sens | « sait faire » (un contrat) | « est une sorte de » (un parent commun) |
| Héritage multiple | Oui, une classe implémente plusieurs interfaces | Non, un seul parent |
| État (champs) | Non | Oui |
| Usage typique | Point d'injection DI, test avec un faux objet | Code commun à une famille de classes |
Génériques
Un générique écrit un algorithme une seule fois pour plusieurs types, en gardant la vérification du compilateur. List<int> ne peut contenir que des entiers : l'erreur est détectée à la compilation, pas en production.
// T est un paramètre de type ; la contrainte "where" limite ce qu'il peut être
public static T Max<T>(T a, T b) where T : IComparable<T> => a.CompareTo(b) >= 0 ? a : b;
Max(3, 7); // T = int → 7
Max("pomme", "kiwi"); // T = string → "pomme"
Délégués, lambdas et événements
Un délégué est une variable qui contient une méthode. Func<…> renvoie une valeur, Action<…> ne renvoie rien, une lambda (x => x * 2) est une façon courte d'écrire la méthode. Un événement est une liste de délégués à prévenir.
Func<decimal, decimal> ttc = ht => ht * 1.20m; // prend un decimal, renvoie un decimal
Action<string> afficher = msg => Console.WriteLine(msg);
afficher($"Prix TTC : {ttc(100m):F2}"); // Prix TTC : 120,00
public sealed class Thermostat
{
public event EventHandler<double>? TemperatureChanged; // les abonnés s'inscrivent avec +=
public void Mesurer(double t) => TemperatureChanged?.Invoke(this, t);
}
LINQ et l'exécution différée
Une requête LINQ ne s'exécute pas quand on l'écrit, mais quand on la parcourt (foreach, ToList, Count…). Et la différence entre IEnumerable et IQueryable décide où le filtre s'exécute.
IQueryable<Order> q = db.Orders.Where(o => o.Total > 100); // rien n'est exécuté
var liste = await q.ToListAsync(ct); // ici : SQL avec WHERE Total > 100
IEnumerable<Order> tout = db.Orders.AsEnumerable();
var gros = tout.Where(o => o.Total > 100).ToList(); // ❌ charge TOUTE la table puis filtre en mémoire
var requete = commandes.Where(o => o.EstUrgente);
var n = requete.Count(); // parcourt une fois
var l = requete.ToList(); // parcourt une deuxième fois : matérialiser une fois avec ToList()
| Type | Où s'exécute le filtre | Exemple |
|---|---|---|
IQueryable<T> | Traduit et exécuté par la base (SQL) | db.Orders.Where(…) |
IEnumerable<T> | En mémoire, dans votre processus | liste.Where(…) |
Conventions de nommage (Microsoft) Junior
| Élément | Convention | Exemple |
|---|---|---|
| Classe, record, struct, enum, méthode, propriété, événement | PascalCase | OrderService.PlaceOrderAsync |
| Interface | I + PascalCase | IOrderRepository |
| Paramètre, variable locale | camelCase | customerId |
| Champ privé | _ + camelCase | _logger |
| Champ statique privé | s_ + camelCase (convention runtime) | s_cache |
| Constante | PascalCase | MaxRetryCount |
| Méthode asynchrone | Suffixe Async | GetByIdAsync |
| Paramètre générique | T ou T + nom | TEntity, TKey |
VB.NET suit les mêmes casses mais n'est pas sensible à la casse : customerId et CustomerId désignent le même identifiant, d'où l'usage du préfixe _ pour les champs de stockage.
Faire respecter les normes par le build
Directory.Build.props — à la racine de la solution
<Project>
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<AnalysisLevel>latest-recommended</AnalysisLevel>
<EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
</PropertyGroup>
</Project>
Ajoutez un .editorconfig versionné (règles IDE* et CA* avec leur sévérité) : le style devient une erreur de compilation, pas un sujet de revue de code. En VB, l'équivalent indispensable est Option Strict On, Option Explicit On et Option Infer On au niveau du projet.
SOLID, version concrète Confirmé
| Principe | En une phrase | Signe de violation en .NET |
|---|---|---|
| S — Responsabilité unique | Une classe a une seule raison de changer | Un OrderService de 2 000 lignes qui valide, calcule, envoie des mails et écrit en base |
| O — Ouvert / fermé | On ajoute un comportement sans modifier l'existant | Un switch sur un type qu'on modifie à chaque nouveau cas |
| L — Substitution de Liskov | Un sous-type respecte le contrat de son parent | Un override qui lève NotSupportedException |
| I — Ségrégation des interfaces | Des interfaces petites et ciblées | Un IRepository à 25 méthodes dont chaque client utilise 3 |
| D — Inversion des dépendances | Dépendre d'abstractions injectées | new SmtpClient() au milieu d'un handler métier |
Le polymorphisme sous ses quatre formes Junior
| Forme | Mécanisme | Résolu | Exemple |
|---|---|---|---|
| Sous-typage (héritage) | virtual / override, abstract | À l'exécution (vtable) | Forme.Aire() |
| Par interface | interface + implémentations | À l'exécution | IPaymentGateway : Stripe, PayPal |
| Ad hoc (surcharge) | Même nom, signatures différentes | À la compilation | Log(string), Log(Exception) |
| Paramétrique | Génériques avec contraintes | Compilation + JIT | Repository<TEntity> where TEntity : Entity |
Formepublic abstract class Forme
{
public abstract double Aire(); // doit être redéfinie
public override string ToString() => $"{GetType().Name} ({Aire():F2})";
}
public sealed class Cercle(double rayon) : Forme // constructeur primaire
{
public override double Aire() => Math.PI * rayon * rayon;
}
public class Rectangle(double l, double h) : Forme
{
public override double Aire() => l * h;
}
public sealed class Carre(double c) : Rectangle(c, c); // hérite de Aire()
// Hiérarchie fermée : le pattern matching est une alternative lisible
static string Decrire(Forme f) => f switch
{
Cercle => "rond",
Carre => "carré",
Rectangle => "rectangle",
_ => throw new UnreachableException()
};Public MustInherit Class Forme
Public MustOverride Function Aire() As Double
Public Overrides Function ToString() As String
Return $"{Me.GetType().Name} ({Aire():F2})"
End Function
End Class
Public NotInheritable Class Cercle
Inherits Forme
Private ReadOnly _rayon As Double
Public Sub New(rayon As Double)
_rayon = rayon
End Sub
Public Overrides Function Aire() As Double
Return Math.PI * _rayon * _rayon
End Function
End Class
' Correspondances : virtual = Overridable, override = Overrides,
' abstract = MustOverride / MustInherit, sealed = NotInheritable / NotOverridable,
' new (masquage) = Shadows, interface = Implementsnew(VB :Shadows) n'est pasoverride: la méthode masquée est choisie selon le type déclaré, pas le type réel. Résultat différent selon la variable utilisée.- Appel virtuel dans un constructeur : l'override de la classe dérivée s'exécute avant que son propre constructeur ait initialisé ses champs.
- Héritage profond : au-delà de deux niveaux, préférez la composition et les interfaces.
C# moderne à connaître Confirmé
| Fonctionnalité | Version | Usage |
|---|---|---|
| Types référence nullables | C# 8 | Le compilateur signale les NullReferenceException possibles |
record, init, with | C# 9 | DTO et Value Objects immuables, égalité par valeur |
required, raw string literals | C# 11 | Propriétés obligatoires à l'initialisation ; JSON/SQL lisibles |
Constructeurs primaires, expressions de collection [1, 2] | C# 12 | Moins de code d'injection ; initialisation uniforme |
Type System.Threading.Lock, params collections | C# 13 | Verrou dédié plus rapide qu'un object |
Mot-clé field, membres d'extension, affectation ?.= | C# 14 | Propriétés avec logique sans champ explicite ; propriétés d'extension |
// C# 14 : le mot-clé field remplace le champ de stockage explicite
public string Nom
{
get;
set => field = value?.Trim() ?? throw new ArgumentNullException(nameof(value));
}
// C# 14 : bloc d'extension, avec propriétés
public static class EnumerableExtensions
{
extension<T>(IEnumerable<T> source)
{
public bool IsEmpty => !source.Any();
}
}
Exceptions
throw;conserve la pile d'appels,throw ex;la perd.- Ne capturez
Exceptionqu'aux frontières : middleware, boucle d'un worker, handler d'UI. - Une exception signale un cas exceptionnel. Pour un échec métier attendu (validation, introuvable), un type résultat (
Result<T>,OneOf) est plus explicite et moins coûteux.
VB.NET est supporté sur .NET moderne pour les bibliothèques, la console, WinForms et WPF, mais Microsoft n'y ajoute plus de nouvelles fonctionnalités de langage. Il n'y a pas de Razor ni de Blazor en VB. Stratégie courante : garder le code VB existant, écrire les nouveaux modules en C# dans des projets séparés de la même solution — l'interopérabilité via l'IL est totale.
À retenir
- Les normes sont vérifiées par le build (
.editorconfig, analyseurs,TreatWarningsAsErrors), pas par les revues. - Polymorphisme par interface pour les points d'extension, pattern matching pour les hiérarchies fermées.
- Composition plutôt qu'héritage ;
sealedpar défaut. - Nullable activé partout, y compris sur l'existant migré progressivement.