Simpler Design

In addition to OLOO providing ostensibly simpler (and more flexible!) code, behavior delegation as a pattern can actually lead to simpler code architecture. Let’s examine one last example that illustrates how OLOO simplifies your overall design.

The scenario we’ll examine is two controller objects, one for handling the login form of a web page, and another for actually handling the authentication (communication) with the server.

We’ll need a utility helper for making the Ajax communication to the server. We’ll use jQuery (though any framework would do fine), since it handles not only the Ajax for us, but it returns a promise-like answer so that we can listen for the response in our calling code with .then(..).

Note: We don’t cover Promises here, but we will cover them in a future title of the “You Don’t Know JS” series.

Following the typical class design pattern, we’ll break up the task into base functionality in a class called Controller, and then we’ll derive two child classes, LoginController and AuthController, which both inherit from Controller and specialize some of those base behaviors.

  1. // Parent class
  2. function Controller() {
  3. this.errors = [];
  4. }
  5. Controller.prototype.showDialog = function(title,msg) {
  6. // display title & message to user in dialog
  7. };
  8. Controller.prototype.success = function(msg) {
  9. this.showDialog( "Success", msg );
  10. };
  11. Controller.prototype.failure = function(err) {
  12. this.errors.push( err );
  13. this.showDialog( "Error", err );
  14. };
  1. // Child class
  2. function LoginController() {
  3. Controller.call( this );
  4. }
  5. // Link child class to parent
  6. LoginController.prototype = Object.create( Controller.prototype );
  7. LoginController.prototype.getUser = function() {
  8. return document.getElementById( "login_username" ).value;
  9. };
  10. LoginController.prototype.getPassword = function() {
  11. return document.getElementById( "login_password" ).value;
  12. };
  13. LoginController.prototype.validateEntry = function(user,pw) {
  14. user = user || this.getUser();
  15. pw = pw || this.getPassword();
  16. if (!(user && pw)) {
  17. return this.failure( "Please enter a username & password!" );
  18. }
  19. else if (pw.length < 5) {
  20. return this.failure( "Password must be 5+ characters!" );
  21. }
  22. // got here? validated!
  23. return true;
  24. };
  25. // Override to extend base `failure()`
  26. LoginController.prototype.failure = function(err) {
  27. // "super" call
  28. Controller.prototype.failure.call( this, "Login invalid: " + err );
  29. };
  1. // Child class
  2. function AuthController(login) {
  3. Controller.call( this );
  4. // in addition to inheritance, we also need composition
  5. this.login = login;
  6. }
  7. // Link child class to parent
  8. AuthController.prototype = Object.create( Controller.prototype );
  9. AuthController.prototype.server = function(url,data) {
  10. return $.ajax( {
  11. url: url,
  12. data: data
  13. } );
  14. };
  15. AuthController.prototype.checkAuth = function() {
  16. var user = this.login.getUser();
  17. var pw = this.login.getPassword();
  18. if (this.login.validateEntry( user, pw )) {
  19. this.server( "/check-auth",{
  20. user: user,
  21. pw: pw
  22. } )
  23. .then( this.success.bind( this ) )
  24. .fail( this.failure.bind( this ) );
  25. }
  26. };
  27. // Override to extend base `success()`
  28. AuthController.prototype.success = function() {
  29. // "super" call
  30. Controller.prototype.success.call( this, "Authenticated!" );
  31. };
  32. // Override to extend base `failure()`
  33. AuthController.prototype.failure = function(err) {
  34. // "super" call
  35. Controller.prototype.failure.call( this, "Auth Failed: " + err );
  36. };
  1. var auth = new AuthController(
  2. // in addition to inheritance, we also need composition
  3. new LoginController()
  4. );
  5. auth.checkAuth();

We have base behaviors that all controllers share, which are success(..), failure(..) and showDialog(..). Our child classes LoginController and AuthController override failure(..) and success(..) to augment the default base class behavior. Also note that AuthController needs an instance of LoginController to interact with the login form, so that becomes a member data property.

The other thing to mention is that we chose some composition to sprinkle in on top of the inheritance. AuthController needs to know about LoginController, so we instantiate it (new LoginController()) and keep a class member property called this.login to reference it, so that AuthController can invoke behavior on LoginController.

