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

17

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

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

Так ясно, как грязь?

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

OpenFile
SetHeaderString
WriteDataLine
SetTrailerString
CloseFile

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

Менее крайним примером может быть опускание заголовков и трейлеров.

Еще одним может быть установка заголовка и строк трейлера до открытия файла.

Это принцип дизайна интерфейса, который признан, или это просто способ POLA до того, как ему дали имя?

NB не увязайте в деталях этого интерфейса, это всего лишь пример для этого вопроса.

Робби Ди
источник
10
Принцип «наименьшего удивления» гораздо более распространен в дизайне пользовательского интерфейса, чем в дизайне «интерфейса программиста приложения». Причина в том, что от пользователя веб-сайта или программы нельзя ожидать каких-либо инструкций перед его использованием, в то время как программист должен, по крайней мере в принципе, читать документы API перед программированием с ними.
Килиан Фот
1
Связанный: programmers.stackexchange.com/questions/187457/…
Бен Коттрелл
7
@KilianFoth: Я почти уверен, что Википедия ошибается по этому поводу - POLA касается не только дизайна пользовательского интерфейса, термин «принцип наименьшего удивления» (который совершенно аналогичен) также используется Бобом Мартином для разработки функций и классов в его «Чистый код» книга.
Док Браун
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значение. Чтобы устранить эту зависимость заказа не является ни ISP, ни SRP, это просто POLA.
Док Браун
Когда порядок имеет значение, вы все равно можете удовлетворить POLA, определив операции таким образом, что для выполнения более поздних шагов требуется значение, возвращаемое из предыдущих шагов, что обеспечивает порядок с системой типов. FileWriterможет затем потребовать значение из последнего ContentBuilderшага в Writeметоде, чтобы убедиться, что весь входной контент завершен, что делает InvalidContentExceptionненужным.
Дэн Лайонс
@DanLyons Я чувствую, что это довольно близко к ситуации, которую пытается избежать аскер; где пользователь API должен знать или заботиться о заказе. В идеале, сам API должен обеспечивать порядок, иначе он может попросить пользователя нарушить DRY. Вот причина, по которой можно разделить ContentBuilderи позволить FileWriter.Writeинкапсулировать этот бит знаний. Исключение будет необходимо в случае, если с содержимым что-то не так (например, отсутствует заголовок). Возврат также может сработать, но я не фанат превращения исключений в коды возврата.
Бен Коттрелл
Но определенно стоит добавить еще заметки о СУХОЙ и упорядоченности к ответу.
Бен Коттрелл
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 ответ был слишком длинным для комментария, смотрите мое обновление
Фабиан Шменглер
1
Если имя файла является необязательным, вы можете указать OpenFileперегрузку, которая не требуется.
5gon12eder