Нарушают ли общие объекты инкапсуляцию?

Принцип инкапсуляции гласит: «Вы должны скрывать атрибуты вашего класса и делать их доступными только через методы. Это гарантирует достоверность инвариантов класса».

Я полностью согласен с этим принципом, но мне было интересно: «Нарушает ли совместное использование одного и того же объекта несколькими классами инкапсуляцию?»

Пример: у нас есть объект класса А.

  • класс B имеет ссылку на этот объект
  • класс C имеет ссылку на этот объект

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

Например: B изменяет состояние объекта A без согласия C, потому что оно не проходит через его методы? То же самое касается C, который не проходит через B.

Меня так запутали все эти принципы, что мне кажется, что они противоречат друг другу. Возможно, я неправильно понимаю принцип инкапсуляции?

public class A {
  private int value = 0;

  public void increment(){
    value += 1;
  }

  public void decrement() {
    value -= 1;
  }
}


public class B {
  private final A a;
  
  public B(A a){
    this.a = a;
  }

  public void doSomething(){
    // Do some stuff
    a.increment();
  }
}

public class C {
  private final A a;
  
  public C(A a){
    this.a = a;
  }

  public void doSomethingElse(){
    // Do some stuff
    a.decrement();
  }
}

Что, если бы у них были копии A вместо того же A? Что, если A неизменяем? Также рассмотрим принцип замены Лискова, можете ли вы заменить A любым подклассом A?

Elliott Frisch 19.06.2024 15:33
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
В компьютерном программировании биты играют важнейшую роль в представлении и манипулировании данными на двоичном уровне. Побитовые операции...
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Приходилось ли вам сталкиваться с требованиями, в которых вас могли попросить поднять тревогу или выдать ошибку, когда метод Java занимает больше...
Полный курс Java для разработчиков веб-сайтов и приложений
Полный курс Java для разработчиков веб-сайтов и приложений
Получите сертификат Java Web и Application Developer, используя наш курс.
2
1
95
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Ответ принят как подходящий

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

Это не неизбежно

Многие классы неизменяемы. Вы ничего не сможете в них изменить. Тривиально, например, строки. Если два экземпляра «разделяют» строку, а один из них вызывает x.toLowerCase(), ничего не происходит. Потому что toLowerCase() ничего не меняет. Он создает совершенно новую строку и возвращает ее. Если один класс пишет x = x.toLowerCase(), он обновляет свою ссылку — это не изменяет ни ссылку (ни объект), на которую ссылается поле другого класса.

Таким образом, совместное использование неизменяемых объектов не приводит к этой проблеме.

Суть архитектурной дискуссии

Вот одно правило, не имеющее исключений: «У каждого правила есть исключения».

Принцип инкапсуляции является руководством. И, как и все рекомендации, настоящая компетентность предполагает знание того, когда их следует игнорировать.

В качестве общего принципа инкапсуляция имеет преимущества.

Но то же самое происходит и с общим состоянием.

Достоинства и недостатки совершенно разные. Итак, это компромисс: написанный вами код, который определенным образом нарушает инкапсуляцию, имеет определенные недостатки. Например, трудно обновить один класс, не приняв во внимание, по крайней мере, влияние, которое он оказывает на другие классы, участвующие в «разрыве». Но зачастую 2 класса всё равно фундаментально связаны — изменить один, не учитывая существование другого, всё равно проблема. В этот момент этот конкретный недостаток не имеет значения.

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

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

Если какой-то архитектурный проект, который вы придумали, обернулся катастрофой, и если бы вы приложили больше усилий, чтобы избежать инкапсуляции, можно было бы его избежать, [A] это не означает, что альтернатива была бы лучше, и [B] хорошо , вы знаете, как начинающие программисты превращаются в опытных программистов? С помощью инструмента, известного как «ошибка». Берегите их. И помните, что вскрытие ваших решений по кодированию имеет решающее значение. Вам следует потратить больше времени на обдумывание полного влияния решений, которые вы сделали в прошлом, на то, насколько удобно поддерживать ваше программное обеспечение сегодня, чем пытаться читать благонамеренную философскую тарабарщину об абстрактных понятиях, таких как «инкапсуляция». Я не говорю, что такие вещи, как инкапсуляция, бессмысленны; нисколько. Я говорю: не имея личного опыта, осмысленно применить советы невероятно сложно.

