Geek Time-La différence entre les cinq interfaces de la théorie des modèles de conception et des classes abstraites?

Dans la programmation orientée objet, les classes abstraites et les interfaces sont deux concepts grammaticaux fréquemment utilisés. Ce sont les quatre principales caractéristiques de la programmation orientée objet, ainsi que de nombreux modèles de conception, idées de conception et principes de conception. Par exemple, nous pouvons utiliser des interfaces pour implémenter des fonctionnalités abstraites orientées objet, des fonctionnalités polymorphes et des principes de conception basés sur des interfaces plutôt que sur l'implémentation, et utiliser des classes abstraites pour implémenter des fonctionnalités d'héritage orientées objet et des modèles de conception de modèles.

Cependant, tous les langages de programmation orientés objet ne prennent pas en charge ces deux concepts grammaticaux. Par exemple, un langage de programmation comme C ++ ne prend en charge que les classes abstraites et ne prend pas en charge les interfaces; tandis qu'un langage de programmation dynamique comme Python ne prend pas en charge les classes abstraites ou L'interface n'est pas prise en charge. Bien que certains langages de programmation ne fournissent pas de syntaxe prête à l'emploi pour prendre en charge les interfaces et les classes abstraites, nous pouvons toujours simuler ces deux concepts de syntaxe par certains moyens.

Ces deux concepts grammaticaux sont souvent utilisés non seulement dans le travail, mais aussi souvent mentionnés dans les entretiens. Par exemple, "Quelle est la différence entre interface et classe abstraite? Quand utiliser l'interface? Quand utiliser une classe abstraite? Quelle est la signification de classe abstraite et d'interface? Quels problèmes de programmation peuvent être résolus?", Etc.

Que sont les classes abstraites et les interfaces? Où est la différence?

Tout d'abord, voyons comment nous définissons des classes abstraites dans un langage de programmation comme Java.

Le code suivant est un scénario d'utilisation typique des classes abstraites (mode de conception de modèle). Logger est une classe abstraite pour la journalisation. FileLogger et MessageQueueLogger héritent de Logger et implémentent deux méthodes de journalisation différentes: la journalisation dans un fichier et la journalisation dans une file d'attente de messages. Les deux sous-classes de FileLogger et MessageQueueLogger réutilisent les propriétés name, enabled, minPermittedLevel et la méthode log () dans la classe parent Logger, mais comme les deux sous-classes écrivent les journaux de différentes manières, elles remplacent chacune le doLog ( ) Méthode.


// 抽象类
public abstract class Logger {
    
    
  private String name;
  private boolean enabled;
  private Level minPermittedLevel;

  public Logger(String name, boolean enabled, Level minPermittedLevel) {
    
    
    this.name = name;
    this.enabled = enabled;
    this.minPermittedLevel = minPermittedLevel;
  }
  
  public void log(Level level, String message) {
    
    
    boolean loggable = enabled && (minPermittedLevel.intValue() <= level.intValue());
    if (!loggable) return;
    doLog(level, message);
  }
  
  protected abstract void doLog(Level level, String message);
}
// 抽象类的子类:输出日志到文件
public class FileLogger extends Logger {
    
    
  private Writer fileWriter;

  public FileLogger(String name, boolean enabled,
    Level minPermittedLevel, String filepath) {
    
    
    super(name, enabled, minPermittedLevel);
    this.fileWriter = new FileWriter(filepath); 
  }
  
  @Override
  public void doLog(Level level, String mesage) {
    
    
    // 格式化level和message,输出到日志文件
    fileWriter.write(...);
  }
}
// 抽象类的子类: 输出日志到消息中间件(比如kafka)
public class MessageQueueLogger extends Logger {
    
    
  private MessageQueueClient msgQueueClient;
  
  public MessageQueueLogger(String name, boolean enabled,
    Level minPermittedLevel, MessageQueueClient msgQueueClient) {
    
    
    super(name, enabled, minPermittedLevel);
    this.msgQueueClient = msgQueueClient;
  }
  
  @Override
  protected void doLog(Level level, String mesage) {
    
    
    // 格式化level和message,输出到消息中间件
    msgQueueClient.send(...);
  }
}

À travers l'exemple ci-dessus, examinons les caractéristiques de la classe abstraite. J'ai résumé les trois points suivants.

