Принцип наименьшего знания

32

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

Один из примеров этого принципа (на самом деле, как его не использовать), который я нашел в книге Head First Design Patterns, указывает на то, что неправильно вызывать метод для объектов, которые были возвращены из вызова других методов, в терминах этого принципа. ,

Но, похоже, иногда очень необходимо использовать такую ​​возможность.

Например: у меня есть несколько классов: класс захвата видео, класс кодировщика, класс streamer, и все они используют какой-то другой базовый класс VideoFrame, и, поскольку они взаимодействуют друг с другом, они могут делать, например, что-то вроде этого:

streamer код класса

...
frame = encoder->WaitEncoderFrame()
frame->DoOrGetSomething();
....

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

ransh
источник
возможный дубликат метода цепочки против инкапсуляции
комнат
4
Это, вероятно, более близкий дубликат.
Роберт Харви
1
Связанный: действительно ли код как это - "крушение поезда" (в нарушение закона Деметры)? (полное раскрытие: это мой собственный вопрос)
CVn

Ответы:

21

Принцип, о котором вы говорите (более известный как Закон Деметры ) для функций, можно применить, добавив еще один вспомогательный метод к вашему классу стримеров, например

  {
    frame = encoder->WaitEncoderFrame()
    DoOrGetSomethingForFrame(frame); 
    ...
  }

  void DoOrGetSomethingForFrame(Frame *frame)
  {
     frame->DoOrGetSomething();
  }  

Теперь каждая функция «общается только с друзьями», а не с «друзьями друзей».

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

Док Браун
источник
Этот подход имеет то преимущество, что делает его гораздо проще для модульного тестирования, отладки и анализа вашего кода.
gntskn
+1. Тем не менее, есть очень веская причина не вносить такое изменение кода в том, что интерфейс класса может раздуться от методов, работа которых заключается в том, чтобы просто выполнить один или несколько вызовов нескольких других методов (и без дополнительной логики). В некоторых типах среды программирования, например, C ++ COM (особенно WIC и DirectX), добавление каждого отдельного метода в интерфейс COM сопряжено с большими затратами по сравнению с другими языками.
Rwong
1
В C ++ COM-дизайне разложение больших классов на маленькие (что означает, что у вас больше шансов общаться с несколькими объектами) и минимизация количества методов интерфейса - это две цели проектирования с реальными преимуществами (сокращением затрат), основанными на глубоком понимании внутренней механики (виртуальные таблицы, возможность многократного использования кода и многое другое). Поэтому программисты C ++ COM обычно должны игнорировать LoD.
Rwong
10
+1: Закон Деметры действительно должен называться «Предложение Деметры»: YMMV
двоичный беспорядок
Закон Деметры - это совет для архитектурного выбора, в то время как вы предлагаете косметическое изменение, чтобы, кажется, вы соблюдали этот «закон». Это было бы неправильным представлением, потому что синтаксическое изменение не означает, что вдруг все в порядке. Насколько я знаю, закон деметра в основном имел в виду: если вы хотите сделать ООП, тогда прекратите писать процедурный код с функциями-получателями повсюду.
user2180613
39

Принцип наименьшего знания или Закон Деметры - это предупреждение против запутывания вашего класса деталями других классов, которые пересекают слой за слоем. Он говорит вам, что лучше разговаривать только со своими «друзьями», а не с «друзьями друзей».

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

Метафоры хороши, но что мы на самом деле подразумеваем под другом? Любая вещь, которую объект знает, как создать или найти, является другом. Кроме того, объект может просто попросить передать другие объекты , о которых он знает только интерфейс. Они не считаются друзьями, потому что не ожидают, как их получить. Если объект не знает, откуда он взялся, потому что что-то еще прошло / впрыснуло это, то это не друг друга, это даже не друг. Это то, что объект знает только как использовать. Это хорошая вещь.

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

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

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

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

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

Это на самом деле следует другому принципу: отделить использование от строительства.

frame = encoder->WaitEncoderFrame()

Используя этот код, вы взяли на себя ответственность за то, чтобы каким-то образом приобрести Frame. Вы еще не взяли на себя ответственность за разговор с Frame.

frame->DoOrGetSomething(); 

Теперь вы должны знать, как разговаривать Frame, но замените это на:

new FrameHandler(frame)->DoOrGetSomething();

И теперь вам нужно только знать, как разговаривать с вашим другом, FrameHandler.

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

У каждого хорошего правила есть исключение. Лучшие примеры, которые я знаю, это внутренние языки, специфичные для предметной области . A DSL сек цепной методпохоже, все время нарушает закон Деметры, потому что вы постоянно вызываете методы, которые возвращают разные типы, и используете их напрямую. Почему это нормально? Потому что в DSL все, что возвращается, это тщательно разработанный друг, с которым вам нужно поговорить напрямую. По замыслу вы получили право ожидать, что цепочка методов DSL не изменится. У вас нет этого права, если вы просто случайно копаетесь в кодовой базе, объединяя все, что найдете. Лучшие DSL - это очень тонкие представления или интерфейсы с другими объектами, в которые вы, вероятно, не должны углубляться. Я упоминаю об этом только потому, что обнаружил, что понял закон деметры гораздо лучше, когда узнал, почему DSL - хороший дизайн. Некоторые заходят так далеко, что говорят, что DSL даже не нарушают настоящий закон Деметры.

Другое решение состоит в том, чтобы позволить что-то еще внедрить frameв вас. Если вы frameпришли от сеттера или, предпочтительно, от конструктора, вы не берете на себя никакой ответственности за создание или приобретение фрейма. Это означает, что ваша роль здесь намного больше, чем FrameHandlersдолжна была быть. Вместо этого теперь вы - тот, с кем общаетесь Frameи заставляете что-то еще выяснить, как это сделать. Frame В каком-то смысле это то же самое решение с простой сменой перспективы.

В SOLID принципов являются большим я стараюсь следовать. Здесь соблюдаются два принципа: принцип единой ответственности и принцип инверсии. Это действительно трудно уважать этих двоих и все же в конечном итоге нарушать закон Деметры.

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

candied_orange
источник
2
«Это говорит о том, что лучше разговаривать только со своими« друзьями », а не с« друзьями друзей »» ++
RubberDuck
1
Второй абзац, это украдено откуда-то или вы его придумали? Это заставило меня понять концепцию полностью . Это сделало его вопиющим к тому, что "друзья друзей", и какие недостатки есть, чтобы запутывать слишком много классов (суставов). + Цитата.
умереть
2
@diemaus Если я украл этот абзац щита откуда-то, его источник с тех пор просочился из моей головы. В то время мой мозг просто думал, что это умно. Что я помню, так это то, что я добавил его после многих голосов, так что приятно видеть, что оно подтверждено. Рад, что это помогло.
candied_orange
19

Функциональный дизайн лучше, чем объектно-ориентированный дизайн? Это зависит.

MVVM лучше чем MVC? Это зависит.

Амос и Энди или Мартин и Льюис? Это зависит.

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

[Некоторая книга] говорит, что [что-то] не так.

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

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

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

Роберт Харви
источник
2

Ответ Дока Брауна показывает классическую реализацию закона Деметры в учебнике - и раздражение / неорганизованность - добавление десятков методов таким образом, вероятно, поэтому программисты, в том числе и я, часто не беспокоятся об этом, даже если им и следует.

Существует альтернативный способ отделить иерархию объектов:

Предоставляйте interfaceтипы, а не classтипы, через ваши методы и свойства.

В случае оригинального плаката (OP) encoder->WaitEncoderFrame()будет возвращаться IEncoderFrameвместо a Frameи будет определяться, какие операции допустимы.


РЕШЕНИЕ 1

