Skip to content

Latest commit

 

History

History
332 lines (246 loc) · 18.6 KB

01_04_Инкапсуляция.md

File metadata and controls

332 lines (246 loc) · 18.6 KB

Инкапсуляция

Два основных определения инкапсуляции:

  1. Инкапсуляция - это языковая конструкция, позволяющая связать данные с методами, предназначенными для обработки этих данных.
  2. Инкапсуляция - это механизм языка, позволяющий ограничить доступ одних компонентов программы к другим;

Wikipedia

Инкапсуляция - это механизм ограничения доступа к данным и предоставление интерфейса для взаимодействия с ними. Под интерфейсом подразумевается набор открытых методов.

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

Что именно в данном примере является инкапсуляцией?

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

То есть инкапсуляция ограничивает доступ к двигателю и предоставляет удобный интерфейс взаимодействия с ним.

Инкапсуляция очерчивает круг связанных данных и функций. За пределами этого круга данные невидимы и доступны только некоторые функции. Воплощение этого понятия можно наблюдать в виде приватных членов данных и общедоступных членов-функций класса.

— Роберт Мартин (Чистая Архитектура)

Проблема

В некоторых языках программирования (C, C++), есть возможность создавать глобальные поля. Значение таких полей могут менять из любого места программы.

Пример на С++:

#include <iostream>

// Глобальное поле
int counter = 0;

// Увеличиваем счётчик
void IncreaseCounter () {
    ++counter;
}

// Выводим сообщения и снова увеличивается счётчик
void ShowMessage () {
    std::cout << "Show message..." << std::endl;
    ++counter;
}

// Убиваем игрока и снова увеличивается счётчик
void KillPlayer () {
    std::cout << "Kill player..." << std::endl;
    ++counter;
}

int main() {
    std::cout << counter << std::endl; // 0

    IncreaseCounter();
    ShowMessage(); // Show message...
    KillPlayer(); // Kill player...
    
    std::cout << counter << std::endl; // 3
}

В данном примере целых 3 функции меняют значение одной переменной counter. Ее значение должно было менять только одна функция IncreaseCounter.

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

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

Решение

На помощь пришли объекты с инкапсуляцией. Инкапсуляция позволяет объединить поля и функции в одном объекте, таким образом запретив случайный доступ к полям. Объект является защитной обёрткой данных, а методы безопасным способом доступа к ним.

#include <iostream>

// Счётчик находится внутри класса
class Counter {
    public:
    // Только два метода для взаимодействия с переменной counter
    void IncreaseCounter () {
        ++counter;
    }

    int GetCounter () {
        return counter;
    }

    private:
    // Переменная скрыта
    int counter = 0;
};

void ShowMessage () {
    std::cout << "Show message..." << std::endl;
}

void KillPlayer () {
    std::cout << "Kill player..." << std::endl;
}

int main() {
    Counter counter; // Обёртка счётчика
    std::cout << counter.GetCounter() << std::endl; // 0
    
    // Доступ только через метод класса
    counter.IncreaseCounter();
    ShowMessage();
    KillPlayer();
    
    // Ошибка: 'counter' is a private member of 'Counter'
    // counter.counter = 1;

    std::cout << counter.GetCounter() << std::endl; // 1
}

Получить доступ напрямую к переменной уже не получится, так как поле скрыто внутри класса:

// Ошибка: 'counter' is a private member of 'Counter'
counter.counter = 1;

Если взять пример человека, все его органы снова внутри тела и никто не может их потрогать.

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

Сокрытие данных или методов происходит при помощи модификаторов доступа: public, protected, private, internal. Таким образом можно ограничить уровень доступа к полям и методам.

Придерживаясь инкапсуляции нельзя получить доступ к полям напрямую. Обращаться или менять состояние объекта можно только через методы (конструктор тоже является методом). Состояние объекта — это текущее значение всех полей объекта.

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

Инкапсуляцию можно применять не только на классах, но и на модулях/сборках.

Сокрытие vs Инкапсуляция

Первое и самое важно - инкапсуляция и сокрытия это два разных понятия, и инкапсуляция не является сокрытием, но в инкапсуляцию входят задачи сокрытия.

Сокрытие информации (information hiding). Процесс сокрытия всех секретов объекта, которые не относятся к его существенным характеристикам. Обычно скрывается структура объекта и реализация его методов.

