Internal and external interface

One of the most important principles of OOP is the separation of the internal interface from the external.

It's a must practice in developing anything more complicated than hello world.

To understand this, we will digress from the development and translate the view on the objects of the real world.

As a rule, the devices with which we are dealing are very complex. But the separation of the interface into external and internal allows you to use them without the slightest problem.

An example from life

For example, a coffee maker. Simple outside: button, indicator, holes, ... And, of course, the result is coffee.

But inside ... (a picture from the manual for repair)

Mass of parts. But we can use it without knowing it at all.

Coffee makers are pretty reliable, are not they? You can use the years, and only when something goes wrong - will have to bear to the master.

The secret of reliability and simplicity of the coffee maker is that all the details are debugged and hidden inside.

If you remove the protective casing from the coffee maker, then its use will be more difficult (where to press?) And dangerous (current can hit).

As we will see, objects are very similar to coffee makers.

Only in order to hide internal parts, it is not the case that is used, but special means of language and agreement.

Internal and external interface

In programming, we will divide the methods and properties of the object into two groups:

  • The internal interface is properties and methods that can be accessed only from other methods of the object, they are also called "private" (there are other terms, we will meet them further).
  • The external interface is the properties and methods available outside the object, they are called "public".

If we continue the analogy with the coffee machine - what is hidden inside the coffee machine: the boiler tube, the heating element, the thermal fuse and so on - is its internal interface.

The internal interface is used to ensure the operability of the object, its details are used by each other. For example, the boiler tube is connected to a heating element.

But outside the coffee maker is closed with a special casing, so that no one gets to them. Details are hidden and inaccessible. Only the external interface is visible.

Having received the object, all you need to use it is to know the external interface. It is not necessary to know about the internal at all.

These were general words on the theory of programming.

Next, we implement a coffee maker in JavaScript with private and public properties. There are a lot of details in the coffee maker, of course, we will not model each cog, but focus on the basic techniques of development.

Step 1: public and private property

The designer of coffee makers will be called CoffeeMachine.

function CoffeeMachine(power) {
  this.waterAmount = 0; // Amount of water in the coffee machine

  alert( 'A coffeeMachine maker with capacity: ' + power + ' watt' );
}

// create coffeeMachine
var coffeeMachine = new CoffeeMachine(100);

// pour the water
coffeeMachine.waterAmount = 200;

Local variables, including constructor parameters, can be considered private properties.

In the example above power - the power of the coffee machine, which is indicated at the time of creation and will be used to calculate the boiling time.

Local constructor variables can not be accessed from the outside, but they are available within the constructor itself.

The properties recorded in this can be considered public.

Here, the property waterAmount is written into the object, and therefore - available for modification from the outside. You can add and pour out water in any quantity.

Question of terminology

Next, we will refer to power as a "local variable", and "private property" of the object.

This, depending on which side to look at.

The terms "private property / method", "public property / method" refer to the general theory of OOP. And their concrete implementation in the programming language can be different.

Here the OOP principle of the "private property" is implemented through local variables, therefore both the "local variable" and the "private property" are the correct terms, depending on where you look at the code or the architecture of the OOP.

Step 2: public and private methods

We add the public method run, which starts the coffee machine, as well as the auxiliary internal methods getBoilTime and onReady:

function CoffeeMachine(power) {

  this.waterAmount = 0;

  // Calculation of time for boiling
  function getBoilTime() {
    return 1000; // The exact calculation formula will be later
  }

  // What to do after the end of the process
  function onReady() {
    alert( 'Coffee is ready!' );
  }

  this.run = function() {
    // setTimeout - Built-in function,
    // She will launch onReady across getBoilTime() milliseconds
    setTimeout(onReady, getBoilTime());
  };
}

var coffeeMachine = new CoffeeMachine(100);
coffeeMachine.waterAmount = 200;

coffeeMachine.run();

Private methods, such as onReady, getBoilTime can be declared as nested functions.

As a result, it is natural that access to them (through the closure) has only other functions declared in the same constructor.

Step 3: Constant

To calculate the time for boiling water, the formula c * m * ΔT / power is used, where:

  • C - coefficient of heat capacity of water, physical constant equal to 4200.
  • M is the mass of water to be heated.
  • ΔT is the temperature to be heated, we will assume that initially water - room temperature 20 ° C, that is, up to 100 °, should be heated by ΔT = 80.
  • Power - power.

We use it in a more realistic version of getBoilTime (), including the use of private properties and a constant:

"use strict"

function CoffeeMachine(power) {

  this.waterAmount = 0;

  // The physical constant is the specific heat of water for getBoilTime
  var WATER_HEAT_CAPACITY = 4200;

  // Calculation of time for boiling
  function getBoilTime() {
      return this.waterAmount * WATER_HEAT_CAPACITY * 80 / power; // error!
    }

  // What to do after the end of the process
  function onReady() {
    alert( 'Coffee is ready!' );
  }

  this.run = function() {
    setTimeout(onReady, getBoilTime());
  };

}