В простейшем случае, Frameи Encoderклассы находятся под вашим контролем, IEncoderFrameявляется подмножеством методов Рама уже публично разоблачает, и Encoderкласс не на самом деле все равно , что вы делаете для этого объекта. Затем реализация тривиальна ( код на c # ):

interface IEncoderFrame {
    void DoOrGetSomething();
}

class Frame : IEncoderFrame {
    // A method that already exists in Frame.
    public void DoOrGetSomething() { ... }
}

class Encoder {
    private Frame _frame;
    public IEncoderFrame TheFrame { get { return _frame; } }
    ...
}

РЕШЕНИЕ 2

В промежуточном случае, когда Frameопределение не находится под вашим контролем, или было бы неуместно добавлять IEncoderFrameметоды к Frame, тогда хорошим решением является Адаптер . Это то , что ответ CandiedOrange в обсуждает, как и new FrameHandler( frame ). ВАЖНО: если вы сделаете это, будет более гибко, если вы представите его как интерфейс , а не как класс . Encoderдолжен знать о class FrameHandler, но клиенты должны знать только interface IFrameHandler. Или, как я его назвал, interface IEncoderFrame- чтобы указать, что это именно Frame, как видно из POV Encoder :

interface IEncoderFrame {
    void DoOrGetSomething();
}

// Adapter pattern. Appropriate if no access needed to Encoder.
class EncoderFrameWrapper : IEncoderFrame {
    Frame _frame;
    public EncoderFrameWrapper( Frame frame ) {
        _frame = frame;
    }
    public void DoOrGetSomething() {
        _frame....;
    }
}

class Encoder {
    private Frame _frame;

    // Adapter pattern. Appropriate if no access needed to Encoder.
    public IEncoderFrame TheFrame { get { return new EncoderFrameWrapper( _frame ); } }

    ...
}

СТОИМОСТЬ: Выделение и сборщик мусора нового объекта, EncoderFrameWrapper, каждый раз encoder.TheFrameвызывается. (Вы можете кэшировать эту оболочку, но это добавляет больше кода. И надежно кодировать легко, только если поле кадра кодировщика не может быть заменено новым кадром.)


РЕШЕНИЕ 3

В более сложном случае новая обертка должна была бы знать и то, Encoderи другое Frame. Этот объект сам по себе будет нарушать LoD - он манипулирует отношениями между Encoder и Frame, что должно быть ответственностью Encoder - и, вероятно, будет проблемой, чтобы получить права. Вот что может произойти, если вы начнете идти по этому пути:

interface IEncoderFrame {
    void DoOrGetSomething();
}

// *** You will end up regretting this. See next code snippet instead ***
class EncoderFrameWrapper : IEncoderFrame {
    Encoder _owner;
    Frame _frame;
    public EncoderFrameWrapper( Encoder owner, Frame frame ) {
        _owner = owner;   _frame = frame;
    }
    public void DoOrGetSomething() {
        _frame.DoOrGetSomething();
        // Hmm, maybe this wrapper class should be nested inside Encoder...
        _owner... some work inside owner; maybe should be owner-internal details ...
    }
}

class Encoder {
    private Frame _frame;

    ...
}

Это стало ужасно. Существует менее запутанная реализация, когда оболочка должна коснуться деталей своего создателя / владельца (Encoder):

interface IEncoderFrame {
    void DoOrGetSomething();
}

class Encoder : IEncoderFrame {
    private Frame _frame;

    // HA! Client gets to think of this as "the frame object",
    // but its really me, intercepting it.
    public IEncoderFrame TheFrame { get { return this; } }

    // This is the method that the LoD approach suggests writing,
    // except that we are exposing it only when the instance is accessed as an IEncoderFrame,
    // to avoid extending Encoder's already large API surface.
    public void IEncoderFrame.DoOrGetSomething() {
        _frame.DoOrGetSomething();
       ... make some change within current Encoder instance ...
    }
    ...
}

Конечно, если бы я знал, что окажусь здесь, я бы этого не сделал. Могли бы просто написать методы LoD и покончить с этим. Нет необходимости определять интерфейс. С другой стороны, мне нравится, что интерфейс объединяет связанные методы. Мне нравится, каково это делать «подобные фрейму операции» с тем, что похоже на фрейм.


ЗАКЛЮЧИТЕЛЬНЫЕ КОММЕНТАРИИ

Подумайте над этим: если разработчик Encoderполагал, что экспонирование Frame frameсоответствовало их общей архитектуре или было «намного проще, чем реализация LoD», то было бы намного безопаснее, если бы вместо этого он выполнил первый фрагмент, который я показываю, - выставил ограниченное подмножество Рамка, как интерфейс. По моему опыту, это часто вполне работоспособное решение. Просто добавьте методы в интерфейс по мере необходимости. (Я говорю о сценарии, в котором мы «знаем», что у Frame уже есть необходимые методы, или их было бы легко и без споров добавить. Работа по «реализации» для каждого метода заключается в добавлении одной строки в определение интерфейса.) И Знайте, что даже в худшем будущем этот API можно сохранить работоспособным - здесь, удалив IEncoderFrameиз .FrameEncoder

Также обратите внимание , что если у вас нет разрешения на добавление IEncoderFrameк Frame, или необходимые методы не очень хорошо подходят к общему Frameклассу, и решение # 2 не подходит, возможно , из-за дополнительного объекта создания-и-разрушения, Решение № 3 можно рассматривать как простой способ организации методов Encoderвыполнения LoD. Не просто пройти через десятки методов. Оберните их в интерфейс и используйте «явную реализацию интерфейса» (если вы находитесь в c #), чтобы к ним можно было получить доступ только тогда, когда объект просматривается через этот интерфейс.

Еще один момент, который я хочу подчеркнуть, заключается в том, что решение выставить функциональность в качестве интерфейса , обрабатывается всеми 3 ситуациями, описанными выше. Во-первых, IEncoderFrameэто просто набор Frameфункций России. Во вторых, IEncoderFrameэто адаптер. В третьих, IEncoderFrameэто разделение по Encoderфункциональности. Не имеет значения, изменяются ли ваши потребности между этими тремя ситуациями: API остается неизменным.

ToolmakerSteve
источник
Соединение вашего класса с интерфейсом, возвращаемым одним из его соавторов, а не конкретным классом, хотя и является улучшением, все еще является источником связи. Если интерфейс должен измениться, это означает, что ваш класс должен будет измениться. Важно помнить, что связывание не является плохим по своей сути, если вы избегаете ненужного соединения с нестабильными объектами или внутренними структурами . Вот почему закон Деметры нужно принимать с большим количеством соли; он инструктирует вас всегда избегать того, что может или не может быть проблемой, в зависимости от обстоятельств.
Периата Breatta
@PeriataBreatta - я, конечно, не могу и не согласен с этим. Но я хотел бы отметить одну вещь: интерфейс по определению представляет то, что необходимо знать на границе между двумя классами. Если это «нужно изменить», это принципиально - никакой альтернативный подход не мог бы каким-то волшебным образом избежать необходимого кодирования. Сравните это с выполнением любой из трех описанных мной ситуаций, но без использования интерфейса - вместо этого верните конкретный класс. В 1, кадр, в 2, EncoderFrameWrapper, в 3, кодировщик. Вы фиксируете себя в этом подходе. Интерфейс может адаптироваться к ним всем.
ToolmakerSteve
@PeriataBreatta ... который демонстрирует преимущество явного определения его как интерфейса. Я надеюсь в конечном итоге улучшить IDE, чтобы сделать ее настолько удобной, что интерфейсы будут использоваться гораздо интенсивнее. Большинство многоуровневых доступов будет осуществляться через некоторый интерфейс, поэтому управлять изменениями будет намного проще. (Если это «излишне» в некоторых случаях, анализ кода в сочетании с аннотациями о том, где мы готовы пойти на риск отсутствия интерфейса в обмен на небольшое повышение производительности, может «скомпилировать его» - заменить его на один из этих 3 -х конкретных классов от «решений» 3).
ToolmakerSteve
@PeriataBreatta - и когда я говорю «интерфейс может адаптироваться к ним всем», я не говорю «аааа, это замечательно, что эта функция на одном языке может охватывать эти разные случаи». Я говорю, что определение интерфейсов минимизирует изменения, которые вам могут понадобиться. В лучшем случае дизайн может полностью измениться от самого простого случая (решение 1) до самого сложного случая (решение 3) без изменения интерфейса - только внутренние потребности производителя стали более сложными. И даже когда изменения необходимы, они, как правило, менее распространены, ИМХО.
ToolmakerSteve