The post has been translated automatically. Original language: Russian
One of the most famous Unix rules is simple: a program must do one thing and do it well. At first glance, this seems almost a banality. But there is much more to this phrase than the advice to write small programs.
It's about a way to deal with complexity. Instead of creating a single system that knows everything and can do everything, Unix suggests breaking the task down into small parts. Each part should have a clear purpose, a simple interface, and predictable behavior.
A small program is easier than a large one
Let's present two programs. The first one can search for text, sort results, convert data, save it to different formats and send it over the network. The second one can only search for strings based on a given condition.
The first one looks more powerful. But it inevitably contains more states, settings, dependencies, and behaviors. It is more difficult to understand, verify and change it. If an error appears in it, you must first determine in which part of the huge system it occurred.
The second program has fewer features, but its purpose is clear almost immediately. It receives data, performs one operation, and outputs the result. If sorting is required, you can assign this work to another program.
This way, the complexity does not disappear, but ceases to be in one place.
Why did it turn out to be so durable
Many Unix programs have been around for decades. They have survived several generations of processors, operating systems, programming languages, and user interfaces.
One of the reasons is the narrow specialization. If a program solves a simple and well-defined task, the world around it must change very much in order for this task to cease to exist.
Text search is still needed today. Sorting is needed. File comparison is necessary. Transferring the data stream from one program to another is also necessary.
The less the program knows about the surrounding system, the less reason there is to rewrite it.
A big program knows too much
The problem with large universal systems is not only the size of the source code. They're starting to know too much about other parts of the world.
One component knows the internal structure of the other. The data format is related to the user interface. The rules of operation are hardwired into the processing code. A change in one place unexpectedly requires changes in five more.
Gradually, the system turns into a structure where it is almost impossible to replace one part without affecting the rest. It is this kind of connectivity, rather than the number of lines of code per se, that makes programs heavy.
The Unix approach suggests the opposite: let each part know only what it really needs.
A simple program does not mean primitive
It's easy to misunderstand the principle here. "Doing one thing" does not mean that the program should consist of ten lines or solve only elementary tasks.
A compiler is a complex program. The database management system is the same. Even a text editor can be a huge project.
It's not so much a question of size as of responsibility. Is it possible to explain in one sentence why this component exists? Does it have a clear boundary? Is it possible to use it independently from the rest of the system?
If these questions are difficult to answer, the program may be trying to do too much.
Unix has shown the power of composition
Small programs are especially useful because they can be connected.
One program reads the data. The other one selects the necessary lines. The third one sorts. The fourth calculates the result. None of them is required to know the entire task.
It is the user or another program that determines how to put these parts together.
This is a very important idea. Complexity is transferred from individual components to their composition. Instead of one huge program, a set of simpler building blocks appears.
Such a system is often more flexible because the same tool can be used in completely different tasks.
Reuse appears by itself
In large systems, code reuse often has to be designed in advance. Universal libraries, common layers, and complex internal interfaces are being created.
In the Unix approach, reuse often comes naturally. If the program does one clear thing and accepts data in a simple way, it can be applied where the author originally did not plan at all.
A good tool doesn't know why the user called it. He's just doing his job.
This makes it universal.
Where is this principle violated
Modern development is constantly pushing us in the opposite direction. I want to add new features to the product. Each new request seems quite small: another mode, another setting, another format, another integration.
Individually, such solutions look reasonable. But in a few years, the program may turn into a system in which it is no longer possible to formulate a single purpose.
A characteristic feature arises: changing a small function requires more and more knowledge about the entire system.
At this point, the problem is no longer a lack of opportunities. On the contrary, there are too many opportunities in one place.
Microservices by themselves do not save
One might think that microservices are automatically the modern embodiment of this principle. But dividing the program into dozens of network services does not make the architecture simple.
If services constantly know the internal state of each other, are called by long chains and cannot work independently, the complexity has not disappeared anywhere. She just moved from one process to the network.
The Unix principle goes deeper than physical separation. It is not important whether the components are located in different programs or on different servers. It is important how clear the responsibility of each part is and how little it should know about the rest.
Artificial intelligence makes this principle important again.
Today it has become much easier to write a large program. Artificial intelligence is able to quickly create hundreds and thousands of lines of code, add a new layer, library or subsystem.
This is convenient, but it creates a new danger. Previously, complexity at least took time to write. Now it can be produced almost instantly.
The developer asks to add a function, artificial intelligence creates a new class, another level of abstraction, several auxiliary methods and a new dependency. Everything is working, so there is a desire to move on.
After a while, the system can become complex faster than a person can understand it.
This is where the old Unix rule takes on a new meaning. Before asking artificial intelligence to write more code, it's useful to ask: what one thing should this component do?
The smaller the task, the better the artificial intelligence works.
There is also a practical side. It is easier for artificial intelligence to work with a clearly limited task than with the entire system at once.
If a component has a clear input, a clear output, and a single responsibility, it is easier to describe, write, test, and replace it if necessary. The error is also localized much faster.
When a single query is tasked with redoing several interconnected parts of a large system at once, the result becomes more difficult to verify. The code may look convincing, but the effects of the changes are distributed throughout the architecture.
Therefore, the principle of small, clear components is no less useful today than in the days of the first Unix systems.
Simplicity requires discipline
The paradox is that writing a large universal program is sometimes easier than keeping the system small.
A new feature is almost always requested in an existing component. Creating another boundary, defining a simple interface, and deciding exactly where responsibility should be located requires additional thought.
Simplicity rarely appears on its own.
She has to be protected.
You need to be able to say that this function belongs to another component, that this dependency is superfluous here, and this seemingly convenient step connects the two parts of the system too much.
That is why simplicity is not an absence of architecture. This is the result of a good architecture.
One thing is not limitation, but freedom.
When a program does one thing, it seems less powerful. But if there are many such programs and they can be freely connected, the system as a whole gets much more features.
This is one of the paradoxes of Unix. Limited tools together turn out to be more flexible than a single tool that tries to plan everything in advance.
Therefore, the "do one thing and do it well" rule has outlived specific versions of Unix and older computers.
This is not a rule about the size of the program.
This is a rule about boundaries.
The clearer the boundaries of the system parts, the easier it is to understand, check, replace, and connect them. This means that the longer the system itself can remain manageable.
Одно из самых известных правил Unix звучит просто: программа должна делать одну вещь и делать её хорошо. На первый взгляд это кажется почти банальностью. Но за этой фразой скрывается гораздо больше, чем совет писать маленькие программы.
Речь идёт о способе борьбы со сложностью. Вместо того чтобы создавать одну систему, которая знает обо всём и умеет всё, Unix предлагает разбить задачу на небольшие части. Каждая часть должна иметь понятное назначение, простой интерфейс и предсказуемое поведение.
Маленькая программа легче большой
Представим две программы. Первая умеет искать текст, сортировать результаты, преобразовывать данные, сохранять их в разные форматы и отправлять по сети. Вторая умеет только искать строки по заданному условию.
Первая выглядит мощнее. Но она неизбежно содержит больше состояний, настроек, зависимостей и вариантов поведения. Её труднее понимать, проверять и изменять. Если в ней появляется ошибка, нужно сначала определить, в какой именно части огромной системы она возникла.
У второй программы возможностей меньше, зато её назначение понятно почти сразу. Она получает данные, выполняет одну операцию и отдаёт результат. Если требуется сортировка, эту работу можно поручить другой программе.
Так сложность не исчезает, но перестаёт находиться в одном месте.
Почему это оказалось так долговечно
Многие Unix-программы существуют десятилетиями. Они пережили несколько поколений процессоров, операционных систем, языков программирования и пользовательских интерфейсов.
Одна из причин — узкая специализация. Если программа решает простую и хорошо определённую задачу, окружающий мир должен измениться очень сильно, чтобы эта задача перестала существовать.
Поиск текста нужен и сегодня. Сортировка нужна. Сравнение файлов нужно. Передача потока данных от одной программы другой тоже нужна.
Чем меньше программа знает об окружающей системе, тем меньше причин её переписывать.
Большая программа знает слишком много
Проблема больших универсальных систем не только в размере исходного кода. Они начинают знать слишком много о других частях мира.
Один компонент знает внутреннее устройство другого. Формат данных связан с пользовательским интерфейсом. Правила работы оказываются зашиты в код обработки. Изменение в одном месте неожиданно требует изменений ещё в пяти.
Постепенно система превращается в конструкцию, где почти невозможно заменить одну часть, не затронув остальные. Именно такая связанность, а не количество строк кода само по себе, делает программы тяжёлыми.
Unix-подход предлагает обратное: пусть каждая часть знает только то, что ей действительно необходимо.
Простая программа не означает примитивная
Здесь легко неправильно понять принцип. «Делать одну вещь» не означает, что программа должна состоять из десяти строк или решать только элементарные задачи.
Компилятор — сложная программа. Система управления базами данных — тоже. Даже текстовый редактор может быть огромным проектом.
Вопрос не столько в размере, сколько в ответственности. Можно ли в одном предложении объяснить, зачем существует этот компонент? Есть ли у него чёткая граница? Можно ли использовать его независимо от остальных частей системы?
Если на эти вопросы трудно ответить, возможно, программа пытается делать слишком многое.
Unix показал силу композиции
Небольшие программы особенно полезны потому, что их можно соединять.
Одна программа читает данные. Другая выбирает нужные строки. Третья сортирует. Четвёртая подсчитывает результат. Ни одна из них не обязана знать всю задачу целиком.
Именно пользователь или другая программа определяет, как собрать эти части вместе.
Это очень важная идея. Сложность переносится из отдельных компонентов в их композицию. Вместо одной огромной программы появляется набор более простых строительных блоков.
Такая система часто оказывается гибче, потому что один и тот же инструмент можно использовать в совершенно разных задачах.
Повторное использование появляется само
В больших системах повторное использование кода часто приходится проектировать заранее. Создаются универсальные библиотеки, общие слои и сложные внутренние интерфейсы.
В Unix-подходе повторное использование часто возникает естественно. Если программа делает одну понятную вещь и принимает данные в простом виде, её можно применить там, где автор первоначально вообще не планировал.
Хороший инструмент не знает, зачем пользователь его вызвал. Он просто выполняет свою работу.
Это и делает его универсальным.
Где этот принцип нарушается
Современная разработка постоянно подталкивает нас в противоположную сторону. В продукт хочется добавлять новые возможности. Каждый новый запрос кажется достаточно небольшим: ещё один режим, ещё одна настройка, ещё один формат, ещё одна интеграция.
По отдельности такие решения выглядят разумно. Но через несколько лет программа может превратиться в систему, у которой уже невозможно сформулировать одно назначение.
Возникает характерный признак: изменение небольшой функции требует всё больше знаний о всей системе.
В этот момент проблема уже не в недостатке возможностей. Наоборот, возможностей стало слишком много в одном месте.
Микросервисы сами по себе не спасают
Можно подумать, что современным воплощением этого принципа автоматически являются микросервисы. Но разделение программы на десятки сетевых служб ещё не делает архитектуру простой.
Если сервисы постоянно знают внутреннее состояние друг друга, вызываются длинными цепочками и не могут работать независимо, сложность никуда не исчезла. Она просто переехала из одного процесса в сеть.
Принцип Unix глубже физического разделения. Важно не то, находятся ли компоненты в разных программах или на разных серверах. Важно, насколько ясна ответственность каждой части и насколько мало она должна знать об остальных.
Искусственный интеллект снова делает этот принцип важным
Сегодня написать большую программу стало значительно легче. Искусственный интеллект способен быстро создать сотни и тысячи строк кода, добавить новый слой, библиотеку или подсистему.
Это удобно, но создаёт новую опасность. Раньше сложность хотя бы требовала времени на написание. Теперь её можно производить почти мгновенно.
Разработчик просит добавить функцию, искусственный интеллект создаёт новый класс, ещё один уровень абстракции, несколько вспомогательных методов и новую зависимость. Всё работает, поэтому возникает желание двигаться дальше.
Через некоторое время система может стать сложной быстрее, чем человек успевает её понять.
Именно здесь старое правило Unix приобретает новый смысл. Перед тем как просить искусственный интеллект написать больше кода, полезно спросить: какую одну вещь должен делать этот компонент?
Чем меньше задача, тем лучше работает искусственный интеллект
Есть и практическая сторона. Искусственному интеллекту проще работать с чётко ограниченной задачей, чем со всей системой сразу.
Если компонент имеет понятный вход, понятный выход и одну ответственность, его легче описать, написать, проверить и при необходимости заменить. Ошибка тоже локализуется гораздо быстрее.
Когда же одному запросу поручают переделать сразу несколько взаимосвязанных частей большой системы, результат становится труднее проверить. Код может выглядеть убедительно, но последствия изменений распределяются по всей архитектуре.
Поэтому принцип маленьких, ясных компонентов полезен сегодня не меньше, чем во времена первых Unix-систем.
Простота требует дисциплины
Парадокс в том, что написать большую универсальную программу иногда проще, чем сохранить систему маленькой.
Новая возможность почти всегда просится в уже существующий компонент. Создать ещё одну границу, определить простой интерфейс и решить, где именно должна находиться ответственность, требует дополнительного размышления.
Простота редко появляется сама.
Её приходится защищать.
Нужно уметь сказать, что эта функция относится к другому компоненту, что эта зависимость здесь лишняя, а этот удобный на первый взгляд шаг слишком сильно связывает две части системы.
Именно поэтому простота — не отсутствие архитектуры. Это результат хорошей архитектуры.
Одна вещь — не ограничение, а свобода
Когда программа делает одну вещь, она кажется менее мощной. Но если таких программ много и их можно свободно соединять, система в целом получает гораздо больше возможностей.
В этом и состоит один из парадоксов Unix. Ограниченные инструменты вместе оказываются более гибкими, чем один инструмент, который пытается предусмотреть всё заранее.
Поэтому правило «делай одну вещь и делай её хорошо» пережило конкретные версии Unix и старые компьютеры.
Это не правило о размере программы.
Это правило о границах.
Чем яснее границы частей системы, тем проще их понимать, проверять, заменять и соединять. А значит, тем дольше сама система может оставаться управляемой.