Что это значит, когда кто-то говорит «Инкапсулировать то, что меняется»?

25

Один из принципов ООП, с которыми я столкнулся, это: - Инкапсулируйте то, что меняется.

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

Харис Гаури
источник
См. En.wikipedia.org/wiki/Encapsulation_(computer_programming), где это хорошо объясняется. Я думаю, что «что меняется» не правильно, так как вы также должны иногда инкапсулировать константы.
qwerty_so
I don't know how exactly would it contribute to a better designИнкапсуляция деталей - это слабая связь между «моделью» и деталями реализации. Чем менее привязана «модель» к деталям реализации, тем более гибким является решение. И это облегчает его развитие. «Абстрагируйся от деталей».
Laiv
@Laiv Таким образом, «меняется» относится к тому, что развивается в течение жизненного цикла вашего программного обеспечения, или что изменяется во время выполнения вашей программы, или и того, и другого?
Харис Гаури
2
@HarisGhauri оба. Сгруппируйте то, что меняется вместе. Изолировать то, что меняется независимо. С подозрением относитесь к тому, что, по вашему мнению, никогда не изменится.
candied_orange
1
@laiv думает, что «абстрактный» - это хороший момент. Это может чувствовать себя подавляющим. В любом объекте вы должны нести одну ответственность. Самое приятное в этом то, что вам нужно только тщательно обдумать это здесь. Когда детали остальной проблемы - проблема кого-то другого, это облегчает ситуацию.
candied_orange

Ответы:

30

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

if (pet.type() == dog) {
  pet.bark();
} else if (pet.type() == cat) {
  pet.meow();
} else if (pet.type() == duck) {
  pet.quack()
}

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

pet.speak();

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

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

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

Вы можете посмотреть на этот пример и подумать, что я объединяю полиморфизм и инкапсуляцию. Я не. Я объединяю «что меняется» и «детали».

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

Разъедините, отделите и скройте детали от остальной части кода. Не позволяйте знаниям о деталях распространяться по вашей системе, и вы будете хорошо следовать «инкапсуляции того, что меняется».

candied_orange
источник
3
Это действительно странно. «Инкапсулировать то, что меняется» для меня означает скрывать изменения состояния, например, никогда не иметь глобальных переменных. Но ваш ответ тоже имеет смысл, даже если он чувствует больше ответа на полиморфизм, чем на инкапсуляцию :)
David Arno
2
@DavidArno Полиморфизм является одним из способов сделать эту работу. Я мог бы просто превратить структуру if в pet, и все выглядело бы неплохо благодаря инкапсуляции питомца. Но это будет просто перемещать беспорядок вместо того, чтобы убирать это.
candied_orange
1
«Инкапсулировать то, что меняется» для меня означает скрывать изменения состояния . Нет, нет. Мне нравится комментарий CO. Ответ Дерика Элкина идет глубже, прочитайте его не раз. Как сказал @JacquesB: «Этот принцип на самом деле довольно глубокий»
radarbob
16

«Варьируется» здесь означает «может меняться со временем из-за меняющихся требований». Это основной принцип разработки: разделять и изолировать части кода или данных, которые в будущем могут потребоваться изменить отдельно. Если меняется одно требование, в идеале требовать от нас изменения соответствующего кода в одном месте. Но если кодовая база плохо спроектирована, т. Е. Сильно взаимосвязана и логика для требований распределена во многих местах, то изменения будут трудными и с высоким риском возникновения неожиданных последствий.

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

  • ставка налога с продаж - это буквально прописная буква везде в приложении, где рассчитывается налог с продаж.

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

  • Существует единственный метод, calculateSalesTax(product)который называется единственной, где используется ставка налога с продаж.

  • ставка налога с продаж указана в файле конфигурации или поле базы данных.

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

Теперь рассмотрим другую константу, Pi, которая также используется во многих местах кода. Имеет ли место тот же принцип дизайна? Нет, потому что Пи не собирается меняться. Извлечение его в файл конфигурации или поле базы данных просто создает ненужную сложность (а при прочих равных условиях мы предпочитаем самый простой код). Имеет смысл сделать его глобальной константой, а не жестко кодировать его в нескольких местах, чтобы избежать несоответствий и улучшить читаемость.

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

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

JacquesB
источник
2
Налоги являются хорошим примером. Законы и налоги могут меняться от одного дня к другому. Если вы внедряете систему налоговой декларации, вы сильно привязаны к такого рода изменениям. Также меняется из одной локали в другую (страны, провинции, ...)
Laiv
"Пи не собирается меняться" заставил меня смеяться. Это правда, Пи вряд ли изменится, но предположим, что вам больше не разрешено это использовать? Если у некоторых людей будет свой путь, Пи будет осужден. Предположим, что становится требованием? Надеюсь, у вас есть счастливый день Тау . Хороший ответ, кстати. Глубоко, действительно.
candied_orange
14

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

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