Обратите внимание, что мой предыдущий совет о неизменяемых объектах все равно должен соответствовать всему этому. Не повторяйте: «Я увидел свет, и ВСЕ БУДЕТ НЕИЗМЕННО!» Пора выбросить все наше программное обеспечение и начать все сначала!» - это делает ту же ошибку. Это инструмент из набора инструментов, и если вы продолжаете сталкиваться с проблемами, которые кажутся проблемой «инкапсуляции», вы можете попробовать несколько функций с более неизменяемой архитектурой. Поверьте мне, вы столкнетесь с совершенно новым набором недостатков и головной боли при обслуживании. Но теперь вы узнали и теперь можете взвесить недостатки предыдущей правильной инкапсуляции и недостатки ее чрезмерной индексации. В следующем проекте вы, возможно, наконец сможете принять правильное решение о том, какой компромисс является лучшим для проекта, над которым вы работаете.

Я думаю, что все это правда, но с точки зрения младшего инженера это также ужасно неудовлетворительно. Метод проб и ошибок — единственный способ учиться? Похоже, индустрии программного обеспечения будет трудно когда-либо развиваться, если мы не сможем учиться на ошибках друг друга и не повторять их.

jaco0646 19.06.2024 15:53

@rzwitserloot Спасибо за ответ! Я думаю, что откажусь от всех этих хороших принципов программирования и просто напишу код. Обычно ответ на вопрос по программированию звучит так: «Это выходит из-под контроля». Это может очень расстроить новичка (как я), потому что у нас нет четкого ответа, но ответ абсолютно правильный, потому что мы можем ответить на него только с опытом и, следовательно, совершая ошибки, как вы сказали. Итак, на данный момент моим единственным советом будет «сначала писать код, а потом учиться на своих ошибках».

xerox0213 19.06.2024 16:10

Это пример того, что называется «внедрением зависимостей». Класс B и C зависят от класса A, который вы вводите через конструктор классов.

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

Если вы думаете о классе A как о классе Engine, а о классе B как о классе Car, то не имеет смысла иметь один объект двигателя для двух объектов автомобилей, поэтому два объекта должны совместно использовать один объект зависимостей. Внедрение зависимостей пригодится, когда у вас внезапно появится класс, наследуемый от Engine. В этом случае вам просто нужно будет изменить конструкцию объектов движка, а не проводить рефакторинг каждого класса, содержащего один движок.

См. следующий пример.

public class Engine {
  private int kilometerCount = 0;

  public void incrementKm(){
    kilometerCount += 1;
  }

  public int getKilometerCount() {
    return kilometerCount;
  }
}

public class Car {
  private final Engine a;
  
  public C(Engine a){
    this.a = a;
  }

  public void drive(){
    // Do some stuff
    a.incrementKm();
  }
}

public class Main {
  public static void main(String[] args) {
    Engine engine = new Engine();
    Engine engine2 = new Engine();


    Car car = new Car(engine);
    Car car2 = new Car(engine2);

    car.drive();
    car2.drive();
    System.out.println(engine.getKilometerCount());
  }
}

А затем вы меняете Изменить класс двигателя:


public class PowerfulEngine extends Engine {
  private int kilometerCount = 0;

  public void enableBoost(){
    System.out.println("Its hammer time!");
  }
}

public class Main {
  public static void main(String[] args) {
    Engine engine = new Engine();
    Engine engine2 = new PowerfulEngine();


    Car car = new Car(engine);

    engine2.enableBoost();
    Car car2 = new Car(engine2);

    car.drive();
    car2.drive();
    System.out.println(engine.getKilometerCount());
  }
}

Сейчас изменились только две строчки.

Примечание. Я не хочу сказать, что совместное использование одного объекта никогда не имеет смысла. Просто это должно быть полезно в вашем контексте.
Я мог бы представить, что класс с именем «Дорога» можно внедрить в несколько объектов «Автомобиль» одновременно, однако это сильно зависит от вашей конкретной ситуации.

Другие вопросы по теме