什么样的代码,组件,架构才是好的? (section 1) - 823126028/book_reader GitHub Wiki
如何写出好的代码
经常吐槽和遇到其他同学吐槽bad code(嗯,没错我也经常被吐槽). 所以也经常反思怎样才能写出好的code.什么是好的代码?
- 可读性 易于阅读的代码, 同事理解起来容易, 易于维护, 利他为快乐之本~
- 易于变化 因为需求会不断被提出,无论是进化点和变化点都会给不易变化的代码带来结构上的破坏.
- 可测试性 快速迭代周期下,测试人员普遍不够,可测试性对各情景的覆盖程度有很大的影响.(嗯,为了提升覆盖率达到上线标准绞尽脑汁)
那如何能让代码符合上述特征呢
-
封装功能内聚的小函数(类), 让代码有更强的复用性, 如果要修改也更为集中.(封装性,可读性) 当然这里一切以还要以实际情况来说, 如果一串代码没有复用可能,也没有改动的可能, 也未必一定要抽到很小的函数.封装永远是为了应对变化.
-
变化点依赖于抽象而不是具体 (预留变化点) 这里有两层意思:
- 只针对变化点去做抽象扩展, 过分的抽象(使用设计模式)会让代码层层叠叠难以阅读. 曾经在和某同学在尝试重构账号系统时候,发现一个业务流程几乎不太变化的 验ticket-》判断账号状态 => 返回登陆态 的流程被改成了pipeline, 代码的可阅读性大大减弱. 而且上层应用添加代码的成本也相对较低, 这种留变化其实是代码的退化.
- 用多态来为变化点做扩展, 因为抽象的往往相对稳定. 依赖稳定的结构,往往不会有被动修改的成本. 为什么要预留变化点呢, 因为大佬说改动过去的代码是很多bug的根源, (敏捷开发认为一切代码都可改,只要通过测试).
- 少用继承 为什么要单独把继承拎出来呢,因为继承的问题太大了. C++ 到java去掉了多重继承, GOF也明确说明组合大于继承,继承的问题有哪些呢?
- 继承破坏了封装性, 这点问题还不算太大.
- 由于父类的不稳定, 子类强行耦合父类. 父类的改动会带动子类的修改. 举一个LSP中的经典例子:鸡,鹌鹑,鸭,鹅都是鸟, 鸭鹅可游水, 这时候会出现进退两难的地步, 为了增加功能, 要么在鸟这个类里加游水function,让鸡鹌鹑空实现, 要么增加一个water bird的中间类,增加复杂性. 无论是LSP里讲的啰嗦的子类父类, 还是大众哩语说的 is a 和 has a. 其实总结而言, 都是能让上层一视同仁的调用一批封装好的对象,而得到统一的结果, 撇除这种归一化的能力, 就要考虑到父类修改,对子类造成的后果. 如果有大量的空实现和覆盖父类的方法,那大概率不太适合用继承.
啥命名,注释也不来凑字数了, clean code也讲的很多, 多说说自己的思考吧.
上面的三点其实有点少, 但是似乎想想GOF的 SOLID 大多也就说的上面3点:
- SRP: 封装可应对需求变化的单一责任组件来隔离变化. (第一点)
- OCP: 对不变封闭,对易变扩展.(第二点), 这边一定要强调是变化点, 很多同学看了23种设计模式,在代码中疯狂使用(我), 往往得不偿失, 在对外来变化无法判断的情况下, 过于复杂的代码未来改起来更痛苦, 然后代码越复杂,可读性越差. 记住 keep it simple, 炫技能你就破功了.
- LSP: 少用继承,什么时候用继承 (第三点)
- ISP: 接口隔离上面没提到, 但是浅薄的知识让我觉得这并不重要, 但是代码层面的unsupported operation 总是丑陋的.
- DIP: 依赖抽象而不是具体, (第二点)
- LOD: 迪米特法则上面也没提到, 哩语不要和陌生人说话, 出现在成员变量, 方法的输入输出参数中的类, 自己生产的类称为熟人类。而出现在方法体内部的类就是陌生人, 主要是防止因为依赖对象改动,造成二次修改带来的麻烦. 具体实施见仁见智吧,毕竟少量应用能降低传参复杂度,都是度的问题.
术的方面能力有限,大概只能想这么多了.
道嘛, 用某位大佬的话作为总结: 封装可以和固定的接口来应对变化的需求, 通过归一化的方式来简化使用者成本. 之前一直在想为什么大多数人都那么爱用(不好的)设计模式呢, 最近有了结果:因为GOF的设计模式本身是为了告诉大家有设计模式,然后以简单的例子为他们取个名字, 然后设计模式本来就不是为了解决简单问题的. 然后对于有些同学在模块设计的时候,往往遇到这样也行那样也行的问题. 毕竟模块设计大多是通过用例抽象演绎而成, 现实生活中的联系往往多种多样,这个时候可以从共同变化的层面去想想.本身我们做架构的目的就是为了减少不必要的改动,或者控制改动范围来减少工作人日,这边如果有机会在[好的架构]这章会说说自己的思考.