有个抽象类 Job,代码如下
public abstract class Job {

public boolean start(){
int id = a();
processId(id);
String data = b();
processData(data);
return c();
}

public abstract int a();
public abstract String b();
public abstract boolean c();

public void processId(int id){
...
}

public void processData(String data){
...
}
}

Job 封装了一个任务的主流程:a()->b()->c(),其中 abc 方法均为抽象类,由子类实现,中间穿插一些公共的方法,如 processId,processData,在父类实现。
这是一种比较常见的封装,乍一看没什么问题。
但是当 Job 的实现类很多,同时整个主流程变得复杂的时候,各种抽象方法和父类的公共方法穿插调用,特别是其他人去看代码的时候,就会变得特别痛苦,需要不断从子类和父类中跳转,以看清整个流程的全貌,此时子类的可读性就会变得很差。
作为读者,在面对多个子类任务的时候,我希望每个类都能看懂整个流程,作为开发者,有没有什么好的设计模式,能够在适当的封装下,又提高代码的可读性呢?

说明你读代码的顺序有点问题,抽象类就是抽象出整个流程的大概逻辑,下面的子类在去实现小的差别。
如果你每个具体的方法都要深入下去看具体实现,像深度优先遍历一样读代码,肯定是要花费大量时间的,一般有空闲了才这么读。
快速读完大概逻辑,碰到确实需要了解的细节再去看下层实现,这样比较高效。

这不是抽象模板吗,从抽象类可以获取整个流程的逻辑,具体的操作由子类实现,如果抽象类里面比较多的一些默认方法,阅读起来确实有点麻烦,但是遵循一次只阅读一个字类的逻辑还是很容易理清楚逻辑的。
只要不出现子类互相调用这种神操作

不管读代码还是优秀框架的源码,最基本的一点都是不要陷入无穷的细节里。有些方法你最开始只需要知道它大概负责什么,完成了什么,即便实现里有再精巧的设计,也放到把框架和流程理清楚之后再深入。

在父类的流程控制方法里打个断点找个用例跑一遍就可以了吧,或者打在子类方法里也可以,在调用栈里就能方便的在子类和父类中跳转。不过看代码是下下策,维护好文档就能避免折腾这套了...

如果是我的话,我会这样实现,Job 是一个接口。AbstractJob 实现了 processId 和 processData 方法。

任务的主流程封装为 Executor 类,使用 模板模式,直接调用 抽象接口 job.a -> job.b -> job.c -> processId -> processData,这样大家在看到 Executor 这个主流程就会非常清晰是这样的执行顺序。

尽量不要让 processId 和 processData 是在中间执行,因为最清晰的一定是,每个 job 的执行流程是一样的,都是 abc,id,data 这种,让大家一通百通。

如果想要一个任务主流程是 先执行流程 X 子类(X 实现了 a b c 三个接口),而后又要执行流程 Y 子类(Y 实现了 a b c 三个接口),那就使用 组合模式 或 责任链模式,定义一个 SyncJobChain (同理也可以实现 AsyncJobChain ),SyncJobChain 的 a 方法分别调用 X.a 和 Y.a,b 方法分别调用 X.b,Y.b,实现 SyncJobChain 的好处就是让任务整体都是一个执行顺序。看起来很舒服。

如果这样无法满足的话,也可以再扩展一些,即 job.abc 方法 不做整个逻辑的执行,abc 方法分别封装三个都是就绪状态的 Event,想办法把 Event 执行的逻辑搞成一样的,如果是 Spring 的话,通过 Publisher 发布出去,不是 Spring 的话,也可以自己定义一个线程池,扔到线程池里跑。在最后的 Spring Listener 或者线程池中完成 Event 的执行。

如果更灵活的话,可以加入 final 方法,在重写 a,b,c 的基础上,重写 final 方法,final 方法用于区分 X 类和 Y 类的最后节点任务(例如 X 是调用 Dubbo 接口,Y 是发送 MQ )

如果业务逻辑比较复杂,也可以参考 DDD 的设计思路进行设计。还有,如果很明确的东西,就尽量不要用 Interface 去搞 了,直接写 class 就好。

不知道答的在不在点子上,希望可以帮到忙。

我吃过这种亏读源码一开始最忌讳陷入细节 .
如果能按照自顶向下的思路搞清楚框架的设计脉络再去细读就不会难受了

讀代碼的正確姿勢應該是關注接口和抽象類 避免深入具體實現啊

最近刚实现了一个批量处理任务,就是这么写的。
同事说看不懂我写的

先实现功能, 后优化细节,重构。 业务都没有跑通,就开始讲设计模型扯淡。产品那边 2 天一小变,1 周一大变,烂点的设计文档都没有。你怎么知道你的设计模型可以满足产品部各种变态需求。

抽象类 /抽象方法本来就是 Java 里面的糟粕。。。

大概你不懂 Java 吧,这是精髓

代码可能命名质量不高,导致阅读起来困难,如果高层次的抽象类把命名写得很清晰,其实你是不用去关心细节,或者在你需要的时候,你只需要关心特定的细节实现。

因为大部分初学者写代码大多时候都是自底向上,等把细节跟程序大流程弄明白了再自顶向下进行抽象,而高手程序员大多会开始进行全盘思考,在一开始就考虑剥离共通的部分以及较难以变化的部分,如果你掌握了 OOAD 的精髓以及 SOLID 原则,读这种代码不会费劲的,所有的面向对象程序设计都是在干一件事情,把容易变化的部分与较难以变化的部分进行剥离,把细节与高层次的逻辑进行剥离,而这两句话大多时候是重叠的,细节容易变化(例如把数据库从 SQLServer 换到 MySQL 或者换一个 ORM 框架 或者换一个 HTTPClient ),而大多时候描述高层次逻辑的代码却难以变化。

而做这些 OOAD 工作的本质是因为大部分的人脑是有限的,人脑无法理解过于复杂的事务,我们的大脑在设计之初就无法存储大量上下文用于理解复杂的事物,对于超级大脑来讲,整个操作系统的代码都放在一个 C 语言的方法里面实现也并不是一件困难的事情。

楼主这个类,从设计者的思路来看,大的流程是难以变化的,而部分流程细节偏多存在多种实现,所以交给不同的子类去做,这样可以减轻阅读者的心智负担,阅读别人代码的时候,一定要考虑到设计者减轻你阅读心智负担的意图。

抽象类 /抽象方法是精髓,那接口算啥子?

(精髓 2

接口和抽象类两个东西完全重复了
你完全可以写一个具有部分实现的接口,甚至这些实现基于抽象方法。Python 采用动态的方法实现了这一点,Rust (可能还有 C++)采用了静态的方法实现了这一点。

据说人脑工作记忆只有 7 个( 5~9 个),但 7 个「什么」是比较诡异的问题。可以是 7 个英文字母,也可以是 7 句谚语,有些人认为完全可以是 7 篇文章。