Как писать на машинном коде
Перейти к содержимому

Как писать на машинном коде

  • автор:

Машинный код пример

Машинный код — это ни что иное, как набор инструкций, написанных на машинном языке и понимаемых компьютером. Для большинства программистов, особенно для начинающих, машинный код может показаться сложным и запутанным. Однако, понимание машинного кода является важным навыком для любого разработчика программного обеспечения. В этой статье мы рассмотрим несколько примеров машинного кода, которые помогут вам лучше понять эту технологию.

Один из примеров машинного кода, который можно использовать для изучения, — это код, который складывает два числа. Например, если у нас есть два числа — 5 и 7, то мы можем написать машинный код, который сложит эти два числа и выведет результат. Код может выглядеть примерно так:

00110001 00000100 00001000

Другой интересный пример машинного кода — это код, который осуществляет простую арифметическую операцию — умножение. Если мы хотим умножить два числа — 3 и 4, то мы можем использовать следующий машинный код:

00000011 00000101 00000100

Это только небольшая часть того, что можно сделать с машинным кодом. Как программист, вы можете использовать машинный код для создания сложных алгоритмов, обработки данных и многого другого. Понимание машинного кода может не только помочь вам лучше понять внутреннее устройство компьютера, но и дать вам больше контроля над вашим программным обеспечением.

Что такое машинный код?

Машинный код обычно представлен в виде последовательности байтов (цифр в двоичной системе счисления), которые кодируют определенные операции процессора. Каждая команда машинного кода может выполнять основные операции, такие как сложение, вычитание, перемещение данных и управление выполнением программы.

Машинный код полностью зависит от конкретного процессора и его архитектуры. Каждый процессор имеет свой набор инструкций и формат данных, поэтому машинный код, предназначенный для одного процессора, может быть непонятен или неисполним на другом.

Программистам обычно не требуется писать программы на машинном коде, так как это очень низкоуровневый и сложный язык. Однако понимание машинного кода может быть полезным для оптимизации и отладки программ, а также для создания компиляторов и интерпретаторов.

Несмотря на то, что машинный код может показаться громоздким и трудным для чтения, с определенным опытом программисты могут научиться интерпретировать и анализировать его. Исходный код программы, написанной на высокоуровневом языке программирования, может быть скомпилирован в машинный код для исполнения компьютером.

Понятие и основные принципы

Основные принципы машинного кода:

  • Низкоуровневость — машинный код понимает и выполняет процессор компьютера без каких-либо преобразований. Каждая инструкция машинного кода соответствует определенному микрооперации, выполняемой процессором.
  • Зависимость от архитектуры компьютера — каждый процессор имеет свою собственную архитектуру и набор инструкций машинного кода. Поэтому программы, написанные для одной архитектуры, не будут работать на другой.

Для удобства восприятия машинного кода программистами разработаны высокоуровневые языки программирования, которые позволяют писать код на более понятном для человека уровне. Однако для понимания работы компьютеров и оптимизации программы важно понимать основные принципы и уметь работать с машинным кодом.

Смотрите также: Установка последней версии Python на Ubuntu

Примеры машинного кода для программистов

Программистам может быть полезно изучать машинный код, чтобы лучше понимать, как работает компьютер и как выполняются их программы на более высокоуровневых языках программирования. Ниже приведены некоторые примеры машинного кода для программистов:

  1. MOV AX, 5 Эта инструкция перемещает значение 5 в регистр AX. В языке программирования x86, AX — это 16-битный регистр общего назначения, который может использоваться для хранения целочисленных значений.
  2. ADD BX, AX Эта инструкция складывает значения, хранящиеся в регистрах BX и AX, и сохраняет результат в регистре BX. Подобные инструкции позволяют программисту выполнять арифметические операции, такие как сложение, вычитание и умножение.
  3. CMP CX, 10 Эта инструкция сравнивает значение, хранящееся в регистре CX, с числом 10. Результат сравнения сохраняется во флагах процессора и может быть использован для принятия решения о переходе к другой части программы.
  4. JNZ LOOP_START Эта инструкция отвечает за условный переход в программе. Если во флагах процессора результат предыдущего сравнения не равен нулю, то выполнение программы будет перенесено к метке LOOP_START, которая определена в другом месте программы.

