L'héritage à trois niveaux¶
🤖 Rédaction assistée par IA.
Définition¶
L'héritage à trois niveaux combine une interface, une classe abstraite et des classes concrètes :
- L'interface définit le contrat dont dépend le code client.
- La classe abstraite implémente l'interface et met en facteur l'implémentation commune à plusieurs classes concrètes. Certaines de ses méthodes sont déclarées sans être implémentées (méthodes abstraites).
- Les classes concrètes héritent de la classe abstraite et implémentent uniquement ce qui leur est spécifique.
Cas d'école¶
Considérons une interface Logger permettant d'écrire des messages de différents niveaux (debug, info, warn, error) et deux implémentations : ConsoleLogger (affichage dans la console) et DatabaseLogger (stockage en base de données).
Si chaque implémentation réalise directement l'interface, les méthodes debug, info, warn et error seront dupliquées dans chacune d'elles alors qu'elles se contentent d'appeler log avec le niveau correspondant :
class ConsoleLogger implements Logger {
debug(message: string): void {
this.log(LogLevel.DEBUG, message);
}
info(message: string): void {
this.log(LogLevel.INFO, message);
}
// ... warn, error
log(level: LogLevel, message: string): void {
console.log(`[${level}] ${message}`);
}
}
class DatabaseLogger implements Logger {
// ... debug, info, warn et error à l'identique
log(level: LogLevel, message: string): void {
// insertion en base de données
}
}
Solution¶
Le principe consiste à introduire une classe abstraite AbstractLogger qui implémente debug, info, warn et error en s'appuyant sur la méthode abstraite log :
classDiagram
class Logger {
<<interface>>
+debug(message: string)
+info(message: string)
+warn(message: string)
+error(message: string)
+log(level: LogLevel, message: string)
}
class LogLevel {
<<enumeration>>
DEBUG
INFO
WARN
ERROR
}
class AbstractLogger {
<<abstract>>
+debug(message: string)
+info(message: string)
+warn(message: string)
+error(message: string)
+log(level: LogLevel, message: string)*
}
class ConsoleLogger {
+log(level: LogLevel, message: string)
}
class DatabaseLogger {
+log(level: LogLevel, message: string)
}
Logger <|.. AbstractLogger
AbstractLogger <|-- ConsoleLogger
AbstractLogger <|-- DatabaseLogger
enum LogLevel {
DEBUG = "DEBUG",
INFO = "INFO",
WARN = "WARN",
ERROR = "ERROR",
}
interface Logger {
debug(message: string): void;
info(message: string): void;
warn(message: string): void;
error(message: string): void;
log(level: LogLevel, message: string): void;
}
abstract class AbstractLogger implements Logger {
debug(message: string): void {
this.log(LogLevel.DEBUG, message);
}
info(message: string): void {
this.log(LogLevel.INFO, message);
}
warn(message: string): void {
this.log(LogLevel.WARN, message);
}
error(message: string): void {
this.log(LogLevel.ERROR, message);
}
abstract log(level: LogLevel, message: string): void;
}
class ConsoleLogger extends AbstractLogger {
log(level: LogLevel, message: string): void {
console.log(`[${level}] ${message}`);
}
}
const logger: Logger = new ConsoleLogger();
logger.info("démarrage"); // [INFO] démarrage
enum LogLevel { DEBUG, INFO, WARN, ERROR }
interface Logger {
void debug(String message);
void info(String message);
void warn(String message);
void error(String message);
void log(LogLevel level, String message);
}
abstract class AbstractLogger implements Logger {
@Override
public void debug(String message) {
log(LogLevel.DEBUG, message);
}
@Override
public void info(String message) {
log(LogLevel.INFO, message);
}
@Override
public void warn(String message) {
log(LogLevel.WARN, message);
}
@Override
public void error(String message) {
log(LogLevel.ERROR, message);
}
}
class ConsoleLogger extends AbstractLogger {
@Override
public void log(LogLevel level, String message) {
System.out.println("[" + level + "] " + message);
}
}
Logger logger = new ConsoleLogger();
logger.info("démarrage"); // [INFO] démarrage
from abc import ABC, abstractmethod
from enum import Enum
from typing import Protocol
class LogLevel(Enum):
DEBUG = "DEBUG"
INFO = "INFO"
WARN = "WARN"
ERROR = "ERROR"
class Logger(Protocol):
def debug(self, message: str) -> None: ...
def info(self, message: str) -> None: ...
def warn(self, message: str) -> None: ...
def error(self, message: str) -> None: ...
def log(self, level: LogLevel, message: str) -> None: ...
class AbstractLogger(ABC):
def debug(self, message: str) -> None:
self.log(LogLevel.DEBUG, message)
def info(self, message: str) -> None:
self.log(LogLevel.INFO, message)
def warn(self, message: str) -> None:
self.log(LogLevel.WARN, message)
def error(self, message: str) -> None:
self.log(LogLevel.ERROR, message)
@abstractmethod
def log(self, level: LogLevel, message: str) -> None: ...
class ConsoleLogger(AbstractLogger):
def log(self, level: LogLevel, message: str) -> None:
print(f"[{level.value}] {message}")
logger: Logger = ConsoleLogger()
logger.info("démarrage") # [INFO] démarrage
enum class LogLevel { Debug, Info, Warn, Error };
std::string toString(LogLevel level) {
switch (level) {
case LogLevel::Debug: return "DEBUG";
case LogLevel::Info: return "INFO";
case LogLevel::Warn: return "WARN";
case LogLevel::Error: return "ERROR";
}
return "";
}
class Logger {
public:
virtual ~Logger() = default;
virtual void debug(const std::string& message) = 0;
virtual void info(const std::string& message) = 0;
virtual void warn(const std::string& message) = 0;
virtual void error(const std::string& message) = 0;
virtual void log(LogLevel level, const std::string& message) = 0;
};
class AbstractLogger : public Logger {
public:
void debug(const std::string& message) override { log(LogLevel::Debug, message); }
void info(const std::string& message) override { log(LogLevel::Info, message); }
void warn(const std::string& message) override { log(LogLevel::Warn, message); }
void error(const std::string& message) override { log(LogLevel::Error, message); }
};
class ConsoleLogger : public AbstractLogger {
public:
void log(LogLevel level, const std::string& message) override {
std::cout << "[" << toString(level) << "] " << message << std::endl;
}
};
std::unique_ptr<Logger> logger = std::make_unique<ConsoleLogger>();
logger->info("démarrage"); // [INFO] démarrage
enum LogLevel: string
{
case Debug = 'DEBUG';
case Info = 'INFO';
case Warn = 'WARN';
case Error = 'ERROR';
}
interface Logger
{
public function debug(string $message): void;
public function info(string $message): void;
public function warn(string $message): void;
public function error(string $message): void;
public function log(LogLevel $level, string $message): void;
}
abstract class AbstractLogger implements Logger
{
public function debug(string $message): void
{
$this->log(LogLevel::Debug, $message);
}
public function info(string $message): void
{
$this->log(LogLevel::Info, $message);
}
public function warn(string $message): void
{
$this->log(LogLevel::Warn, $message);
}
public function error(string $message): void
{
$this->log(LogLevel::Error, $message);
}
}
class ConsoleLogger extends AbstractLogger
{
public function log(LogLevel $level, string $message): void
{
echo "[{$level->value}] {$message}\n";
}
}
$logger = new ConsoleLogger();
$logger->info('démarrage'); // [INFO] démarrage
L'ajout d'un DatabaseLogger se limite alors à l'implémentation de la méthode log.
Variantes¶
Dans les bibliothèques¶
Ce modèle se retrouve dans de nombreuses bibliothèques :
- En PHP, la norme PSR-3 définit l'interface
LoggerInterfaceet le paquetpsr/logfournit une classe AbstractLogger dont les implémentations n'ont plus qu'à fournir la méthodelog. - En Java, l'interface
Listest implémentée par la classe abstraite AbstractList dont hériteArrayList.
Patron Template Method¶
Les méthodes debug, info, warn et error de la classe abstraite s'appuient sur une méthode log implémentée par les classes concrètes : il s'agit d'une application du patron Template Method.
Mises en garde¶
- Le code client doit dépendre de l'interface (
Logger) et non de la classe abstraite (AbstractLogger) : la classe abstraite n'est qu'une facilité pour l'implémentation. - Une classe concrète doit rester libre d'implémenter directement l'interface si la classe abstraite ne lui convient pas.
- Il n'est pas question de multiplier les niveaux d'héritage (voir L'héritage et le polymorphisme - Mises en garde).