Абстракция в ООП: как создать гибкую архитектуру программы
Для кого эта статья:
- Программисты и разработчики, заинтересованные в объектно-ориентированном программировании
- Студенты и начинающие специалисты в области программирования, обучающиеся концепциям абстракции и ООП
Архитекторы программного обеспечения и опытные разработчики, ищущие улучшения в архитектуре своих систем через применение абстракции
Представьте, что вы управляете бойцовским клубом из одноименного фильма, но вместо бойцов у вас — концепции кода. Абстракция — это тот самый первый боец, который выходит на ринг и устанавливает правила игры для всего объектно-ориентированного программирования. Она позволяет программисту видеть лес, не отвлекаясь на деревья, и работать с сущностями на уровне идей, а не технических деталей. Давайте разберемся, как этот фундаментальный принцип ООП трансформирует хаос кода в элегантную архитектуру. 🧠
Ищете способ перейти от теоретических знаний об абстракции к практическому применению в реальных проектах? Курс Java-разработки от Skypro построен на принципе "от простого к сложному", где вы не просто изучите абстракцию и другие концепции ООП, но и примените их в разработке корпоративных приложений под руководством действующих разработчиков. Получите не только знания, но и опыт проектирования устойчивых программных систем.
Что такое абстракция в ООП и её роль среди принципов программирования
Абстракция — это процесс выделения существенных характеристик объекта, отличающих его от других объектов, и игнорирования несущественных деталей. В контексте ООП абстракция позволяет создать упрощенную модель сложной системы, фокусируясь на том, что объект делает, а не на том, как он это делает.
Этот принцип помогает справиться с растущей сложностью программных систем, позволяя разработчикам мыслить на более высоком уровне и работать с объектами, как с "черными ящиками", которые предоставляют определенные интерфейсы без раскрытия внутренних механизмов их работы.
Дмитрий Ковалев, архитектор программного обеспечения
Несколько лет назад наша команда столкнулась с необходимостью полностью переписать платежную систему, обрабатывающую более миллиона транзакций в день. Исходный код был монолитным, с множеством тесно связанных компонентов и дублирующейся логикой.
Мы решили применить строгий подход к абстракции, создав иерархию платежных провайдеров. Сначала мы определили, что абсолютно все провайдеры должны поддерживать три операции: проверку возможности оплаты, выполнение транзакции и отмену транзакции. Это стало нашим базовым интерфейсом.
Важно отметить, что мы абстрагировались от конкретных технологий, используемых каждым провайдером. Неважно, что внутри — SOAP, REST или вообще прямое подключение к базе данных. Снаружи все провайдеры выглядели одинаково.
Результат оказался впечатляющим. Система стала не только более поддерживаемой — мы смогли добавить семь новых платежных методов всего за квартал, тогда как раньше интеграция одного метода занимала до двух месяцев. А ещё абстракция помогла нам увеличить покрытие кода тестами с 15% до 87% благодаря возможности создавать мок-объекты на основе интерфейсов.
Абстракция не существует в вакууме — она тесно связана с другими принципами ООП и составляет фундамент для их реализации:
| Принцип ООП | Связь с абстракцией | Практическое значение |
|---|---|---|
| Инкапсуляция | Абстракция определяет, что должно быть инкапсулировано | Скрытие реализации и защита данных от внешнего вмешательства |
| Наследование | Абстракция определяет общие свойства и методы, которые наследуются | Создание иерархий классов и повторное использование кода |
| Полиморфизм | Абстракция создает основу для полиморфного поведения | Возможность использовать объекты разных классов через общий интерфейс |
Уровни абстракции в программировании можно представить как своеобразную пирамиду:
- Высокий уровень — бизнес-объекты и их поведение (пользователь, заказ, корзина)
- Средний уровень — технические абстракции (хранилище, сессия, подключение)
- Низкий уровень — системные абстракции (файл, поток, сокет)
Чем выше уровень абстракции, тем меньше технических деталей видно программисту и тем ближе код к бизнес-языку. Именно это и делает принципы ООП в программировании столь ценными для разработки крупных систем. 🧩

