オタクタイム-デザインパターン理論と抽象クラスの5つのインターフェースの違いは?

オブジェクト指向のプログラミングでは、抽象クラスとインターフェイスは、頻繁に使用される2つの文法概念であり、オブジェクト指向の4つの主要な特性であり、多くの設計パターン、設計アイデア、および設計原則です。たとえば、インターフェイスを使用して、オブジェクト指向の抽象機能、多態性機能、および実装ではなくインターフェイスに基づく設計原則を実装し、抽象クラスを使用してオブジェクト指向の継承機能とテンプレート設計パターンを実装できます。

ただし、すべてのオブジェクト指向プログラミング言語がこれら2つの文法概念をサポートしているわけではありません。たとえば、C ++のようなプログラミング言語は抽象クラスのみをサポートし、インターフェイスはサポートしていませんが、Pythonのような動的プログラミング言語は抽象クラスまたはインターフェイスをサポートしていません。インターフェイスはサポートされていません。一部のプログラミング言語は、インターフェイスと抽象クラスをサポートするための既製の構文を提供していませんが、何らかの方法でこれら2つの構文の概念をシミュレートできます。

これらの2つの文法概念は、仕事だけでなく、インタビューでもよく使用されます。たとえば、「インターフェイスと抽象クラスの違いは何ですか?インターフェイスを使用する場合は?抽象クラスを使用する場合は?抽象クラスとインターフェイスの意味は何ですか?どのようなプログラミングの問題を解決できますか?」などです。

抽象クラスとインターフェースとは何ですか?違いはどこですか?

まず、Javaなどのプログラミング言語で抽象クラスを定義する方法を見てみましょう。

次のコードは、抽象クラス(テンプレートデザインモード)の一般的な使用シナリオです。Loggerは、ログを記録する抽象クラスです。FileLoggerとMessageQueueLoggerは、Loggerを継承し、ファイルへのログ記録とメッセージキューへのログ記録という2つの異なるログ記録方法を実装します。FileLoggerとMessageQueueLoggerの2つのサブクラスは、親クラスLoggerのname、enabled、minPermittedLevelプロパティ、およびlog()メソッドを再利用しますが、2つのサブクラスは異なる方法でログを書き込むため、それぞれdoLog( ) 方法。


// 抽象类
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(...);
  }
}

上記の例を通して、抽象クラスがどのような特性を持っているかを見てみましょう。以下の3点をまとめました。

●抽象クラスはインスタンス化できません。継承のみが可能です。つまり、抽象クラスの新しいオブジェクトを作成することはできません(Logger logger = new Logger(...);コンパイルエラーが報告されます)。

●抽象クラスには、属性とメソッドを含めることができます。このメソッドには、コードの実装(Loggerのlog()メソッドなど)を含めることも、含まない(LoggerのdoLog()メソッドなど)こともできます。コードの実装を含まないメソッドは、抽象メソッドと呼ばれます。

●サブクラスは抽象クラスを継承し、すべての抽象メソッドを抽象クラスに実装する必要があります。サンプルコードに対応するのは、Logger抽象クラスを継承するすべてのサブクラスがdoLog()メソッドをオーバーライドする必要があるということです。

抽象クラスを定義する方法について説明しました。次に、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) {
    
    
      // ...处理过滤结果...
    }
    // ...省略其他处理逻辑...
  }
}

上記のコードは、典型的なインターフェースの使用シナリオです。Javaのinterfaceキーワードを使用してFilterインターフェースを定義します。AuthencationFilterとRateLimitFilterは、インターフェイスの2つの実装クラスであり、それぞれRPC要求認証と電流制限のフィルタリング機能を実装します。

コードは非常に簡潔です。コードと組み合わせて、インターフェースの特性を見てみましょう。また、3つのポイントをまとめました。

●インターフェイスに属性(つまり、メンバー変数)を含めることはできません。

●インターフェイスはメソッドのみを宣言でき、メソッドにコード実装を含めることはできません。

●クラスがインターフェイスを実装する場合、インターフェイスで宣言されているすべてのメソッドを実装する必要があります。

先ほど、抽象クラスとインターフェイスの定義、およびそれぞれの文法上の特徴について説明しました。文法的な特徴の比較から、この2つはまったく異なります。たとえば、属性とメソッドの実装は抽象クラスで定義できますが、属性はインターフェイスで定義できず、メソッドにコード実装を含めることはできません。文法的な特徴に加えて、デザインの観点からも2つの間に大きな違いがあります。

抽象クラスは実際にはクラスですが、特別なクラスです。このクラスはオブジェクトとしてインスタンス化することはできませんが、サブクラスによってのみ継承できます。継承関係はis-a関係であることがわかっています。抽象クラスはクラスに属しているため、is-a関係も表します。抽象クラスのis-a関係と比較すると、インターフェイスはhas-a関係を表します。つまり、特定の機能があります。インターフェースには、より鮮明な名前、つまり合意(契約)があります。

抽象クラスとインターフェースはどのようなプログラミングの問題を解決できますか?

抽象クラスとインターフェースの定義と違いを学びました。次に、抽象クラスとインターフェースの意味について学びましょう。それが何であるかを教えてください。

まず、見てみましょう。なぜ抽象クラスが必要なのですか?どのようなプログラミングの問題を解決できますか?