Это только несколько примеров машинного кода, которые могут быть полезны программистам. Изучение машинного кода помогает лучше понять работу компьютера, оптимизировать программы и владеть различными низкоуровневыми техниками программирования.

Простые примеры для новичков

Машинный код может показаться сложным и непонятным для новичков в программировании. Однако, с помощью простых примеров можно легко понять основы и начать разбираться в этой теме.

  1. Пример простого машинного кода для сложения двух чисел:
    • mov ax, 5
    • add ax, 3

В данном примере мы загружаем значение 5 в регистр ax с помощью команды mov, а затем прибавляем к нему значение 3 с помощью команды add. Результат будет сохранен в том же регистре ax.

  • mov ah, 0x09
  • mov dx, offset message
  • int 0x21
  • message db ‘Hello, world!’,0

В данном примере мы используем прерывание 0x21 для вызова функции, которая будет отображать текст на экране. С помощью команд mov мы загружаем значения регистров ah и dx, указывающие на соответствующие данные, а затем вызываем прерывание int 0x21. В переменной message хранится сам текст, который будет выведен.

  • mov ax, 5
  • cmp ax, 10
  • jg greater
  • less:
  • mov bx, 0
  • jmp end
  • greater:
  • mov bx, 1
  • end:

В данном примере мы сравниваем значение регистра ax с числом 10 с помощью команды cmp. Затем с помощью команды jg мы проверяем условие и переходим к метке greater, если регистр ax больше 10. В противном случае, мы переходим к метке less. В зависимости от условия, мы выполняем различные инструкции, в данном случае, просто сохраняем значение 0 или 1 в регистр bx.

Таким образом, посредством этих простых примеров можно начать понимать и использовать машинный код при программировании.

Примеры сложного машинного кода

Машинный код представляет собой набор инструкций, которые процессор может прочитать и выполнить. Чем сложнее вычисления в программе, тем сложнее будет машинный код, соответствующий этим вычислениям. Рассмотрим несколько примеров сложного машинного кода:

1. Пример сортировки массива