● Les classes abstraites ne peuvent pas être instanciées, elles peuvent uniquement être héritées. En d'autres termes, vous ne pouvez pas créer un nouvel objet d'une classe abstraite (Logger logger = new Logger (...); une erreur de compilation sera signalée).

● Les classes abstraites peuvent contenir des attributs et des méthodes. La méthode peut contenir une implémentation de code (comme la méthode log () dans Logger) ou non (comme la méthode doLog () dans Logger). Les méthodes qui n'incluent pas l'implémentation de code sont appelées méthodes abstraites.

● Les sous-classes héritent des classes abstraites et doivent implémenter toutes les méthodes abstraites dans les classes abstraites. Ce qui correspond à l'exemple de code, c'est que toutes les sous-classes qui héritent de la classe abstraite Logger doivent remplacer la méthode doLog ().

Nous venons de parler de la manière de définir des classes abstraites, voyons maintenant comment nous définissons les interfaces dans un langage de programmation comme Java.


// 接口
public interface Filter {
    
    
  void doFilter(RpcRequest req) throws RpcException;
}
// 接口实现类:鉴权过滤器
public class AuthencationFilter implements Filter {
    
    
  @Override
  public void doFilter(RpcRequest req) throws RpcException {
    
    
    //...鉴权逻辑..
  }
}
// 接口实现类:限流过滤器
public class RateLimitFilter implements Filter {
    
    
  @Override
  public void doFilter(RpcRequest req) throws RpcException {
    
    
    //...限流逻辑...
  }
}
// 过滤器使用Demo
public class Application {
    
    
  // filters.add(new AuthencationFilter());
  // filters.add(new RateLimitFilter());
  private List<Filter> filters = new ArrayList<>();
  
  public void handleRpcRequest(RpcRequest req) {
    
    
    try {
    
    
      for (Filter filter : filters) {
    
    
        filter.doFilter(req);
      }
    } catch(RpcException e) {
    
    
      // ...处理过滤结果...
    }
    // ...省略其他处理逻辑...
  }
}

Le code ci-dessus est un scénario d'utilisation d'interface typique. Nous définissons une interface Filter via le mot-clé interface en Java. AuthencationFilter et RateLimitFilter sont les deux classes d'implémentation de l'interface, qui implémentent respectivement les fonctions de filtrage de l'authentification des requêtes RPC et de la limitation de courant.

Le code est très concis. Combiné avec le code, jetons un œil aux caractéristiques de l'interface. J'ai également résumé trois points.

