Вступление
Этот пост направлен на улучшение понимания протоколов Elixir путем сравнения их с иерархиями классов в Java. Хотя эти два языка сильно отличаются друг от друга, у них есть общая мощная особенность: полиморфизм.
Когда вы впервые начинаете изучать Elixir, первое, что люди говорят, что вам нужно сделать, это сосредоточиться на функциональном программировании (FP). Это может означать отказ от некоторых старых привычек, сформировавшихся в результате использования традиционно объектно-ориентированных языков, таких как Java и Python, и немного иного мышления при написании кода и разработке программного обеспечения.
Хотя каждый разработчик Elixir расскажет вам, насколько хорош FP и почему он им нравится, менее известной особенностью для начинающих разработчиков Elixir является поддержка полиморфизма с помощью протоколов.
Полиморфизм – это концепция, лежащая в основе объектно-ориентированного программирования, что делает ее особенностью, которая, по-видимому, противоречит ядру языка Elixir. Мы поймем, почему это полезно, и сравним их со знакомыми базовыми функциями Java.
Для кого предназначена эта статья?
В этой статье рассказывается об использовании протоколов в Elixir для достижения полиморфной функциональности. Если вы никогда раньше не слышали об Эликсире, обязательно посетите веб-сайт Эликсира для получения более подробной информации об этом захватывающем молодом языке.
Если вы вообще знакомы с объектно-ориентированным программированием (ООП) на Java, этот пост поможет выявить сходства между иерархиями классов Java и протоколами Elixir.
Что такое полиморфизм?
Полиморфизм – это когда наша программа отправляет функции или методы на основе типа или класса данных.
Например, в Java у нас есть возможность определить интерфейс, который содержит только методы определения , описывающие некоторые функциональные возможности. Затем мы предоставляем реализации для определений методов интерфейса, когда мы определяем класс, который реализует
этот интерфейс.
Если сигнатура метода принимает экземпляр интерфейса и вызывает метод в этом экземпляре, то во время выполнения Java решает, какой метод определение отправлять на основе базового реализации класса.
Это позволяет нам отделить наш код, не привязывая нашу реализацию одного модуля к реализации другого модуля.
Иерархии классов Java
Давайте воспользуемся здесь простым примером. Американцы измеряют свой рост в дюймах, в то время как весь остальной мир использует сантиметры. Я уверен, что в некоторых частях мира используются другие единицы измерения, но для простоты мы будем придерживаться этого предположения.
Давайте определим интерфейс для измерения американцев или кого-либо из остального мира в сантиметрах или дюймах:
public interface MeasuredPerson { double measureInInches(); double measureInCentimetres(); }
Теперь мы можем реализовать этот интерфейс в нескольких классах, которые представляют людей из Америки и остального мира:
public class American implements MeasuredPerson { private String name; private double heightInInches; public American(String name, double heightInInches) { this.name = name; this.heightInInches = heightInInches; } public double measureInInches() { return heightInInches; } public double measureInCentimetres() { return heightInInches * 2.54; } } public class RestOfTheWorld implements MeasuredPerson { private String name; private double heightInCentimetres; public American(String name, double heightInCentimetres) { this.name = name; this.heightInCentimetres = heightInCentimetres; } public double measureInInches() { return heightInCentimetres * 0.39; } public double measureInCentimetres() { return heightInCentimetres; } }
Теперь, когда у нас есть пара классов, которые мы можем использовать, мы можем написать метод, который принимает Измеренного человека
и выводит его рост в дюймах.
public class Main { public static void main(string[] args) { American american = new American("John", 72); RestOfTheWorld restOfTheWorld = new RestOfTheWorld("Jean", 178); printInches(american); printInches(restOfTheWorld); } private void printInches(MeasuredPerson measuredPerson) { System.out.println("The person's height is: " + measuredPerson.measureInInches()); } }
Этот код выведет рост в дюймах как американца, так и человека из остального мира. Метод печать в дюймах
отправляет вызов измерение в дюймах
во время выполнения на основе базового типа данного объекта!
Протоколы эликсира
Протоколы в Elixir позволяют нам определять аналогичные отношения в наших программах. Во-первых, мы определяем протокол, который представляет собой набор функций определений , которые описывают некоторые функциональные возможности. Затем мы предоставляем реализации для определений функций протокола, когда мы определяем модуль реализации для типа Эликсира.
Типы эликсиров включают Список
, Карту
и Ключевое слово
, но они также могут включать пользовательские структуры, которые мы определяем. Ниже приведен пример двух пользовательских структур, аналогичных нашим классам Java, которые мы определили выше:
defmodule Person.American do defstruct [:name, :height_in_inches] end defmodule Person.RestOfTheWorld do defstruct [:name, :height_in_centimetres] end
Американцы измеряют свой рост в дюймах, а весь остальной мир измеряет свой рост в сантиметрах. Что, если мы хотим измерить человека, не беспокоясь о том, из Америки он или откуда-то еще в мире?
Ключевое слово defprotocol
Подобно тому, как мы определили интерфейс в Java, мы можем определить протокол для измерения роста человека. Протокол ожидает единицы измерения, с помощью которых пользователь хочет измерить данного человека.
defprotocol Person.Height do @doc """ Accepts the person to measure as the first argument, and accepts either `:inches` or `:centimetres` as the second argument to indicate which unit of measurement to return. """ def measure(person, unit_of_measurement) end
Обратите внимание, что мы не предоставляем блок do
для нашей функции мера/2
. В следующем разделе будет показано, как мы определяем реализацию для нашего нового протокола.
Ключевое слово defimpl
Следующие модули используют ключевое слово defimpl
для обеспечения реализации наших пользовательских структур персоналий:
defimpl Person.Height, for: Person.American do def measure(%Person.American{height_in_inches: height_in_inches}, :inches) do height_in_inches end def measure(%Person.American{height_in_inches: height_in_inches}, :centimetres) do height_in_inches * 2.54 end end defimpl Person.Height, for: Person.RestOfTheWorld do def measure(%Person.RestOfTheWorld{height_in_centimetres: height_in_centimetres}, :inches) do height_in_centimetres * 0.39 end def measure(%Person.RestOfTheWorld{height_in_centimetres: height_in_centimetres}, :centimetres) do height_in_centimetres end end
defimpl
принимает модуль протокола (определенный с помощью defprotocol
) и сопоставление модуля с типом, для которого мы хотим предоставить реализацию. Таким образом, первый аргумент наших функций measure/2
, определенных в наших модулях реализации, является структурой такого типа.
Есть ключевое отличие от нашей реализации Java: вместо реализации поведения внутри Person. Американец
и Человек. Остальной мир
модули, мы реализуем поведение в отдельном модуле. Поскольку сами структуры не могут иметь поведения (т.Е. вы не можете вызывать american.measure(:сантиметры)
), вместо этого структура должна быть передана функции; см. Ниже, как мы это делаем.
Связывая все это воедино
Теперь, когда мы определили пару модулей реализации для нашего Человека. Американец
и Человек. Остальной мир
структуры, мы можем назвать Человеком.Рост.мера/2
функция для измерения данного человека в нашей предпочтительной единице измерения!
%Person.American{name: "John", height_in_inches: 72} |> Person.Height.measure(:centimetres) |> IO.inspect() # 182.88 %Person.RestOfTheWorld{name: "Jean", height_in_centimetres: 178} |> Person.Height.measure(:inches) |> IO.inspect() # 70.0787
Сила протоколов
Здесь мы использовали простой пример, чтобы показать, как мы можем создавать ваши собственные протоколы. Но в чем тут дело?
Использование протоколов является мощным, потому что мы можем определить единый интерфейс с четко определенным поведением один раз , а затем реализуйте это поведение в другом месте на основе нашей пользовательской структуры.
Большим преимуществом протоколов Elixir перед иерархиями классов Java является то, что мы можем предоставлять реализации наших собственных протоколов для структур из внешних библиотек. В Java вам придется редактировать исходный код других библиотек, чтобы реализовать свой пользовательский интерфейс. Но в Elixir все, что нам нужно сделать, это предоставить реализацию для типа (структуры), независимо от того, какая библиотека/фреймворк предоставляет этот тип!
Знакомый пример
В Elixir мы используем модуль Перечисление
для обхода списков и карт. Все, что мы передаем в качестве первого аргумента функциям в Перечисление
модуль должен иметь реализацию для Перечисляемого
протокола! Чтобы узнать больше об этом ознакомьтесь с Перечисляемыми
документами .
Ресурсы
Я хотел бы предоставить некоторые дополнительные ресурсы для изучения протоколов Elixir, чтобы вы могли продолжить изучение этой концепции ООП на традиционно функциональном языке.
- Протоколы эликсира : введение в протоколы эликсира на официальном сайте Elixir lang.
- Эликсир
Протокол
документы : если вы хотите глубже погрузиться в то, как работают протоколы, это хорошее место для начала. Документы по Эликсиру написаны так хорошо, что обычно я не смотрю в другое место. - Программный эликсир.6 : эта книга – один из лучших инструментов в моем арсенале. Я не мог бы порекомендовать лучшую книгу как для начинающих, так и для экспертов. Я все время использую его в качестве ориентира.
Я написал этот пост для тех из нас, кто знаком с возможностями ООП, и для демонстрации потрясающей функции в Elixir, которая подходит всем, кто знаком с ООП на Java. Спасибо за чтение!
Оригинал: “https://dev.to/hugecoderguy/polymorphism-elixir-vs-java-14i0”