понедельник, 5 сентября 2016 г.

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

В области проектирования существует два понятия, которые часто используются совместно – инкапсуляция (encapsulation) и сокрытие информации (information hiding).

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

(*) – иногда понятие инкапсуляции применяется в более широком смысле. Например, говорят, что фабрика «инкапсулирует» информацию о конкретном типе создаваемого объекта. В этом контексте инкапсуляция является синонимом сокрытия информации.

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

В результате, при проектирования класса/модуля, появляется две составляющие: открытая часть – интерфейс, и закрытая часть – реализация. При этом интерфейс класса или модуля должен не просто дублировать закрытую часть через аксессоры (свойства или get/set-методы), но он должен давать клиенту более высокоуровневый, абстрактный интерфейс. Другими словами, открытая часть класса должна в большей степени говорить о том, что делает этот класс или модуль, и скрывать от клиентов ненужные детали, скрывать, как он это делает.

Абстракция и икапсуляция дополняют друг друга: абстрагирование направлено на наблюдаемое поведение объекта, а инкапсуляция занимается внутренним устройством.
Гради Буч, «Объектно-ориентированный анализ и проектирование»

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

Вот небольшой и наивный пример:

public class Employee { }
public class Paystub
{
   
private readonly List<Employee> _employees = new List<Employee
>();

   
public IList<Employee> Employees =>
_employees;

   
public decimal
ComputePayroll()
    {
       
// Используем _employees
        return 42
;
    }
}

public class Paystub2
{
   
private readonly List<Employee> _employees = new List<Employee
>();

   
public void AddEmployee(Employee e) { _employees.
Add(e); }

   
public decimal
ComputePayroll()
    {
       
// Используем _employees
        return 42;
    }
}

В обоих случаях у нас нет открытых данных, но «качество» дизайна явно разное. Оба класса обладают инкапсуляцией, но первая реализация скрывает детали реализации не полностью.

Вот еще один пример, который показывает, что сокрытие информации – это нечто большее, нежели закрытые данные.

Допустим, вы разрабатываете ынтырпрайз приложение, которое делает что-то важное. Любое нормальное приложение обычно требует некоторой конфигурации: ну, параметры подключения к БД или серверу, и другую ценную информацию. И вот, матерый артихектор вместе с не менее матерым клиентом просят прочитать конфигурацию из конфигурационного файла.

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

Теперь вы становитесь перед выбором: вы можете «размазать» сведения о конфигурации ровным слоем по всему приложению. Каждый компонент, которому нужны некоторые параметры, сам полезет в app config, вытянет оттуда нужные данные, пропарсит xml или json и будет готов служить. С другой стороны, очевидно, что решение о том, где именно хранится конфигурация и в каком формате, может измениться в будущем. Поэтому более вменяемым решением будет скрыть информацию о местоположении и формате конфигурации в одном модуле, например, с помощью классов Configuration и ConfigurationProvider. В этом случае, когда (да, именно, «когда», а не «если») требования изменятся, то поменяется лишь реализация класса ConfigurationProvider, а все остальные пользователи этого класса или конфигурации останутся неизменными. Аналогично, при изменении формата, поменяется тоже только процесс парсинга, а не потребители конфигурации.

Этот пример кажется надуманным, но это не так! Мы довольно часто сталкиваемся с изменчивостью требований, но используем, к сожалению, один из двух подходов:

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

Более же разумный подход находится где-то по середине. Каждый раз, когда я начинаю разработку некоторой фичи, я думаю, сколько кусков в коде придется поменять, если требования или детали реализации существенно изменятся. При этом я не стараюсь свести количество изменений к 1 (ну, типа, если мы следуем SRP, то должно быть только одно место, в случае изменения требованй). Я стараюсь, чтобы этих мест было мало, а изменения были простыми.

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

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

Теперь у нас есть достаточно примеров, чтобы понять, что такое сокрытие информации.

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

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

