Статьи

Банда четырех паттернов с классами типов и имплицитами в Scala (часть 2)

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

Шаблон адаптера является наиболее широко используемым и узнаваемым классом типов, использующим шаблон GoF в сообществе Scala. Стандартная библиотека включает в себя несколько примеров, таких как Ordering и Numeric . Как и в случае любой хорошо спроектированной и реализованной библиотеки, их использование прозрачно и невидимо для потребителей библиотеки. Многие

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

Шаблон адаптера

Шаблон адаптера (иногда называемый шаблоном оболочки) представляет собой концепцию ОО-проекта, изобретенную для решения проблемы дублирования кода и содействия повторному использованию кода при наличии разнородных интерфейсов. Это достигается за счет объединения кода вокруг общего интерфейса и воплощения философии GoF «программирования для интерфейса». Из этого краткого описания шаблоны мостов и адаптеров могут показаться неразличимыми, но их цели совершенно разны. В то время как шаблон моста используется для независимого изменения N концепций за N интерфейсами, шаблон адаптера используется для сокращения N интерфейсов до одного, когда базовая концепция одинакова. Иными словами, он используется в качестве «клея» для связывания или адаптации несовместимых API (отсюда и название).

Адаптеры — более удобный инструмент для прагматического построения кода. Распространенным случаем использования является наличие ранее существующего компонента или библиотеки, которая должна быть настроена для работы в кодовой базе, не предназначенной для ее размещения. Как правило, они создаются заранее, только когда библиотекам требуется обратная совместимость между версиями или они служат точками расширения для будущих пользователей библиотеки. Так как же это выглядит? Для примера возьмем два интерфейса:

1
2
3
4
5
6
trait Addable{
  def add(x: Int, y: Int) = x+y
}
trait Summable{
  def plus(x: Int)(y: Int) = x+y
}

где метод «плюс» Summable раскрывает каррированную форму метода «добавить» Addable . Если бы мы хотели сделать эти два интерфейса взаимодействующими в чисто ОО мире, мы бы создали:

1
2
3
4
5
6
class Add2SumAdapter(addable: Addable) extends Summable{
  def plus(x: Int)(x: Int) = addable add (x, y)
}
class Sum2AddAdapter(sum: Summable) extends Addable{
  def add(x: Int, x: Int) = sum plus (x) (y)
}

Который по существу упаковывает один интерфейс для другого. Освобождение библиотеки достигнуто. Кроме того, утверждается, что по мере продвижения к функциональному стилю программирования мы можем уменьшить потребность и шаблон, требуемый для шаблона адаптера. В языках FP адаптер превращается в каррирование , составление функций и лифтинг . Это изменение в динамике важно понимать, так как смягчение не равнозначно устранению. Нужно подумать о сигнатуре типа функций выше, чтобы увидеть, как потребность в адаптации продолжает оставаться верной в функциональных терминах:

1
2
type <b>Plus</b> = (Int => Int => Int)
type <b>Add</b> = ((Int, Int) => Int)

Просто не существует способа избежать несоответствия типов. Это должно быть где-то обработано.

Шаблон адаптера в классах типов

Прежде чем мы начнем с шаблона адаптера и классов типов , давайте сделаем шаг назад и поговорим о более серьезных проблемах; вопросы области библиотеки. Что мы имеем в виду, давайте поговорим о концепции, связанной с шаблоном адаптера, принципом инверсии зависимости или DIP для краткости. Отличительной чертой кода, написанного с использованием этой техники, является отделение модулей более высокого уровня от модулей более низкого уровня путем принудительного соответствия модулей более низкого уровня интерфейсу, определенному на более высоком уровне. Таким образом, инверсия модуля более высокого уровня определяет строительные блоки, на которых он построен, а не наоборот.

