Javascript开闭原则

开闭原则的核心是:对扩展开放,对修改关闭

白话意思就是我们改变一个软件时(比如扩展其他功能),应该通过扩展的方式来达到软件的改变,而不应爱修改原有代码来实现变化

说白了,就是这些需要执行多样行为的实体应该设计成不需要修改就可以实现各种的变化,坚持开闭原则有利于用最少的代码进行项目维护。

下面是使用开闭原则的一个简单示例

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Document</title>
</head>
<body>
<div id="test"></div>
</body>
<script type="text/javascript">

//一个面向对象的JS例子,很好的支持了开闭原则

function HtmlControl(options) { //定义一个方法

    var el = options.element;  //el赋值为dom元素
    //下面是为el元素添加参数样式
    el.style.width = options.width;

    el.style.height = options.height;

    el.style.top = options.top;

    el.style.background = options.background;

    el.innerHTML=options.text;
    console.log(el)

}



var option = { //为方法定义一个参数对象

    element: document.getElementById('test'),

    left: 50,

    top: 0,

    width: 100,

    height: 200,

    background: '#ccc',

    text:'什么是开闭原则'

}

option.background = 'red'; //对参数对象进行扩展



HtmlControl(option); //调用

</script>
</html>

这是一个简单的例子,意思就是说封装一个功能,这个功能有默认参数,对外提供接口,接口参数可扩展改变,但是需要修改功能是不可以的,也就是说在功能接口内扩展修改该功能,就可以得到不同的效果。

为了直观地描述,我们来举个例子演示一下,下属代码是动态展示question列表的代码(没有使用开闭原则)。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Document</title>
</head>
<body>
<div id="questions"></div>
</body>
<script type="text/javascript">
// 问题类型
var AnswerType = {
    Choice: 0,
    Input: 1
};

// 问题实体
function question(label, answerType, choices) {
    return {
        label: label,
        answerType: answerType,
        choices: choices // 这里的choices是可选参数
    };
}

var view = (function() {
    // render一个问题
    function renderQuestion(target, question) {
        var questionWrapper = document.createElement('div');
        questionWrapper.className = 'question';

        var questionLabel = document.createElement('div');
        questionLabel.className = 'question-label';
        var label = document.createTextNode(question.label);//创建文本节点
        questionLabel.appendChild(label);//在指定节点的最后一个子节点列表之后添加一个新的子节点。

        var answer = document.createElement('div');
        answer.className = 'question-input';

        // 根据不同的类型展示不同的代码:分别是下拉菜单和输入框两种
        if (question.answerType === AnswerType.Choice) {
            var input = document.createElement('select');
            var len = question.choices.length;
            for (var i = 0; i < len; i++) {
                var option = document.createElement('option');
                option.text = question.choices[i];
                option.value = question.choices[i];
                input.appendChild(option);
            }
        } else if (question.answerType === AnswerType.Input) {
            var input = document.createElement('input');
            input.type = 'text';
        }

        answer.appendChild(input);
        questionWrapper.appendChild(questionLabel);
        questionWrapper.appendChild(answer);
        target.appendChild(questionWrapper);
    }

    return {
        // 遍历所有的问题列表进行展示
        render: function(target, questions) {
            for (var i = 0; i < questions.length; i++) {
                renderQuestion(target, questions[i]);
            };
        }
    };
})();

var questions = [
    question('Have you used tobacco products within the last 30 days?', AnswerType.Choice, ['Yes', 'No']),
    question('What medications are you currently using?', AnswerType.Input)
];

var questionRegion = document.getElementById('questions');
view.render(questionRegion, questions);

</script>
</html>

实现效果

image.png

上面的代码,view对象里包含一个render方法用来展示question列表,展示的时候根据不同的question类型使用不同的展示方式,一个question包含一个label和一个问题类型以及choices的选项(如果是选择类型的话)。如果问题类型是Choice那就根据选项生产一个下拉菜单,如果类型是Input,那就简单地展示input输入框。

该代码有一个限制,就是如果再增加一个question类型的话,那就需要再次修改renderQuestion里的条件语句,这明显违反了开闭原则。

重构代码

让我们来重构一下这个代码,以便在出现新question类型的情况下允许扩展view对象的render能力,而不需要修改view对象内部的代码。

先来创建一个通用的questionCreator函数:

function questionCreator(spec, my) {
    var that = {};
 
    my = my || {};
    my.label = spec.label;
 
    my.renderInput = function () {
        throw not implemented; 
        // 这里renderInput没有实现,主要目的是让各自问题类型的实现代码去覆盖整个方法
    };
 
    that.render = function (target) {
        var questionWrapper = document.createElement('div');
        questionWrapper.className = 'question';
 
        var questionLabel = document.createElement('div');
        questionLabel.className = 'question-label';
        var label = document.createTextNode(spec.label);
        questionLabel.appendChild(label);
 
        var answer = my.renderInput();
        // 该render方法是同样的粗合理代码
        // 唯一的不同就是上面的一句my.renderInput()
        // 因为不同的问题类型有不同的实现
 
        questionWrapper.appendChild(questionLabel);
        questionWrapper.appendChild(answer);
        return questionWrapper;
    };
 
    return that;
}

该代码的作用组合要是render一个问题,同时提供一个未实现的renderInput方法以便其他function可以覆盖,以使用不同的问题类型,我们继续看一下每个问题类型的实现代码:

function choiceQuestionCreator(spec) {
 
    var my = {},
that = questionCreator(spec, my);
             
    // choice类型的renderInput实现
    my.renderInput = function () {
        var input = document.createElement('select');
        var len = spec.choices.length;
        for (var i = 0; i < len; i++) {
            var option = document.createElement('option');
            option.text = spec.choices[i];
            option.value = spec.choices[i];
            input.appendChild(option);
        }
 
        return input;
    };
 
    return that;
}
 
