Принцип найменшого здивування (POLA) та інтерфейси


17

Добру чверть століття тому, коли я вивчав C ++, мене вчили, що інтерфейси повинні прощати і, наскільки це можливо, не піклуватися про порядок виклику методів, оскільки споживач може не мати доступу до джерела чи документації замість це.

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

Ясний, як грязь?

Розглянемо інтерфейс із цими методами (для створення файлів даних):

OpenFile
SetHeaderString
WriteDataLine
SetTrailerString
CloseFile

Тепер ви, звичайно, можете просто перейти через ці порядок, але сказати, що вам було байдуже ім’я файлу (подумайте a.out) або те, який заголовок та рядок трейлера включені, ви можете просто зателефонувати AddDataLine.

Менш крайнім прикладом може бути пропуск заголовків та причепів.

Ще одна може бути встановлення рядків заголовка та трейлера до відкриття файлу.

Це принцип розпізнавання інтерфейсів, який розпізнається або просто спосіб POLA до того, як йому було надано ім'я?

Зверніть увагу: не зациклюйтеся на деталях цього інтерфейсу, це лише приклад заради цього питання.


10
Принцип "найменшого здивування" набагато більше поширений у дизайні інтерфейсу користувача, ніж у дизайні "Інтерфейс програміста додатків". Причина полягає в тому, що від користувача веб-сайту або програми не можна очікувати, що він прочитає будь-які інструкції перед його використанням, тоді як програміст, як правило, повинен читати документи API перед програмуванням з ними.
Кіліан Фот


7
@KilianFoth: Я впевнений, що Вікіпедія помиляється з цього приводу - ПОЛА не стосується лише дизайну інтерфейсу користувача, термін "принцип найменшого сюрпризу" (який зовсім однаковий) також використовується Боб Мартіном для розробки функцій та класів у своїй Книга «Чистий код».
Doc Brown

2
Часто все-таки краще незмінний інтерфейс. Ви можете вказати всі дані, які ви хочете встановити під час створення. Не залишається ніяких двозначностей, і клас стає простіше писати. (Іноді ця схема неможливо, звичайно.)
usr

4
Повністю не згодні з тим, що POLA не застосовується до API. Це стосується всього, що людина створює для інших людей. Коли речі діють так, як очікувалося, їх простіше концептуалізувати і, отже, створюють меншу пізнавальну навантаження, дозволяючи людям робити більше речей з меншими зусиллями.
Gort the Robot

Відповіді:


25

Один із способів, коли можна дотримуватися принципу найменшого здивування, - це розглянути інші принципи, такі як ISP і SRP , або навіть DRY .

У конкретному прикладі, який ви подали, здається, що існує певна залежність замовлення для управління файлом; але ваш API контролює як доступ до файлів, так і формат даних, що трохи нагадує порушення SRP.

Редагування / оновлення: воно також передбачає, що сам API просить користувача порушити DRY, оскільки їм потрібно буде повторювати ті самі кроки щоразу, коли вони використовують API .

Розглянемо альтернативний API, коли операції вводу-виводу є окремими від операцій з даними. і де сам API "володіє" замовленням:

ContentBuilder

SetHeader( ... )
AddLine( ... )
SetTrailer ( ... )

FileWriter

Open(filename) 
Write(content) throws InvalidContentException
Close()

З вищезазначеним розділенням, ContentBuilderне потрібно насправді нічого робити, крім магазину рядків / заголовків / трейлерів (можливо, також ContentBuilder.Serialize()метод, який знає порядок). Дотримуючись інших принципів SOLID, це вже не має значення, чи встановлюєте ви заголовок або трейлер до чи після додавання рядків, оскільки нічого в ContentBuilderфайлі насправді не записується до файлу до його передачі FileWriter.Write.

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

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


Саме те, що я шукав - дякую! Зі статті провайдера: "(ISP) зазначає, що жоден клієнт не повинен бути змушений залежати від методів, якими він не користується"
Роббі Ді

5
Це не є поганою відповіддю, але все-таки конструктор контенту може бути реалізований таким чином, коли порядок дзвінків SetHeaderабо AddLineмає значення. Для усунення цього порядку не залежить ні провайдер, ні SRP, це просто POLA.
Doc Brown

Якщо питання порядку має значення, ви все ще можете задовольнити POLA, визначивши операції таким чином, що для виконання пізніших кроків потрібне значення, повернене з попередніх кроків, тим самим забезпечивши порядок у системі типу FileWriterТоді може знадобитися значення з останнього ContentBuilderкроку Writeметоду, щоб забезпечити повний вміст вводу, що робить InvalidContentExceptionнепотрібним.
Дан Ліонс

@DanLyons Я вважаю, що це досить близько до ситуації, яку прагне уникнути; де користувач API повинен знати або дбати про замовлення. В ідеалі, сам API повинен виконувати замовлення, інакше потенційно просить користувача порушити DRY. Це причина для того, щоб розколотись ContentBuilderі дати можливість FileWriter.Writeінкапсулювати цю частину знань. Виняток буде необхідним у випадку, якщо щось не суперечить вмісту (наприклад, такий, як відсутні заголовок). Повернення також може працювати, але я не прихильник перетворення винятків у коди повернення.
Бен Коттрелл

Але, безумовно, варто додати більше приміток про DRY та замовити відповідь.
Бен Коттрелл

12

Мова йде не лише про POLA, а й про запобігання недійсного стану як можливого джерела помилок.

Подивимось, як ми можемо надати певні обмеження для вашого прикладу, не надаючи конкретної реалізації:

Перший крок: Не дозволяйте нічого викликати до відкриття файлу.

CreateDataFileInterface
  + OpenFile(filename : string) : DataFileInterface

DataFileInterface
  + SetHeaderString(header : string) : void
  + WriteDataLine(data : string) : void
  + SetTrailerString(trailer : string) : void
  + Close() : void

Тепер має бути очевидним, що CreateDataFileInterface.OpenFileпотрібно викликати для отримання DataFileInterfaceекземпляра, куди можуть бути записані фактичні дані.

Другий крок: Переконайтеся, що заголовки та причепи завжди встановлені.

CreateDataFileInterface
  + OpenFile(filename : string, header: string, trailer : string) : DataFileInterface

DataFileInterface
  + WriteDataLine(data : string) : void
  + Close() : void

Тепер ви повинні надати всі необхідні параметри наперед, щоб отримати DataFileInterface: ім'я файлу, заголовок та трейлер. Якщо рядок трейлера недоступний, поки не будуть записані всі рядки, ви також можете перемістити цей параметр у Close()(можливо, перейменувавши метод на WriteTrailerAndClose()), щоб файл принаймні не міг бути закінчений без рядка з трейлером.


Щоб відповісти на коментар:

Мені подобається розділення інтерфейсу. Але я схильний думати, що ваші пропозиції щодо примусового виконання (наприклад, WriteTrailerAndClose ()) опираються на порушення SRP. (Це те, з чим я неодноразово боровся, але ваша пропозиція здається можливим прикладом.) Як би ви відповіли?

Правда. Я не хотів більше концентруватися на прикладі, ніж потрібно, щоб зробити свою думку, але це гарне питання. У цьому випадку я думаю, що я б назвав це Finalize(trailer)і стверджую, що це робить не надто багато. Написання трейлера та закриття - лише деталі реалізації. Але якщо ви не погоджуєтесь або маєте подібну ситуацію, коли це інакше, ось можливе рішення:

CreateDataFileInterface
  + OpenFile(filename : string, header : string) : IncompleteDataFileInterface

IncompleteDataFileInterface
  + WriteDataLine(data : string) : void
  + FinalizeWithTrailer(trailer : string) : CompleteDataFileInterface

CompleteDataFileInterface
  + Close()

Я б насправді цього не робив для цього прикладу, але він показує, як слід вести техніку.

До речі, я припускав, що методи насправді потрібно викликати в такому порядку, наприклад, щоб послідовно писати багато рядків. Якщо цього не потрібно, я б завжди віддав перевагу будівельнику, як це запропонував Бен Коттрел .


1
Ви, на жаль, потрапили в пастку, я відверто попередив вас уникати від самого початку. Ім’я файлу не потрібно - ні заголовок, ні трейлер. Але загальна тема розбиття інтерфейсу хороша, тому +1 :-)
Роббі Ді

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

Мені подобається розділення інтерфейсу. Але я схильний думати, що Ваша пропозиція щодо примусового виконання (наприклад WriteTrailerAndClose()) обмежується порушенням СРП. (Це те, з чим я неодноразово боровся, але ваша пропозиція здається можливим прикладом.) Як би ви відповіли?
kmote

1
@kmote відповідь була занадто довгою для коментаря, дивіться моє оновлення
Fabian Schmengler

1
Якщо ім'я файлу необов’язкове, ви можете надати OpenFileперевантаження, яке не потребує такого.
5gon12eder
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.