文章

当你手上只有锤子的时候,看什么都像钉子。

引言

设计模式往往被用来检验一个程序员是否高端,因为从道理上来讲,
在很多地方即使你写的代码和意大利面一样,它只要能实现功能。你通常就不会被老板找麻烦。
然而只要公司上了一定的规模,有了编程规范,甚至复查,你要再写的和意大利面一样,可能很快你就吃不起意大利面了。
虽然,通常如果你只会写意大利面一般的代码,你也进不去这样的地方。
扯远了,我只是想说明,当程序有了一定的规模,编程范式就会显示出它的作用,无论是从编写还是维护两方面来说。
那么什么是设计模式呢?
设计模式的定义为:在面向对象软件设计过程中,针对特定问题的简洁而优雅的解决方案。
说人话就是,特定场景的某种问题的解决方案,它也不是多么遥不可及的内容,很有可能大家在日常的生产生活中无数次的用到过。
只是没有给起个响亮的名字罢了。
GOF总结的设计模式共有23种,在这里我并不想去讨论所有23种设计模式。
我的宗旨,技术是为了解决问题而存在的,正好,我现在存在一个场景,我认为用享元模式和职责链来重构会更加优雅。
虽然我之前看过它们的介绍,但并没有应用于实践中过,于是这篇笔记随之而生。
这篇笔记的讨论点将偏向于设计原则和技巧。
范式方面将着重介绍享元模式。
和之前的所有笔记一般,这篇笔记具备偏颇和主观两个特点,旨在为我在实践中遇到的问题提供解决思路。

设计模式漫谈-原则与技巧

说到设计原则,常听到的无非几种:单一职责原则(SRP),最少知识原则(LKP),开放封闭原则(OCP)。
谈到技巧,最常听见的大概就是面向对象,或者面向接口,也许还有函数式编程了。
它们通常如字面意思一般,概念上很简单明了,但是实践起来可谓博大精深。
驾驭不了的话,原本为了便于维护引用的原则可能会使代码复杂度加深,反而变得不那么好维护,很多设计模式在实践中也会有类似的问题。
这并不是说,它们不对,或者不好,仅仅是你自身暂时达不到那么境界所以应用不当。
实际上在我初学JavaScript之后紧接着就进行了有关Js设计模式的学习,这也是为什么我能意识到,我最近遇到的场景可以使用这两个模式重构。
然而,在相当长的时间里,我并没有机会在实践中应用它们,无非能力不足,同事并不在意这些,时间不够这么几个因素。
这是很常见的,当你挖空心思优化一个模块,而之后在这个模块做扩展的同事并不卖账,简单粗暴的怎么方便怎么来。
不出一些时日,你之前的优化还不如不做,因为它们看起来更难懂,而功能又都差不多。

从单一职责原则说起:

单一职责原则简单来说就是:一个对象(方法)只做一件事情。
举例来说,假如你想用爬虫爬取某个网站的图片,大概需要以下几个过程。

1、请求目标地址,获得响应的到Html结构
2、解析Html结构,获得图片地址
3、下载图片地址

如果你想,你可以把它们写成一个巨大的函数,只需要传入一个Url就能自动下载到图片。

function getImg(url){
  .....
}
//执行完就能下载一张图片。

然而这不是一个函数应该干的事情,它不利于维护,如果应用单一职责原则的话,它需要被拆分成至少以下几个函数。

getHtml //请求URL地址得到Html结构 
handleHtml //解析Html获得Img下载地址
downImg //下载图片
//你可以写一个对象,加上start函数,就像这样。
var main={
    start:(url)=>{
       var html= main.getHtml(url);
       var imglist = main.handleHtml(html);
       downImg(imgList);
    },
    getHtml:(url)=>{
        ....
    },
    handleHtml(html)=>{
        ...
    },
    downImg(imgList)=>{
        ...
    }
}

