Что такое инкапсуляция?

03.11.2019 1 Автор Jeff

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

Я хочу, чтобы Вы кое о чем подумали:

Это два варианта реализации класса TimeInterval:

public class TimeInterval {
    public Date start;
    public Date end;

    public TimeInterval(Date start, Date end) {
        this.start = start;
        this.end = end;
    }
}


public class TimeInterval {
    public Date start;
    public Date end;

    public TimeInterval(Date start, Date end) {
        this.start = start;
        this.end = end;
    }
}


public class TimeInterval {
    private Date start;
    private Date end;

    public TimeInterval(Date start, Date end) {
        this.start = start;
        this.end = end;
    }

    public Date getStart() {
        return start;
    }

    public void setStart(Date start) {
        this.start = start;
    }

    public Date getEnd() {
        return end;
    }

    public void setEnd(Date end) {
        this.end = end;
    }
}

public class TimeInterval {
    private Date start;
    private Date end;

    public TimeInterval(Date start, Date end) {
        this.start = start;
        this.end = end;
    }

    public Date getStart() {
        return start;
    }

    public void setStart(Date start) {
        this.start = start;
    }

    public Date getEnd() {
        return end;
    }

    public void setEnd(Date end) {
        this.end = end;
    }
}

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

А вот фиг вам! Ни черта это не инкапсуляция!
Вот подумайте. Если для нас важно чтобы start <= end, то какая вообще разница, меняем мы значение переменной start через сеттер или напрямую?

Если Вы думаете, что инкапсуляция это просто “спрятать все поля внутри класса от пользователя”, то Вы заблуждаетесь. Это лишь имитация, а инкапсуляция это нечто другое.

При желании можно вообще оставить все поля публичными, но при этом договориться о том, как работать с таким объектом не повреждая его. Вернее предоставить методы для корректного манипулирования данными.

 

Так что такое инкапсуляция?

Это механизм позволяющий нам обеспечить согласованность данных объекта или модуля, от приведения их в некорректное состояние. Обычно для этого прячут все поля и предоставляют только простой набор методов. Как я уже говорил, нам не обязательно даже прятать всё и вся. Достаточно просто запретить “контрактом” или документацией использовать такую возможность. Существуют языки в которых нет спецификаторов доступа (private, protected, public), но в них успешно используют инкапсуляцию.

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

 

Чем плох данный пример (Код 2)?

 

Просто мы можем очень легко привести TimeInterval в некорректное состояние (рассогласовать start и end). Для этого достаточно в любой сеттер передать дату которая не будет соответствовать другой. Мы могли бы, конечно, прикрутить валидацию в сеттеры и это немного улучшило бы ситуацию. Но стало бы неудобно использовать данный класс.

Я бы сделал так:

public class TimeInterval {
    private Date start;
    private Date end;

    public TimeInterval(Date start, Date end) {
        setInterval(start, end);
    }

    public TimeInterval(Date start, long durationInMillis) {
        setInterval(start, durationInMillis);
    }

    private void setInterval(Date start, long durationInMillis) {
        // validate start and durationInMillis
        this.start = start;
        this.end = new Date(start.getTime() + durationInMillis);
    }

    private void setInterval(Date start, Date end) {
        // validate start and end
        this.start = start;
        this.end = end;
    }

    public Date getStart() {
        return start;
    }

    public Date getEnd() {
        return end;
    }
}

public class TimeInterval {
    private Date start;
    private Date end;

    public TimeInterval(Date start, Date end) {
        setInterval(start, end);
    }

    public TimeInterval(Date start, long durationInMillis) {
        setInterval(start, durationInMillis);
    }

    private void setInterval(Date start, long durationInMillis) {
        // validate start and durationInMillis
        this.start = start;
        this.end = new Date(start.getTime() + durationInMillis);
    }

    private void setInterval(Date start, Date end) {
        // validate start and end
        this.start = start;
        this.end = end;
    }

    public Date getStart() {
        return start;
    }

    public Date getEnd() {
        return end;
    }
}

Теперь нам гораздо сложнее рассогласовать переменные start и end. Приведу еще один пример из жизни:

public class Car {
    public void startMoving() {
        turnKey();
        startEngine();
        warmEngine();
        removeParkingBrake();
        pressGas();
    }

    private void pressGas() {}

    public void removeParkingBrake() {}

    private void warmEngine() {}

    private void startEngine() {}

    private void turnKey() {}
}

public class Car {
    public void startMoving() {
        turnKey();
        startEngine();
        warmEngine();
        removeParkingBrake();
        pressGas();
    }

    private void pressGas() {}

    public void removeParkingBrake() {}

    private void warmEngine() {}

    private void startEngine() {}

    private void turnKey() {}
}

В классе Car нельзя делать метод removeParkingBrake публичным, потому что машина покатится и врежется в столб, и выйдет из строя. Нельзя разрешать использовать этим методом. А если у пользователя будет возможность вызывать те методы, что находятся в startMoving, но напрямую, то это тоже плохо. Он может забыть прогреть двигатель и в итоге испортит его. Это все утечка внутренней логики работы класса. Нарушение инкапсуляции.

Смотрите метод startMoving. Первоначально задумывалось, что пользователь может использовать только startMoving. В таком случае ничего плохого не произойдет.

 

Зачем же нам нужна инкапсуляция?

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

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

Когда Вы работаете один на проекте, то Вы свой код обычно держите в голове. И, скорее всего, знаете все нюансы использования Ваших компонентов. Но в большой команде, где каждый занимается своим модулем, для Вас другие модули как черный ящик. Если на Ваш вопрос о инкапсуляции к таким разработчикам, слышите “Инкапсуляция?! Не не слышал”, то будьте уверены – прикрутив новый функционал, Вы сломаете все приложение.

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

 

Преимущества и недостатки использования инкапсуляции?

Преимущества:

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

 

Недостатки:

  1. Если Вы нашли ошибку в библиотеке которую используете, то Вам будет трудно ее исправить.
  2. Снижается скорость работы приложения.

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

 

Почему же задают такой простой вопрос на собеседовании?

 

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

Это как лакмусовая бумажка. И это лично мое мнение.

На этом все.