抽象类
语法特性
- 抽象类不允许被实例化,只能被继承,直接new 抽象类编译会报错。
- 抽象类可以包含属性和方法,方法可以包含代码实现也可以不包含代码实现(抽象方法)。
- 子类继承抽象类,必须重写全部的抽象方法。
场景
如果是is-a的关系,并且为了解决代码复用的问题。
意义
- 抽象类可以解决代码复用问题
- 抽象类相对于继承来说实现思路更优雅。-- 如果仅仅用继承的化,会出现如下三种风险:
- 影响代码可读性,如果下代码:Logger中定义一个空方法,会影响代码的可读性,需要Logger、FileLogger、MessageQueueLogger 之间的继承关系,才能弄明白其设计意图。
- 如果方法较多的情况下,容易忘记重写。
- 增加了方法误用的风险,如Logger方法实例化之后调用自身的空方法log();
public class Logger {
private String name;
private boolean enabled;
private Level minPermittedLevel;
protected boolean isLoggable(Level level) {
return enabled && (minPermittedLevel.intValue() <= level.intValue());
}
//
public void log(Level level, String mesage) { // do nothing... }
public static void main(String[] args) {
//case1: 父类:非抽象类,就是普通的类. 删除了log(),doLog(),新增了isLoggable()
//Logger 中并没有定义 log() 方法 ,编译报错
//Logger logger = new FileLogger("access-log", true, Level.WARN, "*.log");
//logger.log(Level.ERROR, "This is a test log message.");
//case2: Logger中定义log()空方法,实际调用了Logger中的空方法。
Logger logger = new Logger("access-log", true, Level.WARN, "*.log");
logger.log(Level.ERROR, "This is a test log message.");
}
}
/**
* 输出日志到文件
*/
public class FileLogger extends Logger {
private Writer fileWriter;
public void log(Level level, String mesage) throws IOException {
if (isLoggable(level)) {
return;
}
fileWriter.write("");
}
}
/**
* 输出日志到消息中间件(比如kafka)
*/
public class MessageQueueLogger extends Logger {
private MessageQueueClient msgQueueClient;
public void log(Level level, String mesage) {
if (!isLoggable(level)) return;
// 格式化level和message,输出到消息中间件
msgQueueClient.send("");
}
}
接口
语法特性
- 接口不能包含属性(成员变量)
- 接口只能声明方法,方法不能包含代码实现
- 类实现接口方法的时候,必须实现全部方法。
场景
如果是has-a 的关系,并且为了提供代码扩展性问题。
意义
接口是对方法的抽象,是一种 has-a 关系,表示具有某一组行为特性,是为了解决解耦问题,隔离接口和具体的实现,提高代码的扩展性。
抽象类和接口的区别
什么时候该用抽象类?什么时候该用接口?实际上,判断的标准很简单。如果要表示一种 is-a 的关系,并且是为了解决代码复用问题,我们就用抽象类;如果要表示一种 has-a 关系,并且是为了解决抽象而非代码复用问题,那我们就用接口。
基于接口而非实现编程原则
原则中”接口“的理解
设计初衷:“接口”就是一组“协议”或者“约定”,是功能提供者提供给使用者的一个“功能列表”。该原则可以有效的提供代码质量。应用这条原则,可以将接口和实现相分离,封装不稳定的实现,暴露稳定的接口。上游系统面向接口而非实现编程,不依赖不稳定的实现细节,这样当实现发生变化的时候,上游系统的代码基本上不需要做改动,以此来降低耦合性,提高扩展性。
在软件开发中,最大的挑战之一就是需求的不断变化,这也是考验代码设计好坏的一个标准。越抽象、越顶层、越脱离具体某一实现的设计,越能提高代码的灵活性,越能应对未来的需求变化。好的代码设计,不仅能应对当下的需求,而且在将来需求发生变化的时候,仍然能够在不破坏原有代码设计的情况下灵活应对。
注意事项
- 函数的命名不能暴露任何实现细节,命名要足够通用,不能包含跟具体实现相关的字眼。
- 封装具体的实现细节,与特定实现有关的方法不要定义在接口中。
使用场景
从设计初衷上来看,如果在我们的业务场景中,某个功能只有一种实现方式,未来也不可能被其他实现方式替换,那我们就没有必要为其设计接口,直接使用实现类就可以了。
越是不稳定的系统,我们越是要在代码的扩展性、维护性上下功夫。相反,如果某个系统特别稳定,在开发完之后,基本上不需要做维护,那我们就没有必要为其扩展性,投入不必要的开发时间。
/**
* 需求:存储图片接口用于上传、下载。
*
*/
public interface ImageStore {
/**
*接口命名没有暴露细节,且足够通用。
*/
public String upload(Image image, String bucketName);
Image download(String url);
}
public class AliyunImageStore implements ImageStore {
public String upload(Image image, String bucketName) {
createBucketIfNotExisting(bucketName);
String accessToken = generateAccessToken();
//...上传图片到阿里云...
// ...返回图片在阿里云上的地址(url)...
return "aliyunUrl";
}
public Image download(String url) {
String accessToken = generateAccessToken();
//...从阿里云下载图片...
}
/**
* 封装具体的实现细节。跟阿里云相关的流程不应该暴露给调用者。
*
*/
private void createBucketIfNotExisting(String bucketName) {
// ...创建bucket...
// ...失败会抛出异常..
}
private String generateAccessToken() {
// ...根据accesskey/secrectkey等生成access token
}
}
组合和继承
继承存在的问题
继承是面向对象的四大特性之一,表示is-a的关系,用于解决代码复用的问题,但是如果继承层次过深,关系过复杂,也会影响到代码的可读性和可维护性
组合相对于继承的优势?
继承主要有三个作用:表示 is-a 关系,支持多态特性,代码复用。而这三个作用都可以通过其他技术手段来达成。比如 is-a 关系,我们可以通过组合和接口的 has-a 关系来替代;多态特性我们可以利用接口来实现;代码复用我们可以通过组合和委托来实现。所以,从理论上讲,通过组合、接口、委托三个技术手段,我们完全可以替换掉继承,在项目中不用或者少用继承关系,特别是一些复杂的继承关系。
组合和继承分别的使用场景?
-
如果类之间的继承结构稳定(不会轻易改变),继承层次比较浅(比如,最多有两层继承关系),继承关系不复杂,我们就可以大胆地使用继承。反之,系统越不稳定,继承层次很深,继承关系复杂,我们就尽量使用组合。
-
设计模式会固定使用继承或者组合。比如,装饰者模式(decorator pattern)、策略模式(strategy pattern)、组合模式(composite pattern)等都使用了组合关系,而模板模式(template pattern)使用了继承关系。
-
特殊的应用场景,会固定使用继承或者组合。比如Crawler 类和 PageAnalyzer 类,它们都用到了 URL 拼接和分割的功能,但并不具有继承关系(既不是父子关系,也不是兄弟关系)这个时候,使用组合就更加合理、更加灵活;同理:如果你不能改变一个函数的入参类型,而入参又非接口,为了支持多态,只能采用继承来实现
/** * 需求:请设计出:一个关于鸟的特征关系类图。 * 要求如下: * 会叫:鸵鸟、麻雀 * 会飞:麻雀 * 会下蛋:鸵鸟、麻雀 * 思路:1. 针对“会飞”这样一个行为特性,我们可以定义一个 Flyable 接口,只让会飞的鸟去实现这个接口。 * 对于会叫、会下蛋这些行为特性,我们可以类似地定义 Tweetable 接口、EggLayable 接口 * 2. 针对三个接口再定义三个实现类,它们分别是:实现了 fly() 方法的 FlyAbility 类、 * 实现了 tweet() 方法的 TweetAbility 类、实现了 layEgg() 方法的 EggLayAbility 类。 * 然后,通过组合和委托技术来消除代码重复 */ public interface Flyable { /** * 飞 */ void fly(); } public interface Tweetable { /** * 叫 */ void tweet(); } public interface EggLayable { /** * 下蛋 */ void layEgg(); } public class FlyAbility implements Flyable{ public void fly() { // ... } } public class TweetAbility implements Tweetable{ public void tweet() { // ... } } public class EggLayAbility implements EggLayable{ public void layEgg() { // ... } } public class Ostrich implements Tweetable, EggLayable {//鸵鸟 private Tweetable tweetable = new TweetAbility();//组合 private EggLayAbility eggLayAbility = new EggLayAbility(); //组合 public void layEgg() { eggLayAbility.layEgg();//委托 } public void tweet() { tweetable.tweet();//委托 } } public class Sparrow implements Flyable, Tweetable, EggLayable {//麻雀 private Tweetable tweetable = new TweetAbility();//组合 private EggLayAbility eggLayAbility = new EggLayAbility();//组合 private Flyable flyable = new FlyAbility();//组合 public void layEgg() { eggLayAbility.layEgg();//委托 } public void tweet() { tweetable.tweet();//委托 } public void fly() { flyable.fly();//委托 } }
网友评论