简体   繁体   中英

Composition over inheritance, what is a nicer way to add additional functionality to a view without resorting to inheritance

I've read a lot in the past about composability over inheritance, and I am completely sold on the concept and make use of this principle a lot in my code.

However, I run up against problems in my day to day work where inheritance tends to creep into views, and I struggle to see how I can implement something more composable instead (not helped by the fact that I use Backbone in my day to day work). These tend to be when I want to use all the functionality of an existing Backbone view, while adding in some additional functionality on top.

Take this hypothetical example where we have a ecommerce type page with multiple Product view's, each representing a collection of basketable options for a particular product:

var ProductView = (function(Backbone, JST) {
  'use strict';

  return Backbone.View.extend({
    className: 'product',
    template: JST['application/templates/product']

    initialize: function(options) {
      this.options = options || {};
      this.collection.fetch();
      this.listenTo(this.collection, 'loaded', this.render);
    },

    render: function() {
      this.$el.html(
        this.template(this.collection)
      );

      return this;
    },
  }, {
    create: function(el) {
      var endpoint = '/api/options/' + el.getAttribute('data-basket-id') + '/' + el.getAttribute('data-product-id');

      new ProductView({
        el: el,
        collection: new ProductCollection(null, { url: endpoint })
      });
    }
  });
})(Backbone, JST);

Say we then want to display some products that require the visitor be prompted with a confirmation box (let's say for insurance reasons, this particular product must be sold with insurance, so we need to prompt the user about this when they add it to their basket):

var InsuranceProductView = (function (_, ProductView) {
  'use strict';

  return ProductView.extend({
    consentTemplate: JST['application/templates/product/insurance_consent'],

    initialize: function (options) {
      this.listenTo(this.model, 'change:selected', function (model) {
        if (!model.get('selected')) {
          this.removeMessage()
        }
      });

      ProductView.prototype.initialize.apply(this, arguments);
    },

    events: function () {
      return _.extend({}, ProductView.prototype.events, {
        'change input[type=radio]': function () {
          this.el.parentElement.appendChild(this.consentTemplate());
        },
        'change .insurance__accept': function () {
          ProductView.prototype.onChange.apply(this);
        },
      });
    },

    removeMessage: function () {
      var message = this.el.parentElement.querySelector('.insurance__consent');
      message.parentNode.removeChild(message);
    },
  });
})(_, ProductView);

Is there a more composable way of writing this? Or is this a situation where it is the right thing to break off via inheritance?

For that specific case, inheritance works well. The argument on composability over inheritance is futile, use what's best for the situation at hand.

But, there's still improvement that could be made to ease inheritance. When I make a Backbone class that I'm going to inherit, I try to make it next to invisible for the child class.

One way to achieve that is to put the initialization of the parent into the constructor, leaving the initialize function all to the child. And same thing with the events hash.

var ProductView = Backbone.View.extend({
    className: 'product',
    template: JST['application/templates/product'],
    events: {},

    constructor: function(options) {
        // make parent event the default, but leave the event hash property
        // for the child view
        _.extend({
            "click .example-parent-event": "onParentEvent"
        }, this.events);

        this.options = options || {};
        this.collection.fetch();
        this.listenTo(this.collection, 'loaded', this.render);

        ProductView.__super__.constructor.apply(this, arguments);
    },

    /* ...snip... */
});

And the child view becomes:

var InsuranceProductView = ProductView.extend({
    consentTemplate: JST['application/templates/product/insurance_consent'],

    events:{
        'change input[type=radio]': 'showConsent',
        'change .insurance__accept': 'onInsuranceAccept'
    }

    initialize: function(options) {
        this.listenTo(this.model, 'change:selected', function(model) {
            if (!model.get('selected')) {
                this.removeMessage()
            }
        });
    },

    showConsent: function() {
        // I personally don't like when component go out of their root element.
        this.el.parentElement.appendChild(this.consentTemplate());
    },

    onInsuranceAccept: function() {
        InsuranceProductView.__super__.onChange.apply(this);
    },

    removeMessage: function() {
        var message = this.el.parentElement.querySelector('.insurance__consent');
        message.parentNode.removeChild(message);
    },
});

Also, Backbone extend adds a __super__ property with the prototype of the parent. I like to use that because I can change the parent class without worrying about the use of its prototype somewhere down in a function.


I find that composition works really well when building a view with smaller components.

The following view has almost nothing in it, except the configuration for smaller components, with each one handling most of the complexity:

var FoodMenu = Backbone.View.extend({
    template: '<div class="food-search"></div><div class="food-search-list"></div>',

    // abstracting selectors out of the view logic
    regions: {
        search: ".food-search",
        foodlist: ".food-search-list",
    },

    initialize: function() {

        // build your view with other components
        this.view = {
            search: new TextBox({
                label: 'Search foods',
                labelposition: 'top',
            }),
            foodlist: new FoodList({
                title: "Search results",
            })
        };
    },

    render: function() {
        this.$el.empty().append(this.template);

        // Caching scoped jquery element from 'regions' into `this.zone`.
        this.generateZones();
        var view = this.view,
            zone = this.zone;
        this.assign(view.search, zone.$search)
            .assign(view.foodlist, zone.$foodlist);

        return this;
    },

});

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM