Должен ли я всегда полностью инкапсулировать внутреннюю структуру данных?

11

Пожалуйста, рассмотрите этот класс:

class ClassA{

    private Thing[] things; // stores data

    // stuff omitted

    public Thing[] getThings(){
        return things;
    }

}

Этот класс предоставляет массив, который он использует для хранения данных, любому клиентскому коду.

Я сделал это в приложении, над которым я работаю. У меня был ChordProgressionкласс, который хранит последовательность Chords (и делает некоторые другие вещи). У него был Chord[] getChords()метод, который возвращал массив аккордов. Когда структура данных должна была измениться (с массива на ArrayList), весь клиентский код сломался.

Это заставило меня задуматься - может быть, следующий подход лучше:

class ClassA{

    private Thing[] things; // stores data

    // stuff omitted

    public Thing[] getThing(int index){
        return things[index];
    }

    public int getDataSize(){
        return things.length;
    }

    public void setThing(int index, Thing thing){
        things[index] = thing;
    }

}

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

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

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


Этот подход распространен? Что ты думаешь об этом? Какие минусы есть у других? Разумно ли, чтобы включающий класс реализовывал как минимум три открытых метода только для делегирования внутренней структуре данных?

Авив Кон
источник

Ответы:

14

Код как:

   public Thing[] getThings(){
        return things;
    }

Не имеет особого смысла, так как ваш метод доступа не делает ничего, кроме непосредственного возврата внутренней структуры данных. С тем же успехом вы можете просто заявить Thing[] thingsо себе public. Идея метода доступа состоит в том, чтобы создать интерфейс, который изолирует клиентов от внутренних изменений и не позволяет им манипулировать реальной структурой данных, за исключением скрытых способов, разрешенных интерфейсом. Как вы обнаружили, когда весь ваш клиентский код сломался, ваш метод доступа этого не сделал - это просто напрасный код. Я думаю, что многие программисты, как правило, пишут такой код, потому что они где-то узнали, что все должно быть инкапсулировано с методами доступа - но это по причинам, которые я объяснил. Делать это просто для того, чтобы «следовать форме», когда метод доступа не служит какой-либо цели, - это просто шум.

Я определенно рекомендую ваше предлагаемое решение, которое решает некоторые из наиболее важных задач инкапсуляции: предоставление клиентам надежного, незаметного интерфейса, который изолирует их от внутренних деталей реализации вашего класса и не позволяет им касаться внутренней структуры данных. ожидайте способами, которые вы решите, уместны - «закон наименее необходимой привилегии». Если вы посмотрите на большие популярные платформы ООП, такие как CLR, STL, VCL, предложенный вами шаблон широко распространен именно по этой причине.

Вы должны всегда делать это? Не обязательно. Например, если у вас есть вспомогательные или дружественные классы, которые, по сути, являются компонентом вашего основного рабочего класса и не являются «фронтальными», это необязательно - это излишнее количество, которое добавит много ненужного кода. И в этом случае я бы вообще не использовал метод доступа - как объяснено, это бессмысленно. Просто объявите структуру данных так, чтобы она была доступна только для основного класса, который ее использует - большинство языков поддерживают способы сделать это - friendили объявив ее в том же файле, что и основной рабочий класс, и т. Д.

Единственный недостаток, который я вижу в вашем предложении, заключается в том, что это больше работы для кодирования (и теперь вам придется перекодировать ваши потребительские классы - но вы все равно должны были это делать). Но это не совсем недостаток - Вы должны сделать это правильно, а иногда это требует больше работы.

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

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

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

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

Вектор
источник
1
Спасибо за ответ. Вопрос: а что, если внутренняя структура данных имеет, может быть, 5 открытых методов, - которые все должны быть показаны открытым интерфейсом моего класса? Например, Java ArrayList имеет следующие методы: get(index), add(), size(), remove(index), и remove(Object). Используя предложенную технику, класс, который содержит этот ArrayList, должен иметь пять открытых методов только для делегирования внутренней коллекции. И цель этого класса в программе, скорее всего, заключается не в инкапсуляции этого ArrayList, а скорее в выполнении чего-то другого. ArrayList - это просто деталь. [...]
Авив Кон
Внутренняя структура данных - это просто обычный член, который использует описанную выше технику - требует, чтобы он содержал класс, чтобы иметь дополнительные пять открытых методов. По вашему мнению - это разумно? А также - это распространено?
Авив Кон
@Prog - Как насчет того, если внутренняя структура данных имеет, может быть, 5 открытых методов ... IMO, если вы обнаружите, что вам нужно обернуть весь вспомогательный класс внутри вашего основного класса и представить его таким образом, вам нужно переосмыслить дизайн - ваш публичный класс делает слишком много и / или не представляет соответствующий интерфейс. Класс должен иметь очень осторожную и четко определенную роль, а его интерфейс должен поддерживать эту роль и только эту роль. Подумайте о том, чтобы разбить и разделить на классы Класс не должен быть «кухонной раковиной», которая содержит всевозможные объекты во имя инкапсуляции.
Вектор
Если вы представляете всю структуру данных через класс-оболочку, IMO вам нужно подумать о том, почему этот класс вообще инкапсулируется, если не просто обеспечить более безопасный интерфейс. Вы говорите, что содержащий класс не существует для этой цели - поэтому что-то не так с этим дизайном.
Вектор
1
@Phoshi - только для чтения это ключевое слово - я могу согласиться с этим. Но OP не говорит только о чтении. например removeне только для чтения. Насколько я понимаю, ОП хочет обнародовать все - как в оригинальном коде до предлагаемого изменения. public Thing[] getThings(){return things;}Это то, что мне не нравится.
вектор
2

Вы спросили: я должен всегда полностью инкапсулировать внутреннюю структуру данных?

Краткий ответ: да, большую часть времени, но не всегда .

Длинный ответ: я думаю, что классы делятся на следующие категории:

  1. Классы, которые инкапсулируют простые данные. Пример: 2D точка. Легко создавать публичные функции, которые предоставляют возможность получать / устанавливать координаты X и Y, но вы можете легко скрыть внутренние данные без особых проблем. Для таких классов раскрытие деталей внутренней структуры данных не требуется.

  2. Контейнерные классы, которые инкапсулируют коллекции. STL имеет классические контейнерные классы. Я считаю std::stringи std::wstringсреди тех тоже. Они обеспечивают богатый интерфейс для борьбы с абстракциями , но std::vector, std::stringи std::wstringтакже обеспечить возможность получить доступ к необработанным данным. Я бы не торопился называть их плохо разработанными классами. Я не знаю оправдания для этих классов, выставляющих их необработанные данные. Тем не менее, в своей работе я счел необходимым выставлять необработанные данные при работе с миллионами узлов сетки и данными на этих узлах сетки по соображениям производительности.

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

  3. Классы, которые в основном функциональны по своей природе. Примеры: std::istream, std::ostream, итераторы таких контейнеров STL. Совершенно глупо раскрывать внутренние детали этих классов.

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

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

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

Вместо того, чтобы возвращать необработанные данные напрямую, попробуйте что-то вроде этого

class ClassA {
  private Things[] things;
  ...
  public Things[] asArray() { return things; }
  public List<Thing> asList() { ... }
  ...
}

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

class ClassA {
  private List<Thing> things;
  ...
  public Things[] asArray() { return things.asArray(); }
  public List<Thing> asList() { return things; }
  ...
}

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

BobDalgleish
источник
Умная идея для обратной совместимости. Но: теперь у вас есть правильная инкапсуляция, скрыть детали реализации - не совсем. Клиентам еще приходится разбираться с нюансами List. Методы доступа, которые просто возвращают элементы данных, даже с помощью преобразования, чтобы сделать вещи более надежными, не очень хорошая инкапсуляция IMO. Рабочий класс должен обрабатывать все это, а не клиент. Чем «тупее» должен быть клиент, тем он будет надежнее. Кроме того, я не уверен, что вы ответили на вопрос ...
Вектор
1
@Vector - вы правы. Возвращенная структура данных все еще изменчива, и побочные эффекты убьют сокрытие информации.
BobDalgleish
Возвращенная структура данных все еще изменчива, и побочные эффекты убьют сокрытие информации - да, это тоже - это опасно. Я просто думал с точки зрения того, что требуется от клиента, что было в центре внимания вопроса.
Вектор
@BobDalgleish: почему бы не вернуть копию оригинальной коллекции?
Джорджио
1
@BobDalgleish: Если нет веских причин для производительности, я бы посоветовал возвращать ссылку на внутренние структуры данных, чтобы позволить пользователям изменять их, как очень плохое проектное решение. Внутреннее состояние объекта следует изменять только с помощью соответствующих открытых методов.
Джорджио
0

Вы должны использовать интерфейсы для этих вещей. Не поможет в вашем случае, так как массив Java не реализует эти интерфейсы, но вы должны делать это с этого момента:

class ClassA{

    public ClassA(){
        things = new ArrayList<Thing>();
    }

    private List<Thing> things; // stores data

    // stuff omitted

    public List<Thing> getThings(){
        return things;
    }

}

Таким образом , вы можете изменить ArrayListк LinkedListили что - нибудь еще, и вы не будете нарушать любой код , так как все коллекции Java (кроме массивов) , которые имеют (псевдо?) Произвольный доступ, вероятно , реализует List.

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

Идан Арье
источник
-1 - плохой компромисс и не особо безопасный IMO: вы предоставляете клиенту внутреннюю структуру данных, просто маскируете ее и надеетесь на лучшее, потому что «коллекции Java ... вероятно, реализуют List». Если бы ваше решение было действительно полиморфным / основанным на наследовании - что все коллекции неизменно реализуются Listкак производные классы, это имело бы больше смысла, но просто «надеяться на лучшее» не очень хорошая идея. «Хороший программист смотрит в обе стороны на улице с односторонним движением».
Вектор
@Vector Да, я предполагаю, что будущие коллекции Java будут реализованы List(или Collection, по крайней мере Iterable). В этом весь смысл этих интерфейсов, и обидно, что массивы Java не реализуют их, но они являются официальным интерфейсом для коллекций в Java, поэтому не так уж и сложно предположить, что любая коллекция Java будет их реализовывать - если эта коллекция не старше чем List, и в этом случае очень легко обернуть его с помощью AbstractList .
Идан Арье
Вы говорите, что ваше предположение практически гарантировано, так что все в порядке - я уберу отрицательный голос (когда мне позволят), потому что вы были достаточно порядочны, чтобы объяснить, а я не Java-парень кроме как осмосом. Тем не менее, я не поддерживаю эту идею разоблачения внутренней структуры данных, независимо от того, как это делается, и вы прямо не ответили на вопрос OP, который на самом деле касается инкапсуляции. т.е. ограничение доступа к внутренней структуре данных.
Вектор
1
@Vector Да, пользователи могут использовать Listих ArrayList, но это не значит, что реализация может быть защищена на 100% - вы всегда можете использовать отражение для доступа к закрытым полям. Делать это не одобряется, но кастинг также не одобряется (хотя и не так сильно). Смысл инкапсуляции не в том, чтобы предотвратить злонамеренный взлом, а скорее в том, чтобы пользователи не зависели от деталей реализации, которые вы, возможно, захотите изменить. Использование Listинтерфейса делает именно это - пользователи класса могут зависеть от Listинтерфейса, а не от конкретного ArrayListкласса, который может измениться.
Идан Арье
Вы всегда можете использовать отражение, чтобы получить доступ к закрытым полям - если кто-то хочет написать плохой код и испортить дизайн, он может это сделать. скорее, чтобы предотвратить пользователей ... - это одна из причин инкапсуляции. Другой - обеспечить целостность и согласованность внутреннего состояния вашего класса. Проблема не в «злонамеренном взломе», а в плохой организации, которая приводит к неприятным ошибкам. «Закон наименьшей необходимой привилегии» - дайте потребителю вашего класса только то, что обязательно - не более. Если вы обязательно сделаете всю внутреннюю структуру данных общедоступной, у вас возникнут проблемы с дизайном.
Вектор
-2

Это довольно часто, чтобы скрыть вашу внутреннюю структуру данных от внешнего мира. Иногда это излишне, особенно в DTO. Я рекомендую это для модели домена. Если вообще требуется его выставить, верните неизменную копию. Наряду с этим я предлагаю создать интерфейс с такими методами, как get, set, remove и т. Д.

VGaur
источник
1
Похоже, это не дает ничего существенного по сравнению с 3 предыдущими ответами
Гнат