Armed with what we've learned about OOP, let's try and find out how to write and structure code correctly in class languages.
In OOP, the SOLID principles are what are considered to be the answer to how to write code correctly. However, life shows that knowing these principles isn't much use when it comes to organizing code.
Take the SRP principle (the single responsibility principle, the S in SOLID). It says the following:
Each class should have one and only one reason to change. Robert Martin.
There are other ways of saying it, but this is the most succinct. What's wrong with this principle? It's a very general one. You might as well say, “if you do it well, it'll be OK”. It doesn't give any formal criteria by which you can determine if there's a problem with the class. If you read articles about this principle, everything might seem to always make sense. But that's only because the author will have already suggested a division of responsibilities. In reality, it's different. If you ask several people about the same situation, they'll give completely different answers, or even contradictory answers. Actually, it all comes down to individual programmers' gut feelings.
Let's take the HTTP request execution library as an example. What is the starting point for designing it?
The right thing to do is to start with how to use it. Imagine the library has already been written, and we try to use it (this is what Test-Driven Development pushes you to do, which is why it's so powerful). Before you see the code, try to answer this question: “Are classes and OOP required to implement this library?”
An HTTP session is an operation that has an end and a beginning. Do you need objects to express it? No, of course not. There are enough functions for operations. So in the simplest case, our library might look like this:
import { get, post } from 'http-client';
let response = get('https://hexlet.io/blog');
console.log(response.body);
response = post('https://hexlet.io/users', {
name: 'mira',
email: 'mira@example.com',
});
console.log(response.statusCode);
Now the library interface is ready, you can start implementing it. How much attention do we need to pay to how it's done internally? Frankly, it doesn't matter. What happens inside, stays inside, nobody is going to know about it, and it'll never get too big (it's just an HTTP library after all). This means that we can rewrite them at any time. And it's better to do after, not before, once there's been some experience with using it and supporting it. At this point there will an understanding of how best to structure the library internally.
The general idea is as follows: proper abstraction is the key to success. Designate boundaries, consider how it might be used, and find a way to implement it all.
The example above hasn't been pulled from thin air, you can see for yourself that the most popular HTTP library axios in JavaScript is actually a set of functions:
import axios from 'axios';
await axios.get('https://hexlet.io/users');
await axios.post('https://hexlet.io/users', {
firstName: 'Fred',
lastName: 'Flintstone',
});
But if we need to, we can create an object, but only so that we can remember the configuration inside to avoid duplication:
import axios from 'axios';
const client = new axios.Axios({ timeout: 1000 });
const response = await client.get('https://hexlet.io/lessons.rss');
console.log(response.statusCode);
What principles should be used to understand the internal architecture and the number of classes? Common sense is enough to get you started. We have the client itself, which is represented by an object (but its state is configuration, not requests and responses), and we have the result of an HTTP request. The result is represented by an object that's returned after the query is executed. Inside it stores all information regarding interaction with the server, such as sent and received headers, and the code and the body of the response.
No further division is necessary. It may never be necessary. And if you do, you need to feel the need first, and then implement it where it needs to be implemented. And the main reason for this division isn't an abstract single responsibility, but the allocation of pure code, which isn't related to side effects.
Inside our library, we have code that performs network queries, and we have code that works with data, converts it into a normal form, cleans it up, and structures it somehow. The first thing we need to do is to keep track of such code and separate it at function or method level. Any operation that can be purely computational is a potential candidate for separation.
Another example, where analyzing side effects allows you to understand how to do the right thing. In the materials on OOP, they often talk about the class that's responsible for generating the report. Let's suppose it works like this:
import Reporter from './Reporter.js';
// Whether to make Reporter a data abstraction (i.e., an object describing a specific report)
// or not, that's the big question.
// Don't do this by default, otherwise the entire code
// be useless (new Reporter('path/to/file'))->generate()
const reporter = new Reporter();
const report = reporter.generate('/path/to/report');
What's the first thing you should pay attention to? This class does both the dirty work (with side effects): it reads a file from disk, and the clean work: it processes the data to generate a report. This doesn't mean you should rush to rewrite the code, but it's the first thing you need to pay attention to. The code above is more difficult to test and debug than code with operations divided by side effect. In addition, if you take the file reading outside, the reporter becomes much more versatile. It'll be able to work not just with data on the disk, but also data that have been obtained in some other way, for example, by http through a form. After making a few changes, we get this code:
import fs from 'fs';
import Reporter from './Reporter.js';
const reporter = new Reporter();
const data = fs.readFileSync('/path/to/report');
const report = reporter.generate(data);
The other principles require knowledge that you'll acquire in the next course: polymorphism in js. That's where we look at them.
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.
A professional subscription will give you full access to all Hexlet courses, projects and lifetime access to the theory of lessons learned. You can cancel your subscription at any time.