Нужно ли всегда знать, что делает API, просто взглянув на код?

20

Недавно я занимался разработкой своего собственного API, и с таким большим интересом к разработке API меня очень интересовало, как я могу улучшить свой дизайн API.

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

Например, посмотрите это обсуждение на GitHub для диско- репо, оно выглядит примерно так:

foo.update_pinned(true, true);

Просто взглянув на код (не зная имен параметров, документации и т. Д.), Невозможно догадаться, что он собирается делать - что означает второй аргумент? Предлагаемое улучшение должно иметь что-то вроде:

foo.pin()
foo.unpin()
foo.pin_globally()

И это проясняет ситуацию (я полагаю, 2-й аргумент был о том, стоит ли связывать foo глобально), и я согласен, что в этом случае последующее, безусловно, будет улучшением.

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

Например, я раскрываю один метод SetVisibility(bool, string, bool)на FalconPeer и подтверждаю, что просто смотрю на строку:

falconPeer.SetVisibility(true, "aerw3", true);

Вы бы не знали, что он делает. Он устанавливает 3 различных значения, которые управляют «видимостью» falconPeerв логическом смысле: принимать запросы на соединение, только с паролем и отвечать на запросы на обнаружение. Разделение этого на 3 вызова метода может привести к тому, что пользователь API установит один аспект «видимости», забыв указать другим, о которых я заставляю их думать, только выставив один метод для установки всех аспектов «видимости» . Кроме того, когда пользователь хочет изменить один аспект, он почти всегда захочет изменить другой аспект и теперь может сделать это за один вызов.

markmnl
источник
13
Именно поэтому некоторые языки имеют именованные параметры (иногда даже принудительные именованные параметры). Например , вы могли группа много настроек в простом updateметоде: foo.update(pinned=true, globally=true). Или: foo.update_pinned(true, globally=true). Таким образом, ответ на ваш вопрос должен также учитывать особенности языка, потому что хороший API для языка X может не подходить для языка Y и наоборот.
Бакуриу
Договорились - давайте прекратим использовать логические выражения :)
Ven
2
Он известен как «логическая ловушка»
user11153
@Bakuriu Даже у C есть перечисления, даже если они замаскированные целые числа. Я не думаю, что есть какой-то язык реального мира, где логические выражения - это хороший дизайн API.
Довал
1
@Doval Я не понимаю, что вы пытаетесь сказать. Я использовал булевы значения в этой ситуации просто потому, что ОП их использовал, но моя точка зрения совершенно не связана с переданным значением. Например: setSize(10, 20)не так читабельно, как setSize(width=10, height=20)или random(distribution='gaussian', mean=0.5, deviation=1). В языках с принудительными именованными параметрами булевы значения могут передавать точно такой же объем информации, что и использование перечислений / именованных констант, поэтому они могут быть полезны в API.
Бакуриу

Ответы:

27

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

Вы можете использовать перечисления:

falconPeer.SetVisibility(JoinRequestOptions.Accept, "aerw3", DiscoveryRequestOptions.Reply);

Или даже перечисление флагов (если ваш язык поддерживает это):

falconPeer.SetVisibility(VisibilityOptions.AcceptJoinRequests | VisibilityOptions.ReplyToDiscoveryRequests, "aerw3");

Или вы можете использовать объект параметров :

var options = new VisibilityOptions();
options.AcceptJoinRequests = true;
options.ReplyToDiscoveryRequest = true;
options.Password="aerw3";
falconPeer.SetVisibility(options);

Шаблон Parameter Object дает вам несколько других преимуществ, которые могут оказаться полезными. Это облегчает передачу и сериализацию набора параметров, и вы можете легко задавать неопределенные параметры «значения по умолчанию».

Или вы можете просто использовать логические параметры. Кажется, Microsoft все время делает это с помощью .NET Framework API, так что вы можете просто пожать плечами и сказать: «Если это достаточно хорошо для них, это достаточно хорошо для меня».

BenM
источник
У шаблона Parameter Object все еще есть проблема, указанная в OP: « Разделение этого на 3 вызова метода может привести к тому, что пользователь API установит один аспект« видимости », забыв установить другие ».
Лоде
Правда, но я думаю, что это делает ситуацию лучше (если не идеальной). Если пользователь вынужден создать экземпляр объекта VisibilityOptions и передать его, он может (если повезет) напомнить им, что существуют другие свойства объекта VisibilityOptions, которые он может захотеть установить. С подходом трех вызовов методов все, что им нужно, чтобы напомнить им, это комментарии к методам, которые они вызывают.
BenM
6

Очевидно, что из этого правила всегда есть исключения, но, как вы хорошо объяснили сами, есть определенные преимущества в том, что у вас есть читаемый API. Булевы аргументы особенно утомительны, так как 1) они не раскрывают намерение и 2) они подразумевают, что вы вызываете функцию, где у вас фактически должно быть два, потому что в зависимости от логического флага будут происходить разные вещи.

Скорее всего, главный вопрос: сколько усилий вы хотите вложить, чтобы сделать ваш API более читабельным? Чем больше она обращена наружу, тем больше усилий можно легко оправдать. Если это API, который используется только одним другим модулем, то это не такая уж большая проблема. Если вы говорите о REST API, в котором вы планируете позволить всему миру потерять его, то вы можете также приложить больше усилий, чтобы сделать его более понятным.

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

Visibility visibility = Visibility.builder()
  .acceptJoinRequests()
  .withPassword("aerw3")
  .replyToDiscoveryRequests()
  .build();
falconPeer.setVisibility(visibility);
Фрэнк
источник