var coffeeMachine = new CoffeeMachine(1000);
coffeeMachine.waterAmount = 200;

coffeeMachine.run();

The specific heat capacity of WATER_HEAT_CAPACITY is highlighted in large letters, since this is a constant.

Attention, when you run the code above, there will be an error in the getBoilTime method. Why do you think?

Step 4: Access the object from the internal method

The internal method is called as follows: getBoilTime (). And what does this equal to? ... As you probably remember, in the modern standard it will be undefined (in the old window - window), because of this when reading this.waterAmount an error will occur!

It can be solved by calling getBoilTime with an explicit context: getBoilTime.call (this):

function CoffeeMachine(power) {
  this.waterAmount = 0;
  var WATER_HEAT_CAPACITY = 4200;

  function getBoilTime() {
    return this.waterAmount * WATER_HEAT_CAPACITY * 80 / power;
  }

  function onReady() {
    alert( 'Coffee is ready!' );
  }

  this.run = function() {
    setTimeout(onReady, getBoilTime.call(this));
  };

}

// I create a coffee machine, capacity 100000W to boil quickly
var coffeeMachine = new CoffeeMachine(100000);
coffeeMachine.waterAmount = 200;

coffeeMachine.run();

This approach will work, but it is not very convenient. After all, it turns out that now wherever we want to call getBoilTime, we need to explicitly specify the context, i.e. Write getBoilTime.call (this).

Fortunately, there are more elegant solutions.

Bind to bind

It is possible to bind getBoilTime to an object via bind at the declaration, then the context issue will disappear by itself:

function CoffeeMachine(power) {
  this.waterAmount = 0;

  var WATER_HEAT_CAPACITY = 4200;

  var getBoilTime = function() {
    return this.waterAmount * WATER_HEAT_CAPACITY * 80 / power;
  }.bind(this);

  function onReady() {
    alert( 'Coffee is ready!' );
  }

  this.run = function() {
    setTimeout(onReady, getBoilTime());
  };

}

var coffeeMachine = new CoffeeMachine(100000);
coffeeMachine.waterAmount = 200;

coffeeMachine.run();

This solution will work, now the function can simply be called without a call. But the function declaration became less beautiful.

Saving this in the closure

Perhaps the most convenient and often applied solution path is to first copy this into an auxiliary variable and access it from internal functions already.

Like this:

function CoffeeMachine(power) {
  this.waterAmount = 0;

  var WATER_HEAT_CAPACITY = 4200;

  var self = this;

  function getBoilTime() {
      return self.waterAmount * WATER_HEAT_CAPACITY * 80 / power;
    }

  function onReady() {
    alert( 'Coffee is ready!' );
  }

  this.run = function() {
    setTimeout(onReady, getBoilTime());
  };

}

var coffeeMachine = new CoffeeMachine(100000);
coffeeMachine.waterAmount = 200;

coffeeMachine.run();

Now getBoilTime gets self from the closure.

Of course, for this to work, we should not change self, and all private methods that want access to the current object must use self instead of this.

Instead of self, you can use any other variable name, for example var me = this.

Total

So, we made a coffee machine with public and private methods and made them work correctly.

In the OOP terminology, the separation and protection of the internal interface is called encapsulation.

Briefly list the bonuses that it gives:

Protect users from shots to themselves in the leg

Imagine a team using a coffee maker. The coffee maker was created by the company "Best Coffee Makers" and, in general, works well, but it was removed with a protective cover and, thus, the internal interface became available.

All developers are civilized - and use the coffee machine as usual. But the crafty Vasya decided that he was the smartest, and twisted something inside the coffee maker to make coffee stronger. Vasya did not know that the changes he had made would lead to the coffee maker deteriorating in two days.

Of course, not only Vasya, but also the one who removed the protective cover from the coffee machine, is guilty, and of course allowed Vasya to conduct manipulations.

In programming, the same thing. If the user of the object changes what is not intended for change from the outside, the consequences can be unpredictable.

Convenience in supporting

The situation in programming is more complicated than with a coffee machine, because The coffee maker once bought everything, and the program can be improved and refined.

If there is a clearly distinguished external interface, the developer can freely change internal properties and methods, without regard for colleagues.

It is much easier to develop if you know that a number of methods (all internal ones) can be renamed, changed their parameters, and generally, rewritten as you like, since the external code does not exactly refer to them.

The closest analogy in real life is when a "new version" of the coffee machine comes out, which works much better. The developer could remake everything inside, but it's still easy to use, because the external interface is preserved.

Managing complexity

People love to use things that are simple in appearance. And what's inside is the tenth thing.

Programmers are no exception.

It is always convenient when the implementation details are hidden, and a simple, clearly documented external interface is available.