Preface
What we are going to explain in this chapter is the second part of JavaScript language implementation of the five principles of S.O.L.I.D, the Open/Closed Principle OCP (The Open/Closed Principle).
The description of the opening and closing principle is:
Software entities (classes, modules, functions, etc.) should be open for extension but closed for modification.
Software entities (classes, modules, methods, etc.) should be Open for extension and closed for modification, that is, the software entity should be extended without modification.
Copy code
Open for extension means that when new needs arise, the existing model can be extended to achieve the goal. Close for modification means that no modification is allowed to the entity. To put it bluntly, these entities that need to perform various behaviors should be designed to achieve various changes without modification, and insist on opening and closing. The principle facilitates project maintenance with minimal code.
English original text: http://freshbrewedcode.com/derekgreer/2011/12/19/solid-javascript-the-openclosed-principle/
Problem code
To describe it intuitively, let’s give an example To demonstrate, the subordinate code is the code that dynamically displays the question list (the opening and closing principle is not used).
// Question type
var AnswerType = {
Choice: 0,
Input: 1
};
// Question entity
function question(label, answerType, choices) {
return {
label: label,
answerType: answerType,
choices: choices // choices here are optional parameters
};
}
var view = (function () {
// render a question
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';
// Display different codes according to different types: drop-down menus and input boxes respectively
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 {
// Traverse all question lists for display
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);
In the above code, the view object contains a render method. To display the question list, different display methods are used according to different question types. A question contains a label, a question type and choices options (if it is a selection type). If the question type is Choice, generate a drop-down menu based on the options. If the question type is Input, simply display the input input box.
This code has a limitation, that is, if you add another question type, you need to modify the conditional statement in renderQuestion again, which obviously violates the opening and closing principle.
Refactor the code
Let us refactor this code to allow extending the render capabilities of the view object when new question types appear, without modifying the code inside the view object.
First create a general questionCreator function:
function questionCreator(spec, my) {
var that = {};
my = my || {};
my.label = spec.label;
my. renderInput = function () {
throw "not implemented";
// Here renderInput is not implemented, the main purpose is to let the implementation code of the respective problem type cover the entire method
};
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();
// The render method is the same rough and reasonable code
// The only difference is the above sentence my.renderInput()
// Because different question types have different implementations
questionWrapper.appendChild(questionLabel );
questionWrapper.appendChild(answer);
return questionWrapper;
};
return that;
}
The function combination of this code is render one question, while providing an unimplemented renderInput method so that other functions can override it to use different question types. Let's continue to look at the implementation code of each question type:
function choiceQuestionCreator(spec) {
var my = {},
that = questionCreator(spec, my);
//RenderInput implementation of choice type
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);
//RenderInput implementation of input type
my.renderInput = function () {
var input = document.createElement('input');
input.type = 'text';
return input;
};
return that;
}
The choiceQuestionCreator function and the inputQuestionCreator function correspond to the renderInput implementation of the drop-down menu and input box respectively. They call the unified questionCreator(spec, my) internally and then return the that object (the same type).
The code of the view object is very fixed.
var view = {
render: function(target , questions) {
for (var i = 0; i < questions.length; i ) {
target.appendChild(questions[i].render());
}
}
};
So when we declare a problem, we only need to do this, and it will be 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?'
})
];
The final usage code, we can use it like this:
var questionRegion = document.getElementById('questions');
view.render(questionRegion, questions);
Final code after refactoring
function questionCreator(spec, my) {
var that = {};
my = my || {};
my.label = spec.label;
my.renderInput = function() {
throw "not implemented";
};
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();
questionWrapper.appendChild(questionLabel);
questionWrapper.appendChild(answer);
return questionWrapper;
};
return that;
}
function choiceQuestionCreator(spec) {
var my = {},
that = questionCreator(spec, my);
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);
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({
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);
上面的代码里应用了一些技术点,我们来逐一看一下:
首先,questionCreator方法的创建,可以让我们使用模板方法模式将处理问题的功能delegat给针对每个问题类型的扩展代码renderInput上。
其次,我们用一个私有的spec属性替换掉了前面question方法的构造函数属性,因为我们封装了render行为进行操作,不再需要把这些属性暴露给外部代码了。
第三,我们为每个问题类型创建一个对象进行各自的代码实现,但每个实现里都必须包含renderInput方法以便覆盖questionCreator方法里的renderInput代码,这就是我们常说的策略模式。
通过重构,我们可以去除不必要的问题类型的枚举AnswerType,而且可以让choices作为choiceQuestionCreator函数的必选参数(之前的版本是一个可选参数)。
总结
重构以后的版本的view对象可以很清晰地进行新的扩展了,为不同的问题类型扩展新的对象,然后声明questions集合的时候再里面指定类型就行了,view对象本身不再修改任何改变,从而达到了开闭原则的要求。
另:懂C#的话,不知道看了上面的代码后是否和多态的实现有些类似?其实上述的代码用原型也是可以实现的,大家可以自行研究一下。