当然也可以写成单独的函数不写进一个Obj中,它们明显要比维护一个庞大复杂的函数要明智的多,当你想爬取另一个网站时,只需替换中间的Html解析部分就可以了。
想扩展也可以很轻易的找到需要添加的位置,或者需要修改的函数。
看起来似乎很简单,然而单一职责模式并不是那么容易被正确应用,因为并不是所有职责都应该分离,而且它虽然带来了稳定性,但是也使得编写代码的复杂度加深,同时增加了这些对象之间联系的难度,这有时也会使得这些对象并不那么易用。

最少知识原则

最少知识原则简单来说则是:尽量减少对象之间的交互,如果两个对象之间不必直接通信那么这两个对象就不要直接相互联系。
通常使用中介对象来做联系就是它的应用了,当某一部分发生更改只需要更改中介对象中的就可以了。
举例来说,上面那个爬虫列子start就是一个中介对象,我不关心你在爬取下载前经过了多少步骤,我只是想下个图片,大概就是这个意思。
总结来说:最小知识原则减少了对象之间的依赖,但有可能会制造一个庞大到难以维护的第三方对象,所以在实际使用中还需要分情况讨论。

开放-封闭原则。

定义:软件实体(类,模块,函数)等应该是可以扩展的,但是不可修改。
这条概念好像有些难以理解,其实就是,当你需要新加一个需求的时候,在原系统上增加新的代码,而不是去修改现用的模块。
因为修改模块的话,有可能Bug会越改越多。
实例:

//假如你有一个非常庞大的If else 或者switch case分支。
//这明显违背开放封闭原则,因为要扩展它不可避免的要去修改原本的代码。  
//来一段经典的鸭子发声作为实例。
function makeSound(animal){
    switch(animal){
        case 'duck':
            console.log('嘎嘎嘎');
           break;
        case 'dog':
            console.log('汪汪汪')
        .....
    }
}
//简单的动物发声,要是扩展的话,就得对这个switch case语句下手,当然这个列子看起来修改这个switch case似乎没什么困难的,这里只是为了举例。  
function makeSound(animal){
    animal.sound();
}
function Duck(){
    
}
Duck.prototype.sound=()=>{
    console.log('嘎嘎嘎');
}
makeSound(new Duck);
//扩展的话,写新的函数就可以了

//只是我觉得,要我写的话..
function makeSound(animal){
    makeSound.prototype[animal].sound();
}
makeSound.prototype.Duck={
    sound:()=>{
        console.log('嘎嘎嘎');
    }
}
makeSound('Duck')
//扩展的话给它的protoype挂新的函数就行了,也算是不去修改原函数。

上面是利用多态来重构switch case 使它符合开放封闭原则以增加可扩展性。
核心原则就一个,找出程序要发生变化的地方,然后把变化封装起来。
常用的方式:使用钩子,或者使用回调。
在这里不再展开,因为对此我也是一知半解,除了这个重构选择暂时没想到新的应用场景,缺乏实践动力。
实践完职责链也许会对其理解更深。

以接口和面向接口编程结束

定义:接口是对象能响应的请求的集合。
说API估计没有程序员不知道它是啥,那什么是面向接口编程呢?
简单来说就是,只关心这些调用的类能做什么而不关心怎么做。
在我来说,则是当别人来引用你开发的大模块中某一个小模块时,它并不需要关注你的实现,只需要知道调用会产生什么结果。
以上面的爬虫列子来说,就是无论你只是想获得一堆Html,还是想按一定的规则解析Html,还是只是想下载一个图片,都可以引用上面那个大模块中的某个函数来达成目的,而不需要在意其它函数是怎么实现的。
具体展开似乎有一大串,介于我个人不是太感兴趣,摘抄两句,这个部分我们就先跳过吧。

面向接口编程,而不是面向实现编程。

如果它走起路来像鸭子,叫起来也是鸭子,那么它就是鸭子。

前者在我看来和面向对象并无二致,后者则可以参考下Js中庞大的类数组形数据调用数组的方法,这些接口并不关心它们拿到的是不是一个真正的数组。

享元模式

