# Level-based design —JS: Building abstractions with data

Consider another simple system - rational numbers and operations you can perform on them. Remember that a rational number is a number representable as a fraction `a/b` where a is the numerator of the fraction and b is the denominator. And b must not be zero, since division by zero is not allowed.

JS does not support rational numbers, so we'll create an abstraction for them ourselves. As usual, we will need the constructor and selectors:

``````const num = makeRational(1, 2); // created a rational number, one half
const numer = getNumer(num); // 1
const denom = getDenom(num); // 2
``````

Using three functions, we've defined a rational number. One function (constructor) build it from parts, and others (selectors) allow each component to be extracted. What `num` is from the language's perspective is irrelevant. It can be a function (not impossible), an array, or an object. You can even use strings in the internal implementation:

``````const makeRational = (numer, denom) => `\${numer}/\${denom}`;

const getNumer = (rational) => rational.split('/');

const getDenom = (rational) => rational.split('/');

console.log(makeRational(10, 3)); // => 10/3
``````

Although we've learned how to represent rational numbers, this abstraction itself is of little use. Abstraction becomes useful when it becomes possible to operate on it. For rational numbers, arithmetical operations are the basic ones, such as addition, subtraction, or multiplication. Multiplication of rational numbers is the simplest operation. To do this, multiply the numerators and denominators:

``````3/4 * 4/5 = (3 * 4)/(4 * 5) = 12/20
``````

The most interesting part begins in the implementation process. If we assume that the real structure of a rational number looks like this: `{ numer: 2, denom: 3 }`, then, purely technically, the solution could be this:

``````const mul = (rational1, rational2) => {
return {
numer: rational1['numer'] * rational2['numer'],
denom: rational1['denom'] * rational2['denom']
};
};
``````

From the caller's perspective, everything is fine, and the abstraction is preserved. `mul` input is a rational number, and the output is a rational number. But inside, there is no abstraction; we treat rational numbers knowing how they are implemented. Any change in the internal implementation of rational numbers would require rewriting all operations that work with rational numbers directly, i.e., without selectors or a constructor. This code violates the single-level abstraction principle.

When developing complex systems, we use a level design approach. It consists in structuring the system using successive levels. Each level is built by combining parts considered elementary at that level. The parts built at each level act as elementary ones (primitives) at the next level.

Stratified design pervades the engineering of complex systems. For example, in computer engineering, resistors and transistors are combined (and described using a language of analog circuits) to produce parts such as and-gates and or-gates, whichform the primitives of a language for digital-circuit design. These parts are combined to build processors, bus structures, and memory systems, which are in turn combined to form computers, using languages appropriate to computer architecture. Computers are combined to form distributed systems, using languages appropriate for describing network interconnections, and so on. (c) SICP

``````const mul = (rational1, rational2) => {
return makeRational(
getNumer(rational1) * getNumer(rational2),
getDenom(rational1) * getDenom(rational2)
);
};
``````

In our example, the base level consists of types built into the language itself: numbers and objects. On top of this, we create a level for representing rational numbers: `makeRational`, `getDenom`, `getNumer`. Then there's a level of arithmetic operations on rational numbers: addition, subtraction, multiplication, and so on.

I should emphasize that we are talking about the level implementation itself. For example, the addition operation relies entirely on the constructor and selectors but cannot know anything about the inner workings of rational numbers themselves. On the other hand, this does not mean that functions from different levels can't appear in one place. They can, and this is normal in many cases. For example:

``````const f = (rational1, rational2) => {
const rational3 = sum(rational1, rational2);
const denom = getDenom(rational3);
const numer = getNumer(rational3);
console.log(`Denom: \${denom}`);
console.log(`Numer: \${numer}`);
};
`````` ## Are there any more questions? Ask them in the Discussion section.

The Hexlet support team or other students will answer you.

## For full access to the course you need a professional subscription.

130
courses
1000
exercises
2000+
hours of theory
3200
tests

• 130 courses, 2000+ hours of theory
• 1000 practical tasks in a browser
• 360 000 students
By sending this form, you agree to our Personal Policy and Service Conditions    Suggested learning programs
profession
Development of front-end components for web applications
10 months
from scratch
Start at any time

## Use Hexlet to the fullest extent! 