未加星标

4 Keys to a Clean Angular Implementation

字体大小 | |
[前端(javascript) 所属分类 前端(javascript) | 发布者 店小二03 | 时间 2016 | 作者 红领巾 ] 0人收藏点击收藏

I have recently been thinking about fundamental best practices for separation of concerns in Angular. Separation of concerns is not a new topic in software development, by any means. It certainly goes back farther, but let’s “only” go back to 2004 to start honing in on a useful perspective for modern web development. That year saw the publication of Terence Parr’s seminal paper, Enforcing Strict Model-View Separation in Template Engines , published in ACM’s Proceedings of the 13th International World Wide Web Conference . As far as web technologies, so much has changed in those intervening 12 years, yet much of what Parr reflects upon is still useful today. But you could discern that just from the title. We still use the model-view-controller (MVC) paradigm, and its variants, as a matter of course to develop cleaner code today. And we certainly use template engines, Angular being among the most popular today.

Why a Template Engine?

In the earliest days of the web, web pages were static. A web page existed in a constant form, as a file on a disk, and was served up, byte for byte identical each time to every user. Very quickly, though, developers realized dynamic web pages were necessary in order to do anything useful (other than publishing a book online, perhaps). The earliest dynamic web pages were generated manually by server side code, notably by Perl and later by Java. But the task of generating HTML by writing raw strings is terribly tedious, time-consuming, and error-prone. So along came the notion of a template engine . The idea was that you, the developer, provide an HTML file (a template ) that contains place holders to be filled in at runtime by the template engine. Even with a very simple template language you could allow the user to fill in anything from individual values to complex tables. The template engine marries the template written in your template language with the necessary data to produce a finished web page that is sent to the client browser for rendering. Template engines provided a useful way, then, to separate your data from the presentation of that data.

Why Separate Concerns?

It has been well established for quite a long time that separation of concerns in software design is a good thing. Parr’s paper itemizes the key reasons, shown below with my commentary, though you have likely seen variations of this list before:

Encapsulation : the view is in the template; the business logic is in the model / controller. Clarity : A template is written in HTML (with the slight addition of some notation for place holders) rather than code, so it is a presentation-layer document rather than “source code” per se. And depending upon your choice of editor, you might even work with the template in a WYSIWYG designer. Division of labor: designers who are good at user interfaces (user experience, usability, and so forth) could create templates, while developers who are good at coding could write the code. Component reuse : a template could include other templates, so you could re-use discrete pieces (e.g. for navigation bars, search boxes, etc.) Single point of change/maintenance/interchangeable views: When you want to change how data is displayed or manipulated at the UI level―without changing any underlying functionality―you only need to edit the template.

Actually, all of these advantages are really quite interrelated, suggesting that it is more productive to compartmentalize the view or UI layer just as you compartmentalize all other aspects of your software design. Parr’s final consideration in this list is security, though I’m not convinced that separation of concerns here leads to improved security; there are still plenty of things you can do to weaken your defenses!

Rules of Separation

Parr suggests that a good template engine enforces strict separation of the model and the view. Specifically, the view should not :

modify the model perform computations upon dependent data (e.g. $price * .90) compare dependent data (e.g. $bloodPressure < 120 ) make data type assumptions get any layout information from the data

At the time he wrote his paper, Parr found that, among the dozen or so template engines he considered, the entanglement index the number of rules that a template engine violates is either 1 or 5. Two interesting observations here. First, it is not possible to have an index of 0, because rule 5 cannot be enforced (e.g. is a value of “Red” a man’s name or a color to render?). Second, there was no middle ground (template engines ranking 2, 3, or 4); apparently once you open the barn door, so to speak, everything floods in! Parr goes on to suggest that enforcement of separation (by the template engine) is much preferred to encouragement of separation (i.e. by convention).

Well, Maybe Suggestions of Separation

And here is where I feel that current web standards require a different approach. First, I agree in principle that enforcement is better than encouragement. For example, I would not want my compiler to suggest that I should fix compiler errors―I want the compiler to fail if there are serious errors. But there are a plethora of other processors in a build sequence that emit red flags yet do not cause an aborted compile: ReSharper warnings, JSHint warnings, CSS warnings, even code warnings (as opposed to errors) from the compiler itself. In other words, encouragement is not unprecedented and is, in fact, widespread.

Besides those machine-recognizable issues, which could be enforced, there are a variety of code issues that can only be dealt with by encouragement. Here I’m speaking of generally accepted best practices as well as code conventions of your specific team and/or company; which brings us squarely back to web development and more specifically to the framework I want to focus on here, Angular. Angular is a template engine and its entanglement index is 5. It is so powerful and flexible, you can do anything you darn well please. But that does not mean you should !

The remainder of this article, then, provides my recommended and encouraged best practices for Angular and specifically focuses on accessing the model in the view with respect to Parr’s first three rules.

On modifying the model in the view

Say you have a button to switch your web app to edit mode:

<button ng-click="EditMode = true">Edit</button>

Elsewhere in your template you react to that variable―here we hide the ‘ delete ’ button if we’re in edit mode:

<a ng-click="Delete()" ng-hide="EditMode">Delete</a>

Of course, we’ll need a ‘ cancel ’ button that reveals itself during edit mode, and upon being clicked, turns off edit mode.

<button ng-show="EditMode" ng-click="EditMode = false">Cancel</button>

That seems clean and simple. Indeed, some might argue it is preferable to manipulate the model (EditMode) in the view because it is all self-contained in the view. While that is an advantage, it is outweighed by the disadvantage of (a) having less testable code (because you do not really have any way to test that bit of code in the view), and (b) violating rule 1 (modifying the model in the view), which weakens separation of concerns.

Furthermore, we need one more button to save the work done in edit mode.

< button ng-show =” EditMode “ ng-click =” EditMode = false; SaveItem () “> Save </ button >

Whew, did you catch that foul odor? Quite a code smell there―putting multiple javascript statements in an attribute! Yes, doing that allowed maintaining the EditMode model data completely in the view, but that’s not really such a good thing after all, as just described above. I would suggest that button should contain just a single method call upon click:

< button ng-show =” EditMode “ ng-click =” SaveItem () “> Save </ button >

And then the code-behind function should be, in part:

$scope.SaveItem = function () {
$scope.EditMode = false;
// other code to save item here...
}

Similarly, I would advocate that the button to enter edit mode should be changed to eliminate direct manipulation of the model in the view as well:

<button ng-show="EditMode" ng-click="BeginEditMode()">Cancel</button>

With the corresponding code in the controller:

$scope.BeginEditMode = function () {
$scope.EditMode = true;
}

So, by using methods to delegate model manipulations to the controller, we have eliminated any direct manipulation of the model in the view. This means you can then write unit tests for your controller code. Moreover, with this separation you have now encapsulated your business logic in the controller, leaving the developers free to rename the EditMode variable should they decide on a better name down the road. Or, should the need arise, they could change what it means to enter and leave edit mode without necessarily needing any changes in the view.

Note, though, that Parr’s rule 1 regarding no model manipulation in the view applied to direct or indirect manipulation; he considered that it was just as bad to call controller methods to manipulate the model. At that time, any such method calls were in the global namespace so, yes, that would be reasonably bad. But with Angular, you have much more fine-grained control, calling methods on a specific controller, so encapsulation saves the day here, in my opinion. Like any good library API, your controller should expose just sufficient methods to the view for the view to be effective and accomplish what it needs to do.

So I’m suggesting the following best practice:

Guideline #1: Do not directly modify the model in the view;

use functions on the controller

On accessing dependent data in the view

Consider this Angular fragment:

<span ng-if="ContentType == 'Simple'">

That’s perfectly valid, yet quite ill-advised. Because Angular lets you use any JavaScript that you please, it is impossible to prevent this type of construct; we must rely on encouraging your offending colleague, perhaps adding some teaching and maybe some cajoling. (Oh, unless you wrote that fragment in the view…).

So why is this bad? First, the dependent data―the string “Simple” in this case―is part of your business domain. It has no business (pun intended!) being here in the view. What if your marketing guy decides to dress up the product a bit and change the “Simple” designation to “Snazzy”? Then you have to modify both the code and the view! That’s bad; you should only have to modify the code (your store of product types) somewhere on the backend.

Second, what if the product owner wants to adapt the product to a new potential client, and needs that span to be revealed for two content types? Again, you have to modify the view for a change in business logic! If instead the span was defined like this:

<span ng-if="IsSelected(ContentType)">

Then you could manage the controller code with a function like that shown next, where the ContentTypeList could contain a single element, as the current version of your product uses, or could contain two elements for the new prospective client:

$scope.IsSelected = function (ContentType) {
return ContentTypeList.indexOf(ContentType) >= 0;
}

The above showed that a magic string in the view made it quite brittle. Similarly, you do not want to use computation operators in the view with magic values . Example to show the calculated value of a 6% tax to the user:

<span>Tax: {{Price * .06}}</span>

A hard-coded tax rate has no business being in the view. It might change for many reasons: your company relocates, your municipality changes its tax rate, you expand to online operations which uses its own peculiar tax rules, etc.

Thus, whether it is a comparison with dependent data (Parr’s rule 3) or a computation with dependent data (Parr’s rule 2), the idea is the same, hence my lumping the two rules together.

Guideline #2: Avoid magic numbers, magic strings, etc. especially in the view.

On using logical expressions in the view

Here is a typical Angular expression to dynamically set the class name of an element based on several criteria. This sets the class attribute to include “mid-range” if one property is not null or another not empty:

ng-class="{ 'mid-range':Item.LabelOverride !== null || Item.Description !== '' }"

At first glance, that is reasonable-looking enough. But if you consider for a moment, you’ll see that each term of the expression is using a comparison operator, so each by itself would, by our previous guideline, be less than ideal. So let’s use functions instead; is this better?

ng-class="{ 'mid-range':OverrideIsNotNull(Item) || DescriptionIsNotEmpty(Item) }"