享元(flyweight)是一种性能优化的模式,'fly'在这里是苍蝇的意思,意为蝇量级。享元模式的核心是运用共享技术来有效支持大量细粒度的对象。
重点是划分内外状态,目的是减少共享对象的数量。
我不会举那个男女模特和内衣工厂的列子
简单复述一下就是,如果用代码实现男女模特穿一千件内衣拍照,直接写的话,往往会需要男女模特各一百个。
而稍微注意一点,则可以,男女模特各一个换着穿一千件内衣拍照,有兴趣的可以自行百度。
经验指引:

  • 内部状态存储于对象内部
  • 内部状态可以被一些对象共享
  • 内部状态独立于具体场景,通常不会改变。
  • 外部状态取决于具体场景,并根据场景而变化,外部状态不能被共享。

在这里我附上一个进程池的列子:
进程池的场景很常见,比如之前的Redis,NodeRedis库每次读取会重新连接,如果多次连接,每次都新开一个连接很明显不够高效。
正确的做法是开一个连接,将这个连接对象依次往下传递直到所有的读取都结束为止。
有并发的存在那么就开三个。
其它,我以前做过一个在地图上标小气泡的页面。
正常的思路是,获取用户定位地点为中心的圆,标注气泡,移动,重新标注,或添加新的气泡。
假如重新标注,清除之前的标注对象,再新建也很显然不够高效。
正确的做法是回收它们,再重新定位它们的位置。
附上一个简单实现:

function divFactory(){
    var divPool=[];
    return{
        create:this.create,
        pool:divPool
    }
}
divFactory.protoype.create=function(num){
    var needDiv = num;
    var result  = [];
    if(this.pool.length<needDiv){
        var neddLength = needDiv - this.pool.length;
        for(var i= 0;i<needLength;i++){
            var div = document.createElement('div');
            this.pool.push('div');
        }
        result = this.pool
    }else{
        for(var i=0;i<needDiv;i++){
            result.push(this.pool[i])
        }
    }
    return result;
}

//调用
var createFactory = new divFactory();
var res = createFactory.create(3);
for( i in res){
    res[i].innerHTML = 'Create '+i;
    document.body.appendChild(res[i]);
}
//页面上出现3个写着Create 的Div还有编号
var res2 = createFactory.create(5);
for(i in res2){
    var tip = res[i].innerHTML || 'No'
    res2[i].innerHTML=tip+'Create2'+i;
    document.body.appendChild(res[i]);
}
//页面上出现五个div其中前3个有Create+ 编号

上面这个列子以地图上的小泡为列的话,可以重新修改已经创建的小泡的名称和位置,以图性能优化。
附带一提,当年我负责的那个项目这个地图部分(不是我做的)最后选择了一次性将所有可能出现的点都导到地图上,一次性标注完成,不管你客户滑到哪如果存在肯定能看到应该有的点,全国差不多几千个吧。
前两天我还特的去看了眼,依旧是这个做法,它已经平稳运行两年了,侧面说明,前端优化,有的时候并没有面试时强调的那么重要,当然我还是提倡能优化自然要优化一下。

结语

这篇笔记花的时间比之前任意一篇笔记的时间都长,原本我还以为可以记录下职责链和代码重构,但事实证明,我下一篇笔记也许有内容了。
编程范式这类的,其实和代码编写规范一般,没有标准答案,而且往往需要分情况讨论,我作为经验尚浅的开发人员是不敢妄议的。
但它既然经过了这么多年的检验,自是有历史沉淀在里面的。
作为一个土木系毕业的工科生对这些工程化模块化有天然的好感。
作为一个半路出家的前端,构造函数和原型链依旧还不习惯使用。
设计模式博大精深,而这篇笔记怕还不能揭示其中的一鳞半爪,然作为我个人的学习记录已经足够。
如若在将来能起到帮助它人理解之效,那便是倍感欣慰,念及此虽并无关联,而今天我便可放心的搁下键盘。

评论

This is just a placeholder img.