7 комментариев:

  1. Вот кстати у меня был жизненный пример как раз с чтением конфигурации.
    Мы сразу же выносили её в отдельные классы и всё было вроде бы хорошо (контракт был естественно синхронным).
    А потом появилась необходимость использовать азуровский KeyVault, у которого асинхронные методы доступа. И вот тут начались большие проблемы, потому что тот человек который это сделал, просто взял и написал task.Result для того чтобы сохранить синхронный контракт. В итоге, была очень весёлая ситуация, когда наши сервисы стали наглухо падать через час работы (скорее всего во время обновления токена что-то лочилось). Хорошо еще что я подозревал где могла быть проблема и воткнул костыль для того чтобы оно всё работало (и даже угадал что это было именно там).
    Хотелось потом переписать всё на нормальные async методы, но объём работы нас испугал - слишком много где использовались конфигурационные классы и не вышло так просто поменять всё. Так что, скорее всего этот быстрый хакфикс так и останется в проекте.

    ОтветитьУдалить
  2. Согласен, что сокрытие информации увеличивает «качество» дизайна. Но жизнь вносит нюансы:

    1) EF для ленивой загрузки списка потребует свойства с интерфейсом ICollection (тут печальная рожица)

    2) Увеличивается количество кода при написании тестов, т. к. для получения инициализированного экземпляра приходится использовать AddSomething(), SetSomething() и пр. вместо инициализации свойств.

    Для себя использую сокрытие информации только для объектов и сервисов предметной области. Все же DTO делаю с публичными get/set.

    ОтветитьУдалить
    Ответы
    1. Лично я для себя пришел к концепции "классы работающие с базой отдельно, внешний контракт слоя работы с базой отдельно".
      Я тоже довольно долго пытался делать "по книжкам", в которых ОРМ магическим образом избавляет нас от проблем с персистентностью. Но на практике это всегда разбивается о кучу проблем - для того чтобы держать какой-то баланс между доменной моделью и базой приходилось городить кучу неочевидных вещей типа хитрых маппингов и вот таких уступок для коллекций.
      Так что в итоге решил что классы для "слепка базы" одни, но они internal и служат только для работы с базой и контекстом. А вот доменная модель уже public и я конвертирую их туда-сюда.
      Попутно избавился от lazy (т.к. считаю что от него больше вреда чем пользы) и загружаю всё что надо явным образом. Или не загружаю, но это отображается на публичной модели.

      Удалить
    2. >2) Увеличивается количество кода при написании тестов, т. к. для получения инициализированного экземпляра приходится использовать AddSomething(), SetSomething() и пр. вместо инициализации свойств.

      В .NET как вариант можно делать свойства internal и с помощью атрибута InternalsVisibleTo открыть доступ для проектов с тестами. Я очень часто так делаю.

      Удалить
  3. Могу дополнить цитатой из книги "Growing object-oriented software, guided by tests" (p. 49):
    Encapsulation and Information Hiding
    We want to be careful with the distinction between “encapsulation” and “information
    hiding.” The terms are often used interchangeably but actually refer to two separate,
    and largely orthogonal, qualities:
    Encapsulation
    Ensures that the behavior of an object can only be affected through its API.
    It lets us control how much a change to one object will impact other parts of
    the system by ensuring that there are no unexpected dependencies between
    unrelated components.
    Information hiding
    Conceals how an object implements its functionality behind the abstraction
    of its API. It lets us work with higher abstractions by ignoring lower-level details
    that are unrelated to the task at hand.
    We’re most aware of encapsulation when we haven’t got it. When working with
    badly encapsulated code, we spend too much time tracing what the potential
    effects of a change might be, looking at where objects are created, what common
    data they hold, and where their contents are referenced. The topic has inspired
    two books that we know of, [Feathers04] and [Demeyer03].

    ОтветитьУдалить
    Ответы
    1. Также где-то там же в этой книге высмеивалась идея, когда делают члены класса приватными, но тут же открывают к ним доступ через set/get-методы. Я тут согласен. В таком случае сокрытием информации и не пахнет. :)

      Удалить
    2. Алексей, спасибо за дополнения и отличную цитату.

      Удалить