Шпаргалка по принципам ООП
Чтобы стать программистом, нужно знать принципы ООП как Отче наш. Держите структурированную шпаргалку по объектно-ориентированному программированию.
Главное
- Инкапсулируйте все, что может изменяться;
- Уделяйте больше внимания интерфейсам, а не их реализациям;
- Каждый класс в вашем приложении должен иметь только одно назначение;
- Классы — это их поведение и функциональность.
Базовые принципы ООП
- Абстракция — отделение концепции от ее экземпляра;
- Полиморфизм — реализация задач одной и той же идеи разными способами;
- Наследование — способность объекта или класса базироваться на другом объекте или классе. Это главный механизм для повторного использования кода. Наследственное отношение классов четко определяет их иерархию;
- Инкапсуляция — размещение одного объекта или класса внутри другого для разграничения доступа к ним.
Используйте следующее вместе с наследованием
- Делегация — перепоручение задачи от внешнего объекта внутреннему;
- Композиция — включение объектом-контейнером объекта-содержимого и управление его поведением; последний не может существовать вне первого;
- Агрегация — включение объектом-контейнером ссылки на объект-содержимое; при уничтожении первого последний продолжает существование.
Не повторяйся (Don’t repeat yourself — DRY)
Избегайте повторного написания кода, вынося в абстракции часто используемые задачи и данные. Каждая часть вашего кода или информации должна находиться в единственном числе в единственном доступном месте. Это один из принципов читаемого кода.
Принцип единственной обязанности
Для каждого класса должно быть определено единственное назначение. Все ресурсы, необходимые для его осуществления, должны быть инкапсулированы в этот класс и подчинены только этой задаче.
Принцип открытости/закрытости
Программные сущности должны быть открыты для расширения, но закрыты для изменений.
Принцип подстановки Барбары Лисков
Методы, использующие некий тип, должны иметь возможность использовать его подтипы, не зная об этом.
Принцип разделения интерфейсов
Предпочтительнее разделять интерфейсы на более мелкие тематические, чтобы реализующие их классы не были вынуждены определять методы, которые непосредственно в них не используются.
Принцип инверсии зависимостей
Система должна конструироваться на основе абстракций “сверху вниз”: не абстракции должны формироваться на основе деталей, а детали должны формироваться на основе абстракций.
Перевод статьи «Object-Orientated Design Principles»
Объектно-ориентированное программирование (C#)
C# — это объектно-ориентированный язык программирования. Четыре основных принципа объектно-ориентированного программирования следующие.
- Абстракция. Моделирование требуемых атрибутов и взаимодействий сущностей в виде классов для определения абстрактного представления системы.
- Инкапсуляция. Скрытие внутреннего состояния и функций объекта и предоставление доступа только через открытый набор функций.
- Наследование. Возможность создания новых абстракций на основе существующих.
- Полиморфизм. Возможность реализации наследуемых свойств или методов отличающимися способами в рамках множества абстракций.
Из предыдущего руководства с общими сведениями о классах вы узнали об абстракции и инкапсуляции. Абстракция банковского счета реализована с помощью класса BankAccount . Эту реализацию можно изменить, не влияя на код, в котором использовался класс BankAccount . Классы BankAccount и Transaction позволяют реализовать инкапсуляцию компонентов, требуемых для описания этих концепций в коде.
В этом руководстве показано, как расширить приложение, чтобы использовать наследование и полиморфизм для добавления новых функций. Вы также добавите компоненты в класс BankAccount , чтобы использовать преимущества абстракции и инкапсуляции, о которых вы узнали при работе с предыдущим руководством.
Создание разных типов счетов
После создания этой программы вы получите запросы на добавление в нее функций. Это прекрасно работает в ситуации, когда у вас есть только один тип банковского счета. С течением времени потребуется внести изменения, так как будут запрашиваться связанные типы счетов:
- Счет для начисления процентов в конце каждого месяца.
- Кредитная линия, которая может иметь отрицательный баланс и с которой в этом случае ежемесячно взимаются проценты.
- Счет для предоплаченной подарочной карты с депозитом, используемый только для оплаты. Его можно пополнять один раз в начале каждого месяца.
Все эти счета можно реализовать с помощью класса BankAccount , определенного в предыдущем руководстве. Можно скопировать этот код, переименовать классы и внести изменения. Этот метод сработает в краткосрочной перспективе, но со временем ситуация усложнится. Все изменения нужно будет копировать во все затронутые классы.
Вместо этого можно создать новые типы банковских счетов, которые наследуют методы и данные из класса BankAccount , созданного при работе с предыдущим руководством. Эти новые классы могут расширить класс BankAccount за счет конкретного поведения, требуемого для каждого типа:
public class InterestEarningAccount : BankAccount < >public class LineOfCreditAccount : BankAccount < >public class GiftCardAccount : BankAccount
Каждый из этих классов наследует общее поведение общего базового класса BankAccount . Напишите реализации новых отличающихся функций в каждом из производных классов. Эти производные классы уже имеют поведение, определенное в классе BankAccount .
Рекомендуется создавать новый класс в другом исходном файле. В Visual Studio можно щелкнуть правой кнопкой мыши проект и выбрать Добавить класс, чтобы добавить новый класс в новый файл. В Visual Studio Code выберите Файл и Создать, чтобы создать исходный файл. В любом из этих средств присвойте файлу имя, соответствующее имени класса: InterestEarningAccount.cs, LineOfCreditAccount.cs и GiftCardAccount.cs.
При создании классов, как показано в примере выше, вы обнаружите, что ни один из производных классов не компилируется. За инициализацию объекта отвечает конструктор. Конструктор производного класса должен инициализировать производный класс и предоставить инструкции по инициализации объекта базового класса, включенного в производный класс. Правильная инициализация обычно выполняется без дополнительного кода. Класс BankAccount объявляет один открытый конструктор со следующей сигнатурой:
public BankAccount(string name, decimal initialBalance)
Компилятор не создает конструктор по умолчанию при самостоятельном определении конструктора. Это означает, что каждый производный класс должен явно вызывать этот конструктор. Вы объявляете конструктор, который может передавать аргументы конструктору базового класса. В следующем коде показан конструктор для InterestEarningAccount :
public InterestEarningAccount(string name, decimal initialBalance) : base(name, initialBalance)
Параметры для этого нового конструктора соответствуют типу и именам параметра конструктора базового класса. Для указания вызова конструктора базового класса используйте синтаксис : base() . Некоторые классы определяют несколько конструкторов, и этот синтаксис позволяет выбрать вызываемый конструктор базового класса. После обновления конструкторов можно разработать код для каждого производного класса. Требования к новым классам можно сформулировать следующим образом.
- Счет для начисления процентов:
- Будут начисляться 2 % от текущего баланса в конце месяца.
- Может иметь отрицательный баланс, который не превышает абсолютное значение кредитного лимита.
- Будут списываться проценты каждый месяц, в конце которого баланс не равен 0.
- Будет взиматься комиссия за каждый вывод средств, превышающий кредитный лимит.
- Может пополняться на указанную сумму в последний день каждого месяца.
Как видите, для каждого из этих типов счетов предусмотрено действие, которое выполняется в конце каждого месяца, и эти действия отличаются. Для реализации этого кода используется полиморфизм. Перейдите к методу virtual в классе BankAccount :
public virtual void PerformMonthEndTransactions()
В приведенном выше коде показано, как использовать ключевое слово virtual для объявления метода в базовом классе, для которого производный класс может предоставить другую реализацию. virtual определяет метод, в котором любой производный класс может использовать повторную реализацию. Производные классы используют ключевое слово override для определения новой реализации. Обычно это называется переопределением реализации базового класса. Ключевое слово virtual указывает, что производные классы могут переопределить поведение. Вы также можете объявить методы abstract , где производные классы должны переопределять поведение. Базовый класс не предоставляет реализацию для метода abstract . Далее необходимо определить реализацию для двух новых классов, которые вы создали. Начните с InterestEarningAccount :
public override void PerformMonthEndTransactions() < if (Balance >500m) < decimal interest = Balance * 0.02m; MakeDeposit(interest, DateTime.Now, "apply monthly interest"); >>Добавьте следующий код в LineOfCreditAccount . Код обнуляет баланс для расчета положительной процентной ставки, снятой со счета:
public override void PerformMonthEndTransactions() < if (Balance < 0) < // Negate the balance to get a positive interest charge: decimal interest = -Balance * 0.07m; MakeWithdrawal(interest, DateTime.Now, "Charge monthly interest"); >>Классу GiftCardAccount требуются два изменения, чтобы реализовать соответствующее действие, выполняемое в конце месяца. Во-первых, измените конструктор, включив в него дополнительную сумму, добавляемую ежемесячно:
private readonly decimal _monthlyDeposit = 0m; public GiftCardAccount(string name, decimal initialBalance, decimal monthlyDeposit = 0) : base(name, initialBalance) => _monthlyDeposit = monthlyDeposit;Конструктор предоставляет значение по умолчанию для значения monthlyDeposit , поэтому вызывающие объекты могут опускать 0 при отсутствии ежемесячного депозита. Во-вторых, переопределите метод PerformMonthEndTransactions , чтобы добавить месячный депозит, если в конструкторе было задано ненулевое значение:
public override void PerformMonthEndTransactions() < if (_monthlyDeposit != 0) < MakeDeposit(_monthlyDeposit, DateTime.Now, "Add monthly deposit"); >>Переопределение применяет набор месячных депозитов в конструкторе. Добавьте следующий код в метод Main , чтобы проверить эти изменения для GiftCardAccount и InterestEarningAccount :
var giftCard = new GiftCardAccount("gift card", 100, 50); giftCard.MakeWithdrawal(20, DateTime.Now, "get expensive coffee"); giftCard.MakeWithdrawal(50, DateTime.Now, "buy groceries"); giftCard.PerformMonthEndTransactions(); // can make additional deposits: giftCard.MakeDeposit(27.50m, DateTime.Now, "add some additional spending money"); Console.WriteLine(giftCard.GetAccountHistory()); var savings = new InterestEarningAccount("savings account", 10000); savings.MakeDeposit(750, DateTime.Now, "save some money"); savings.MakeDeposit(1250, DateTime.Now, "Add more savings"); savings.MakeWithdrawal(250, DateTime.Now, "Needed to pay monthly bills"); savings.PerformMonthEndTransactions(); Console.WriteLine(savings.GetAccountHistory());Проверьте результаты. Теперь добавьте аналогичный набор тестового кода для LineOfCreditAccount :
var lineOfCredit = new LineOfCreditAccount("line of credit", 0); // How much is too much to borrow? lineOfCredit.MakeWithdrawal(1000m, DateTime.Now, "Take out monthly advance"); lineOfCredit.MakeDeposit(50m, DateTime.Now, "Pay back small amount"); lineOfCredit.MakeWithdrawal(5000m, DateTime.Now, "Emergency funds for repairs"); lineOfCredit.MakeDeposit(150m, DateTime.Now, "Partial restoration on repairs"); lineOfCredit.PerformMonthEndTransactions(); Console.WriteLine(lineOfCredit.GetAccountHistory());Когда вы добавите приведенный выше код и запустите программу, вы увидите примерно такую ошибку:
Unhandled exception. System.ArgumentOutOfRangeException: Amount of deposit must be positive (Parameter 'amount') at OOProgramming.BankAccount.MakeDeposit(Decimal amount, DateTime date, String note) in BankAccount.cs:line 42 at OOProgramming.BankAccount..ctor(String name, Decimal initialBalance) in BankAccount.cs:line 31 at OOProgramming.LineOfCreditAccount..ctor(String name, Decimal initialBalance) in LineOfCreditAccount.cs:line 9 at OOProgramming.Program.Main(String[] args) in Program.cs:line 29Фактический результат включает полный путь к папке с проектом. Имена папок опущены для краткости. Кроме того, в зависимости от формата кода номера строк могут отличаться.
Этот код завершается ошибкой, так как в BankAccount предполагается, что исходный баланс должен быть больше 0. Еще одно допущение, реализованное в классе BankAccount , заключается в том, что баланс не может быть отрицательным. Вместо этого любой вывод средств, превышающий сумму на счете, отклоняется. Оба эти предположения необходимо изменить. Баланс кредитного счета равен 0 и, как правило, в дальнейшем будет иметь отрицательное значение. Кроме того, если клиент займет слишком много денег, с него будет списана комиссия. Транзакция принимается, она просто требует дополнительных затрат. Первое правило можно реализовать, добавив необязательный аргумент к конструктору BankAccount , который определяет минимальный баланс. Значение по умолчанию — 0 . Для второго правила требуется механизм, который позволяет производным классам изменять алгоритм по умолчанию. В некотором смысле базовый класс уточняет у производного типа, что должно произойти при перерасходе. Поведение по умолчанию — отклонить транзакцию, вызвав исключение.
Начнем с добавления второго конструктора, который включает необязательный параметр minimumBalance . Этот новый конструктор выполняет все действия, выполняемые существующим конструктором. Кроме того, задается свойство, определяющее минимальный баланс. Вы можете скопировать текст существующего конструктора, но это означает, что в будущем изменится два расположения. Вместо этого можно использовать цепочки конструкторов, чтобы один конструктор вызывал другой. В следующем коде показаны два конструктора и новое дополнительное поле:
private readonly decimal _minimumBalance; public BankAccount(string name, decimal initialBalance) : this(name, initialBalance, 0) < >public BankAccount(string name, decimal initialBalance, decimal minimumBalance) < Number = s_accountNumberSeed.ToString(); s_accountNumberSeed++; Owner = name; _minimumBalance = minimumBalance; if (initialBalance >0) MakeDeposit(initialBalance, DateTime.Now, "Initial balance"); >В приведенном выше коде показаны два новых метода. Во-первых, поле minimumBalance помечается как readonly . Это означает, что значение нельзя изменить после создания объекта. После создания BankAccount minimumBalance не может измениться. Во вторых, конструктор, принимающий два параметра, использует : this(name, initialBalance, 0) < >в качестве реализации. Выражение : this() вызывает другой конструктор, который имеет три параметра. Этот метод позволяет применить одну реализацию для инициализации объекта, даже несмотря на то, что клиентский код может выбрать один из многих конструкторов.
Эта реализация вызывает MakeDeposit , только если исходный баланс превышает 0 . Таким образом, мы придерживаемся правила, согласно которого депозиты должны быть положительными, а кредитный счет открывается с балансом, равным 0 .
Теперь, когда класс BankAccount имеет доступное только для чтения поле для определения минимального баланса, последнее изменение заключается в изменении прописанного в коде значения 0 на minimumBalance в методе MakeWithdrawal :
if (Balance - amount < _minimumBalance)После расширения класса BankAccount можно изменить конструктор LineOfCreditAccount , чтобы вызвать новый базовый конструктор, как показано в следующем коде:
public LineOfCreditAccount(string name, decimal initialBalance, decimal creditLimit) : base(name, initialBalance, -creditLimit)
Обратите внимание, что конструктор LineOfCreditAccount изменяет знак параметра creditLimit , чтобы он соответствовал значению параметра minimumBalance .
Разные правила, связанные с перерасходом
Последняя добавляемая функция позволяет LineOfCreditAccount взимать комиссию за превышение кредитного лимита вместо того, чтобы отклонять транзакцию.
Для этого можно, например, определить виртуальную функцию, в которой реализуется требуемое поведение. Класс BankAccount выполняет рефакторинг метода MakeWithdrawal , чтобы получить два метода. Новый метод выполняет указанное действие, когда при выводе средств баланс получает значение ниже определенного минимума. Существующий метод MakeWithdrawal имеет следующий код:
public void MakeWithdrawal(decimal amount, DateTime date, string note) < if (amount if (Balance - amount < _minimumBalance) < throw new InvalidOperationException("Not sufficient funds for this withdrawal"); >var withdrawal = new Transaction(-amount, date, note); _allTransactions.Add(withdrawal); >Замените его следующим кодом.
public void MakeWithdrawal(decimal amount, DateTime date, string note) < if (amount Transaction? overdraftTransaction = CheckWithdrawalLimit(Balance - amount < _minimumBalance); Transaction? withdrawal = new(-amount, date, note); _allTransactions.Add(withdrawal); if (overdraftTransaction != null) _allTransactions.Add(overdraftTransaction); >protected virtual Transaction? CheckWithdrawalLimit(bool isOverdrawn) < if (isOverdrawn) < throw new InvalidOperationException("Not sufficient funds for this withdrawal"); >else < return default; >>Добавленный метод protected предполагает, что его можно вызывать только из производных классов. Это объявление предотвращает вызов метода другими клиентами. А ключевое слово virtual означает, что производные классы могут изменять поведение. Тип возвращаемого значения — Transaction? . Аннотация ? указывает, что метод может возвращать null . Добавьте следующую реализацию в LineOfCreditAccount , чтобы списывать комиссию при превышении лимита на вывод средств.
protected override Transaction? CheckWithdrawalLimit(bool isOverdrawn) => isOverdrawn ? new Transaction(-20, DateTime.Now, "Apply overdraft fee") : default;В этом случае переопределение возвращает транзакцию с комиссией. Если при выводе средств лимит не превышен, метод возвращает транзакцию null . Это означает, что комиссия не взимается. Чтобы проверить эти изменения, добавьте следующий код в метод Main в классе Program :
var lineOfCredit = new LineOfCreditAccount("line of credit", 0, 2000); // How much is too much to borrow? lineOfCredit.MakeWithdrawal(1000m, DateTime.Now, "Take out monthly advance"); lineOfCredit.MakeDeposit(50m, DateTime.Now, "Pay back small amount"); lineOfCredit.MakeWithdrawal(5000m, DateTime.Now, "Emergency funds for repairs"); lineOfCredit.MakeDeposit(150m, DateTime.Now, "Partial restoration on repairs"); lineOfCredit.PerformMonthEndTransactions(); Console.WriteLine(lineOfCredit.GetAccountHistory());Запустите программу и проверьте результаты.
Сводка
Если у вас возникли проблемы, изучите исходный код для этого руководства, размещенный в репозитории GitHub.
В этом руководстве показаны разные методы, используемые в объектно-ориентированном программировании:
- Вы использовали абстракции, когда определяли классы для каждого из типов счетов. Эти классы описывали поведение для каждого типа счета.
- Вы использовали инкапсуляцию, когда сохраняли в каждом классе много сведений ( private ).
- Вы использовали наследование, когда применяли реализацию, уже созданную в классе BankAccount для сохранения кода.
- Вы использовали полиморфизм, когда создавали методы virtual , которые производные классы могут переопределить для создания определенного поведения для этого типа счета.
Совместная работа с нами на GitHub
Источник этого содержимого можно найти на GitHub, где также можно создавать и просматривать проблемы и запросы на вытягивание. Дополнительные сведения см. в нашем руководстве для участников.
Принципы ООП в примерах для начинающих

Как создатель и руководитель курсов по C# я вижу, что часто у людей, начинающих изучать этот язык, принципы Объектно-Ориентированного Программирования вызывают затруднения в понимании. А так как один из лучших способов что-то понять, это посмотреть применение на примерах, то я решил написать статью с примерами принципов. Рекомендую найти какую-нибудь статью или книгу, где прочитать основную теорию, а в этой статье уже посмотреть примеры применения этой теории, чтобы понять её лучше.
На текущий момент есть различные точки зрения на то, сколько же в ООП всё-таки принципов и в этой статье мы будем считать, что этих принципов четыре: Инкапсуляция, Наследование, Полиморфизм и Абстракция. Примеры будут приведены на языке C#, однако, они очень простые, да и сама суть не зависит от языка, поэтому будет полезна всем начинающим изучать ООП программистам.
Инкапсуляция
Инкапсуляция в программировании является объединением данных и кода, работающего с этими данными, в большинстве случае это сводится к тому, чтобы не давать доступа к важным данным напрямую. Вместо этого мы создаем ограниченный набор методов, с помощью которых можно работать с нашими данными. Давайте рассмотрим несколько повседневных примеров, чтобы лучше понять это.
Пример: Уровень заряда батареи смартфона
public class Smartphone < private int _batteryLife; // Метод заряжает батарею, но не имеет доступа к уровню заряда public void Charge(int amount) < // Устанавливаем свои правила для работы с переменной if (amount _batteryLife += amount; > // Метод получает текущее значение, но не может его изменить public int GetBatteryLife() < return _batteryLife; >>Здесь _batteryLife — это наши важные данные. У нас есть методы для зарядки и показа текущего значения, однако мы не даем доступ к самой переменной _batteryLife , поэтому, например, пользователи класса не смогут убавить значение нашей переменной.
Пример: Избранные песни
Давайте представим музыкальное приложение, в котором можно добавлять или удалять песни из своего списка избранного.
public class MusicApp < private List_favoriteSongs = new List(); public void AddToFavorites(string songName) < if(!string.IsNullOrEmpty(songName) && !_favoriteSongs.Contains(songName)) < _favoriteSongs.Add(songName); >> public void RemoveFromFavorites(string songName) < _favoriteSongs.Remove(songName); >public List GetFavorites() < return new List(_favoriteSongs); > >В этом примере инкапсулирован, то есть спрятан от доступа извне класса, список наших избранных песен ( _favoriteSongs ). Мы предоставляем методы для управления списком, но не даем возможности работать со списком напрямую.
Пример: Переписка в соцсетях
В самом простом случае все, что мы можем сделать при общении в соцсети - отправить кому-то сообщение и прочитать сообщения, отправленные нам. В таком случае, чтобы не дать возможности другим программистам, которые будут использовать наш класс SocialMediaPlatform случайно перезаписать наши сообщения, лучше будет предоставить им всего два метода - SendMessage и ShowMyMessages .
public class SocialMediaPlatform < private List_privateMessages = new List(); public void SendMessage(string message) < if(!string.IsNullOrEmpty(message)) < _privateMessages.Add(message); >> public List ShowMyMessages() < return new List(_privateMessages); > >Как мы видим, сообщения инкапсулированы в списке _privateMessages и код, использующий наш класс, не может делать с нашими сообщения ничего, кроме получения текущих и добавления новых.
Наследование
Наследование в какой-то степени похоже с биологическим наследованием. Вы получаете какие-то черты от своих родителей, но, в то же время, отличаетесь от них. Или представьте это как базовую модель гаджета, к которой затем добавляются улучшенные версии с дополнительными функциями. Давайте рассмотрим несколько примеров, чтобы лучше понять это.
Пример: Игры и дополнения
Предположим, вы купили игру в Стиме и через какое-то время у неё появляются два дополнения: HD version и HotA, которые основаны на оригинальной игре, но изменяют её части.
public class HeroesOfMightAndMagic3 < public void Play() < Console.WriteLine("Запускаем классическую версию игры. "); >> public class HeroesOfMightAndMagic3Hd : HeroesOfMightAndMagic3 < public void PlayHd() < Console.WriteLine("Запускаем игру в высоком разрешении (HD). "); >> public class HeroesOfMightAndMagic3Hota : HeroesOfMightAndMagic3 < public void PlayHota() < Console.WriteLine("Запускаем игру с двумя новыми городами. "); >>Классы HeroesOfMightAndMagic3Hd и HeroesOfMightAndMagic3Hota наследуют метод Play для запуска оригинальной версии игры, но также каждый добавляет свои уникальные методы.
Пример: Версии смартфона
Рассмотрим смартфон, у которого есть базовая модель и есть версия Pro, которая наследует все базовые функции, плюс, добавляет некоторые продвинутые.
public class BasicSmartphone < public void Call() < Console.WriteLine("Совершаем звонок. "); >> public class ProSmartphone : BasicSmartphone < public void VideoCall() < Console.WriteLine("Совершаем видеозвонок. "); >>ProSmartphone может звонить так же, как и BasicSmartphone , но также имеет дополнительную функцию видеозвонка.
Пример: Онлайн кинотеатр
Онлайн кинотеатры часто предоставляют различные подписки для своих пользователей. Рассмотрим пример, где у такого кинотеатра есть базовый тариф и премиальный тариф, который предлагает все основные функции плюс эксклюзивный контент.
public class BasePlan < public void StreamStandardContent() < Console.WriteLine("Показываем контент базового плана. "); >> public class PremiumPlan : BasePlan < public void StreamExclusiveShows() < Console.WriteLine("Показываем контент премиального плана. "); >>PremiumPlan предлагает все, что и BasePlan , но также добавляет и новый, эксклюзивный контент.
Полиморфизм
Полиморфизм немного напоминает универсальный пульт дистанционного управления, который может адаптироваться для управления различными устройствами. В программировании это означает, что один интерфейс может использоваться для управления разными методами, давая разные результаты в зависимости от контекста.
Пример: Музыкальный плеер
Представьте себе музыкальный плеер, который может воспроизводить разные аудиоформаты, такие как mp3, wav и flac. Для каждого формата требуется свой метод воспроизведения, однако, вместо создания методов Play , PlayMp3 , PlayWav , PlayFlac , правильнее будет использовать общий метод Play .
public class MusicPlayer < public virtual void Play() < Console.WriteLine("Воспроизводим аудио в стандартном формате. "); >> public class Mp3Player : MusicPlayer < public override void Play() < Console.WriteLine("Воспроизводим mp3. "); >> public class WavPlayer : MusicPlayer < public override void Play() < Console.WriteLine("Воспроизводим wav. "); >> public class FlacPlayer : MusicPlayer < public override void Play() < Console.WriteLine("Воспроизводим flac. "); >>В этом примере независимо от аудиоформата у нас есть один постоянный метод Play , выполнение которого меняется в зависимости от формата.
Пример: Виртуальный ассистент
Подумайте о виртуальном ассистенте, который работает на смартфоне, смарт-часах и смарт-колонке. Вы можете попросить все эти устройства "Включить свет", но ответ может быть адаптирован в зависимости от устройства.
public class VirtualAssistant < public virtual void ExecuteCommand(string command) < Show($"Выполняю команду . "); > > public class SmartwatchAssistant : VirtualAssistant < public override void ExecuteCommand(string command) < ShowOnSmallScreen($"Выполняю команду . "); > > public class SmartSpeakerAssistant : VirtualAssistant < public override void ExecuteCommand(string command) < Say($"Выполняю команду . "); > >Команда одинакова, но ее выполнение адаптируется в зависимости от контекста устройства. В базовом случае мы просто выводим сообщение о том, что команда выполняется, на экран ( Show ). У умных часов экран маленький, поэтому нам нужен особый способ вывода сообщения на экран ( ShowOnSmallScreen ), а у умной колонки вообще может не быть экрана, поэтому сообщение лучше озвучить голосом ( Say ).
Пример: Потоковое видео
Рассмотрим платформу потокового видео, которая изменяет свое качество воспроизведения в зависимости от скорости интернета пользователя: HD, SD или 4K.
public class VideoStreamer < public virtual void Stream() < Console.WriteLine("Показываем в обычном качестве. "); >> public class HDStreamer : VideoStreamer < public override void Stream() < Console.WriteLine("Показываем в HD качестве. "); >> public class SDStreamer : VideoStreamer < public override void Stream() < Console.WriteLine("Показываем в SD качестве. "); >> public class FourKStreamer : VideoStreamer < public override void Stream() < Console.WriteLine("Показываем в 4K качестве. "); >>Независимо от качества интернета, пользователь просто запускает метод Stream, а платформа корректирует качество трансляции сама.
Абстракция
Абстракция похожа на использование умного устройства, не зная его сложной схемы. Например, чтобы переключить канал на телевизоре, мы просто нажимаем на кнопку на пульте, как кодируется пультом нажатие на кнопку, передается на телевизор и декодируется нам не важно. Важно чтобы канал переключился, а не тонкости радиотехники. Вот и в программировании абстракция означает предоставление основных функций без погружения в детали.
Пример: Автомобиль
Чтобы управлять автомобилем, нам в базовом случае достаточно знать о том, где находится руль, педаль тормоза и газа (да-да, и педаль сцепления для механики). То есть чтобы ехать нам совсем не нужно понимать тонкости работы двигателя, передачи крутящего момента, как устроен гидро или электроусилитель руля. Мы просто нажимаем на газ и машина едет, крутим руль и она поворачивает. Это и есть абстракция.
public abstract class Car < public void Accelerate() < Console.WriteLine("Разгоняемся. "); >public void Brake() < Console.WriteLine("Тормозим. "); >// Абстрактный метод запуска, различающийся для разных двигателей public abstract void TurnOnEngine(); > public class ElectricCar : Car < public override void TurnOnEngine() < Console.WriteLine("Запускаем электрический двигатель. "); >> public class DieselCar : Car < public override void TurnOnEngine() < Console.WriteLine("Запускаем дизельный двигатель. "); >>Независимо от типа автомобиля, мы запускаем двигатель нажатием на кнопку Start, не обращая внимания на то, что на самом деле процесс под капотом различается.
Пример: Погода
Что мы делаем, чтобы узнать прогноз погоды на сегодня? Просто открываем приложение на телефоне и оно показывает нам погоду. Как оно собирает для этого данные, как их обрабатывает, все это скрыто от нас.
public abstract class WeatherApp < public void DisplayForecast() < Console.WriteLine("Показываем текущий прогноз погоды. "); >// Абстрактный метод получения данных, различающийся для текущего способа связи public abstract void GetWeatherData(); > public class WifiWeatherApp : WeatherApp < public override void GetWeatherData() < Console.WriteLine("Запрашиваем данные по WiFi. "); >> public class MobileWeatherApp : WeatherApp < public override void GetWeatherData() < Console.WriteLine("Запрашиваем данные по мобильной сети. "); >>Мы просто видим прогноз погоды на экране, хотя способ его получения может отличаться.
Пример: Кофемашина
Чтобы приготовить кофе в кофемашине мы заливаем воду, засыпаем кофейные зерна и выбираем тип кофе. Как дальше кофемашина заваривает его, скрыто от нас.
public abstract class CoffeeMachine < public void PourWater() < Console.WriteLine("Заливаем воду. "); >public void AddBeans() < Console.WriteLine("Засыпаем зерна. "); >// Абстрактный метод, специфичный для каждой кофемашины public abstract void BrewCoffee(); > public class EspressoMachine : CoffeeMachine < public override void BrewCoffee() < Console.WriteLine("Варим с использованием пара под высоким давлением. "); >> public class DripCoffeeMachine : CoffeeMachine < public override void BrewCoffee() < Console.WriteLine("Пропускаем горячую воду через зерна. "); >>В обоих случаях мы в результате получаем кофе, но метод заваривания (абстрагированный процесс) различается между машинами.
Заключение
Хоть эти концепции и могут казаться абстрактными, я очень надеюсь, что аналогии из реальной жизни и примеры кода помогают их понять. При этом, важно помнить, что ООП - это не серебрянная пуля и не высеченные в камне истины, которым всегда и везде нужно следовать. Ведь самое главное в нашей работе - это создание кода, который решает реальные проблемы, ну и желательно, чтобы его было удобно поддерживать и масштабировать.
Хочу также пригласить вас на бесплатный вебинар, где я расскажу про пять ключевых программных парадигм в C#: процедурное, объектно-ориентированное, функциональное, событийное и компонентно-ориентированное программирование. Мы рассмотрим основные характеристики каждого подхода, их преимущества и недостатки, а также примеры их применения на практике. Регистрируйтесь, будет интересно!
Принципы объектно-ориентированного программирования
Когда речь заходит о классических паттернах проектирования, нельзя не вспомнить о самом объектно-ориентированном программировании. Ведь паттерны GoF являются паттернами именно объектно-ориентированного программирования. В функциональном же программировании есть свои собственные паттерны.
Вообще устроено все следующим образом: есть само объектно-ориентированное программирование. У него есть принципы. Из принципов объектно-ориентированного программирования следуют разобранные нам шаблоны GRASP (как вариант — SOLID принципы), из которых, в свою очередь, следуют шаблоны GoF. Из них же следует ряд интересных вещей, например, enterprise паттерны.
Объектно-ориентированная парадигма
Определение гласит, что «Объектно-ориентированное программирование – это парадигма программирования, в которой основной концепцией является понятие объекта, который отождествляется с предметной областью.»
Таким образом, система представляется в виде набора объектов предметной области, которые взаимодействуют между собой некоторым образом. Каждый объект обладает тремя cоставляющими: идентичность (identity), состояние (state) и поведение (behaviour).
Состояние объекта — это набор всех его полей и их значений.
Поведение объекта — это набор всех методов класса объекта.
Идентичность объекта — это то, что отличает один объект класса от другого объекта класса. С точки зрения Java, именно по идентичности определяется метод equals.
Принципы объектно-ориентированного программирования
Объектно-ориентированное программирование обладает рядом принципов. Представление об их количестве расходится. Кто-то утверждает, что их три (старая школа программистов), кто-то, что их четыре (новая школа программистов):
- Абстрация
- Инкапсуляция
- Наследование
- Полиморфизм
Инкапсуляция
Вопреки мнению многих собеседующихся (а иногда и собеседуемых), инкапсуляция это не «когда все поля приватные». Инкапсуляция является фундаментальнейшим принципом проектирования ПО, ее следы наблюдаются на только на уровне микро-, но и на уровне макропроектирования.
Научное определение гласит, что «Инкапсуляция – это принцип, согласно которому любой класс и в более широком смысле – любая часть системы должны рассматриваться как «черный ящик»: пользователь класса или подсистемы должен видеть только интерфейс (т.е. список декларируемых свойств и методов) и не вникать во внутреннюю реализацию.»
Таким образом, получается, что если класс A обращается к полям класса B напрямую, это приводит не к тому, что «нарушается информационная безопасность», а к тому, что класс A завязывается на внутренне устройство класса B, и попытка изменить внутреннее устройство класса B приведет к изменению класса А. Более того, класс A не просто так работает с полями класса B, он работает по некоторой бизнес-логике. То есть логика по работе с состоянием класса В лежит в классе А, и когда мы захотим переиспользовать класс В, это не удастся сделать, ведь без кусочка класса А класс В может быть бесполезным, что приведет к тому, что класс В придется отдавать вместе с классом А. Экстраполируя это на всю систему, получается, что переиспользовать можно будет только всю систему целиком.
Инкапсуляция является самым недооцененным принципом, который, к сожалению, мало кем интерпретируется правильно. Она позволяет минимизировать число связей между классами и подсистемами и, соответственно, упростить независимую реализацию и модификацию классов и подсистем.
Наследование
Наследование — это возможность порождать один класс от другого с сохранением всех свойств и методов класса-предка (суперкласса), добавляя при необходимости новые свойства и
методы.Наследование является самым переоцененным принципом. Когда-то считалось, что «У идеального программиста дерево наследования уходит в бесконечность и заканчивается абсолютно пустым объектом», потому как когда-то люди не очень хорошо понимали то, что наследование — это способ выразить такое свойство реального мира как иерархичность, а не способ переиспользовать код, отнаследовав машину от холодильника, потому что у обоих предметов есть ручка. Наследования желательно по возможности избегать, потому что наследование является очень сильной связью. Для уменьшения количества уровней наследования рекомендуется строить дерево «снизу-вверх».
Полиморфизм
Полиморфизм — это возможность использовать классы – потомки в контексте, который был предназначен для класса – предка.
За самым садистским определением кроется возможность языка программирования для декомпозиции задачи и рефакторинга if'ов и switch'ей.