zoukankan      html  css  js  c++  java
  • 代码可复用性问题兼谈团队协作

    可复用性的概念与现实

    工作了一两年的软件工程师,大概没有不听说过“可复用性”的概念。可复用性可以从两个视角来体现:

    • 创造者: 可以提供通用的服务和成果,别人直接在其工作的基础上构建自己的成果。整个过程不断叠加构建出恢弘的建筑。
    • 使用者: 可以直接使用已有的服务和成果,无需重复创作,达到高效。

    理性学科,比如数学、物理是遵循可复用性的生动体现和典范。因为大家都知道,不充分了解别人的成果,根本无法进行自己的工作,而且即使耗费大量时间和精力作出成果,如果与已有成果重复了,基本等于零。遗憾的是,工程领域里,却充斥着大量的重复创作和浪费,且很少有人察觉这种浪费。

    当你想要一个小功能点的时候,从以下场景上想想:

    • 你发现有一个函数能够实现想要的功能,一行代码调用就能搞定,心欢喜焉;
    • 你发现有一个函数能够实现想要的功能,做一个简单的适配就能搞定,心戚戚然焉;
    • 你发现有一个函数能够实现想要的功能,但参数不是想要的,得改改参数,抽离出一个子函数出来;
    • 你发现有一个函数能够实现大部分功能,还需要做点修改才能适配自己的需要,于是在里面添加了一个新的逻辑;
    • 你发现有一个函数能够实现,但这个函数做了太多事情,需要从中抽离出自己想要的部分;
    • 你发现有一个函数能够实现,但这个函数做了太多事情,而且依赖比较复杂,从中抽离出自己想要的部分有点麻烦,但还可以接受;
    • 你发现有一个函数能够实现,但这个函数做了太多事情,而且依赖比较复杂,从中抽离出自己想要的部分有点麻烦,但不困难;
    • 你发现有一个函数能够实现,但这个函数做了太多事情,而且依赖比较复杂,从中抽离出自己想要的部分有些困难,而且容易导致问题,需要大量回归测试。
    • 你发现有一个函数能够实现,但这个函数做了太多事情,而且依赖比较复杂,从中抽离出自己想要的部分很困难,不如重写一个。
    • 你没有发现现有函数能够实现。自己重写一个。

    以上场景,可复用性逐渐降低,而重复创造和浪费的概率逐渐增大。软件行业发展到至今,几乎不太存在最后一种情况,但很尴尬的是,据我所接手的业务工程,处于第一种情形的极少,处于前三种情形的也不多,大多数都落在了后六种情形上。

    是什么阻碍了做到代码可复用性

    现在,让我们来看看,是什么阻碍了代码可复用性。

    违反单一事实原则

    违反单一事实原则,可以说是阻碍可复用性的首要“罪魁祸首”。“单一事实”原则,可以说是中高级工程师耳熟能详的一个基本代码原则。遗憾的是,真正做到这一点的并不多。这体现了一个有趣的事实:越是简单的事情,越做不好。起床、吃饭都是极为简单而且基本没有“技术含量”的事情,人体天然能够支持的能力,然而,真正坚持专注做到做好的有多少人呢?

    举出违反单一事实的例子非常容易,在代码工程里俯拾即是。随便拿一个函数,看看它干了什么事,想想它能够在什么情形下被复用。大多数已有的函数和类,不是把技术逻辑和业务逻辑混杂在一起,就是恨不得一下子把所有事情都做了,瀑布一泻而下,疑是银河落九天。

    先不要谈什么设计原则或设计模式,先把“单一事实”原则切实做好。如何做好单一事实原则?

    • 想清楚这个函数做什么事,遏制想做两件及以上事情的冲动; 克制!大家都知道产品设计要克制,过于臃肿的产品谁都不想用,但过于臃肿的代码谁又想去碰呢?
    • 如果需要做两件事,拆分出两个可以组合的函数;
    • 如果想要新增流程或逻辑,新增函数,然后在其中调用;
    • 仔细定义参数,参数最小化;
    • 每个方法不超过 50 行(除去空行);
    • 拆分,新增函数,成为无意识的习惯!

    惟有养成这样严格遵循“单一事实”原则的习惯,才能在项目工期很赶的时候,依然能够写出质量不错的不臃肿的代码。

    参数过多

    由于特别容易做很多事情的倾向性,因此参数往往过多。当你发现一个函数里的参数过多,这个函数很可能违反了单一事实职责。或者是,在某种程度上,逻辑拆分存在问题。

    参数过多的直接后果就是:

    • 如果你想复用这个函数,就得推敲和构造这么多参数;
    • 你需要猜测每个参数的含义,与自己所需功能的相关性,过滤掉不需要设置的参数;
    • 当传空的参数时,往往是一种不佳的代码体现; 而当传很多空的参数时,很难看;
    • 理工科的人,需要一点艺术的熏陶。只懂逻辑不懂表达和设计的人只能写出难看的代码,虽然或许很管用。

    领域逻辑没有抽离

    说起来,软件工程领域落到实处的进步真是很慢(大概对于任何涉及多人协作的事情都是如此)。从最初的一团面条,到后来有人提出了 Controller-Service-Dao 分层理念,大家才开始有了分层抽离的概念,知道把参数校验写在 Controller 里,把应用业务逻辑写在 Service 里,把数据访问相关写在 Dao 层里。

    不过,这样还远远不够。Controller 和 Dao 倒是清晰了,但 Service 还是很臃肿。为什么?因为程序员习惯于把各种业务逻辑、技术逻辑、业务流程都扔到 Service 里,哪怕有些技术逻辑是一个可以复用的工具类,哪怕有些业务逻辑是可以复用的领域知识。

    DDD 是一种设计理念,但看上去掌握这种设计理念的人不多。实际上,我觉得 DDD 对代码编写更有启发。把 DDD 相关的领域知识,放到富血模型里或者领域层里。比如,对于一个安全业务工程,安全检测相关的逻辑,就很适合单独抽离到领域层里;比如,K8S 相关的资产,就适合把 Pod, Controller, Container Image 这些基础概念的关联关系抽离出来,放在领域层里,应用业务层只需要使用领域层的方法即可,而无需充斥在各种 Service 里。

    DDD 的核心思想就是:领域知识是一个业务工程里的核心资产。领域层是需要持续沉淀而稳定提升的。如果系统要做大的技术重构,按道理领域层应该是不用动的,动的是技术层。对于复杂业务工程来说,一定要有领域层。


    代码拆分太粗放

    代码放置太粗放,也是导致代码可复用性差的一个因素。只按照 Controller - Service - Dao 来拆分代码,自然很多代码重担就落在了 Service 里,因为 Controller 和 Dao 的职责比较固定,逻辑也偏少。

    更加细致的代码拆分是怎样的?

    以下是业务层的:

    • Controller : 仅作为请求转发层和参数适配层;
    • Service: 参数校验与业务逻辑流程。很多人把参数校验放在 Controller 层,我觉得不妥。因为如果要复用 Service 的方法,参数校验也是要复用的;
    • Dao: 数据库访问层。可以作为数据库中间件的适配和隔离;
    • Model: 与数据库直接交互的数据对象,目前基本是贫血模型,只有属性;
    • DTO: 数据传输对象,目前基本是贫血模式,实际上应该是富写模型,放在领域层;
    • Helper: 业务辅助类,领域层。可复用的业务逻辑判断、业务流程需要放在这里;
    • Exception: 异常处理和错误管理相关;
    • Constants: 业务枚举,业务常量;
    • Config: 系统配置相关;
    • Loader:系统初始化相关;
    • Cache: 业务缓存;
    • Components: 业务组件;
    • Receiver:数据消费接收器;
    • Handler: 事件处理类;
    • Context: 长流程里的上下文语境类;
    • Strategy: 同一业务目标的不同业务处理逻辑的策略类。用于分离不同业务的差异很好用;
    • Plugin: 业务插件,用于扩展;
    • Listener: 监听器;
    • Scheduler: 定时任务调度;
    • Job: 业务任务;
    • Producer: 任务生产者;
    • Consumer: 任务消费者;
    • Switch: 开关控制;
    • Wrapper: 包装器;
    • Adapter: 适配器。

    以下是技术层的:

    • Concurrent: 线程池、分布式锁等。
    • Util: 工具类,可复用的技术逻辑。
    • Interceptor: 拦截器,用于流程修改和业务逻辑转换。
    • [De]Serializer: JSON 自定义序列化与反序列化

    总之,当仔细去思考业务中的大大小小的关注点,把关注点的粒度最小化,就能发现更多放置代码的层次和地方。这并非是固定不变的。

    还有一个问题: 整体工程结构是按技术结构拆分还是按业务功能拆分?

    • 按照技术结构拆分,然后在每个技术子包里有业务拆分的子包。益处是,技术结构清晰;弊端是,业务实现则需要从各个子包里寻找拼凑出来,对业务不友好;
    • 按照业务功能拆分。每个业务下有自己的技术子包。益处是,很容易定位一个业务的所有相关实现;弊端是,公共的部分容易散落在具体业务里,且子包数量会膨胀。

    目前基本是按照技术结构拆分,符合研发同学对设计理念的认知。也许有一天,会开始按照业务拆分。

    不合理的访问级别

    由于倾向于把很多业务逻辑都放在 Service 里,自然很多潜在可复用的方法就会被设置成 private, 外部无法复用。当要复用时:

    • 可复用的方法放在很大的 Service 和 Flow 里,即使可以复用,但依赖这么大一个类或不恰当的层次,心有戚戚然焉;适合被依赖的最好是一个具有单一职责的小组件类;
    • 可复用的方法是 private,需要改成 public ,增加相应接口定义。

    这种情形,通常就是代码放置的层次和地方不合理。应该抽离到一个单独的小组件类里,设置为 public 方法。


    惰性

    惰性是人固有的生理特性,也是阻碍人持续进步的最大的因素。或许是老天爷开的善意玩笑吧。毕竟,生物的基本设定就是,能够躺着就不想坐着。

    简单的事情如果做不好,基本可以归为惰性的缘故。就像单一事实原则,到底是这个原则实际上很难做到,还是因为惰性而不愿意主动多走一步?


    缺乏可复用代码的意识和技巧

    没有思考过,没有学习过,没有被教授过,没有练习过,写可复用代码。这样,写出的代码就是“山大王代码”。这是我的地盘,只能我动,其它人只能远观不能亵玩焉。

    缺乏写可复用代码的意识,如果异常处理做得比较周密,还能成为一个合格的程序员,起码能保证基本的工程质量。但别指望他能做一些对团队整体有益的事情。

    如果有写可复用代码的意识,而缺乏写可复用代码的方法和技巧,那么就从做好单一事实职责开始吧!

    如果要有更强的写可复用代码的意识,每写一段代码,都会思考,哪些部分是通用的,哪些部分是差异的,然后努力将公共的或者差异的部分提取出来,这样,才能逐渐提升写可复用代码的意识和技能。

    缺乏分离与解耦差异的技能

    如果要达到更高层次的可复用性,则需要掌握分离和解耦差异的技能。善于把业务逻辑和流程进行分离和解耦,让每个组件自行负责各自的工作,让关注点清晰可辨,让交互自然流畅。

    为什么代码难以复用?正常情况下,一段代码总会有一些特定差异的逻辑和一些通用的逻辑,而且差异的逻辑往往潜藏在代码的任任意位置。由于缺乏将特定差异的逻辑提取出来的技巧,就会导致代码很正常但就是难以复用。

    函数式编程和设计模式,能够很大程度上提升代码的可复用性。函数式编程,用函数来表达差异;设计模式,确定了明确的组件职责,让交互更加自然,容易扩展。

    细小关注点没有抽离

    软件系统中充满着各种大大小小的关注点,技术关注点,业务关注点。哪怕是一段中文乱码处理,也是一个小关注点。细小的关注点没有抽离出来,就会导致高度的重复。我是见证了职业生涯所遇到的最高程度的代码重复艺术。


    瀑布流代码的益处与弊处

    当然,不能只是负向批评。难以复用的瀑布流代码也是有其益处的:读起来贴合程序员的自然思维。好的瀑布流代码读起来很流畅。遗憾的是, 很少瀑布流代码能够做到这一点。大多数瀑布流代码都会面临后期的各种修改,逐渐因为各种分支语句导致水滴四溅,甚为壮观唯美。

    与其益处相比,其弊处更多:

    • 如果代码只写一次,那么很清爽;
    • 如果代码会被改动多次,每次改动都会影响整个流程,每次都需要大范围的回归测试;
    • 多个人一起改动,容易产生冲突,在代码合并和解决冲突上会耗费很多时间和精力。

    团队协作

    为什么代码可复用性如此重要?

    软件开发是团队协作的生动体现。然而, 团队协作并不是“你在我代码上修修补补,我在你代码上修修补补”这种简单低级的协作。何为真正的团队协作?每个成员都充分分享和贡献自己的创作和成果,同时每个人都能从团队其它成员的创作和成果上获得新知、经验和工具,高效促进自己的工作。

    代码可复用性体现了团队协作的高效程度。

    • 每个成员都致力于创造可复用的构件;
    • 每个成员都把别人写的代码当成自己写的代码,改进和测试所使用到构件;
    • 大多数时候,并不需要从头写起,而是可以直接在某个基础上开始工作;
    • 如果要修改,则是新增而不是在里面修改,即符合开闭原则。

    代码可复用性差,那么研发效率不会高到哪里去。

    如何衡量代码可复用性? 非常简单。当你开始构建一个功能时:

    • 过程:想要一个基础功能时,是否有现成可用;是否专注于构建自己所关注的业务自身相关的逻辑,而不是还要构建技术逻辑,构建一些基础业务逻辑,处理复杂的技术和业务交互;
    • 结果: 最终写了多少行代码。代码越多,说明现有工程的可复用性越差(前提是你已经通读了整个工程的代码,知道没有可复用的,或者复用起来很麻烦)。

    小结

    软件开发是团队协作的生动体现,代码可复用性体现了团队协作的高效程度。本文探讨了阻碍代码可复用性的因素,以及如何做到代码可复用性的方法和技巧,希望对大家有所益处。

    PS: 细想一下,做到代码可复用性还真的不那么容易:

    • 严格遵循单一事实原则。保持克制。避免一个方法做两件事;
    • 最小化输入参数;
    • 领域层逻辑抽离;
    • 识别小业务组件并抽离,设置合理的访问级别;
    • 细致放置代码到合适的地方,合理的包拆分;
    • 细致识别小关注点并分离出来;
    • 思考通用与差异的部分;
    • 练习分离和解耦差异的技巧:函数式编程与设计模式。


  • 相关阅读:
    TextBox 只有下划线
    can't find web control library(web控件库)
    DropDownListSalesAC”有一个无效 SelectedValue,因为它不在项目列表中。
    IDE、SATA、SCSI、SAS、FC、SSD 硬盘类型
    如何打印1px表格
    CSS控制打印 分页
    Virtual Server could not open its emulated Ethernet switch driver. To fix this problem, reenable the Virtual Server Emulated Et
    Xml中SelectSingleNode方法中的xpath用法
    热带水果莫入冰箱?水果存放冰箱大法
    探索Asp.net的Postback机制
  • 原文地址:https://www.cnblogs.com/lovesqcc/p/15706677.html
Copyright © 2011-2022 走看看