DIP приводит к более чистому и более расширяемому коду, но при этом в значительной степени зависит от использования структурных шаблонов проектирования , причем адаптер является единым целым. Scala допускает неявные преобразования между типами, и, таким образом, DIP может быть реализован в терминах неявных преобразований в очень OO-стиле кодирования. Это решило бы проблему слишком большого количества шаблонов адаптации интерфейса в нашем коде, но, как побочный эффект, это привело бы к коду, который было бы неестественно трудно отлаживать, и к ошибкам, которые еще сложнее отследить. Есть причина, по которой они были отключены по умолчанию в 2.10 (и если вы испытали радости изменяемого неявного состояния, у вас было бы болезненное понимание, почему.)

Обобщения DIP на уровне типов приводят к еще более мощной конструкции, известной как специальный полиморфизм . В этом случае мы определяем класс типов, который действует как адаптер, позволяя объектам этого типа выражать конечный набор операций. Этот набор операций определяет интерфейс, в который может быть записан код, независимо от типа объекта. Вместо переноса класса в интерфейс объект и его тип становятся аргументом интерфейса. Предполагается, что неявное разрешение области действия вводит правильный класс типов на основе типа аргумента на сайте вызова функции.

Прежде чем мы продолжим, позвольте мне отметить, что DIP, когда он используется в контексте последствий, как мы только что описали, может звучать похоже на другой метод организации кода, известный как внедрение зависимостей . DIP — это не внедрение зависимостей и не то, что мы только что описали. DIP разрешается во время компиляции, в то время как DI вращается вокруг разрешения во время выполнения. Один может использовать другой, но они настолько различны, насколько могут быть разные.

Давайте рассмотрим два примера реализации даты и времени в Java: java.Date и joda.DateTime . Оба представляют дату и время с методами для модификации. Тем не менее, одна является изменяемой конструкцией, чьи методы работают с побочным эффектом, а другая — неизменной, методы которой возвращают новый экземпляр. Если мы хотим работать с типом даты / времени, но по-прежнему не связаны с конкретной реализацией этого типа, мы закодируем интерфейс поведений в повторно используемом классе типов :

1
2
3
4
5
6
7
trait DateFoo[DateType]{
  def addHours(date: DateType)(hours: Int): DateType
  def addDays(date: DateType)(days: Int): DateType
  def addMonths(date: DateType)(months: Int): DateType
  def addYears(date: DateType)(years: Int): DateType
  def now(): DateType
}

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

1
2
3
4
5
trait TimeTrackDAO{
  def checkin[A](unit: String, key: String, time: A)(implicit date: DateFoo[A]): A
  def checkout[A](unit: String, key: String, time: A)(implicit date: DateFoo[A]): A
  def itemize(unit: String, key: String): Int
}

и до тех пор, пока существует неявно ограниченный класс типов DateFoo для «A», оба метода «checkin» и «checkout» будут просто работать. А если бы не было такого типа? Мы могли использовать только метод «itemize», потому что два других не будут компилироваться! Медитируй об этом на секунду.

Позвольте мне сказать это по-другому, ничто не мешает нам использовать метод « itemize» нашего TimeTrackDAO, если мы не определили какой- либо тип-класс DateFoo в системе. Только когда мы попытаемся использовать методы checkin или checkout с типом, в котором отсутствует DateFoo , наш код не сможет скомпилироваться. Это наш переключатель времени компиляции, упомянутый во вводном параграфе. Классы типов с неявным разрешением позволяют включать или отключать функциональность классов / признаков во время компиляции на основе параметра типа и правил области видимости.

Вывод

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

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

Используя шаблон адаптера с классами типов, вдохновленными FP, мы показали, как сократить шаблон, открыть код для расширения и наложить ограничения на время компиляции. Не было никаких «волшебных» или трудных для отслеживания проблем, подобных тем, которые возникают в результате неявных преобразований или библиотек в стиле DI. Классы типов, использующие шаблон адаптера, являются преднамеренными и явными в использовании, с четко определенным разрешением области действия, которое применяется во время компиляции. Они представляют собой идеальное сочетание принципов OO и FP.

Ссылка: « Банда четырех шаблонов с классами типов и последствиями в Scala» (часть 2) от нашего партнера JCG Овейна Риза в блоге со статической типизацией .