function inputQuestionCreator(spec) {
 
    var my = {},
that = questionCreator(spec, my);
 
    // input类型的renderInput实现
    my.renderInput = function () {
        var input = document.createElement('input');
        input.type = 'text';
        return input;
    };
 
    return that;
}

choiceQuestionCreator函数和inputQuestionCreator函数分别对应下拉菜单和input输入框的renderInput实现,通过内部调用统一的questionCreator(spec, my)然后返回that对象(同一类型哦)。

view对象的代码就很固定了。


var view = {
    render: function(target, questions) {
        for (var i = 0; i < questions.length; i++) {
            target.appendChild(questions[i].render());
        }
    }
};

所以我们声明问题的时候只需要这样做,就OK了:


var questions = [
    choiceQuestionCreator({
    label: 'Have you used tobacco products within the last 30 days?',
    choices: ['Yes', 'No']
  }),
    inputQuestionCreator({
    label: 'What medications are you currently using?'
  })
    ];

最终的使用代码,我们可以这样来用:


var questionRegion = document.getElementById('questions');
 
view.render(questionRegion, questions);

完整代码

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>JS开闭原则</title>
</head>
<body>
<div id="questions"></div>
</body>
<script type="text/javascript">
function questionCreator(spec, my) {

    var that = {};
    
    my = my || {};
    my.label = spec.label;

    my.renderInput = function() {

        // 1这里renderInput没有实现,主要目的是让各自问题类型的实现代码去覆盖整个方法
    };
    that.render = function(target) {
        var questionWrapper = document.createElement('div');
        questionWrapper.className = 'question';

        var questionLabel = document.createElement('div');
        questionLabel.className = 'question-label';
        var label = document.createTextNode(spec.label);
        questionLabel.appendChild(label);

        var answer = my.renderInput();
        console.log(answer)
        // 该render方法是同样的粗合理代码
        // 唯一的不同就是上面的一句my.renderInput()
        // 因为不同的问题类型有不同的实现

        questionWrapper.appendChild(questionLabel);
        questionWrapper.appendChild(answer);
        return questionWrapper;
    };

    return that;
}



function choiceQuestionCreator(spec) {
    var my = {};

    var that = questionCreator(spec, my);

    // choice类型的renderInput实现
    my.renderInput = function() {
        var input = document.createElement('select');
        var len = spec.choices.length;
        for (var i = 0; i < len; i++) {
            var option = document.createElement('option');
            option.text = spec.choices[i];
            option.value = spec.choices[i];
            input.appendChild(option);
        }

        return input;
    };
    //返回一个方法
    return that;
}

function inputQuestionCreator(spec) {

    var my = {},
        that = questionCreator(spec, my);

    // input类型的renderInput实现
    my.renderInput = function() {
        var input = document.createElement('input');
        input.type = 'text';
        return input;
    };

    return that;
}

var view = {
    render: function(target, questions) {
        for (var i = 0; i < questions.length; i++) {
            target.appendChild(questions[i].render());
        }
    }
};

var questions = [
    choiceQuestionCreator({//运行choiceQuestionCreator函数
        label: 'Have you used tobacco products within the last 30 days?',
        choices: ['Yes', 'No']  
    }),
    inputQuestionCreator({
        label: 'What medications are you currently using?'  
    })
];

var questionRegion = document.getElementById('questions');

view.render(questionRegion, questions);
</script>
</html>

重构后的最终代码

上面的代码里应用了一些技术点,我们来逐一看一下:

首先,questionCreator方法的创建,可以让我们使用模板方法模式将处理问题的功能delegat给针对每个问题类型的扩展代码renderInput上。

其次,我们用一个私有的spec属性替换掉了前面question方法的构造函数属性,因为我们封装了render行为进行操作,不再需要把这些属性暴露给外部代码了。

第三,我们为每个问题类型创建一个对象进行各自的代码实现,但每个实现里都必须包含renderInput方法以便覆盖questionCreator方法里的renderInput代码,这就是我们常说的策略模式。

通过重构,我们可以去除不必要的问题类型的枚举AnswerType,而且可以让choices作为choiceQuestionCreator函数的必选参数(之前的版本是一个可选参数)。

总结

重构以后的版本的view对象可以很清晰地进行新的扩展了,为不同的问题类型扩展新的对象,然后声明questions集合的时候再里面指定类型就行了,view对象本身不再修改任何改变,从而达到了开闭原则的要求。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 160,026评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,655评论 1 296
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,726评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,204评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,558评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,731评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,944评论 2 314
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,698评论 0 203
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,438评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,633评论 2 247
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,125评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,444评论 3 255
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,137评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,103评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,888评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,772评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,669评论 2 271

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 170,575评论 25 707
  • 本文出自《Android源码设计模式解析与实战》中的第一章。 1、优化代码的第一步——单一职责原则 单一职责原则的...
    MrSimp1e0阅读 1,710评论 1 13
  • 设计模式之六大原则(转载) 关于设计模式的六大设计原则的资料网上很多...
    霄霄霄霄阅读 874评论 0 1
  • 一座小城, 藏着童年的记忆, 那是我心灵的花苞出土的地方, 天然的,浓浓的纯净, 现在,小城愈加洁净, 洁净地很难...
    童淑阅读 331评论 2 2
  • 一个人不能没有梦想,我的梦想是成为天使守护你...... 一个人不能没有理想,我的理想是掌握天下所有技能为你服务....
    babybus_hentai阅读 186评论 0 0