Well, that follows guideline #2 by the letter of the law, but not quite the spirit. Those functions are still tied very closely to implementation details rather than usage, so still a bit of a code smell. Something more like this abstracts away those implementation details:

ng-class="{ 'mid-range':IsOverrideValid(Item) || IsDescriptionValid(Item) }"

It defers the interpretation of valid to the code-behind, again for similar reasons as those discussed previously. But then there are still two terms connected with a disjunction. It probably will not surprise you that I claim that such logical operators, too, should be avoided. Using this instead:

ng-class="{ 'mid-range':IsValid(Item) }"

…allows the backend the flexibility to update the single meaning of valid as business needs evolve.

Furthermore, the more terms that are joined with logical operators, the more foul is the ‘code smell’. I have seen instances with many terms in the expression, here represented symbolically:

( (A || B) && (C || D) && (E || F) && (G || H) )

The pure wave of foul code stench from that expression used in the view should reach you even through your internet connection!

It is important to note what I am not saying here as well. If you carry this argument farther, you could say that the view should never reference properties on the scope except for display purposes. However, I think that this is going past the point of diminishing returns: That is, instead of this property access:

<span ng-if="WorkIsComplete">

…you could use a function on the scope:

<span ng-if="WorkIsCompleteFunction()">

…that supplies that self-same property like this:

$scope. WorkIsCompleteFunction = function () {
return WorkIsComplete;
}

But here, the advantage of single property encapsulation is outweighed by the disadvantage of the extra overhead and extra code in this instance.

Guideline #3: Eschew operators of any kind (arithmetic, comparison, logical) in the view; use functions on the controller.

On tailoring functions for the view

Here is a loop in Angular that generates a table with the HTML whittled down to show just the relevant bits: each row will have a button, to perform some type of optimization, that requires four parameters.

<div ng-repeat="usage in response.Usages">
<tr>
<td>
< button ng-click="Optimize(usage,NumOps,Colors,Score)">Optimize</button>
</td>
</tr>
</div>

There are no magic strings, there are no operators of any kind; it uses properties in the scope (NumOps, Colors, and Score) but does not do any operations on them in the view. Everything is in a function so that the function can be unit-tested thoroughly in the code-behind… but since we’re here talking about it, what is wrong with it?

The short answer: too many parameters . You will hear a typical guideline for functions or methods that they should have no more than seven parameters, or perhaps no more than four… I submit that for a function exposed to the view (i.e. defined on the scope), it should be exactly one parameter if you are in a loop (ng-repeat) or zero parameters otherwise!

Your immediate reaction might be that that is totally impractical. But remember the context―the Angular scope. The code-behind in the controller already has encapsulated access to all those scope properties (NumOps, Colors, and Score) without the view having to explicitly pass them in, or pass them back , to be more accurate. The only thing the controller does not have is a notion of which item in the loop is current so you need to provide that. Thus, the above code fragment should just be this:

<div ng-repeat="usage in response.Usages">
<tr>
<td>
<button ng-click="Optimize(usage)">Optimize</button>
</td>
</tr>
</div>

Guideline #4: Be stingy with adding arguments to a function exposed to the view.

Summary

With the level of interactivity in a modern web page, it is unrealistic to achieve complete separation using the MVC paradigm; and, indeed, it is crucial for the view to be able to manipulate the model, but only via a carefully crafted API that is the “public” scope of the Angular controller. More important, though, is to perform all business logic in the code-behind as directed by the controller or its delegate. The view should only use exposed model properties for either display or standalone logic (i.e. a single property with no operators). All operations on the model (assignments, computations, comparisons, etc.) should be done in the controller:

Modify all model properties in the controller rather than the view; let the view have access to the capability via controller functions. Keep all magic values (if you must have them at all) in the controller accessing them in the view via controller functions. Evaluate all comparison, computation, and logical expressions in the controller, giving the view access to them via controller functions. Pass to a controller function only what it does not already know: the loop parameter if in a loop, or no parameters at all if outside of a loop.

本文前端(javascript)相关术语:javascript是什么意思 javascript下载 javascript权威指南 javascript基础教程 javascript 正则表达式 javascript设计模式 javascript高级程序设计 精通javascript javascript教程

主题: HTMLJavaJavaScriptCSSPerl
分页:12
转载请注明
本文标题:4 Keys to a Clean Angular Implementation
本站链接:http://www.codesec.net/view/479862.html
分享请点击:


1.凡CodeSecTeam转载的文章,均出自其它媒体或其他官网介绍,目的在于传递更多的信息,并不代表本站赞同其观点和其真实性负责;
2.转载的文章仅代表原创作者观点,与本站无关。其原创性以及文中陈述文字和内容未经本站证实,本站对该文以及其中全部或者部分内容、文字的真实性、完整性、及时性,不作出任何保证或承若;
3.如本站转载稿涉及版权等问题,请作者及时联系本站,我们会及时处理。
登录后可拥有收藏文章、关注作者等权限...
技术大类 技术大类 | 前端(javascript) | 评论(0) | 阅读(56)