Статьи

3 простых метода, чтобы сделать API проще в использовании и понимании

Сколько раз вы пытались использовать API только для того, чтобы обнаружить, что вам нужно было заполнить какое-то нелепое количество параметров значениями, о которых вы даже не подозревали?

Если вы когда-нибудь занимались программированием для Windows и вам приходилось обращаться к некоторым из Win32 API, я уверен, что вы испытали эту боль. Мне вообще нужен этот параметр? Какого черта это значение для?

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

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

Наш метод оскорбительного API

Давайте начнем с рассмотрения метода API, который может использовать некоторую очистку. (Примеры в C #, но идея применима ко многим языкам.)

public void LoginAsCustomer(string userName, string password, string customerType, int numberOfTries)
{
  // ...
}

// Code to call the method
LoginAsCustomer("Darth Vader", "ihatewookies", "New Customer", 3);

оскорбительные 3 простых метода, чтобы сделать API проще в использовании и понимании

Конечно, подпись этого метода выглядит не так плохо, но она не самая простая в использовании. Вы должны заполнить 4 различных параметра, и вы должны знать, что такое customerType , и должны принять решение о значении numberOfTries и что это вообще означает.

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

В этом сценарии я намеренно не объясняю, что такое customerType или numberOfTries , потому что вы, как разработчик, пытающийся использовать этот код, тоже не будете знать. Вы должны пойти и выяснить это.

Давайте посмотрим, сможем ли мы немного улучшить сигнатуру этого метода.

1. Уменьшение количества параметров путем создания объектов

Первое, что мы можем сделать, чтобы немного упростить сигнатуру этого метода, — это уменьшить количество параметров, которые он имеет.

Единственная проблема в том, что нам нужны все эти параметры, иначе их бы не было, верно?

Это, вероятно, правда, так что мы можем на самом деле сделать?

Что ж, один действительно распространенный шаг рефакторинга, который, вероятно, не будет для вас большим сюрпризом, — это уменьшение параметров путем объединения их в объект.

(Кстати, если вы ищете классическую, но отличную книгу по рефакторингу, ознакомьтесь с: Рефакторинг Улучшение дизайна существующего кода Мартина Фаулера)

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

Это неправильно, потому что это просто скрывает проблему и теперь требует, чтобы вызывающий код сначала создавал объект, имеющий конструктор, который принимает те же параметры. Если вы делаете вещи таким образом, вы просто пинаете банку вверх по течению.

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

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

public void LoginAsCustomer(LoginInfo loginInfo, string customerType, int numberOfTries)
{
  // ...
}

var loginInfo = new LoginInfo("Darth Vader", "ihatewookes");
LoginAsCustomer(loginInfo, "New Customer", 3);

Теперь для вызова нашего метода требуется больше кода, но этот код упрощен и его легче понять. Когда кто — то пытается вызвать метод , и они видят , они нуждаются в LoginInfo объекте, они могут очень легко видеть , что LoginInfo объект требует имени пользователя и пароля , и это понятно , что цель его есть.

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

2. Использование перечислений для ограничения выбора

Мы еще не закончили. Мы все еще можем сделать немного больше для улучшения этого простого API.

Следующая большая проблема с API заключается в том, что параметр customerType очень расплывчатый. Мы не можем сразу понять, что это значит и какую ценность следует вкладывать туда. Есть ли ограниченный список значений? Это свободная форма? Имеет ли это значение? Или я могу просто поставить пустую строку там?

Мы можем решить эту проблему, ограничив возможные значения с помощью перечисления. В этом примере мы предположим, что customerType может быть одним из 3 различных значений. Мы требовали, чтобы вызывающая сторона набирала строковое представление этих значений вручную — и не делала ошибку — но мы можем улучшить восприятие — и уменьшить частоту появления ошибок — ограничив выбор только тем, что действительно.

public enum CustomerType
{
  NEW_CUSTOMER,
  REWARDS_CUSTOMER,
  REGULAR_CUSTOMER
}

public void LoginAsCustomer(LoginInfo loginInfo, CustomerType customerType, int numberOfTries)
{
  // ...
}
 
var loginInfo = new LoginInfo("Darth Vader", "ihatewookes");
LoginAsCustomer(loginInfo, CustomerType.NEW_CUSTOMER, 3);

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

Поэтому в любое время вы можете ограничить выбор параметра в методе. Перечисления — это самый простой способ, но вы можете использовать и другие методы.

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

public void ValidateCustomer(int maximumAge)
{
  //...
}

ValidateCustomer(25);

// Using an Age class to restrict values

public class Age
{
  public Age(int age)
  {
    if(age < 0 || age > 130)
      throw new Exception("Invalid age. Age must be between 0 and 130");
  }
}

public void ValidateCustomer(Age maximumAge)
{
  // ...
}

var customerAge = new Age(25);
ValidateCustomer(age);

3. Использование значений по умолчанию для уменьшения необходимых параметров

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

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

In our example, we can get rid of the need to specify a customerType and the numberOfTries by providing a reasonable default for both.

public void LoginAsCustomer(LoginInfo loginInfo, CustomerType customerType = CustomerType.NEW_CUSTOMER, int numberOfTries = 3)
{
  // ...
}
 
var loginInfo = new LoginInfo("Darth Vader", "ihatewookes");
LoginAsCustomer(loginInfo);

// What if we need to override a default? No problem.

LoginAsCustomer(loginInfo, customerType: CustomerType.REWARDS_CUSTOMER);

Now, calling our method is dead simple for most cases. We can just provide the loginInfo and if we need more control, we can override the customerType or the numberOfTries.

It’s not rocket science

What we did here was pretty simple, but don’t let the simplicity fool you. We have made small–and arguably obvious–changes, but we’ve greatly reduced the mental burden for the caller of this method.

happy 3 простых метода, чтобы сделать API проще в использовании и понимании

We would see a much larger cumulative benefit from applying these techniques to an entire API or code base. When you start restricting choices and grouping related data into objects instead of raw parameters, you end up with a synergistic effect in your code, because you are able to know more about the data that is being passed around.

For example, changing our customerType to an enumeration made it easier to call the method, but it also makes our logic inside the method simpler, because we don’t have to check for invalid values. Anytime we can rely on data being within certain bounds, we can simplify the logic for handling that data.