抽象クラスはインスタンス化できず、継承することしかできないと述べました。前の章では、継承によってコードの再利用の問題を解決できることにも触れました。したがって、抽象クラスもコードの再利用のために生まれます。複数のサブクラスは、抽象クラスで定義された属性とメソッドを継承して、サブクラスに同じコードを繰り返し書き込むことを回避できます。

ただし、継承自体はコードの再利用の目的を達成でき、継承では親クラスが抽象クラスである必要がないため、抽象クラスを使用せずに継承と再利用を実現できます。この観点から、抽象クラスの構文は必要ないようです。コードの再利用の問題を解決することに加えて、抽象クラスには他の意味がありますか?

説明するためにログを印刷する前の例を見てみましょう。まず、上記のコードを変更してみましょう。変換後のコードでは、Loggerは抽象クラスではなく、通常の親クラスです。LoggerのLog()メソッドとdoLog()メソッドが削除され、isLoggable()メソッドが追加されました。FileLoggerとMessageQueueLoggerは、コードの再利用の目的を達成するために、引き続きLogger親クラスを継承します。具体的なコードは次のとおりです。


// 父类:非抽象类,就是普通的类. 删除了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(...);
  }
}

この設計アイデアはコードの再利用の目的を達成しますが、多態性を使用することはできません。次のようなコードを作成すると、log()メソッドがLoggerで定義されていないため、コンパイルエラーが発生します。


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

この問題は簡単に解決できると言うかもしれません。Logger親クラスで空のlog()メソッドを定義し、サブクラスに親クラスのlog()メソッドを書き換えて、独自のロギングロジックを実装させます。大丈夫ではありませんか?


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(...);
  }
}

この設計アイデアは使用できますが、以前の抽象クラスの実現ほどエレガントではないことは明らかです。なぜ私はそれを言うのですか?主な理由は以下のとおりです。

●ロガーで空のメソッドを定義すると、コードの読みやすさに影響します。Loggerの背後にある設計アイデアに精通しておらず、コードコメントがあまり強力でない場合、Loggerコードを読むときに、なぜ空のlog()メソッドを定義するのか疑問に思うかもしれません。Logger、FileLogger、MessageQueueLoggerをチェックする必要があります。その設計意図を理解するために。

●Logger親クラスを継承する新しいサブクラスを作成するときに、log()メソッドの再実装を忘れる場合があります。以前は抽象クラスの設計アイデアに基づいていましたが、コンパイラはサブクラスにlog()メソッドの書き換えを強制しました。そうしないと、コンパイルエラーが報告されていました。新しいLoggerサブクラスを定義したいのに、log()メソッドを再実装するのを忘れたのはなぜですか?この例は比較的単純です。Loggerには多くのメソッドがなく、数行のコードがあります。ただし、ロガーの行数が数百行で、メソッドがnを超える場合は、ロガーの設計に精通していない限り、log()メソッドの再実装を忘れることはできません。

●ロガーをインスタンス化できます。つまり、新しいロガーを作成して、空のlog()メソッドを呼び出すことができます。これにより、クラスの誤用のリスクも高まります。もちろん、この問題はプライベートコンストラクターを設定することで解決できます。ただし、抽象クラスによるエレガンスは明らかにありません。

次に、もう一度見てみましょう。なぜインターフェイスが必要なのですか?どのようなプログラミングの問題を解決できますか?

抽象クラスはコードの再利用に適していますが、インターフェイスはデカップリングに重点を置いています。インターフェイスは動作を抽象化したものであり、一連のプロトコルまたはコントラクトに相当します。APIインターフェイスは類推によって考えることができます。呼び出し元は、抽象インターフェイスに注意を払うだけでよく、特定の実装を知る必要はありません。特定の実装コードは、呼び出し元に対して透過的です。このインターフェースは、規則と実装の分離を実現します。これにより、コード間の結合を減らし、コードのスケーラビリティを向上させることができます。

実際、インターフェースは抽象的なアプリケーションよりも広範で重要な知識ポイントです。たとえば、よく言及する「実装ではなくインターフェイスに基づくプログラミング」は、ほぼ毎日使用される設計アイデアであり、コードの柔軟性とスケーラビリティを大幅に向上させることができます。

抽象クラスとインターフェースのどちらを使用するかを決定する方法は?

今の説明はやや理論的なものかもしれませんが、実際のプロジェクト開発の観点から見てみましょう。コード設計とプログラミング開発で抽象クラスを使用する必要があるのはいつですか。いつインターフェースを使うべきですか?

実際、判断の基準は非常に単純です。is-a関係を表現したい場合、およびコードの再利用の問題を解決したい場合は、抽象クラスを使用します。has -a関係表現したい場合は、コードの再利用ではなく抽象化を解決することです。問題、それから私達はインターフェースを使用することができます。

クラスの継承レベルの観点から、抽象クラスはボトムアップの設計アイデアです。サブクラスのコードが最初に繰り返され、次に上位の親クラス(つまり、抽象クラス)に抽象化されます。インターフェースは正反対で、トップダウンのデザインアイデアです。プログラミングを行うときは、通常、最初にインターフェイスを設計してから、特定の実装を検討します。

将来的には、記事は個人の公開アカウントに同期されます。交換に注意を払うことを歓迎します

おすすめ

転載: blog.csdn.net/zhujiangtaotaise/article/details/111182676