设计原则与思想: 面向对象(理论五)

106 阅读6分钟

引言:

接口VS抽象类的区别? 如何用普通的类模拟抽象类和接口?

在面向对象编程中, 接口和抽象类是两个经常用到的语法概念, 是面向对象四大特性, 设计思想, 设计原则等编程实现的基础.

如:

  • 用接口来实现面向对象抽象特性, 多态特性和基于接口而非实现的设计原则.
  • 用抽象类来实现面向对象的继承特性和模版设计模式等

提问:

  1. 接口和抽象类的区别是什么?
  2. 什么时候用接口? 什么时候用抽象类?
  3. 抽象类和接口存在的意义是什么?能解决哪些编程问题?

一. 什么是抽象类和接口? 区别在哪里?

设计模式中: 模版设计模式就是一种典型的抽象类的使用例子.

举例子

Logger是一个记录日志的抽象类, FileLogger和MessageQueueLogger都继承Logger, 分别实现2种不同的记录日志的方式; FileLogger记录到文件中, MessageQueueLogger则记录到消息队列中.

2个子类复用了父类中的name, enabled, minPermittedLevel成员属性和log()方法.

由于2个子类实现写日志的方式不一致, 所以他们各自重写了父类的doLog()抽象方法;

1.1 抽象类实现方式

package org.example.case_02;  
  
import java.util.logging.Level;  
  
/**  
* 记录日志 抽象类  
* @Author: Nisy  
* @Date: 2023/06/01/23:36  
*/  
public abstract class Logger {  
  
private String name;  
  
private boolean enabled;  
  
private Level minPermittedLevel;  
  
/**  
* 构造方法  
* @param name  
* @param enabled  
* @param minPermittedLevel  
*/  
public Logger(String name, boolean enabled, Level minPermittedLevel) {  
    this.name = name;  
    this.enabled = enabled;  
    this.minPermittedLevel = minPermittedLevel;  
}  
  
/**  
* 对外暴露的log方法 (这里是模版方法模式)  
* @param level  
* @param message  
*/  
public void log(Level level, String message){  
    //统一校验参数, 达到代码复用  
    boolean loggable = enabled && (minPermittedLevel.intValue() <= level.intValue());  
    if (!loggable){  
        return;  
    }  
  
    //校验完参数后, 记日志, 走各自的抽象方法的重写  
    doLog(level, message);  
}  
  
/**  
* 抽象记录日志的方式  
* @param level  
* @param message  
*/  
protected abstract void doLog(Level level, String message);  
  
}
package org.example.case_02;  
  
import java.io.IOException;  
import java.io.Writer;  
import java.util.logging.Level;  
  
/**  
* 记录日志到文件中  
* @Author: Nisy  
* @Date: 2023/06/01/23:40  
*/  
public class FileLogger extends Logger{  
  
private Writer fileWriter;  
  
public FileLogger(String name, boolean enabled, Level minPermittedLevel, Writer fileWriter) {  
    super(name, enabled, minPermittedLevel);  
    this.fileWriter = fileWriter;  
}  
  
/**  
* 重写抽象方法doLog()  
* @param level  
* @param message  
* @desc: 子类继承抽象类, 必须重写父类中所有的抽象方法  
*/  
@Override  
protected void doLog(Level level, String message) {  
    try {  
        fileWriter.write("写日志到文件中");  
    } catch (IOException e) {  
        throw new RuntimeException(e);  
    }  
 }
 
}
package org.example.case_02;  
  
import java.util.logging.Level;  
  
/**  
* 记录日志到消息队列中  
* @Author: Nisy  
* @Date: 2023/06/01/23:45  
*/  
public class MessageQueueLogger extends Logger{  
  
private MessageQueueClient messageQueueClient;  
  
public MessageQueueLogger(String name, boolean enabled, Level minPermittedLevel, MessageQueueClient messageQueueClient) {  
    super(name, enabled, minPermittedLevel);  
    this.messageQueueClient = messageQueueClient;  
}  
  
/**  
* 重写抽象方法doLog()  
* @param level  
* @param message  
*/  
@Override  
protected void doLog(Level level, String message) {  
    messageQueueClient.send();  
}

}

