• 从外门弟子到化神:面向对象、设计原则、设计模式那些事儿


    周日下午,阳光美好,坐在桌子旁边暖烘烘的,就想总结点儿什么。思来想去,就写点我对软件设计的理解吧。

    系列文章:
    从状态机到状态模式


    我个人是个玄幻修仙小说的爱好者,之前看了不少小说,前面给特战队小朋友培训的时候突然做了一个类比,就先记下来:

    修仙体系等级软件设计者知识
    外门弟子 新手clean code
    炼气期(可能5000年) 算法、数据结构等
    筑基 编程范式:面向过程,面向对象,高内聚、低耦合
    结丹 设计原则:SOLID,DRY,Hollywood…
    元婴 常用设计模式、设计原则理解透彻
    化神 设计模式驾轻就熟
    炼虚 架构原则
    合体 架构模式
    大乘 企业级架构
    渡劫 ……
    真仙 ……

    面向对象

    这是个老生常谈的话题。我估计没有一个程序员没批评过或者没赞美过面向对象。
    一个(批评|赞美)过面向对象的人大概率是个程序员。
    首先呢,我可能是面向对象的拥趸,但是我也特别喜欢其他设计范式,例如函数式编程。我觉得一个程序员首先是个哲学家,兼容并举很重要,吸收一些不同的思想来武装自己的头脑很重要,毕竟我们可以要改变(物理)世界的,而且我们还在创造(数字)世界,创始者是要有思想的。
    所以呢:面向对象是一种思考模式,是一种对世界进行抽象的能力,当然了函数式编程爱好者会说 * 面向对象知识对数据的抽象,函数式编程是对函数的抽象。*
    无论怎么说吧,最核心的是抽象的能力是每个程序员都要淬炼的。
    抽象能力是人与生俱来的,我不知道动物是不是也有,但我估计应该是有的。但抽象能力随着知识、阅历以及经验的增长会不断提高,不断有不同的感悟的,是不是看起来和哲学家更像了。这也是为什么我面试一些人的时候会问最近看了啥闲书,就是看看候选人对不同维度的世界是不是还具有好奇心。

    面向对象的目的

    为什么需要OO,我所理解的面向对象编程的目的是:

    • 封装
      人是一个面向过程思考的动物,很容易陷入细节,封装的好处是隐藏不必要的细节,让人能够不停的看自己力所能及的范围。可以理解为封装是为了提供一个广度优先的环境给程序员的大脑,免得脑袋运算量过大,栈溢出,免得写出太多bug。
      提到封装,有个类似的概念叫做模块化,这里最重要的就是大家耳熟能详的高内聚低耦合了,封装画了一个框,框里面的高内聚,框外面的低耦合。这个框就是一个模块,毕竟一个模块不内聚,就不表达一个概念(聚合根了解一下),如果一个模块耦合度高,就没法到处复用,那你还怎么懒下去呢?:)
    • 重用
      有一句话叫做“懒人改造世界”,前面我也提到了“程序员改造世界”,所以呢,得出“程序员==懒人”应该没啥太大毛病吧?既然程序员是懒人,那么就当然不愿做体力活了吧?所以呢,重用就是最偷懒的实现功能的方式了。如果一个程序员总是喜欢重复造轮子,可以考虑一下是不是还适合这个职业了,except,你写的更好。毕竟程序员还喜欢创造世界嘛。
    • 扩展
      同样的道理,既然程序员创造世界,就不能一蹴而就对吧,上帝造人都花了好几天,你的世界从出生就定型了也丢人不是,毕竟你不是只会写“hello world”。

    我经常听到有人说,某某语言不是OO(面向对象)的,某某语言不是FP(函数式)的,这里我想分享的是,这些设计思想范式其实和语言无关的,只要你深刻理解了,是拿到哪里都可以用的,只是使用的方式不太一样,只有x语言依据y思想而设计,而没有x语言不能使用y思想这种说法。

    总结一下:编程语言是设计思想的实现模式,不同语言有不同的实现方式而已。不相信的可以看看那么多面向对象的实际模式在各种语言是怎么落地开花的,网上一搜一堆设计模式C语言实现不是吗?

    设计是怎么腐烂的

    下面这个图片我从给团队的培训slides中copy过来,算是总结了业内的共识吧。

    1. 人之初性本善:大部分代码都曾经是好孩子,干净纯洁,充满稚气和志气。无论怎么样,大家都会夸可爱;
      这个时候的软件怎么看怎么舒坦

    2. 性相近,习相远(腐化):随着年龄的增长,逐渐有了坏毛病,大家还是会说“毕竟是孩子”,进而有了写坏毛病,思想上和身体上,可能有了一个痣,有了一个疣,但是谁没有呢;
      这个时候的代码,还是功能占主体,看着还是非常不错的设计

    3. 苟不教,性乃迁(腐烂):坏毛病缠身,可能身体上的毛病更多了,头顶生疮,脚底流脓;
      这个时候,坏代码主导了设计,团队的大部分时间是用来处理这些坏代码,也就是说20%的时间在写新功能,80%的时间在修bug

    4. 自撰一良方,服之,卒:人已经彻底腐烂,要么是思想上的坏人,被判x刑了,要么是身体上的坏人,百病缠身,无药可医,只能自撰一良方了。
      在软件上,这个时候可能随便改点什么都无法运行了,如下对话最真实。公司胡子昂Fellow总结的好:当重写的成本小于重构的时候,意味着软件生命周期的终结。

      还需要重构吗?
      要的?
      怎么重构?
      重新写一个呗~~
      bye~~

    腐烂过程

    我们经常觉得我们很厉害,可以在行驶的汽车上换轮子,但我觉得更像拿着M4A1打移动靶,可能运气好打中了,更大的可能是不知道打倒了谁。

    如何识别病症

    贴一篇小时候的文言文《韩非子·喻老|扁鹊见蔡桓公》:

    扁鹊见蔡桓公,立有间。扁鹊曰:“君有疾在腠理,不治将恐深。”桓侯曰:“寡人无疾。”扁鹊出,桓侯曰:“医之好治不病以为功。”
    居十日,扁鹊复见,曰:“君之病在肌肤,不治将益深。”桓侯不应,扁鹊出,桓侯又不悦。
    居十日,扁鹊复见,曰:“君之病在肠胃,不治将益深。”桓侯又不应,扁鹊出,桓侯又不悦。
    居十日,扁鹊望桓侯而还走,桓侯故使人问之。扁鹊曰:“疾在腠理,汤熨之所及也;在肌肤,针石之所及也;在肠胃,火齐之所及也;在骨髓,司命之所属,无奈何也。今在骨髓,臣是以无请也。”
    居五日,桓公体痛,使人索扁鹊,已逃秦矣。桓侯遂死。

    可以各自选角色定位一下:)。

    代码腐烂的症状

    代码腐烂4个征兆:硬脆僵滑以及腠理、肌肤到肠胃的过程:

    硬:代码不好动
    1. 代码难以更改,无论多简单的代码,以为两天能改好,结果两周没搞定
    2. 代码负责人开始害怕新需求,抗拒更改(**硬**气!)
    3. 团队负责人开始害怕新区求,拒绝更改(团队官方潜规则,**硬**气团队)
    
    脆:代码不能动
    1.改动一点点,崩溃一大片,可维护性变低
    2.改动一点点,崩溃一大片,崩溃的地方和改动的地方没关系,可维护性极差
    3.改动一点点,崩溃一大片,修复一点点,再崩溃一大片,没有可维护性
    
    僵:代码不可重用
    1.重用性差,即使功能类似,也要大费周章,**鸡肋**
    2.代码完全绑死在特性上,**废品**
    3.代码白送没人要,**垃圾****
    
    滑:架构不合理,扩展性变差
    1.与其他模块对接变难(太滑了),接口变差
    2.实现同样的功能有多种选择,但不按照设计反而更简单,接口不隔离,同样的功能提供多种风格迥异的接口
    3.不按照规范配置反而更容易,抛弃规范设计(有点像溪村门口那条路,至少有三个方法不规范的方法从溪村到安朴) 
    

    腠理之症

    代码不可测

    合适的运用设计原则,不仅能提高代码的健壮性,还能提高可测性。如果一个代码不可测,必然带来更多的维护成本,无法规模化,全靠人力堆。今天加的班,都是昨天造的孽。时刻把可测性作为对自己代码的一个核心要求,这个不需要团队要求,是一个程序员金子般的品质和荣耀。

    举个栗子,举个荔枝,举个例子:下面的代码可测性就很差,因为repo和ProfileParser耦合紧密,甚至repo的初始化就交给了它,违背了单一职责的原则。如果repo是一个数据库,如何构建测试数据也非常困难,那么必然带来大量的测试成本。

     
    1
     
    2
    class ProfileParser
    3
    {
    4
    public:
    5
    bool Parse(int index)
    6
    {
    7
    repo.init();
    8
    auto data = repo.GetDataByIndex(index);
    9
    //... parse data
    10
    return true;
    11
    }
    12
    private:
    13
    Repo repo;
    14
    };

    简单改造一下,对Repo抽象一个接口出来,这样就很容易在测试的时候构建mock类,针对Parse的逻辑构建合适的数据。

     
    1
    class ProfileParser
    2
    {
    3
    public:
    4
    ProfileParser(IRepo* repo)
    5
    {
    6
    this->repo = repo;
    7
    }
    8
    bool Parse(int index)
    9
    {
    10
    auto data = repo->GetDataByIndex(index);
    11
    //... parse data
    12
    return true;
    13
    }
    14
    private:
    15
    IRepo *repo;
    16
    };

    好代码的特征

    说了很多怀代码的征兆,给点好代码样子:

    • 复用性
    • 重用性
    • 扩展性
      • 重构(响应原始需求)
      • 外部环境的变化(响应不可控需求)
      • 新需求(功能需求)

    设计原则

    在我们日常工作中经常听到设计原则这个词,也听到各种各样的原则,那如果给设计原则下个定义,什么叫设计原则呢?
    先来一个学术点的定义:

    设计原则是用来指导原价设计并且用来评价软件设计的价值主张

    具体点儿就是:

    • 指导软件设计行为 – 怎么做软件设计?
    • 评价软件设计行为 – 设计做的怎么样?
    • 指导软件设计提升价值 – 怎么提升价值?
    • 评估软件设计的价值 – 软件设计的是否有含金量?

    这事儿说大了就是一个软件设计者或者设计群体的价值观。俗话说文无第一武无第二,但评价一个作品还是得有点客观的因素,这些因素就是价值观,符合这个价值观,那么好,你是我们认可的软件设计者,否则呢,你懂得。

    防患于未然

    如何做到代码始终保鲜呢,业内大师Bob大叔(Robert Martin)给出了自己的总结,那就是耳熟能像的SOLID原则,宗旨是响应变化。软件之所以称之为软件就是其对变化响应的灵活性,如果失去了这种灵活性和硬件就没任何区别(硬脆僵滑)。

    我们学了很多算法和数据结构,这些对我们的软件设计的作用是什么?我总结下来:算法和数据结构响应的是数据规模的变化,而一个软件面对的远远不止数据规模,更多的变化是需求的变化。所以可以说,算法应对数据规模,架构应对需求规模。

    需求分为:

    1.功能型需求,满足的是客户的基本需要,这个是软件的底线。

    2.非功能性需求,这是软件的增值部分,非功能需求继续细分下去:

    1.客户的隐含需求:执行质量,基本上对应着:机密性,完整性以及可用性,这三条演化为:性能,安全,韧性等等。这是客户选择一个软件的隐含需求,也是我们必须要考虑的

    2.团队发展的需求:演化质量,架构以及代码的可维护性,可测试性等。这是团队快速反应的根本能力,如果这个需求做的不好,就会导致无法快速响应未来的需求,势必会影响到功能性需求的按时、保质达成。

    需求

    SOLID设计原则

    SOLID

    1. SRP:单一职责
      1. 一个模块应该只有一个变化的原因
      2. 一个模块应该只对一个actor负责
      3. “一个类只做一件事儿”是不准确的

    student_legacy
    略作调整
    student_new

    1. OCP:开闭原则

      1. 模块应该对扩展开放:如何响应新增的需求?
      2. 模块应该对修改关闭:是不是新增需求就要修改所有相关代码
        下面的例子展示:如果做一件事情变化的部分是做事的策略,那么对策略进行了抽象,响应未来策略的变化。
        2e6412744d3f21eb8340_954x295.png@900-0-90-f.png
    2. LSP:里氏替换原则,
      1.这个原则很复杂,建议大家可以读一下Liskov的原文,这里我不展开,以后分享,大致上:

      • 子类方法的参数类型必须与其超类的参数类型相匹配或更加抽象
      • 子类方法的返回值类型必须与超类方法的返回值类型或是其子类别相匹配
      • 子类不应该做出与父类不一致的行为
      • 子类不应该加强前置条件
      • 子类不能削弱后置条件
      • 超类的不变量不能被改变
      • 子类不能修改超类的私有成员变量的值

      2.简单来讲:当修改一个类的时候,调用它的客户端不能被要求修改,应该时刻可以把子类对象作为父类对象来传递。一个使用父类实例(指针或者引用)的地方(例如参数)应该可以无条件的替换为子类(实例的指针或者引用);一个返回子类实例的地方可以无条件的返回父类实例(的指针或者引用)。

    3. ISP:接口隔离原则

      1. 设计针对于客户端(调用者)需要的接口,不要强迫调用者使用它不需要的接口(== 不对客户端暴露它不需要的接口)
      2. 接口尽量少
      3. 接口尽量小
    4. DIP:依赖倒置原则

      1. 上层模块不能依赖于底层模块来设计,而应该依赖于底层模块的抽象接口
      2. 调用者与服务提供者应该依赖于接口作为契约
      3. 抽象接口不依赖于具体实现,具体实现应该依赖于抽象接口

    其他设计原则

    在SOLID之外,也有很多业内大师总结了各种各样的设计原则,无论是SOLID还是下面列出的其他原则,都需要在摸索中理解,并在合适的场合使用,也可能在特定的场合明确的违背,为了性能,为了反原则(例如防止别人更好的理解代码)等。但无论你遵守了哪个原则,或者违背了哪个原则的大前提是你清楚的知道你在做什么。

    1. KISS原则:Keep it simple, stupid.
    2. DRY原则:Don’t Repeat yourself
    3. CQS命令查询分离:经常看到一个反原则的例子是函数叫做GetXXX,实际上还修改了内容
    4. 迪米特原则:最小知识原则,想到什么了吗:封装,ISP…

    设计模式

    给一个定义吧:

    设计模式椒对一些软件设计中的共性问题的典型解决方案,每个设计模式都提供了一个蓝图给软件设计者,使之可以在软件设计过程中来解决一个(可能已经存在于代码中的)特定问题。

    • 设计模式 dp != (copy | paste)
      • 设计模式不是一段现成的代码,是解决通用问题的一个概念
      • 按照一个模式的细节可以实现一个适应自己程序的方案
    • 设计模式 dp != algorithm
      • 算法和设计模式都是解决已知问题的典型概念以及思路
      • 算法会描述达成目标的清晰的步骤,模式的思路层次水平面更高一些,不同编程语言实现相同的模式可能完全不一样
      • 算法就像菜谱,讲解详细步骤;模式更像蓝图,告诉你要达成的目标。
    • 设计模式是一个问题的概念解决方案,dp == concept pattern(string problem)

    设计模式的分类

    什么也不说了,直接贴图。因为不在本文讨论范围内。

    设计模式的选择

    上面这么多设计模式如何应用,设计模式的选择也应该根据项目的情况来选择,同样修一条路,满足多个方向的通行要求,可以选择高架桥,可以选择人性天桥,也可以选择红绿灯控制,甚至可以什么都没有,这就靠架构师的个人经验来判断了。当然了随着路口的复杂逐渐提高,可以加上红绿灯,可以修建天桥,可以修建高架,我们称之为演进性设计。

    关于设计原则和模式一句话总结

    再重复一下,对设计原则的使用应该是在理解的基础上的,不要拿着锤子到处找钉子。用的好:“清水出芙蓉,天然去雕饰。”,用的不好么,“丑女来效颦,还家惊四邻。”

    标签:设计原则设计模式面向对象修仙分类:研发 > 软件技术
  • 相关阅读:
    C++ 类的内存分布
    Hadoop集群安装配置教程_Hadoop2.6.0_Ubuntu/CentOS
    Hadoop安装教程_单机/伪分布式配置_Hadoop2.6.0/Ubuntu14.04
    linux 入门教程
    linux shell 常用基本语法
    linux系统的7种运行级别
    Linux学习之CentOS6下Mysql数据库的安装与配置
    二叉树方面的问题
    先贴出代码C++ 中的单例模式
    C++11 中的线程、锁和条件变量
  • 原文地址:https://www.cnblogs.com/gongxianjin/p/15627845.html
Copyright © 2020-2023  润新知