Chapter 8. Refactoring Controllers

In my experience over multiple enterprise Angular applications, controllers are constantly the biggest source of trouble in Angular applications. Controllers tend to be given far too many responsibilities while simultaneously resisting reuse. The use of inherited scope makes it difficult to trace the origin of methods and properties accessed from a child scope. Most of the work in refactoring Angular applications comes from moving code out of controllers and into pieces that are easier to work with.

The refactorings in this section are variations on relocating application features from controllers into components better suited for maintenance and reuse. Moving features out of controllers is higher-risk refactoring because controllers by their nature resist reusability and foster code that is difficult to reach through testing.

How can we refactor these complicated components? The answer lies in the techniques we’ve already discussed: unit testing, style guide usage, separating concerns into their own modules and files, and moving logic out of view templates and into JavaScript. Moving code out of controllers is a demanding task that requires a developer’s full attention. It’s work that needs to be done slowly and deliberately.

Controllers Contain the Highest-Risk Code

Controllers in Angular have limited means of sharing functionality, lack a useful set of lifecycle hooks, and and are not singletons. Despite these weaknesses it’s typical to find critical application features implemented wholly within controllers, often tightly coupled to Angular view templates. The Angular Developer Guide lists two recommended uses for controllers: to set initial controller state on $scope and to add behavior to the $scope object. Unfortunately it’s extremely common to find Angular tutorials invoking more complicated work from within controllers, a pattern novice Angular developers readily adopt.

It’s a struggle to reuse code within a controller because it lacks a useful way to expose a public application interface. Controller methods can only be accessed by a view or by a child controller. Controllers can’t be directly injected for use by other Angular components (they can only be accessed through the $controller service, which in most cases is bad practice). This leads to one of two results: either copying and pasting from one controller to another, or through defining methods on parent scopes (like $rootScope), which leads to confusing application design.

Controllers lack built-in support for responding to changes in the controler’s lifecycle. This means there’s no standard practice for initializing controllers or for reacting to the creation or destruction of parent or child $scopes. John Papa’s Angular Style Guide (style Y080) recommends declaring a function called activate() containing a controller’s startup logic and manually invoking it following the controller’s variable declarations.

Controllers are not singletons (functions that only have one instance within the application) and therefore should not be used for maintaining state. Any state information for an application should be retrieved and stored using Angular services.

If the code being refactored is backed with unit tests in Karma or integration tests in Protractor, you’ll be a step ahead. Run the tests after each step in refactoring to ensure nothing unexpected breaks. You may see tests break, especially when changing the name of a method exposed through an interface. A good IDE has refactoring tools to help you find these cases both in your application code and your tests. If one of your tests breaks, take the time to find the conflict and fix it immediately while the whole domain is clear in your mind.

If you don’t have unit tests, this is a good time to begin the practice. Unit testing controllers is difficult, but the goal of refactoring controllers is to move reusable code into modular, testable units. Test-driven development while refactoring will help guide you toward writing code that makes sense when used as standalone modules.

Refactoring a Controller

The remainder of this chapter walks through refactoring a controller by taking multiple passes through it. Since controllers can be difficult to work with, the first step is to focus on making the code within the controller easier to understand.

First Pass

To begin, start with simple refactorings within the controller. The purpose of this step is to make the controller easier to reason about. Controllers that handle multiple responsibilities are difficult to read, especially when properties and methods are strewn throughout the the source. Refactoring the controller first will ease all subsequent work.

Limit your efforts to work that is being done solely within the controller. Rename properties and methods to better reflect what your code is doing (remember to update the view template to reflect the new names). Add comments to methods bound to the controller or to private functions with business logic as these will be targets for modularization later on. Investigate suspiciously long functions (long is relative, but more than 10 lines is a call to investigate) and functions that accept a long list of parameters (three or more should draw your attention). Simplify these functions by extracting work into smaller private functions that only have one responsibility.

Second Pass

Up to this point we’ve limited work to code within the controller while updating the view template only if we’ve changed a method or property name on the controller. In this pass, concentrate attention on the view template. There are two goals in this pass. The first is moving logic from the template into the controller. The second is to identify groups of HTML that can be refactored into directives.

Logic in the template is difficult to test, reason about, and maintain. Initialization of controller properties should not be done in the view template (avoid ngInit; read the Angular documentation for more information). Complex presentation logic (e.g., Angular expressions that compare the values of controller methods) should be moved into a method on the controller.

While refactoring the view template, begin identifying targets for directives. Look for blocks of HTML that are repeated and incorporate methods or model bindings from the controller. Look especially for examples where the feature should be separated from the purpose of the controller.

Look for HTML and business logic that can be separated into a directive. If a view is displaying some data or enabling behaviors on a model bound to the view, the model itself is an input for a potential directive. Also consider what new services may be needed to support persistence or component communication.

For example, a view may display a logout button in a controller designed to display a list of movie titles. The work entails an application feature (logging out), some related HTML (the button and label), and a service (user) that can be represented wholly as a directive.

After refactoring your view template, review your controller again using the same process as the first pass. Are the controller’s method and property names still clear? Are there new methods requiring refactoring? In the next pass we will start modularizing the controller.

Third Pass

By now the controller and view template have been subject to close scrutiny, but no code has been moved outside the controller. There’s plenty of value in the work done in the first two passes, but this is the stage where reusable features should be pulled out of the controller and placed into their own files. This work will lead to the creation of new services, filters, and directives. If a feature is general enough we may even want to move it to its own module and re-import it as a dependency into the application.

Which features should be modularized first? Look at the responsibilities of your controller: what work is it doing that is least like the main purpose of the controller? Any work not related to the controller can be moved into its own component. Business or presentation logic related to the controller should also be moved into a service where it is more testable and reusable. In the end the controller should only be facilitating work between the view and a model.

What if there are only one or two methods on the controller? They could be moved into their own service, but they might also be fine left in the controller if the logic within is not being used anywhere else in the application. Remember, refactoring is about making code easier to understand. Let principles guide you, not control you.

Summary

In this section we covered:

  • Why controllers are high risk and why they should be refactored

  • Moving our application logic out of controllers and into components that are more easily tested and reused

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.147.59.219