Сортировка массива – это одна из основных операций в программировании. Пример сложного машинного кода для сортировки массива может выглядеть следующим образом:

  • MOV CX, [Size] – загрузка значения переменной Size в регистр CX
  • MOV SI, OFFSET Array – загрузка адреса массива Array в регистр SI
  • Loop1:
    • MOV BX, [SI] – загрузка значения из памяти по адресу, хранящемуся в регистре SI, в регистр BX
    • CMP [SI+2], BX – сравнение значения в ячейке памяти, находящейся по адресу SI+2, с значением в регистре BX
    • JGE SkipSwap – переход к метке SkipSwap, если предыдущее сравнение было неравно или меньше
    • MOV AX, [SI+2] – загрузка значения в ячейке памяти, находящейся по адресу SI+2, в регистр AX
    • MOV [SI], AX – сохранение значения из регистра AX в ячейку памяти, находящуюся по адресу, хранящемуся в регистре SI
    • MOV [SI+2], BX – сохранение значения из регистра BX в ячейку памяти, находящуюся по адресу SI+2
    • SkipSwap:
    • ADD SI, 2 – прибавление значения 2 к значению в регистре SI
    • LOOP Loop1 – уменьшение значения в регистре CX на 1 и переход к метке Loop1, если значение в регистре CX не равно 0

    2. Пример вычисления факториала

    Вычисление факториала – это еще одна распространенная операция в программировании. Пример сложного машинного кода для вычисления факториала может быть следующим:

    • MOV CX, [Number] – загрузка значения переменной Number в регистр CX
    • MOV AX, 1 – загрузка значения 1 в регистр AX
    • Loop1:
      • MUL CX – умножение значения в регистре AX на значение в регистре CX и сохранение результата в регистре AX
      • SUB CX, 1 – вычитание 1 из значения в регистре CX
      • JNZ Loop1 – переход к метке Loop1, если значение в регистре CX не равно 0

      Это лишь два примера сложного машинного кода, которые могут использоваться в программировании. Машинный код может быть гораздо более сложным, в зависимости от требуемых операций и алгоритмов.

      Описание работы программиста с машинным кодом

      Работа программиста с машинным кодом может включать в себя несколько шагов:

      1. Анализ и понимание задачи. Программист должен внимательно изучить требования и спецификации, определить, какие операции должны быть выполнены процессором, и какие данные должны быть обработаны. Это поможет ему составить алгоритм и определить необходимые инструкции в машинном коде.
      2. Написание кода на языке ассемблера. Машинный код может быть представлен в более удобной форме с помощью языка ассемблера. Язык ассемблера предлагает набор символов и инструкций, которые упрощают написание кода на машинном уровне.
      3. Трансляция ассемблерного кода в машинный код. Ассемблерный код не может быть непосредственно исполнен процессором, поэтому необходима его трансляция в машинный код с помощью специальных программ, называемых ассемблерами. Ассемблер преобразует каждую инструкцию ассемблерного кода в соответствующий машинный код.
      4. Тестирование и отладка. После получения машинного кода программист должен его протестировать и проверить его работоспособность. В случае ошибок, необходимо выполнить отладку и найти и исправить ошибки в коде.

      Работа с машинным кодом требует от программиста глубокого понимания аппаратной архитектуры компьютера, а также владения языком ассемблера и умения анализировать и понимать код на машинном уровне. Это очень важные навыки, которые позволяют программисту создавать эффективный и оптимизированный код, ближе к железу компьютера.

      Вопрос-ответ:

      Какие языки программирования используют машинный код?

      Машинный код используется во всех языках программирования, так как компьютеру понятным является только машинный код. Однако, программисты обычно пишут на высокоуровневых языках программирования, которые компилируются в машинный код.

      Можно ли программировать на машинном коде напрямую?

      Теоретически, можно программировать на машинном коде напрямую, но это крайне трудно и неудобно. Машинный код представляет собой набор инструкций и данных, записанных в виде двоичного кода, который понятен только компьютеру. Поэтому обычно программисты пишут на высокоуровневых языках программирования, которые затем компилируются в машинный код.

      Как выглядит машинный код?

      Машинный код представляет собой последовательность инструкций и операндов, записанную в виде двоичного кода. Каждая инструкция имеет свой определенный формат, состоящий из опкода (операционного кода) и операндов (данных или адресов). Например, машинный код может выглядеть так: 10110010 11001000, где 10110010 — опкод, а 11001000 — операнд.

      Почему нужно знать машинный код?

      Знание машинного кода может быть полезно программистам в некоторых случаях. Например, при оптимизации кода или дебаггинге, знание машинного кода позволяет лучше понять, как работает программа на низком уровне и какие именно инструкции выполняются. Кроме того, зная машинный код, программист может более эффективно использовать аппаратные возможности компьютера.

      Какие есть примеры машинного кода для программистов?

      Примеры машинного кода могут быть разными, в зависимости от архитектуры процессора. Например, для процессора x86 ассемблерные команды могут выглядеть так: mov eax, 5 (переместить значение 5 в регистр eax), add ebx, eax (прибавить значение в регистре eax к значению в регистре ebx). Это лишь простые примеры, а машинный код, как правило, гораздо сложнее и содержит множество инструкций и операндов.

      Какие примеры машинного кода считаются понятными для программистов?

      Для программистов, чтобы код был понятен, он должен быть представлен в виде команд, которые они могут легко прочитать и понять. Примеры машинного кода, которые считаются понятными для программистов, включают команды, такие как «MOV» (переместить значение из одного места в другое), «ADD» (сложить два значения) и «CMP» (сравнить два значения).

      Машинный код и компиляция в него — это как?

      Хочу поработать с этим, потому что всегда мечтал попробовать собрать .exe самостоятельно, без помощи готовых компиляторов.

      Отслеживать
      6,651 6 6 золотых знаков 30 30 серебряных знаков 53 53 бронзовых знака
      задан 21 апр 2020 в 8:02
      Krutos VIP Krutos VIP
      69 2 2 серебряных знака 9 9 бронзовых знаков

      Если идти от сложного: изучить мануалы intel по платформе x86 — чтобы знать как писать машинный код, перед этим еще изучить ассемблер, хотя бы на минимальном уровне; изучить формат исполняемых файлов для вашей системы (portable executable для Windows или ELF для Linux); изучить как вообще создаются компиляторы — тут «Книга Дракона» в помощь.

      21 апр 2020 в 8:22

      Если от более простого — отказаться от идеи самому создавать бинарник, а генерировать например промежуточный Си код, который уже будет компилироваться в бинарный (или воспользоваться инструментарием LLVM). Но опять же в этом случае все равно нужно будет изучить «Книгу Дракона».

      21 апр 2020 в 8:25
      Связанный вопрос: Генерация exe файла
      21 апр 2020 в 8:25
      Не совсем по теме, но, возможно, вдохновит — история одного байта, fermi paradox
      – user249284
      21 апр 2020 в 9:54

      1 ответ 1

      Сортировка: Сброс на вариант по умолчанию

      Baremetal

      Каждый конкретный процессор (например, Intel Core i3-4160 или ARM Cortex-A9) имеет свою микроархитектуру и реализует архитектуру уровня набора команд (англ. instruction set architecture).

      • Микроархитектура определяет структуру процессора на уровне электронных компонентов и логических вентилей.
      • Архитектура уровня набора команд (ISA), грубо говоря, определяет то, какие команды может выполнять процессор. Эта архитектура абстрагированна от микроархитектуры. Процессоры разных комнаний могут реализовывать одну и ту же архитектуру (например, многие процессоры Intel и AMD реализует одно и то же семейство архитектур x86).

      Если два процессора реализуют одну и ту же ISA, то они могут исполнять одни и те же программы. ISA определяет, какие команды доступны программисту, какие регистры он может использовать, как он может использовать страничную адресацию, виртуальную память и т. д. Кроме того, она определяет формат команд, которые понимает процессор.

      Каждая программа процессора — это просто набор подряд идущих команд. При своем запуске процессор выбирает команду из память по адресу, называемому вектором сброса (англ. reset vector) и начинает исполнять эту программу, пока питание не будет отключено.

      Написать программу в машинных кодах достаточно просто — нужно лишь взять справочник по ISA (например, Intel 64 and IA-32 Architectures Software Developer Manuals), которую реализует ваш процессор и написать нужные команды байт за байтом.

      Конечно, в наше время никто в машинных кодах не пишет, потому что человеку тяжело работать с большим объемом чисел и сложными форматами команд (особенно в x86). Из-за таких сложностей были придуманы языки ассемблера, которые вводят простые мнемоники для инструкций процессора.

      Например, одна инструкция ассемблера x86 MOV может кодировать около 20 различных инструкций процессора MOV 1 . Ассемблер читает вашу программу на языке ассемблера и переводит ее в бинарный файл 2 , который, опять же, является просто последовательность байт, кодирующих подряд идущие инструкции процессора.

      Вот так может выглядет отрывок программы на языке ассемблера:

      cli lgdt (gdtr) mov %cr0, %eax or $0x1, %eax mov %eax, %cr0 

      Вот так выглядит программа на машинном языке:

      0000000 05ea 007c 3100 8ec0 8ed8 bcd0 7c00 1688 0000010 7cdb c031 c08e 00bb 8a80 db16 b67c b100 0000020 b502 b000 e830 0053 59e8 8400 75c0 fa30 0000030 010f f416 0f7c c020 8366 01c8 220f eac0 0000040 7c44 0008 b866 0010 d88e c08e e08e e88e 0000050 d08e 00bc 07c0 e800 03a4 0000 ebf4 befd 0000060 7cbc 03e8 f400 fdeb 5350 30fc b4ff ac0e 0000070 c084 0474 10cd f7eb 585b b4c3 cd02 7213 0000080 3102 c3c0 1e9c 0657 fa56 c031 d88e 10bf 0000090 f705 8ed0 bec0 0500 058a 2650 048a 2650 00000a0 04c6 c600 be05 8026 be3c 2658 0488 8858 00000b0 3105 74c0 4001 075e 1f5f c39d 3241 2030 00000c0 7369 6420 7369 6261 656c 2e64 4820 6c61 00000d0 2074 6874 2065 5043 2e55 0000 0000 0000 00000e0 0000 0000 ffff 0000 9a00 00cf ffff 0000 00000f0 9200 00cf 0017 7cdc 0000 0000 0000 0000 

      Очевидно, что асссемблерный код и читать, и писать проще.

      Теперь у вас достаточно знаний, чтобы открыть справочник, как по словарю, написать программу в машинных кодах и исполнить ее на процессоре. Но, это не сработает в случае, если вы хотите написать программу, которая будет работать в какой-либо операционной системе.

      Операционная система

      Операционная система — это еще один уровень абстрации, который полностью лишает нас возможности неограниченно пользоваться нашим процессором, заставляя его исполнять любые наши команды 3 . ОС делает очень много различных вещей, но остановимся только на одной — запуск исполняемых файлов.

      Как я уже сказал, каждая программа процессора — это просто последовательность команд, однако каждая программа операционной системы — это особая последовательность байт, имеющая специальную структуру, в которую входят не только команды процессора.

      Если брать в пример ОС Windows 10, она работает с исполняемыми файлами .exe , которые имеют специальный формат, называемый Portable Executable. Он имеет довольно сложную структуру. Помимо собственно набора машинных команд он содержит в себе информацию необходимую для определения адреса и размера секций, таблиц импорта и экспорта, специальную сигнатуру и т. д.

      Поэтому чтобы вручную написать программу в машинных кодах, которая будет запускаться в Windows 10, например, нам, по-мимо написания самой программы, потребуется привести ее к формату Portable Executable.

      Но и этого будет не достаточно. Нам придется ознакомится с соглашениями, которые называются ABI и написать программу в машинных кодах, используя именно эти соглашения, а не какие-то другие.

      Здесь необходимо, чтобы все части паззла подходили друг к другу по форме: программа должна быть валидной для процессора, формат бинарного файла должен быть понятен операционной системе, программа должна уметь корректно общаться с ОС и т. д. Это все очень сложно обеспечить, если писать программу в шестнадцатеричном редакторе.

      Можете начать с написания программ на языке ассемблера (да, вам придется еще выучить синтаксис конкретного языка ассемблера и диалект Intel или AT&T). «Hello, World» на языке NASM будет выглядеть так:

      ; ---------------------------------------------------------------------------- ; helloworld.asm ; ; This is a Win32 console program that writes "Hello, World" on one line and ; then exits. It needs to be linked with a C library. ; ---------------------------------------------------------------------------- global _main extern _printf section .text _main: push message call _printf add esp, 4 ret message: db 'Hello, World', 10, 0 

      А нужно ли вам это?

      В наше время компьютеры стали очень сложными, с десятками слоями абстраций. Даже инструкции ISA современных процессоров — не атомарные сущности, и процессоры выполняет каждую такую инструкцию как набор еще более мелких инструкций — микрооперации (из таких мокроопераций складывается микрокод).

      На самом деле, умение писать на языке ассемблера (а тем более, на машинном языке) довольно бесполезно. Умение просто читать и понимать ассемблерный листинг гораздо более практично и действительно может вам пригодится.

      А непрактично это в первую очередь потому, что ничего сложнее «Hello, World!» в машинных кодах вы не напишете. На ассемблере — да, напишете, но потратите на это колоссальное количество времени, которое можно было бы потратить на более полезные вещи.

      1. Что интересно, инструкция MOV в x86 является Тьюринг-полной, т. е. любая программа может быть написана с использованием одной только этой инструкции. Есть даже специальный компилятор, который использует только одну эту инструкцию.

      2. Некоторые ассемблеры могут сразу формировать исполняемые файлы в нужном формате. В том числе и Portable Executable.

      3. Я говорю о современных ОС типа Windows или Linux.

      Как написать «Hello World» на машинном коде?

      Недавно наткнулся на видео, где некий сумасшедший написал программу, которая выводит в консоль слова «Hello World» на бинарном/машинном коде (если честно я не уверен что это именно).

      Расскажите пожалуйста, как можно повторить результат (не советуйте скопировать код из видео, хочу именно понимать как такое сделать)? Может какая-то литература по этому поводу?

      • Вопрос задан более трёх лет назад
      • 33472 просмотра

      Комментировать
      Решения вопроса 1

      Вам достаточно изучить ассемблер и всё станет понятно.
      Каждая команда ассемблера транслируется «дословно» в машинный код.
      Т.е. например команда pushl %edx превратится в один байт 82

      Например программа на ассемблере:

      push %ebp mov %esp,%ebp call 0x8048298 cmp $0x41,%eax jne 0x80483ce push $0x80484b0 call 0x80482c8 add $0x4,%esp mov $0x0,%eax mov %ebp,%esp pop %ebp ret

      превратится в машинный код:

      0x55 0x89 0xe5 0xe8 0xfc 0xff 0xff 0xff 0x83 0xf8 0x41 0x75 0x0d 0x68 0x00 0x00 0x00 0x00 0xe8 0xfc 0xff 0xff 0xff 0x83 0xc4 0x04 0xb8 0x00 0x00 0x00 0x00 0x89 0xec 0x5d 0xc3

      Почитать можно здесь и здесь . В целом подойдет любая книга по ассемблеру.

      Ответ написан более трёх лет назад

      Mrrl

      Во времена MSDOS было бы понятно — int 21h, и вывод на консоль в кармане. Можно даже структуры exe-файла не знать, писать сразу в com. А сейчас что делать? Поможет ли ассемблер?

      Mrrl: Не понял ваш вопрос. На чистом асме писать нет смысла — только для общего развития или для векторизации SIMD инструкций и для реверс инжиниринга. Если нужен асм, то самый простой способ — делать вставки в код на С/С++. А трансляцией в машинный код должен заниматься компилятор

      Mrrl

      asd111: Я так понял, что вопрос — как написать программу полностью. Желательно, в 16-ричном редакторе. Любая программа, использующая C/C++ займёт огромный объём, без ошибок вручную его не сгенерируешь и не введёшь. Для com-файла в MSDOS такие трюки были вполне реальны, но как это сделать в современных операционных системах?

      Mrrl: В современных системах даже если писать всё вручную, меньше чем код на С в любом случае не получится, т.к. необходимо сохранить формат исполняемого файла т.е. для Linux например нужно будет прописать все заголовки ELF файла в то время как С — по сути дела высокоуровневый ассемблер, т.е. код на С практически дословно транслируется в код на ассемблере + готовые заголовки под нужную ОС + оптимизации компилятора.

      Mrrl

      asd111: Не забывайте, что если вы пишете на С, то вам будет необходимо иметь правильную версию C-библиотеки, а она тоже занимает место (и в какой-нибудь Embedded XP её может сразу не оказаться). Либо использовать статическую линковку, что очень резко увеличит размер кода. Системные вызовы в этом смысле экономнее. Судя по кодам для Linux, у них есть команда syscall (тоже какое-то прерывание?), которая позволяет написать совсем короткую программу. Про ELF-файл пока не скажу, с исполняемыми файлами для Linux мне разбираться пока не пришлось.

      Принципы SOLID, только понятно

      Когда я только знакомился с принципами SOLID, я искал понятные статьи на Хабр. При этом пришлось прочитать не одну статью, и полное понимание пришло сильно позже. Хотелось бы, чтобы новички на более простых примерах смогли почувствовать, о чем эти принципы:

      Что такое SOLID и зачем оно надо?

      При написании кода программисту следует руководствоваться определенными правилами. Часто эти правила написаны если не кровью, то слезами разработчиков, которые потом стараются исправить ваш код. Если это вообще возможно 🙂

      Принципы S.O.L.I.D. — это 5 принципов, которые желательно принять во внимание программисту. В этой серии постов мы рассмотрим их один за другим. Принципы справедливы почти для любого современного ЯП.

      Single Responsibility Principle — принцип единственной ответственности
      Open Closed Principle — принцип открытости-закрытости
      Liskov Substitution Principle — принцип подстановки Барбары Лисков
      Interface Segregation Principle — принцип разделения интерфейса
      Dependency Inversion Principle — принцип инверсии зависимостей

      Максимально кратко про каждый принцип

      Дисклеймер

      �� Disclaimer — полное понимание, как написать код в той или иной ситуации, приходит только с опытом. Примеры упрощенные и призваны познакомить с концепциями принципов.

      Single Responsibility Principle — принцип единственной ответственности

      Каждый класс должен отвечать только за одну зону ответственности (действий), чтобы его было проще дополнять и менять. Изменение должно минимально затрагивать код. Следует разделить функциональность большого класса на более мелкие, отвечающие за конкретные аспекты, части.

      Пример нарушения принципа:

      struct Robot < void move() < /*Метод для передвижения*/ >void speak() < /*Метод: сказать фразу*/ >>;
      • Класс отвечает за несколько логически разных зон. Если будем менять или дополнять один из методов, изменения могу повлиять на второй метод, что усложняет поддержку кода.
      • Если будем добавлять методы (например, метод для полета робота), то изменение повлияет на весь класс. Пример с роботом — простой. В более сложных структурах изменение будет сделать крайне тяжело. А мы могли изменить только ту часть, которая отвечает за отдельную зону ответственности робота (в данном случае, полет — еще один вариант движения).
      • Код, отвечающий за движение, сложнее переиспользовать в другом классе, например, для самолета. Сейчас он вшит в робота и переиспользовать его невозможно.
      struct Movement < void move() < /*Сложная логика движения*/ >>; struct Speaker < void speak() < /*Сложная логика произнесения фразы*/ >>; class Robot < public: void move() < /*Простое использование movement*/ >void speak() < /*Простое использование speaker*/ >private: Movement movement; // Логика передвижения Speaker speaker; // Логика произнесения фразы >;

      Open Closed Principle — принцип открытости-закрытости

      Класс должен быть закрыт для изменения, но открыт для расширения. Пишем код так, чтобы другие могли легко расширить функционал, не меняя написанный (оттестированный, понравившийся твоему начальнику) код.

      Пример нарушения принципа:

      Допустим, есть игровой персонаж — рыцарь

      struct Character < void displayInfo() < std::cout >;

      Мы добавляем возможность играть еще и за волшебника

      struct Character < void displayInfo(const std::string& type) < if (type == "Knight") std::cout >;
      • При добавлении персонажа приходится добавлять все больше условий. При наличии 1000 персонажей это будет работать медленно.
      • Класс начинает обладать функционалом, который, может, и не нужен. У мага может быть набор заклинаний, у рыцаря его нет.
      • Каждое изменение в основном коде может потребовать изменения во всех зависимых частях кода. Например, придется везде менять меч на посох, меняя написанную логику. Это увеличивает риск ошибок и усложняет процесс добавления изменения.
      • Существующий код уже написан, оттестирован, одобрен. Его изменение может вызвать много лишних проблем. Лучше не трогать то, что уже работает.

      Нужно разработать класс так, чтобы новых персонажей можно было добавлять, не редактируя существующий код. Один из вариантов:

      struct Character < virtual void displayInfo() = 0; >; struct Knight : public Character < void displayInfo() override < std::cout >; struct Wizard : public Character < void displayInfo() override < std::cout >; // Любые другие персонажи int main() < Character* character = new Knight(); character->displayInfo(); // Я Рыцарь delete character; character = new Wizard(); character->displayInfo(); // Я Маг delete character; return 0; >

      Liskov Substitution Principle — принцип подстановки Барбары Лисков

      [Base] -> [Derived]

      Если в коде программы Базовый класс заменить на его Наследника, то программа должна работать, так как в Наследнике есть все операции, которые были в Базовом. В Базовый класс нужно выносить только общую логику, которую наследники будут реализовывать. Наследников создаем только тогда, когда они правильно собираются реализовать логику Базового класса без проблем.

      Пусть есть человек, который умеет только есть и спать

      struct Person < virtual void eat() < std::cout virtual void sleep() < std::cout >;

      И есть студент, который наследуется от Person. Он, помимо есть и спать, может еще учиться

      struct Student : public Person < // То, что умеет человек, а также. void learn() < std::cout >;

      И он умеет делать все то, что умеет человек. Потому что студент — это человек, кто бы что не говорил.

      Пример нарушения всех мыслимых и немыслимых норм мог бы быть таким:

      struct Student : public Person < // . void sleep() override < std::cout void learn() < std::cout >;

      Тут студента уже нельзя назвать человеком. Потому что он не делает все то же, что и человек, плюс что-то дополнительно. Ну и ведет себя как не будем говорить кто.

      Наследник ведь расширяет интерфейс. Значит, только добавляет дополнительный функционал (говорили про это в Open-Closed). Значит, он не должен ломать то, что написано раньше.

      ��Интересный момент: что в данном случае является Базовым классом, а что Наследником?

      • Квадрат
      • Прямоугольник

      Ответ: кажется, что, так как из математики, квадрат — это прямоугольник, то Базовый класс — прямоугольник, а квадрат — его особенная версия. Но на деле все сложнее, тут однозначного ответа нет. Хотите подробнее — читайте Эффективное использование C++. Скотт Майерс. Правило 32. В комментах ссылка на файл.

      Interface Segregation Principle — принцип разделения интерфейса

      Клиенты не должны зависеть от интерфейсов, которые они не используют. Большие интерфейсы следует разбивать на интерфейсы поменьше. Так клиенты смогут использовать только те интерфейсы, которые им нужны. Это делает менее связанный код, уменьшает зависимости между элементами системы, упрощает изменения в коде.

      Пример будет на Java, потому что в Java синтаксически есть интерфейс, в C++ это немного по-другому работает

      Пример нарушения принципа:

      interface Robot

      В этом примере, если какой-нибудь DroneRobot использует метод fly(), но не использует методы move() и speak(), он все равно должен реализовывать интерфейс, который включает эти методы. Это приводит к тому, что класс DroneRobot зависит от интерфейсов, которые ему не нужны.

      interface Movable < void move(); >interface Speakable < void speak(); >interface Flyable < void fly(); >// Реализуем только необходимые интерфейсы class Robot implements Movable, Speakable < @Override public void move() < /*Сложная логика движения*/ >@Override public void speak() < /*Сложная логика произнесения фразы*/ >> // Реализуем только необходимые интерфейсы class Drone implements Flyable < @Override public void fly() < /*Сложная логика полета*/ >>

      Dependency Inversion Principle — принцип инверсии зависимостей

      Модули верхних уровней не должны зависеть от модулей нижних уровней. Оба типа модулей должны зависеть от абстракций. Абстракции не должны зависеть от реализации. Реализация должна зависеть от абстракции.

      Пример нарушения принципа:

      struct Database < void saveData(User user) < /*Сохранение данных в БД*/ >>; class UserService < private: Database database; public: void addUser(User user) < database.saveData(user); // Дополнительная логика >>;

      UserService зависит от конкретного класса Database, что делает его менее гибким и более зависимым от изменений в Database. Если пользователя нужно будет еще записывать в файл, отправлять на другой сервис в определенном виде, то придется переписывать и изменять много кода. Модуль верхнего уровня тесно связан с модулем нижнего уровня, а нужно, чтобы оба они зависели от абстракции.

      Используем абстракцию (интерфейс) для БД:

      class IDatabase < public: virtual void saveData(User user) = 0; >; class Database : public IDatabase < public: void saveData(User user) override < /*Сохранение данных в БД*/ >>; class UserService < private: IDatabase& database; public: UserService(IDatabase& db): database(db) <>void addUser(User user) < database.saveData(user); // Дополнительная логика >>;

      Здесь UserService слабо связан с базой данных, он зависит от абстракции в виде интерфейса. Не важно, какая база данных будет использоваться и как она внутри работает (обрабатывает пользователя), написанный код меняться не будет.

      Заключение

      Вот и все. Рад, если вам понравилось и было полезно. Приглашаю к дискуссии в комментариях, если есть какие-то вопросы/предложения/критика.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *