Статьи

Утка ложь

Далее следует мой опыт работы с Java, PHP и Ruby. Я в основном использую PHP в качестве динамического языка, который поддерживает типизацию утилит, но также и определение Java-подобных интерфейсов, но не форсирует ни один из двух подходов, поскольку вы можете определить интерфейсы, аргументы метода которых принимают любую переменную или вообще не используют интерфейсы.

Является ли утиная типизация , что революция?

Заблуждения, которые я видел

Во-первых, утка не является средством для совместной работы объектов, даже если они разработаны разными людьми или проектами. Например, учитывая метод send (), он может быть реализован во многих версиях (например, send (), sendX (), send (x, y)), что маловероятно, что без определения общего интерфейса объекты будут вызывать каждую посмотри правильно. Даже если сигнатура кажется похожей, объекты, переданные в качестве первого, второго или n-го аргумента, могут фактически отличаться.

Кроме того, можно встретить разные реализации send (), которые на самом деле не должны были соответствовать одному и тому же интерфейсу: подумайте об отправке электронной почты и отправке уведомления Facebook. Это столкновение интерфейсов может быть обнаружено системой типов или во время выполнения на гибридных языках, но приводит к странным ошибкам (например, ошибка неопределенных методов при отправке сообщения с этими аргументами).

Утиная типизация работает хорошо, когда неявные интерфейсы хорошо известны: например, при переопределении оператора + или методов equals () и compareTo ().

Явные интерфейсы

Это не значит, что статических проверок и явных интерфейсов достаточно для предотвращения всех ошибок, поскольку вы всегда можете реализовать интерфейс непоследовательным образом и (без разработки по контракту) ничто не спасет вас:

interface SortingAlgorithm
{
    public function sort(array $objects);
}
class BubbleSort implements SortingAlgorithm { ... }
class Quicksort implements SortingAlgorithm { ... }
class MergeSort implements SortingAlgorithm { ... }

В чем разница между этими двумя классами?

  • Быстрая сортировка в эффективных реализациях не является стабильной сортировкой (алгоритм, который оставляет объекты с одинаковыми ключами в том же порядке, что и в исходной коллекции). Вместо этого Mergesort всегда является стабильной сортировкой.
  • Алгоритмы сортируются в порядке возрастания или убывания?
  • И на каком ключе?

На самом деле, мы можем утверждать, что нам нужна та информация, которая нам нужна, чтобы мы могли изменить наш механизм сортировки после запроса пользователя или нового функционального требования, просто меняя объект. Но интерфейсы Java и PHP не являются контрактами: они определяют только требования к типированию, но не предварительные условия или постусловия.

С помощью утиной типизации картина будет такой (сохраняя PHP как язык для согласованности):

class BubbleSort {
    public function sort(array $object) { ... }
}
class QuickSort {
    public function sort(array $object) { ... }
}
class MergeSort {
    public function sort(array $object) { ... }
}

Но в этом случае мы больше не знаем, что изначально разработчик намеревался сделать эти алгоритмы / объекты совместимыми, или каким требованиям мы должны следовать, чтобы реализовать новый. Некоторые из этих проблем можно разрешить с помощью модели «акты как», но я верю, что не все из них.

Действует как образец

В Ruby, одном из самых популярных языков с утиной типизацией, принято реализовывать типизацию утиной утилитой с помощью внешнего модуля (который должен быть включен в реализацию классов), который принимает вызовы к примитиву act_as_something .

Например, это определение добавит несколько методов в класс, например first ?, insert_at и так далее:

class Product < ActiveRecord::Base
    acts_as_list
end

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

Однако в этих случаях pattern является реализацией механизма Template Template (или, в лучшем случае, шаблона Bridge), в котором внутренний интерфейс между объектом и модулем подвергается той же участи, что и исходная функция sort ():

  • что я должен перейти к acts_as_something?
  • Когда один из аргументов является процедурой, сколько аргументов он принимает и какие сообщения они принимают по очереди?

Более того, если вам нужно было реализовать каждый неявный интерфейс с помощью Template Templates, вы быстро увеличите свои классы. Ruby «исправляет» проблему, позволяя множественное наследование (именно это и есть модули), так что вы можете расширять столько классов, сколько вам нужно, чтобы предоставить столько методов-шаблонов, сколько вам нужно.

Единственная ошибка обратного вызова

Я начинаю ненавидеть PHP-методы, которые думают, что все замыкания равны. Их типичная подпись утки обычно такова:

public function doSomething(callable $callback) { ... }

где $ callback является замыканием или объектом, определяющим __invoke (); то, что вы можете вызвать с помощью $ callback (/ * аргументы здесь * /).

Эта проблема? Я должен задать много вопросов, когда увижу эту подпись:

  • сколько аргументов принимает $ callback? Есть ли у них?
  • Какие методы предоставляет каждый из этих аргументов? То есть я могу вызвать $ firstArgument-> x () или $ secondArgument-> y ()?
  • Если я сознательно нарушаю Закон / Предложение Деметры и вызываю методы для результата $ firstArgument-> x (), что я могу вызвать?
  • Что должен вернуть $ callback, если что?

Вот почему я теперь предпочитаю во все большем числе случаев определять явные интерфейсы, даже когда у меня есть один метод:

interface ThatCallback
{
    public function __invoke(AClass $firstArgument, BClass $secondArgument);
}

Проблема с этим подходом в PHP состоит в том, что вы не можете определить частный внутренний класс, реализующий это, поэтому вам понадобится новый исходный файл:

class MyCallback implements ThatCallback
{
    public function __invoke(AClass $firstArgument, BClass $secondArgument)
    {
        ...
    }
}

Java делает это лучше, позволяя вам даже создавать анонимные классы внутри ваших методов:

new ThatCallback() {
    public void invoke(AClass firstArgument, BClass secondArgument) {
        ...
    }
}

Утиная типизация — это отсутствие типов: предоставить документацию для интерфейса означает записать, какие сообщения он принимает, и в свою очередь, что принимают аргументы и возвращаемое значение методов реализации. С явным интерфейсом вы просто ссылаетесь на типы AClass и BClass из любого места в вашем коде, нарушая рекурсивную спецификацию сообщений на первом уровне. Единственная альтернатива — не документировать.

Выводы

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

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