V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
kerb15
V2EX  ›  Java

抽象类降低子类可读性探讨

  •  
  •   kerb15 · 2021-09-09 15:57:00 +08:00 · 2094 次点击
    这是一个创建于 1174 天前的主题,其中的信息可能已经有所发展或是发生改变。

    有个抽象类 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 的实现类很多,同时整个主流程变得复杂的时候,各种抽象方法和父类的公共方法穿插调用,特别是其他人去看代码的时候,就会变得特别痛苦,需要不断从子类和父类中跳转,以看清整个流程的全貌,此时子类的可读性就会变得很差。

    作为读者,在面对多个子类任务的时候,我希望每个类都能看懂整个流程,作为开发者,有没有什么好的设计模式,能够在适当的封装下,又提高代码的可读性呢?

    20 条回复    2021-09-20 09:02:53 +08:00
    newtype0092
        1
    newtype0092  
       2021-09-09 16:29:14 +08:00
    说明你读代码的顺序有点问题,抽象类就是抽象出整个流程的大概逻辑,下面的子类在去实现小的差别。
    如果你每个具体的方法都要深入下去看具体实现,像深度优先遍历一样读代码,肯定是要花费大量时间的,一般有空闲了才这么读。
    快速读完大概逻辑,碰到确实需要了解的细节再去看下层实现,这样比较高效。
    eric96
        2
    eric96  
       2021-09-09 16:48:58 +08:00
    这不是抽象模板吗,从抽象类可以获取整个流程的逻辑,具体的操作由子类实现,如果抽象类里面比较多的一些默认方法,阅读起来确实有点麻烦,但是遵循一次只阅读一个字类的逻辑还是很容易理清楚逻辑的。
    只要不出现子类互相调用这种神操作
    yamasa
        3
    yamasa  
       2021-09-09 16:50:06 +08:00   ❤️ 1
    不管读代码还是优秀框架的源码,最基本的一点都是不要陷入无穷的细节里。有些方法你最开始只需要知道它大概负责什么,完成了什么,即便实现里有再精巧的设计,也放到把框架和流程理清楚之后再深入。
    11232as
        4
    11232as  
       2021-09-09 16:53:03 +08:00
    在父类的流程控制方法里打个断点找个用例跑一遍就可以了吧,或者打在子类方法里也可以,在调用栈里就能方便的在子类和父类中跳转。不过看代码是下下策,维护好文档就能避免折腾这套了...
    monetto
        5
    monetto  
       2021-09-09 17:13:47 +08:00
    如果是我的话,我会这样实现,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 就好。

    不知道答的在不在点子上,希望可以帮到忙。
    securityCoding
        6
    securityCoding  
       2021-09-09 17:18:01 +08:00
    我吃过这种亏读源码一开始最忌讳陷入细节 .
    如果能按照自顶向下的思路搞清楚框架的设计脉络再去细读就不会难受了
    exonuclease
        7
    exonuclease  
       2021-09-09 17:22:49 +08:00
    讀代碼的正確姿勢應該是關注接口和抽象類 避免深入具體實現啊
    Seayon
        8
    Seayon  
       2021-09-09 17:51:54 +08:00
    最近刚实现了一个批量处理任务,就是这么写的。
    同事说看不懂我写的
    jones2000
        9
    jones2000  
       2021-09-09 22:57:16 +08:00
    先实现功能, 后优化细节,重构。 业务都没有跑通,就开始讲设计模型扯淡。产品那边 2 天一小变,1 周一大变,烂点的设计文档都没有。你怎么知道你的设计模型可以满足产品部各种变态需求。
    xuanbg
        10
    xuanbg  
       2021-09-10 08:25:36 +08:00
    抽象类 /抽象方法本来就是 Java 里面的糟粕。。。
    leonme
        11
    leonme  
       2021-09-10 08:50:28 +08:00 via iPhone   ❤️ 1
    @xuanbg 大概你不懂 Java 吧,这是精髓
    powerman
        12
    powerman  
       2021-09-10 09:38:03 +08:00   ❤️ 1
    代码可能命名质量不高,导致阅读起来困难,如果高层次的抽象类把命名写得很清晰,其实你是不用去关心细节,或者在你需要的时候,你只需要关心特定的细节实现。

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

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

    楼主这个类,从设计者的思路来看,大的流程是难以变化的,而部分流程细节偏多存在多种实现,所以交给不同的子类去做,这样可以减轻阅读者的心智负担,阅读别人代码的时候,一定要考虑到设计者减轻你阅读心智负担的意图。
    xuanbg
        13
    xuanbg  
       2021-09-10 10:15:35 +08:00
    @leonme 抽象类 /抽象方法是精髓,那接口算啥子?
    zxCoder
        14
    zxCoder  
       2021-09-10 12:51:25 +08:00 via Android
    @xuanbg (精髓 2
    2i2Re2PLMaDnghL
        15
    2i2Re2PLMaDnghL  
       2021-09-10 14:58:28 +08:00
    @leonme 接口和抽象类两个东西完全重复了
    你完全可以写一个具有部分实现的接口,甚至这些实现基于抽象方法。Python 采用动态的方法实现了这一点,Rust (可能还有 C++)采用了静态的方法实现了这一点。

    @powerman 据说人脑工作记忆只有 7 个( 5~9 个),但 7 个「什么」是比较诡异的问题。可以是 7 个英文字母,也可以是 7 句谚语,有些人认为完全可以是 7 篇文章。
    powerman
        16
    powerman  
       2021-09-12 09:20:20 +08:00
    @2i2Re2PLMaDnghL
    很难,大部分能记住 7-8 个对象的 数据流转与关系就不错了,人脑的 ROM 很大,RAM 却并不大,瞬时记忆基本上靠抽象理解,而 CCD 只需要 0.0001 秒就能完整记录一张高达 4K 画面的全部细节,而人脑不行
    kerb15
        17
    kerb15  
    OP
       2021-09-19 18:10:56 +08:00
    @monetto 非常棒的建议,看得出层主在这一面向对象设计的实践上有着非常多经验和思考
    kerb15
        18
    kerb15  
    OP
       2021-09-19 18:12:20 +08:00
    @Seayon 是的,当我们从上帝视角去看自己的代码的时候,总觉得设计的很好,但是从别人的角度来看,却完全不是这么一回事,可能你的设计还远远不够清晰完美
    kerb15
        19
    kerb15  
    OP
       2021-09-19 18:13:38 +08:00
    @powerman 对第一句话表示赞同,也许你的设计已经到位了,但是命名没做好,那么最终结果也是让阅读者摸不着头脑。
    powerman
        20
    powerman  
       2021-09-20 09:02:53 +08:00
    @kerb15 是的,所以在代码大全第一章,就重点提到了计算机编程领域的,隐喻,好的命名代表着隐喻,而这种隐喻可以让人很容易去理解代码所描述的系统。

    好的代码结构从一开始就是当作文档来编写的,高层次的命名方式结合 OOAD 就可以描述高层次的概念,而低层次的细节可以放到具体的子类实现,配合好的命名,非常容易理解,当然这种还是母语者有很大的优势
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   3267 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 35ms · UTC 14:01 · PVG 22:01 · LAX 06:01 · JFK 09:01
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.