Объектно-ориентированный анализ и проектирование с примерами приложений - Гради Буч

Сокрытие информации – это сокрытие деталей проектирования, которые могут измениться в будущем.

Сокрытие данных - это сокрытие полей/переменных от доступа, как правило при помощи модификатора доступа private. Сокрытие данных является лишь частью сокрытия информации; сокрытие информации более широкое понятие.

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

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

Объектно-ориентированный анализ и проектирование с примерами приложений - Гради Буч

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

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

Пример (C#)

Пример сокрытия данных:

class Collection {
    private List<string> elements;
    
    public Collection (List<string> list) {
        this.elements = list;
    }
    
    public void Show () {
        foreach (var item in elements) {
            Console.WriteLine (item);
        }
    }
}

Любая функция или метод является примером сокрытия:

class Programs {
    static void Main (string[] args) {
        Console.WriteLine ("Hello World!");
    }
}

Метод WriteLine скрывает реализацию алгоритма вывода текста в консоль.

А теперь пример инкапсуляции:

class Collection {
    private List<string> elements = new List<string>();

    public void AddElement (string element) {
        elements.Add(element);
    }

    public void RemoveElement (string element) {
        elements.Remove (element);
    }

    public void ShowElements () {
        foreach (var item in elements) {
            Console.WriteLine (item);
        }
    }
}

class MainClass {
    public static void Main (string[] args) {
        Collection collection = new Collection ();
        collection.AddElement ("Name 1");
        collection.AddElement ("Name 2");
        collection.AddElement ("Name 3");
        collection.ShowElements ();
        
        // Вывод на консоль
        // Name 1
        // Name 2
        // Name 3
    }
}

Доступ к коллекции можно получить только через методы. Преимущество инкапсуляции в том, что можно полностью контролировать доступ к данным, при этом конечному пользователю не придется менять свой код.

Можно легко расширить поведение класса:

interface ILogger {
    void Add (string message);
    void SaveToFile (string path);
    void Show ();
}

class ConsoleLogger : ILogger {
    private List<string> logMessages = new List<string>();
    
    public void Add (string message) {
        logMessages.Add(message);
    }

    public void SaveToFile (string path) {
        // Save to file...
    }

    public void Show () {
        foreach (var message in logMessages)
            Console.WriteLine (message);
    }
}

class Collection {
    private List<string> elements = new List<string>();
    private ILogger logger;

    public Collection (ILogger logger) {
        this.logger = logger;
    }

    public void AddElement (string element) {
        elements.Add(element);
        logger.Add("CollectionWithLogger:: Item was added.");
    }

    public void RemoveElement (string element) {
        elements.Remove (element);
        logger.Add("CollectionWithLogger:: Item was deleted.");
    }

    public void ShowElements () {
        foreach (var item in elements) {
            Console.WriteLine (item);
        }
    }

    public void ShowLogs () {
        logger.Show ();
    }
}

class MainClass {
    public static void Main (string[] args) {
        ILogger logger = new ConsoleLogger ();
        Collection collection = new Collection (logger);
        collection.AddElement ("Name 1");
        collection.AddElement ("Name 2");
        collection.AddElement ("Name 3");
        collection.ShowElements ();
        collection.ShowLogs ();

        Console.WriteLine ();
        collection.RemoveElement("Name 1");
        collection.ShowElements ();
        collection.ShowLogs ();
        
        // Вывод на консоль
        // Name 1
        // Name 2
        // Name 3
        // CollectionWithLogger:: Item was added.
        // CollectionWithLogger:: Item was added.
        // CollectionWithLogger:: Item was added.
        // 
        // Name 2
        // Name 3
        // CollectionWithLogger:: Item was added.
        // CollectionWithLogger:: Item was added.
        // CollectionWithLogger:: Item was added.
        // CollectionWithLogger:: Item was deleted.
    }
}

Итоги

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

Источники

  1. Clean Architecture - Robert C. Martin - Book
  2. ООП. Инкапсуляция и сокрытие данных - Website
  3. Учебник: объектно-ориентированное программирование - Website
  4. Инкапсуляция (программирование) - Wikipedia
  5. Инкапсуляция и сокрытие информации - Статья Blogspot