1.1.1 抽象类特性总结

  1. 抽象类不能被实例化, 只能被继承
  2. 抽象类可以包含属性和方法, 包含实现的方法可对外提供, 不包含实现的方法叫抽象方法, 由子类去实现
  3. 子类继承抽象类, 必须重写抽象类中所有的抽象方法

1.2 接口的实现方式

过滤器接口:

package org.example.case_02.intr;  
  
import org.example.case_02.intr.resp.RpcRequest;  
  
/**  
* 过滤器接口  
* @Author: Nisy  
* @Date: 2023/06/08/22:45  
*/  
public interface Filter {  
  
    void doFilter(RpcRequest req) throws Exception;  
  
}

接口实现类: 鉴权过滤器

package org.example.case_02.intr;  
  
import org.example.case_02.intr.resp.RpcRequest;  
  
/**  
* 接口实现类: 鉴权过滤器  
* @Author: Nisy  
* @Date: 2023/06/08/22:47  
*/  
public class AuthencationFilter implements Filter {  
  
  
    @Override  
    public void doFilter(RpcRequest req) throws Exception {  
        //鉴权逻辑  
    }  
}

接口实现类: 限流过滤器


package org.example.case_02.intr;  
  
import org.example.case_02.intr.resp.RpcRequest;  
  
/**  
* 接口实现类: 限流过滤器  
* @Author: Nisy  
* @Date: 2023/06/08/22:55  
*/  
public class RateLimitFilter implements Filter{  
  
    @Override  
    public void doFilter(RpcRequest req) throws Exception {  
        //...限流逻辑...  
    }  
}

过滤器使用Demo:


package org.example.case_02.intr;  
  
  
import org.example.case_02.intr.resp.RpcRequest;  
  
import java.util.ArrayList;  
import java.util.List;  
  
/**  
* 过滤器使用demo  
* @Author: Nisy  
* @Date: 2023/06/08/22:55  
*/  
public class Application {  
  
  
    private List<Filter> filters = new ArrayList<>();  
  
    public void handRequest(RpcRequest req){  
        try {  
            for (Filter filter : filters) {  
                filter.doFilter(req);  
            }  
        } catch (Exception e) {  
        //处理过滤异常  
        }  
    }
}

1.2.1 接口特性总结

  1. 接口不能包含属性(成员变量).
  2. 接口只能声明方法, 方法不能包含代码实现(jdk1.8之后, 可以有default实现).
  3. 类实现接口的时候, 必须实现接口中声明的所有方法.

1.3 总结

接口和抽象类的不同:

(1). 语法特性 抽象类: 可以定义属性, 方法实现 接口: 不能定义属性, 方法也不能包含代码实现(jdk8可以用default关键字)

(2). 设计角度 抽象类是一种类的定义, 而接口更像是协议. 前者只能被子类继承, 继承是: is-a关系. 而接口是: has-a关系, 表示具有某些功能.

二. 抽象类和接口能解决什么编程问题?

2.1 为什么需要抽象类? 能解决什么编程问题?

  1. 代码复用
  2. 优雅的多态实现

因为抽象类不能被实例化, 只能被继承.而继承能解决代码复用问题. 所以得出结论: 抽象类是为了代码复用而生.

问: 普通的类也可以实现继承, 为什么非得要抽象类呢? 是不是还存在其他的意义?

答: 类 + 继承可以实现代码复用, 但是抽象 + 类 + 继承可以实现代码复用 + 多态, 让代码更加优美.

2.2 错误示范

还是用之前"打印日志"的例子, 来讲解.

定义一个Logger普通类

package org.example.case_02.abs;  
  
import java.util.logging.Level;  
  
/**  
* 普通logger父类(非抽象类)  
* @Author: Nisy  
* @Date: 2023/06/09/12:37  
*/  
public class Logger {  
  
    private String name;  
  
    private boolean enabled;  
  
    private Level minPermittedLevel;  
  
/**  
* 构造函数  
* @param name  
* @param enabled  
* @param minPermittedLevel  
* @desc 构造函数不变  
*/  
public Logger(String name, boolean enabled, Level minPermittedLevel) {  
    this.name = name;  
    this.enabled = enabled;  
    this.minPermittedLevel = minPermittedLevel;  
}  
  
  
/**  
* 新增isLoggable方法  
* @return  
*/  
protected boolean isLoggable(){  
    boolean loggable = enabled && (minPermittedLevel.intValue() <= Level.INFO.intValue());  
    return loggable;  
   }  
}