Механизмы реализации абстракции: интерфейсы и абстрактные классы
Для реализации абстракции в объектно-ориентированных языках программирования обычно используются два основных механизма: интерфейсы и абстрактные классы. Понимание их различий и правильное применение — ключ к созданию гибкой и поддерживаемой архитектуры.
Интерфейсы
Интерфейс — это контракт, который определяет, какие методы должен реализовать класс, но не указывает, как эти методы должны работать. Это чистая абстракция, своего рода "чертеж" для класса.
// Пример интерфейса на Java
public interface PaymentProcessor {
boolean validatePayment(Payment payment);
TransactionResult processPayment(Payment payment);
boolean cancelPayment(String transactionId);
}
Основные характеристики интерфейсов:
- Содержат только объявления методов без реализации (с Java 8 могут содержать default-методы)
- Могут содержать константы (public static final)
- Класс может реализовывать множество интерфейсов
- Не могут содержать конструкторы или состояние (поля)
Абстрактные классы
Абстрактный класс — это промежуточное звено между интерфейсом и конкретным классом. Он может содержать как абстрактные методы (без реализации), так и конкретные методы с реализацией.
// Пример абстрактного класса на Java
public abstract class AbstractPaymentProcessor {
protected Logger logger = LoggerFactory.getLogger(this.getClass());
public abstract boolean validatePayment(Payment payment);
public TransactionResult processPayment(Payment payment) {
if (!validatePayment(payment)) {
logger.error("Payment validation failed");
return new TransactionResult(false, "Validation failed");
}
// Общая логика обработки платежа
return doProcessPayment(payment);
}
protected abstract TransactionResult doProcessPayment(Payment payment);
public boolean cancelPayment(String transactionId) {
logger.info("Cancelling payment: " + transactionId);
// Общая логика отмены платежа
return doCancelPayment(transactionId);
}
protected abstract boolean doCancelPayment(String transactionId);
}
Основные характеристики абстрактных классов:
- Могут содержать абстрактные и неабстрактные методы
- Могут содержать поля, конструкторы и инициализаторы
- Класс может наследоваться только от одного абстрактного класса
- Могут определять доступ к членам (public, protected, private)
Сравнение интерфейсов и абстрактных классов:
| Критерий | Интерфейс | Абстрактный класс |
|---|---|---|
| Множественное наследование | Поддерживается | Не поддерживается |
| Состояние (поля) | Только константы | Любые поля |
| Методы | Абстрактные (с Java 8 — также default и static) | Абстрактные и конкретные |
| Конструкторы | Нет | Да |
| Модификаторы доступа | Все методы публичные | Любые модификаторы доступа |
Выбор между интерфейсом и абстрактным классом зависит от конкретной ситуации:
- Используйте интерфейс, когда:
- Нужно определить контракт для несвязанных классов
- Требуется множественное наследование
Определяете тип "способности" (например, Comparable, Serializable)
- Используйте абстрактный класс, когда:
- Есть общий код, который должны использовать подклассы
- Нужно определить состояние (поля), доступное подклассам
- Классы в иерархии имеют сильную связь "is-a" (является)
В современных ООП языках оба механизма предоставляют мощные инструменты для абстракции, и их комбинированное использование (классы, реализующие интерфейсы и наследующиеся от абстрактных классов) позволяет создавать гибкие и расширяемые архитектуры. ООП: основные понятия и принципы находят свое практическое воплощение именно через эти механизмы. 🔧
Практическое применение абстракции на разных языках программирования
Абстракция реализуется по-разному в различных языках программирования, но основная концепция остается неизменной. Рассмотрим, как этот принцип ООП в программировании применяется на практике в популярных языках.
Java
Java предоставляет мощные инструменты для абстракции через интерфейсы и абстрактные классы.
// Интерфейс для чтения данных
public interface DataReader {
byte[] read(String source);
void close();
}
// Абстрактная реализация с общей логикой
public abstract class AbstractDataReader implements DataReader {
protected boolean isClosed = false;
@Override
public void close() {
if (!isClosed) {
doClose();
isClosed = true;
}
}
// Абстрактный метод, который должен быть реализован подклассами
protected abstract void doClose();
}
// Конкретная реализация для файлов
public class FileDataReader extends AbstractDataReader {
private FileInputStream inputStream;
public FileDataReader(String filePath) throws FileNotFoundException {
this.inputStream = new FileInputStream(filePath);
}
@Override
public byte[] read(String source) {
// Реализация чтения из файла
// ...
return data;
}
@Override
protected void doClose() {
try {
inputStream.close();
} catch (IOException e) {
// Обработка ошибки
}
}
}
C#
C# также поддерживает абстракцию через интерфейсы и абстрактные классы, но имеет некоторые особенности, например, свойства и явную реализацию интерфейсов.
// Интерфейс с определением свойств
public interface ILogger {
LogLevel MinLevel { get; set; }
void Log(string message, LogLevel level);
bool IsEnabled(LogLevel level);
}
// Абстрактная реализация
public abstract class BaseLogger : ILogger {
public LogLevel MinLevel { get; set; } = LogLevel.Info;
public bool IsEnabled(LogLevel level) {
return level >= MinLevel;
}
public void Log(string message, LogLevel level) {
if (IsEnabled(level)) {
WriteLog($"[{level}] {message}");
}
}
// Абстрактный метод для реализации в подклассах
protected abstract void WriteLog(string formattedMessage);
}
// Конкретная реализация
public class ConsoleLogger : BaseLogger {
protected override void WriteLog(string formattedMessage) {
Console.WriteLine($"{DateTime.Now}: {formattedMessage}");
}
}
Python
Python поддерживает абстракцию через модуль abc (Abstract Base Classes) и миксины, хотя формально интерфейсов как таковых в языке нет.
from abc import ABC, abstractmethod
# Абстрактный базовый класс
class Shape(ABC):
@abstractmethod
def area(self):
pass
@abstractmethod
def perimeter(self):
pass
def describe(self):
return f"This shape has area {self.area()} and perimeter {self.perimeter()}"
# Конкретная реализация
class Rectangle(Shape):
def __init__(self, width, height):
self.width = width
self.height = height
def area(self):
return self.width * self.height
def perimeter(self):
return 2 * (self.width + self.height)
# Использование
rect = Rectangle(5, 10)
print(rect.describe()) # "This shape has area 50 and perimeter 30"
Алексей Петров, ведущий разработчик
Мой опыт работы с крупной системой управления складом показал, насколько важна правильная абстракция. У нас были десятки разных складских операций — приём, отгрузка, перемещение, инвентаризация — и каждая требовала особой обработки.
Изначально система была построена без чёткой абстракции. Каждый тип операции имел свой класс с уникальной логикой, но при этом около 70% кода дублировалось. Когда мне поручили рефакторинг, я решил применить единый интерфейс для всех операций:
JavaСкопировать кодpublic interface WarehouseOperation { ValidationResult validate(); void prepare(); OperationResult execute(); void complete(); void rollback(); }Затем создал абстрактный класс BaseWarehouseOperation, который реализовал общую для всех операций логику. Конкретные классы операций наследовались от него, переопределяя только специфичные методы.
Результат превзошёл ожидания. Объем кода сократился на 40%, а время на разработку новых типов операций — с недели до 1-2 дней. Когда появилось требование добавить аудит всех операций, нам потребовалось изменить только базовый класс, а не модифицировать каждый тип операции по отдельности.
Самым ценным уроком было то, что абстракцию нужно выстраивать на основе поведения, а не структуры данных. Мы абстрагировали жизненный цикл операции (validate → prepare → execute → complete/rollback), а не её содержимое, и это оказалось правильным решением.
TypeScript
TypeScript расширяет JavaScript, добавляя статическую типизацию и поддержку интерфейсов для абстракции.
// Интерфейс для HTTP-клиента
interface HttpClient {
get<T>(url: string, options?: RequestOptions): Promise<T>;
post<T>(url: string, data: any, options?: RequestOptions): Promise<T>;
put<T>(url: string, data: any, options?: RequestOptions): Promise<T>;
delete<T>(url: string, options?: RequestOptions): Promise<T>;
}
// Абстрактный класс с базовой реализацией
abstract class BaseHttpClient implements HttpClient {
protected abstract request<T>(method: string, url: string, data?: any, options?: RequestOptions): Promise<T>;
async get<T>(url: string, options?: RequestOptions): Promise<T> {
return this.request<T>('GET', url, null, options);
}
async post<T>(url: string, data: any, options?: RequestOptions): Promise<T> {
return this.request<T>('POST', url, data, options);
}
async put<T>(url: string, data: any, options?: RequestOptions): Promise<T> {
return this.request<T>('PUT', url, data, options);
}
async delete<T>(url: string, options?: RequestOptions): Promise<T> {
return this.request<T>('DELETE', url, null, options);
}
}
// Конкретная реализация
class FetchHttpClient extends BaseHttpClient {
protected async request<T>(method: string, url: string, data?: any, options?: RequestOptions): Promise<T> {
const response = await fetch(url, {
method,
body: data ? JSON.stringify(data) : undefined,
headers: options?.headers,
// Другие параметры
});
if (!response.ok) {
throw new Error(`HTTP error ${response.status}`);
}
return response.json();
}
}
Несмотря на различия в синтаксисе и механизмах реализации, абстракция в разных языках программирования преследует одни и те же цели:
- Сокрытие сложности и деталей реализации
- Определение четких границ и контрактов между компонентами
- Обеспечение расширяемости и гибкости системы
- Упрощение тестирования через возможность подмены реализаций
Практическое применение абстракции не ограничивается языковыми конструкциями — это в первую очередь подход к проектированию, который должен соответствовать потребностям конкретной системы и ее эволюции. ООП: основные понятия и принципы требуют тщательного осмысления в каждом проекте. 🏗️
Абстракция и другие принципы ООП: синергия в проектировании
Абстракция не существует в изоляции — она тесно переплетается с другими фундаментальными принципами ООП, создавая синергетический эффект в проектировании программных систем. Понимание этих взаимосвязей помогает создавать более элегантные и поддерживаемые решения.
Абстракция и инкапсуляция
Инкапсуляция и абстракция часто рассматриваются вместе, но между ними есть важные различия:
- Абстракция фокусируется на том, что должен делать объект (его интерфейс и поведение)
- Инкапсуляция сосредоточена на том, как скрыть внутреннее устройство объекта
При этом они взаимно дополняют друг друга: абстракция определяет, какие части системы следует выделить и представить в виде классов или интерфейсов, а инкапсуляция обеспечивает защиту их внутренних механизмов.
// Пример синергии абстракции и инкапсуляции
public interface AccountService {
AccountInfo getAccountInfo(String accountId);
TransactionResult deposit(String accountId, BigDecimal amount);
TransactionResult withdraw(String accountId, BigDecimal amount);
}
public class BankAccountService implements AccountService {
// Инкапсулированное состояние
private AccountRepository accountRepository;
private TransactionRepository transactionRepository;
private SecurityService securityService;
// Конструктор для внедрения зависимостей
public BankAccountService(
AccountRepository accountRepository,
TransactionRepository transactionRepository,
SecurityService securityService
) {
this.accountRepository = accountRepository;
this.transactionRepository = transactionRepository;
this.securityService = securityService;
}
@Override
public AccountInfo getAccountInfo(String accountId) {
// Реализация, использующая инкапсулированные компоненты
Account account = accountRepository.findById(accountId);
if (account == null) {
throw new AccountNotFoundException(accountId);
}
return new AccountInfo(account);
}
// Другие методы...
}
Абстракция и наследование
Наследование позволяет создавать иерархии классов, где базовые классы предоставляют абстрактные понятия, а подклассы их конкретизируют. Это один из способов реализации принципа абстракции.
Ключевые аспекты взаимодействия абстракции и наследования:
- Абстрактные базовые классы определяют общий интерфейс для всей иерархии
- Специализация через наследование позволяет уточнять абстракции
- Наследование позволяет повторно использовать код, определенный в абстракциях
Однако чрезмерное использование наследования может привести к проблемам, таким как "хрупкие" иерархии классов и усложнение понимания кода. Поэтому современные подходы часто предпочитают композицию наследованию, что отражено в принципе "Предпочитайте композицию наследованию".
Абстракция и полиморфизм
Полиморфизм — способность объектов с одинаковым интерфейсом иметь различные реализации — напрямую опирается на абстракцию. Фактически, абстракция создает фундамент для полиморфного поведения.
Рассмотрим, как эти принципы работают вместе:
// Абстракция через интерфейс
public interface Notifier {
void send(String message, String recipient);
}
// Различные реализации
public class EmailNotifier implements Notifier {
@Override
public void send(String message, String recipient) {
// Отправка по email
}
}
public class SmsNotifier implements Notifier {
@Override
public void send(String message, String recipient) {
// Отправка через SMS
}
}
public class PushNotifier implements Notifier {
@Override
public void send(String message, String recipient) {
// Отправка через push-уведомления
}
}
// Полиморфное использование
public class NotificationService {
private List<Notifier> notifiers;
public NotificationService(List<Notifier> notifiers) {
this.notifiers = notifiers;
}
public void notifyAll(String message, String recipient) {
for (Notifier notifier : notifiers) {
notifier.send(message, recipient); // Полиморфный вызов
}
}
}
В этом примере NotificationService работает с абстракцией Notifier, не зная о конкретных реализациях. Это позволяет легко добавлять новые типы уведомлений без изменения существующего кода.
SOLID и абстракция
Принципы SOLID тесно связаны с абстракцией и формализуют подходы к её правильному применению:
| Принцип SOLID | Связь с абстракцией |
|---|---|
| S — Single Responsibility (Единственная ответственность) | Помогает определить правильный уровень абстракции для класса |
| O — Open/Closed (Открытость/закрытость) | Абстракции позволяют расширять функциональность без изменения существующего кода |
| L — Liskov Substitution (Подстановка Лисков) | Гарантирует, что конкретные реализации абстракций могут корректно заменять друг друга |
| I — Interface Segregation (Разделение интерфейсов) | Рекомендует создавать узкие абстракции вместо крупных многофункциональных интерфейсов |
| D — Dependency Inversion (Инверсия зависимостей) | Требует зависеть от абстракций, а не от конкретных реализаций |
Особенно важен принцип Dependency Inversion, который напрямую касается абстракций:
- Модули высокого уровня не должны зависеть от модулей низкого уровня. Оба должны зависеть от абстракций.
- Абстракции не должны зависеть от деталей. Детали должны зависеть от абстракций.
Синергия абстракции с другими принципами ООП создает прочную основу для проектирования гибких, расширяемых и поддерживаемых систем. Принципы ООП в программировании работают наиболее эффективно именно в комплексе, усиливая друг друга. 🔄
Типичные ошибки при использовании абстракции и способы их исправления
Даже опытные разработчики допускают ошибки при применении абстракции. Рассмотрим наиболее распространенные проблемы и способы их решения, чтобы повысить качество вашего кода.
1. Избыточная абстракция
Одна из самых распространенных ошибок — создание абстракций "на всякий случай" или следуя принципу "больше — лучше". Это приводит к появлению ненужных слоев кода, которые усложняют понимание и поддержку.
Симптомы:
- Классы с префиксами вроде "Abstract", "Base", "Generic", которые имеют только одну конкретную реализацию
- Интерфейсы, повторяющие структуру конкретных классов один в один
- Цепочки делегирования через несколько уровней абстракции
Решение:
- Следуйте принципу YAGNI (You Ain't Gonna Need It) — не создавайте абстракции, пока не возникла реальная потребность
- Имейте как минимум две различные реализации, прежде чем вводить абстракцию
- Регулярно рефакторите код, удаляя неиспользуемые абстракции
// Плохо: избыточная абстракция
interface UserRepository {
User findById(Long id);
List<User> findAll();
void save(User user);
}
class DatabaseUserRepository implements UserRepository {
// Единственная реализация
}
// Лучше: начать с конкретного класса
class UserRepository {
User findById(Long id) {
// Реализация
}
List<User> findAll() {
// Реализация
}
void save(User user) {
// Реализация
}
}
// И выделить интерфейс только при появлении второй реализации
2. Недостаточная абстракция
Противоположная проблема — недостаточное использование абстракций, когда код тесно связан с конкретными реализациями, что затрудняет его изменение и тестирование.
Симптомы:
- Прямое создание объектов с использованием оператора
newвместо инъекции зависимостей - Проверки типов и приведения типов (instanceof, as, is)
- Большие классы, которые выполняют множество различных функций
Решение:
- Применяйте принцип инверсии зависимостей — зависьте от абстракций, а не от конкретных классов
- Используйте инъекцию зависимостей вместо прямого создания объектов
- Разделяйте классы по принципу единственной ответственности
// Плохо: недостаточная абстракция
class PaymentProcessor {
void processPayment(Order order) {
if (order.getPaymentMethod() == PaymentMethod.CREDIT_CARD) {
CreditCardProcessor processor = new CreditCardProcessor();
processor.charge(order.getCreditCard(), order.getTotal());
} else if (order.getPaymentMethod() == PaymentMethod.PAYPAL) {
PayPalProcessor processor = new PayPalProcessor();
processor.processPayment(order.getPayPalAccount(), order.getTotal());
}
// И так далее для каждого метода оплаты
}
}
// Лучше: использование абстракций
interface PaymentProvider {
void processPayment(Order order);
}
class CreditCardProvider implements PaymentProvider {
@Override
public void processPayment(Order order) {
// Реализация для кредитной карты
}
}
class PayPalProvider implements PaymentProvider {
@Override
public void processPayment(Order order) {
// Реализация для PayPal
}
}
class PaymentProcessor {
private Map<PaymentMethod, PaymentProvider> providers;
// Внедрение провайдеров через конструктор
PaymentProcessor(Map<PaymentMethod, PaymentProvider> providers) {
this.providers = providers;
}
void processPayment(Order order) {
PaymentProvider provider = providers.get(order.getPaymentMethod());
if (provider == null) {
throw new UnsupportedPaymentMethodException(order.getPaymentMethod());
}
provider.processPayment(order);
}
}
3. "Протекающие" абстракции
Термин "протекающая абстракция" (leaky abstraction) описывает ситуацию, когда детали реализации "просачиваются" через абстракцию, нарушая её целостность.
Симптомы:
- Методы абстракции, которые имеют смысл только для определенных реализаций
- Необходимость знать детали реализации для корректного использования абстракции
- Исключения, связанные с конкретными реализациями, выбрасываются через интерфейс
Решение:
- Проектируйте абстракции на основе поведения, а не структуры данных
- Применяйте принцип разделения интерфейсов — создавайте узкие, специализированные интерфейсы
- Используйте адаптеры и фасады для скрытия несовместимых интерфейсов
// Плохо: протекающая абстракция
interface DataSource {
Connection getConnection(); // Специфично для SQL баз данных
void executeQuery(String sql); // Привязка к SQL
ResultSet getResults(); // Привязка к JDBC
}
// Лучше: более абстрактный интерфейс
interface DataSource<T> {
List<T> query(QueryCriteria criteria);
Optional<T> findOne(QueryCriteria criteria);
void save(T entity);
void delete(T entity);
}
4. Неправильный уровень абстракции
Выбор неправильного уровня абстракции может привести к неудобным API и сложностям при расширении системы.
Симптомы:
- Абстракции, которые либо слишком общие (и требуют много кода для конкретных случаев), либо слишком специфичные (и не подходят для других сценариев)
- Необходимость частого изменения абстракций при добавлении новой функциональности
- "Божественные" интерфейсы с десятками методов
Решение:
- Фокусируйтесь на бизнес-требованиях и пользовательских сценариях при проектировании абстракций
- Применяйте принцип разделения интерфейсов
- Используйте шаблоны проектирования, соответствующие решаемой проблеме
Абстракция — это мощный инструмент, но как и любой инструмент, требует правильного применения. Понимание типичных ошибок и способов их исправления поможет вам создавать более качественные и поддерживаемые системы, полностью реализуя потенциал принципов ООП в программировании. 🛠️
Абстракция — это та область программирования, где искусство встречается с инженерией. Она требует не только технических знаний, но и развитого абстрактного мышления, способности видеть систему как на высоком уровне, так и в деталях. Помните, что правильная абстракция не возникает сразу — это результат постоянного анализа требований, рефакторинга и глубокого понимания предметной области. Стремитесь к балансу: слишком мало абстракции приводит к дублированию кода и тесным связям, слишком много — к ненужной сложности. И главное — не бойтесь экспериментировать, ведь именно через практику приходит истинное мастерство в создании элегантных абстракций.
Читайте также
- Объектно-ориентированное программирование на Python: принципы и практика
- Объектно-ориентированное программирование: от хаоса к порядку в разработке
- 7 инженерных решений ООП Python для реальных проектов
- Как воплотить ООП в C: подробное руководство по созданию калькулятора
- ООП в Java: практические задания для опытных разработчиков
- Основы ООП: классы, объекты, атрибуты, методы – базовые концепции
- Интерпретируемые и компилируемые языки: ключевые различия, выбор
- Парадигмы программирования: как выбрать оптимальный подход к коду
- Переменные в программировании: базовое понятие для новичков
- Как создать калькулятор на C: базовые операции и функции