● L'interface ne peut pas contenir d'attributs (c'est-à-dire des variables membres).

● Les interfaces ne peuvent déclarer que des méthodes et les méthodes ne peuvent pas contenir d'implémentations de code.

● Lorsqu'une classe implémente une interface, elle doit implémenter toutes les méthodes déclarées dans l'interface.

Plus tôt, nous avons parlé de la définition des classes abstraites et des interfaces, ainsi que de leurs caractéristiques grammaticales respectives. D'après la comparaison des caractéristiques grammaticales, les deux sont assez différents. Par exemple, l'implémentation d'attributs et de méthodes peut être définie dans des classes abstraites, tandis que les attributs ne peuvent pas être définis dans les interfaces et les méthodes ne peuvent pas contenir d'implémentations de code. En plus des caractéristiques grammaticales, il existe également de grandes différences entre les deux du point de vue de la conception.

Une classe abstraite est en fait une classe, mais une classe spéciale. Cette classe ne peut pas être instanciée en tant qu'objet, mais ne peut être héritée que par des sous-classes. Nous savons que la relation d'héritage est une relation est-une. Puisque la classe abstraite appartient à la classe, elle représente également une relation est-une. Comparée à la relation is-a des classes abstraites, l'interface représente une relation has-a, ce qui signifie qu'elle a certaines fonctions. Pour l'interface, il y a un nom plus vivant, c'est l'accord (contrat).

Quels problèmes de programmation les classes abstraites et les interfaces peuvent-elles résoudre?

Nous venons d'apprendre la définition et la différence entre les classes abstraites et les interfaces. Maintenant, apprenons-en plus sur la signification des classes abstraites et des interfaces. Expliquez-vous ce que c'est.

Tout d'abord, jetons un coup d'œil, pourquoi avons-nous besoin de classes abstraites? Quel problème de programmation peut-il résoudre?

Nous venons de mentionner que les classes abstraites ne peuvent pas être instanciées, elles ne peuvent qu'être héritées. Dans les chapitres précédents, nous avons également mentionné que l'héritage peut résoudre le problème de la réutilisation du code. Par conséquent, les classes abstraites sont également nées pour la réutilisation du code. Plusieurs sous-classes peuvent hériter des attributs et des méthodes définis dans la classe abstraite pour éviter l'écriture répétitive du même code dans la sous-classe.

Cependant, étant donné que l'héritage lui-même peut atteindre l'objectif de la réutilisation du code et que l'héritage n'exige pas que la classe parente soit une classe abstraite, nous pouvons toujours obtenir l'héritage et la réutilisation sans utiliser de classes abstraites. De ce point de vue, il semble que nous n'ayons pas besoin de la syntaxe des classes abstraites. En plus de résoudre le problème de la réutilisation du code, la classe abstraite a-t-elle une autre signification?

Prenons l'exemple précédent d'impression de journaux pour expliquer. Modifions d'abord le code ci-dessus. Dans le code après la transformation, Logger n'est plus une classe abstraite, mais une classe parent ordinaire. Les méthodes Log () et doLog () de Logger ont été supprimées et la méthode isLoggable () a été ajoutée. FileLogger et MessageQueueLogger héritent toujours de la classe parent Logger pour atteindre l'objectif de réutilisation du code. Le code spécifique est le suivant:


// 父类:非抽象类,就是普通的类. 删除了log(),doLog(),新增了isLoggable().
public class Logger {
    
    
  private String name;
  private boolean enabled;
  private Level minPermittedLevel;

  public Logger(String name, boolean enabled, Level minPermittedLevel) {
    
    
    //...构造函数不变,代码省略...
  }

  protected boolean isLoggable() {
    
    
    boolean loggable = enabled && (minPermittedLevel.intValue() <= level.intValue());
    return loggable;
  }
}
// 子类:输出日志到文件
public class FileLogger extends Logger {
    
    
  private Writer fileWriter;

  public FileLogger(String name, boolean enabled,
    Level minPermittedLevel, String filepath) {
    
    
    //...构造函数不变,代码省略...
  }
  
  public void log(Level level, String mesage) {
    
    
    if (!isLoggable()) return;
    // 格式化level和message,输出到日志文件
    fileWriter.write(...);
  }
}
// 子类: 输出日志到消息中间件(比如kafka)
public class MessageQueueLogger extends Logger {
    
    
  private MessageQueueClient msgQueueClient;
  
  public MessageQueueLogger(String name, boolean enabled,
    Level minPermittedLevel, MessageQueueClient msgQueueClient) {
    
    
    //...构造函数不变,代码省略...
  }
  
  public void log(Level level, String mesage) {
    
    
    if (!isLoggable()) return;
    // 格式化level和message,输出到消息中间件
    msgQueueClient.send(...);
  }
}

Bien que cette idée de conception atteigne l'objectif de réutilisation du code, elle ne peut pas utiliser le polymorphisme. Si vous écrivez du code comme celui-ci, une erreur de compilation se produira, car la méthode log () n'est pas définie dans Logger.


Logger logger = new FileLogger("access-log", true, Level.WARN, "/users/wangzheng/access.log");
logger.log(Level.ERROR, "This is a test log message.");

Vous pourriez dire que ce problème est facile à résoudre. Nous définissons une méthode log () vide dans la classe parent Logger, et laissons la sous-classe réécrire la méthode log () de la classe parent pour implémenter sa propre logique de journalisation. N'est-ce pas correct?


public class Logger {
    
    
  // ...省略部分代码...
  public void log(Level level, String mesage) {
    
     // do nothing... }
}
public class FileLogger extends Logger {
    
    
  // ...省略部分代码...
  @Override
  public void log(Level level, String mesage) {
    
    
    if (!isLoggable()) return;
    // 格式化level和message,输出到日志文件
    fileWriter.write(...);
  }
}
public class MessageQueueLogger extends Logger {
    
    
  // ...省略部分代码...
  @Override
  public void log(Level level, String mesage) {
    
    
    if (!isLoggable()) return;
    // 格式化level和message,输出到消息中间件
    msgQueueClient.send(...);
  }
}

Cette idée de conception peut être utilisée, mais elle n'est évidemment pas aussi élégante que la réalisation précédente de classes abstraites. Pourquoi est-ce que je dis ça? Les principales raisons sont les suivantes.

● La définition d'une méthode vide dans Logger affectera la lisibilité du code. Si nous ne sommes pas familiers avec les idées de conception derrière Logger et que les commentaires de code ne sont pas très puissants, lorsque nous lisons le code de Logger, nous nous demandons peut-être pourquoi nous définissons une méthode log () vide. Nous devons vérifier entre Logger, FileLogger et MessageQueueLogger. Afin de comprendre son intention de conception.

● Lors de la création d'une nouvelle sous-classe pour hériter de la classe parent Logger, nous pouvons oublier de réimplémenter la méthode log (). Auparavant, basé sur l'idée de conception des classes abstraites, le compilateur obligeait la sous-classe à réécrire la méthode log (), sinon il rapporterait une erreur de compilation. Vous pourriez dire, puisque je veux définir une nouvelle sous-classe Logger, comment se fait-il que j'oublie de réimplémenter la méthode log ()? Notre exemple est relativement simple: il n'y a pas beaucoup de méthodes dans Logger et peu de lignes de code. Cependant, si le Logger a quelques centaines de lignes et qu'il y a plus de n méthodes, à moins que vous ne soyez très familier avec la conception du Logger, il n'est pas impossible d'oublier de réimplémenter la méthode log ().

● Logger peut être instancié, en d'autres termes, nous pouvons créer un nouveau Logger et appeler la méthode empty log (). Cela augmente également le risque de mauvaise utilisation de la classe. Bien sûr, ce problème peut être résolu en définissant un constructeur privé. Cependant, il n'y a évidemment aucune élégance à travers les classes abstraites.

Deuxièmement, regardons à nouveau, pourquoi avons-nous besoin d'interfaces? Quel problème de programmation peut-il résoudre?

Les classes abstraites sont davantage destinées à la réutilisation du code, tandis que les interfaces sont davantage axées sur le découplage. Une interface est une abstraction du comportement, qui équivaut à un ensemble de protocoles ou de contrats. Vous pouvez considérer une interface API comme une analogie. L'appelant doit seulement prêter attention à l'interface abstraite et n'a pas besoin de connaître l'implémentation spécifique.Le code d'implémentation spécifique est transparent pour l'appelant. L'interface réalise la séparation de la convention et de la mise en œuvre, ce qui peut réduire le couplage entre les codes et améliorer l'évolutivité des codes.

En fait, l'interface est un point de connaissance plus étendu et plus important que les applications abstraites. Par exemple, la «programmation basée sur l'interface plutôt que sur l'implémentation» que nous avons souvent mentionnée est une idée de conception qui est utilisée presque tous les jours et peut grandement améliorer la flexibilité et l'évolutivité du code.

Comment décider d'utiliser une classe abstraite ou une interface?

L'explication à l'instant peut être quelque peu théorique. Examinons-la maintenant du point de vue du développement d'un projet réel. Quand devrions-nous utiliser des classes abstraites dans la conception de code et le développement de programmation? Quand dois-je utiliser l'interface?

En fait, la norme de jugement est très simple. Si nous voulons exprimer une relation is-a, et résoudre le problème de la réutilisation du code , nous utilisons des classes abstraites; si nous voulons exprimer une relation has-a, et c'est pour résoudre l'abstraction plutôt que la réutilisation du code Problème , alors nous pouvons utiliser l'interface.

Du point de vue du niveau d'héritage de la classe, la classe abstraite est une idée de conception ascendante. Le code de la sous-classe est répété en premier, puis il est résumé dans la classe parente supérieure (c'est-à-dire la classe abstraite). L'interface est exactement le contraire, c'est une idée de conception descendante. Lorsque nous programmons, nous concevons généralement d'abord l'interface, puis nous considérons l'implémentation spécifique.

À l'avenir, l'article sera synchronisé avec le compte public personnel, bienvenue pour faire attention à l'échange

Je suppose que tu aimes

Origine blog.csdn.net/zhujiangtaotaise/article/details/111182676
conseillé
Classement