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


32

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

Один із прикладів цього принципу (власне, як його не використовувати), який я знайшов у книзі « Шаблони дизайну Head First Design» вказує на те, що неправильно називати метод на об’єктах, повернутих з виклику інших методів, з точки зору цього принципу .

Але здається, що іноді дуже потрібно використовувати таку можливість.

Наприклад: У мене є кілька класів: клас захоплення відео, клас кодера, клас стримера, і всі вони використовують якийсь основний інший клас, VideoFrame, і оскільки вони взаємодіють між собою, вони можуть зробити, наприклад, щось подібне:

streamer код класу

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

Як бачите, цей принцип тут не застосовується. Чи можна застосовувати цей принцип тут, чи це принцип, який не завжди можна застосовувати в такому дизайні?




4
Це, мабуть, ближчий дублікат.
Роберт Харві

1
Пов’язано: Чи подібний код є «аварією поїзда» (з порушенням Закону про Деметер)? (повне розкриття: це моє власне питання)
CVn

Відповіді:


21

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

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

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

Тепер кожна функція лише "розмовляє з друзями", а не з "друзями друзів".

IMHO - це груба інструкція, яка може допомогти створити методи, які більш чітко слідують принципу єдиної відповідальності. У такому простому випадку, як описаний вище, напевно, дуже впевнено, чи справді це варте клопоту і якщо отриманий код справді "чистіший", чи він просто формально розширить ваш код без будь-якого помітного посилення.


Цей підхід має перевагу в тому, що значно полегшує тестування, налагодження та міркування вашого коду.
gntskn

+1. Хоча є не дуже вагома причина, щоб не змінити цей код, це те, що інтерфейс класу може бути роздутим методами, завдання яких - це лише зробити один чи кілька викликів кількома іншими методами (і без додаткової логіки). У деяких типах середовища програмування, скажімо, C ++ COM (особливо WIC і DirectX), висока вартість пов'язана з додаванням кожного окремого методу до інтерфейсу COM, порівняно з іншими мовами.
rwong

1
У C ++ COM-інтерфейсі, розбиття великих класів на невеликі класи (це означає, що ви маєте більший шанс поговорити з декількома об'єктами) та мінімізація кількості методів інтерфейсу - це дві цілі проектування з реальними перевагами (зниження витрат) на основі глибокого розуміння внутрішньої механіки (віртуальні таблиці, повторне використання коду, багато інших речей). Тому програмісти C ++ COM, як правило, повинні ігнорувати LoD.
rwong

10
+1: Закон Деметера справді має бути названий Пропозиція
Деметера

Закон деметера - це порада для прийняття архітектурного вибору, тоді як ви пропонуєте косметичні зміни, тому, здається, ви відповідаєте цьому "закону". Це було б помилковим уявленням, оскільки синтаксична зміна не означає, що раптом все в порядку. Наскільки я знаю, закон деметера в основному мав на увазі: якщо ви хочете робити OOP, то перестаньте писати продекуральний код з функціями getter скрізь.
користувач2180613

39

Принцип найменших знань або Закон Деметера - це застереження від заплутування вашого класу деталями інших класів, які проходять шар за шаром. Це говорить вам про те, що краще говорити тільки зі своїми "друзями", а не з "друзями друзів".

Уявіть, що вас попросили прив'язати щит до статуї лицаря у блискучій бронзовій броні. Ви обережно розміщуєте щит на лівій руці, щоб він виглядав природним. Ви помічаєте, що на передпліччі, лікті та на надпліччі є три маленькі місця, де щит торкається броні. Ви зварюєте всі три місця, тому що хочете бути впевненими, що зв’язок міцний. А тепер уявіть, що ваш начальник божевільний, бо він не може рухати ліктям своєї броні. Ви припускали, що броня ніколи не рухатиметься, і тому створила нерухомий зв'язок між передпліччям та передпліччям. Щит повинен з'єднуватися лише з його другом, передпліччям. Не передпліч друзів. Навіть якщо вам доведеться додати шматок металу, щоб змусити їх торкатися.

Метафори приємні, але що ми маємо на увазі під товаришем? Будь-яка річ, яку об’єкт знає, як створити чи знайти - це друг. Крім того, об’єкт може просто попросити передати інші об'єкти , з яких він знає лише інтерфейс. Вони не вважаються друзями, тому що ніяких очікувань, як їх отримати, не нав'язується. Якщо об’єкт не знає, звідки вони взялися, тому що щось інше передав / ввів його, то це не товариш друга, це навіть не друг. Це те, що об'єкт знає лише як використовувати. Це гарна річ.

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

Ніхто не хоче робити роботу без причини, тому важливо зрозуміти, що ви отримуєте після цього. У цьому випадку він зберігає ваш код гнучким. Ви можете внести зміни і мати менше інших класів, на які впливають зміни, які турбуються. Це звучить добре, але не допомагає вам вирішити, що робити, якщо ви не сприймаєте це як якусь релігійну доктрину.

Замість того, щоб сліпо слідувати цьому принципу, скористайтеся простою версією цієї проблеми. Напишіть рішення, яке не відповідає принципу та те, що це робить. Тепер, коли у вас є два рішення, ви можете порівняти, наскільки сприйнятливий кожен до змін, намагаючись зробити їх в обох.

Якщо ви не можете вирішити проблему, дотримуючись цього принципу, ймовірно, ще один навик вам не вистачає.

Одним з варіантів вашої конкретної проблеми є введення frameв щось (клас чи метод), який вміє спілкуватися з кадрами, щоб вам не довелося поширювати всі ті деталі кадру в чаті у вашому класі, які тепер лише знають, як і коли робити отримати кадр.

Це наслідок іншого принципу: окреме використання від будівництва.

frame = encoder->WaitEncoderFrame()

Використовуючи цей код, ви взяли на себе відповідальність за те, щоб якось придбати Frame. Ви ще не взяли на себе жодної відповідальності за спілкування зі службою Frame.

frame->DoOrGetSomething(); 

Тепер ви повинні знати, як спілкуватися з a Frame, але замінити це на:

new FrameHandler(frame)->DoOrGetSomething();

А тепер вам залишається лише знати, як спілкуватися зі своїм другом, FrameHandler.

Є багато способів досягти цього, і це, мабуть, не найкращий, але це показує, як дотримання принципу не робить проблему нерозв'язною. Це просто вимагає від вас більше роботи.

Кожне хороше правило має виняток. Найкращі мої приклади - це внутрішні мови, визначені для домену . A DSL сек ланцюгової методЗдається, весь час зловживають Законом Деметра, оскільки ти постійно називаєш методи, які повертають різні типи та використовують їх безпосередньо. Чому це нормально? Тому що в DSL все, що повертається, - це ретельно продуманий друг, з яким ви повинні прямо поговорити. Конструюючи, ви отримали право очікувати, що ланцюжок методів DSL не зміниться. Ви не маєте цього права, якщо просто випадковим чином заглибитесь у кодову базу, що з'єднує все, що ви знайдете. Найкращі DSL - це дуже тонкі уявлення або інтерфейси до інших об'єктів, до яких ви, мабуть, не повинні заглиблюватися. Я згадую це лише тому, що, коли я дізнався, чому DSL - це хороша конструкція, я зрозумів, що закон про деметер набагато краще. Деякі йдуть так далеко, що кажуть, що DSL навіть не порушує справжній закон про поводження.

Ще одне рішення - дозволити щось інше ввести frameу вас. Якщо ви frameприйшли із сетера або бажано з конструктора, ви не несете жодної відповідальності за створення або придбання кадру. Це означає, що ти тут роль набагато більше, як FrameHandlersі раніше. Натомість зараз ви той, хто балакає Frameі робить щось інше, з'ясовуйте, як отримати « Frame Шляхом цього - це те саме рішення з простою зміною точки зору.

Принципи SOLID - це найважливіші принципи, які я намагаюся дотримуватися. Тут поважаються два принципи єдиної відповідальності та інверсії залежності. Поважати цих двох справді важко і все-таки порушувати Закон Деметера.

Менталітет, який переживає порушення Demeter, схожий на їжу в фуршетному ресторані, де ви просто захопите все, що завгодно. Завдяки невеликій роботі ви можете забезпечити собі меню та сервер, які принесуть вам все, що завгодно. Сядьте, розслабтесь і добре поскакайте.


2
"Це говорить вам про те, що краще говорити тільки зі своїми" друзями ", а не з" друзями друзів "" ++
RubberDuck

1
Другий абзац - це вкрадене звідкись, чи ти це вигадав? Це мене цілком зрозуміло . Це зробило очевидним те, що таке "друзі друзів", і які недоліки можуть спричинити заплутування занадто багато класів (суглобів). Цитата +.
die maus

2
@diemaus Якщо я вкрав цей пункт щита звідкись, його джерело з тих пір просочилося з моєї голови. У той час мій мозок просто думав, що це розумно. Що я пам’ятаю, це те, що я додав його після більшої кількості оновлень, тому приємно бачити його підтвердженим. Радий, що це допомогло.
candied_orange

19

Чи краще функціональний дизайн, ніж об’єктно-орієнтований дизайн? Це залежить.

Чи MVVM кращий, ніж MVC? Це залежить.

Амос та Енді чи Мартін та Льюїс? Це залежить.

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

[Деяка книга] говорить, що [якась річ] неправильна.

Коли ви читаєте це в книзі чи блозі, оцініть претензію виходячи з її достоїнства; тобто запитайте, чому. Не існує правильної чи неправильної техніки в розробці програмного забезпечення, є лише "наскільки ця методика відповідає моїм цілям? Ефективна вона чи неефективна? Чи вирішує вона одну проблему, але створює нову? Чи може це бути добре зрозумілим усім команда розвитку, чи це занадто незрозуміло? "

У цьому конкретному випадку - акт виклику методу на об’єкт, повернутий іншим методом - оскільки існує фактична модель дизайну, яка кодифікує цю практику (Factory), важко уявити, як можна було б стверджувати, що це категорично неправильно.

Причина, яку називають "Принципом найменших знань", полягає в тому, що "Низька муфта" - бажана якість системи. Об'єкти, які не щільно пов'язані один з одним, працюють більш незалежно, тому їх легше підтримувати та змінювати окремо. Але, як показує ваш приклад, бувають випадки, коли високе з'єднання є більш бажаним, щоб об'єкти могли ефективніше координувати свої зусилля.


2

Відповідь Дока Брауна показує класичну реалізацію підручника Закону Деметера - і роздратування / неорганізований код-додавання таким чином додавання десятків методів - це, мабуть, тому програмісти, в тому числі я, часто не переймаюся цим, навіть якщо це потрібно.

Існує альтернативний спосіб декупажу ієрархії об'єктів:

Викрийте interfaceтипи, а не classтипи, за допомогою своїх методів та властивостей.

У випадку оригінального плаката (ОП) encoder->WaitEncoderFrame()повертається IEncoderFrameзамість Frame, а визначає, які операції допустимі.


РІШЕННЯ 1

У найпростішому випадку, Frameі Encoderкласи обидва під вашим контролем, IEncoderFrameце підмножина методів, які Frame вже публічно викриває, і 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 of 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 ); } }

    ...
}

ВАРТІСТЬ: Виділення та GC нового об'єкта, 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 - тут,IEncoderFrameFrameEncoder .

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

Ще один момент, який я хочу наголосити, полягає в тому, що рішення розкрити функціональність як інтерфейс , розглядаючи всі 3 описані вище ситуації. По-перше, IEncoderFrameце просто підмножина Frameфункціональності Росії. По-друге, IEncoderFrameце адаптер. По-третє, IEncoderFrameце розділ на Encoderфункціонал s. Не має значення, чи змінюються ваші потреби між цими трьома ситуаціями: API залишається однаковим.


З'єднання вашого класу з інтерфейсом, поверненим одним з його співробітників, а не з конкретним класом, в той час як поліпшення, все ще є джерелом зв'язку. Якщо інтерфейс потрібно змінити, це означає, що ваш клас потребує змін. Важливим моментом, який потрібно зняти, є те, що з'єднання не є по суті поганим , якщо ви переконайтеся, що ви уникаєте непотрібної пари до нестабільних об'єктів або внутрішніх структур . Ось чому Закон Деметера потрібно сприймати з великою щіпкою солі; це вказує вам завжди уникати чогось, що може бути або не бути проблемою, залежно від обставин.
Periata Breatta

@PeriataBreatta - Я, звичайно, не можу і не погоджуюся з цим. Але я хотів би зазначити одне: Інтерфейс за визначенням представляє те, що потрібно знати на межі між двома класами. Якщо це "потрібно змінити", це принципово - жоден альтернативний підхід не міг би якось магічно уникнути необхідного кодування. На противагу цьому, виконуючи будь-яку з трьох описаних нами ситуацій, але не використовуючи інтерфейс, поверніть конкретний клас. В 1, Frame, в 2, EncoderFrameWrapper, в 3, Encoder. Ви замикаєтесь на такому підході. Інтерфейс може адаптуватись до них усіх.
ToolmakerSteve

@PeriataBreatta ..., яка демонструє перевагу, що явно визначає його як інтерфейс. Я сподіваюся, що врешті-решт покращить IDE, щоб зробити це настільки зручно, що інтерфейси будуть використовуватися набагато сильніше. Більшість багаторівневих доступів здійснюватиметься через якийсь інтерфейс, отже, набагато простіше керувати змінами. (Якщо це "надмірна кількість" для деяких випадків, аналіз коду в поєднанні з примітками про те, де ми готові ризикнути не мати інтерфейсу, в обмін на невелике підвищення продуктивності може "скласти його" - замінивши його на один із цих 3 конкретних класів із 3-х "рішень".)
ToolmakerSteve

@PeriataBreatta - і коли я кажу: "інтерфейс може адаптуватись до них усіх", я не кажу "ааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааадимаяазможна. Я кажу, що визначення інтерфейсів мінімізує зміни, які вам можуть знадобитися внести. У кращому випадку дизайн може змінити весь шлях від найпростішого випадку (рішення 1), до найскладнішого випадку (рішення 3), без зміни інтерфейсу взагалі - лише внутрішні потреби виробника стали складнішими. І навіть коли потрібні зміни, вони, як правило, менш поширені, ІМХО.
ToolmakerSteve
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.