Это только косвенно связано с состоянием инкапсуляции, о котором, как мне кажется, думает Дэвид Арно. Этот совет не всегда (но часто делает) предлагает инкапсулирование состояния, и этот совет применим и к неизменным объектам. На самом деле, просто именование констант является (очень простой) формой инкапсуляции того, что меняется.

CandiedOrange явно связывает «что меняется» с «деталями». Это только частично правильно. Я согласен с тем, что любой изменяющийся код в некотором смысле является «деталями», но «детали» могут не меняться (если вы не определите «детали», чтобы сделать это тавтологическим). Могут быть причины для инкапсуляции неизменяемых деталей, но это изложение не одно. Грубо говоря, если бы вы были очень уверены, что «собака», «кошка» и «утка» будут единственными типами, с которыми вам когда-либо придется иметь дело, то это изречение не предполагает рефакторинга, выполняемого CandiedOrange.

Приведя пример CandiedOrange в другом контексте, предположим, что у нас есть процедурный язык, такой как C. Если у меня есть некоторый код, который содержит:

if (pet.type() == dog) {
  pet.bark();
} else if (pet.type() == cat) {
  pet.meow();
} else if (pet.type() == duck) {
  pet.quack()
}

Я могу разумно ожидать, что этот кусок кода изменится в будущем. Я могу «инкапсулировать» его, просто определив новую процедуру:

void speak(pet) {
  if (pet.type() == dog) {
    pet.bark();
  } else if (pet.type() == cat) {
    pet.meow();
  } else if (pet.type() == duck) {
    pet.quack()
  }
}

и использование этой новой процедуры вместо блока кода (то есть рефакторинг "метод извлечения"). На этом этапе добавление типа «корова» или чего-либо еще требует обновления speakпроцедуры. Конечно, в языке OO вы можете вместо этого использовать динамическую диспетчеризацию, на что ссылается ответ CandiedOrange. Это произойдет естественно, если вы получите доступpet через интерфейс. Устранение условной логики с помощью динамической диспетчеризации - это ортогональная проблема, которая была частью того, почему я сделал эту процедурную передачу. Я также хочу подчеркнуть, что это не требует особенностей, специфичных для ООП. Даже в ОО-языке инкапсуляция того, что меняется, не обязательно означает, что необходимо создать новый класс или интерфейс.

В качестве более типичного примера (который ближе к ОО, но не совсем), скажем, мы хотим удалить дубликаты из списка. Допустим, мы реализуем его, перебирая список, отслеживая элементы, которые мы видели до сих пор в другом списке, и удаляя любые элементы, которые мы видели. Разумно предположить, что мы можем захотеть изменить то, как мы отслеживаем увиденные объекты, по крайней мере, по причинам производительности. Изречение инкапсулировать то, что меняется, предполагает, что мы должны построить абстрактный тип данных для представления набора видимых элементов. Наш алгоритм теперь определен для этого абстрактного типа данных Set, и если мы решим переключиться на двоичное дерево поиска, наш алгоритм не нуждается в изменении или заботе. На языке ОО мы можем использовать класс или интерфейс для захвата этого абстрактного типа данных. На таком языке, как SML / O '

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

Хотя это ортогональная проблема, которая не является частью «инкапсуляции того, что меняется», часто естественно абстрагироваться, то есть параметризоваться, теперь инкапсулированной логикой. Это обычно приводит к более гибкому коду и позволяет изменять логику, заменяя альтернативную реализацию вместо модификации инкапсулированной логики.

Дерек Элкинс
источник
О, горько-сладкая ирония. Да, это не только проблема ООП. Вы поймали меня на том, что детали языковой парадигмы загрязняют мой ответ, и справедливо наказали меня за это, «изменив» парадигму.
candied_orange
«Даже в языке OO инкапсуляция того, что меняется, не обязательно означает, что необходимо создать новый класс или интерфейс» - трудно представить ситуацию, когда отсутствие создания нового класса или интерфейса не нарушило бы SRP
taurelas
11

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

Пример:

Например, предположим, что класс Courseотслеживает, Studentsчто может зарегистрироваться (). Вы можете реализовать его с помощью a LinkedListи выставить контейнер, чтобы разрешить итерации по нему:

class Course { 
    public LinkedList<Student> Atendees; 
    public bool register (Student s);  
    ...
}

Но это не очень хорошая идея

  • Во-первых, люди могут испытывать недостаток в хорошем поведении и использовать его как самообслуживание, напрямую добавляя студентов в список, не используя метод register ().
  • Но еще более раздражает: это создает зависимость «использующего кода» от внутренних деталей реализации используемого класса. Это может помешать будущим изменениям класса, например, если вы предпочтете использовать массив, вектор, карту с номером места или вашу собственную постоянную структуру данных.

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

Дополнительное чтение:

Christophe
источник