Note: There might have been a slight temptation to make AuthController inherit from LoginController, or vice versa, such that we had virtual composition through the inheritance chain. But this is a strongly clear example of what’s wrong with class inheritance as the model for the problem domain, because neither AuthController nor LoginController are specializing base behavior of the other, so inheritance between them makes little sense except if classes are your only design pattern. Instead, we layered in some simple composition and now they can cooperate, while still both benefiting from the inheritance from the parent base Controller.

If you’re familiar with class-oriented (OO) design, this should all look pretty familiar and natural.

De-class-ified

But, do we really need to model this problem with a parent Controller class, two child classes, and some composition? Is there a way to take advantage of OLOO-style behavior delegation and have a much simpler design? Yes!

  1. var LoginController = {
  2. errors: [],
  3. getUser: function() {
  4. return document.getElementById( "login_username" ).value;
  5. },
  6. getPassword: function() {
  7. return document.getElementById( "login_password" ).value;
  8. },
  9. validateEntry: function(user,pw) {
  10. user = user || this.getUser();
  11. pw = pw || this.getPassword();
  12. if (!(user && pw)) {
  13. return this.failure( "Please enter a username & password!" );
  14. }
  15. else if (pw.length < 5) {
  16. return this.failure( "Password must be 5+ characters!" );
  17. }
  18. // got here? validated!
  19. return true;
  20. },
  21. showDialog: function(title,msg) {
  22. // display success message to user in dialog
  23. },
  24. failure: function(err) {
  25. this.errors.push( err );
  26. this.showDialog( "Error", "Login invalid: " + err );
  27. }
  28. };
  1. // Link `AuthController` to delegate to `LoginController`
  2. var AuthController = Object.create( LoginController );
  3. AuthController.errors = [];
  4. AuthController.checkAuth = function() {
  5. var user = this.getUser();
  6. var pw = this.getPassword();
  7. if (this.validateEntry( user, pw )) {
  8. this.server( "/check-auth",{
  9. user: user,
  10. pw: pw
  11. } )
  12. .then( this.accepted.bind( this ) )
  13. .fail( this.rejected.bind( this ) );
  14. }
  15. };
  16. AuthController.server = function(url,data) {
  17. return $.ajax( {
  18. url: url,
  19. data: data
  20. } );
  21. };
  22. AuthController.accepted = function() {
  23. this.showDialog( "Success", "Authenticated!" )
  24. };
  25. AuthController.rejected = function(err) {
  26. this.failure( "Auth Failed: " + err );
  27. };

Since AuthController is just an object (so is LoginController), we don’t need to instantiate (like new AuthController()) to perform our task. All we need to do is:

  1. AuthController.checkAuth();

Of course, with OLOO, if you do need to create one or more additional objects in the delegation chain, that’s easy, and still doesn’t require anything like class instantiation:

  1. var controller1 = Object.create( AuthController );
  2. var controller2 = Object.create( AuthController );

With behavior delegation, AuthController and LoginController are just objects, horizontal peers of each other, and are not arranged or related as parents and children in class-orientation. We somewhat arbitrarily chose to have AuthController delegate to LoginController — it would have been just as valid for the delegation to go the reverse direction.

The main takeaway from this second code listing is that we only have two entities (LoginController and AuthController), not three as before.

We didn’t need a base Controller class to “share” behavior between the two, because delegation is a powerful enough mechanism to give us the functionality we need. We also, as noted before, don’t need to instantiate our classes to work with them, because there are no classes, just the objects themselves. Furthermore, there’s no need for composition as delegation gives the two objects the ability to cooperate differentially as needed.

Lastly, we avoided the polymorphism pitfalls of class-oriented design by not having the names success(..) and failure(..) be the same on both objects, which would have required ugly explicit pseudopolymorphism. Instead, we called them accepted() and rejected(..) on AuthController — slightly more descriptive names for their specific tasks.

Bottom line: we end up with the same capability, but a (significantly) simpler design. That’s the power of OLOO-style code and the power of the behavior delegation design pattern.