子类: 输出日志到文件

package org.example.case_02.abs;  
  
import java.io.IOException;  
import java.io.Writer;  
import java.util.logging.Level;  
  
/**  
* 子类: 输出到日志文件中  
* @Author: Nisy  
* @Date: 2023/06/09/12:41  
*/  
public class FileLogger extends Logger{  
  
private Writer fileWriter;  
  
private String filePath;  
  
  
public FileLogger(String name, boolean enabled, Level minPermittedLevel, String filePath) {  
    super(name, enabled, minPermittedLevel);  
    this.filePath = filePath;  
}  
  
  
@Override  
protected boolean isLoggable() {  
    return super.isLoggable();  
}  
  
public void log(Level level, String message){  
    if (!isLoggable()){  
        return;  
    }  
  
    //格式化level和message, 输出到日志文件中  
    try {  
        fileWriter.write("....");  
    } catch (IOException e) {  
        throw new RuntimeException(e);  
    }
 }
}

子类: 输出日志到消息中间件

package org.example.case_02.abs;  
  
import org.example.case_02.MessageQueueClient;  
  
import java.util.logging.Level;  
  
/**  
* 子类: 输出日志到消息中间件中(比如: kafka)  
* @Author: Nisy  
* @Date: 2023/06/09/12:44  
*/  
public class MessageQueueLogger extends Logger{  
  
private MessageQueueClient messageQueueClient;  
  
  
/**  
* 构造函数不变  
* @param name  
* @param enabled  
* @param minPermittedLevel  
* @param messageQueueClient  
*/  
public MessageQueueLogger(String name, boolean enabled, Level minPermittedLevel, MessageQueueClient messageQueueClient) {  
    super(name, enabled, minPermittedLevel);  
    this.messageQueueClient = messageQueueClient;  
}  
  
  
public void log(Level level, String message){  
    if (!isLoggable()){  
    return;  
    }  
  
    //格式化level和message, 输出到消息中间件  
    messageQueueClient.send();  
 }
}

应用:

package org.example.case_02.abs;  
  
import java.util.logging.Level;  
  
/**  
* @Author: Nisy  
* @Date: 2023/06/09/12:47  
*/  
public class LogMain {  
  
public static void main(String[] args) {  
  
    Logger logger = new FileLogger("access-log", true, Level.WARNING, "/users/nisy/access");  
  
    //编译报错, 因为父类Logger中并没有定义log方法  
    logger.log(Level.WARNING, "This is a test log message");  
 } 
}

总结: 我们发现logger.log在编译的时候报错了, 因为我们父类Logger中并没有定义log方法. 虽然以上这段代码达到了代码复用的目的, 但是无法使用多态特性了.

有人会说, 我们可以在Logger类中定义一个空的log()方法, 让子类重写父类的log()方法, 不就可以了吗?

答案是否定的, 有以下几点原因:

  1. Logger类中定义空方法log(), 会影响代码的可读性(在不熟悉logger背后的设计思想时, 会让读者产生疑惑)
  2. 当创建新的子类继承Logger父类时, 我们可能会忘记重写log()方法. 但是基于抽象类的设计思路, 编译器会强制我们重写log方法.
  3. Logger类可以被实例化, 我们可以new一个Logger类, 调用空的log方法, 增加了类的误调用

2.3 为什么需要接口? 它能够解决什么编程问题?

抽象类解决的是代码复用, 而接口侧重于解耦.接口是对行为的一种抽象, 相当于一组协议或契约, 调用者只关心抽象出的接口, 不关心具体的实现. 接口实现了约定与实现相分离, 可以降低代码的耦合性, 提供了代码的可扩展性.

我们经常提到: 基于接口而非实现编程

三. 如何决定该用抽象类还是接口?

如果我们想要代码复用, 想实现的是一种is-a关系, 那么就用抽象类.

如果我们仅仅关注的是行为本身, 解决解耦, 想实现的是has-a关系, 那么就用接口.

从类的继承层次上来看, 抽象类是一种自下而上的设计思路, 先有子类的代码重复, 然后再抽象成上层的父类.而接口洽洽相反, 它是一种自上而下的设计思路, 我们一般都是先设计接口, 再去考虑实现.