Javascript интерфейс — основы

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

Простой JavaScript: знакомимся с mocking, stubbing и интерфейсами

При проверке кода на правильность можно воспользоваться самописными библиотеками, которые имеют несколько существенных минусов – не отличаются быстродействием, и в то же время сложностью моделирования реальной файловой системы. Но можно обратиться к библиотекам Proxyquire и Sinon, с помощью которых можно перераспределять файлы и мутировать методы. Желательно применять при тестировании сразу 2 обозначенных инструмента, поскольку между ними существуют некоторые отличия.

Допустим, имеется модуль A, импортирующий модуль B. Proxyquire после импорта модуля A перераспределяет модуль B. Остальные части программы не затрагиваются после операции импорта. А экспорт модуля B в случае с Sinon оказывает воздействие полностью на весь код – об этом также не нужно забывать.

Почему Stubs – это плохо?

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

Существует ещё один нежелательный момент – наличие блокировки. После замены библиотеки fs на fs-extra-promise, как Sinon, так и Proxyquire потребуют обновления тестов и будут пытаться переопределить fs.readFile.

Простой JavaScript: возможные альтернативы

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

import fs from 'fs'
import { promisify } from 'util'
const readFileAsync = promisify(fs.readFile)
export function readJsonFile (filePath) {
return readFileAsync(filePath).then(JSON.parse)
}
 
import fs from 'fs'
import test from 'ava';
import { stub } from 'sinon'
import proxyquire from 'proxyquire'
 
test('readJsonFile with proxyquire', async function (t) {
  t.plan(2)
  
  const { readJsonFile } = proxyquire('./foo.js', {
    fs: {
      readFile(filePath, callback) {
        t.is(filePath, 'myTestFile')
        
        return callback(null, '{ success: true }')
      }
    }
  })
  
  const results = await readJsonFile('myTestFile')
  t.deepEqual(results, { success: true })
})
 
test('readJsonFile with sinon', async function (t) {
  t.plan(1)
  
  const fsStub = stub(fs, 'readFile')
    .withArgs('myTestFile')
    .callsArg(2, null, '{ success: true }')
  
  const results = await readJsonFile('myTestFile')
  t.deepEqual(results, { success: true })
  fsStub.restore()
})

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

Путь зависимости

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

JavaScript

export default function ({ readFileAsync }) {
  return {
    readJsonFile (filePath) {
     return readFileAsync(filePath).then(JSON.parse)
    }
  }
}

import test from 'ava'

import foo from './foo'

test('foo with dependency inversion', function (t) {
  t.plan(2)
  
  const dependencies = {
    readFileAsync(filePath) {
      t.is(filePath, 'bar')
      
      return Promise.resolve('{ success: true '})
    }
  }
  
  const result = await foo(dependencies).readJsonFile('bar')
  t.deepEqual(result, { success: true })
})




export default function ({ readFileAsync }) {
  return {
    readJsonFile (filePath) {
     return readFileAsync(filePath).then(JSON.parse)
    }
  }
}
 
import test from 'ava'
 
import foo from './foo'
 
test('foo with dependency inversion', function (t) {
  t.plan(2)
  
  const dependencies = {
    readFileAsync(filePath) {
      t.is(filePath, 'bar')
      
      return Promise.resolve('{ success: true '})
    }
  }
  
  const result = await foo(dependencies).readJsonFile('bar')
  t.deepEqual(result, { success: true })
})

Здесь приводятся зависимости, расположенные в точке входа программы. Всё, за исключением index.js, лежит в web-интерфейсе JavaScript. Это даёт возможность программе легко изменяться при необходимости, прибавляет ей гибкости и позволяет без проблем её тестировать.

Какие ещё возможности даёт обращение зависимостей?

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

В качестве примера такого интерфейса выступает компонент React, в TypeScript выглядящий следующим образом:

interface ComponentLifecycle {
        constructor(props: Object);
        componentDidMount?(): void;
        shouldComponentUpdate?(nextProps: Object, nextState: Object, nextContext: any): boolean;
        componentWillUnmount?(): void;
        componentDidCatch?(error: Error, errorInfo: ErrorInfo): void;
        setState(
            state: ((prevState: Object, props: Object) => Object,
            callback?: () => void
        ): void;
        render(): Object | null;
        state: Object;
    }
interface ComponentLifecycle {
        constructor(props: Object);
        componentDidMount?(): void;
        shouldComponentUpdate?(nextProps: Object, nextState: Object, nextContext: any): boolean;
        componentWillUnmount?(): void;
        componentDidCatch?(error: Error, errorInfo: ErrorInfo): void;
        setState(
            state: ((prevState: Object, props: Object) => Object,
            callback?: () => void
        ): void;
        render(): Object | null;
        state: Object;
    }

Данный компонент содержит базовый набор свойств и методов, которые уже можно применить при написании множества других компонентов.

Рассмотрим формулу «открытости-закрытости». В ней упоминается, что наше ПО возможно дополнять и расширять, но запрещено модифицировать. Процедура может быть знакома тем, кто писал ПО на Angular либо React. Они предоставляют базовый интерфейс, который можно разнообразить при написании программ. Иногда удобнее пользоваться написанными собственноручно интерфейсами, нежели обращаться к уже готовым.

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

Если предполагается, что создаваемое приложение должно управлять задачами, логично будет создать интерфейс с базовым функционалом. Каждая задача сможет тогда применять данный интерфейс, либо расширять его. Благодаря инверсии зависимостей и принципу «открытости-закрытости» мы имеем возможность создавать многоразовый и легко тестируемый софт. В Яваскрипте тогда не будет лишнего кода и модули будут работать слаженно по единому отработанному шаблону. Таким образом, разработка интерфейсов, вёрстка и Javascript приобретают более высокие показатели читаемости и качества.

Множественная реализация

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

По схожему принципу функционируют React и React-Native – в них обоих задействован одинаковый компонент и интерфейсы, различается только его реализация. React Native способен работать как с IOS, так и с Android. Разнообразие реализаций даёт возможность единожды написанной логике исполняться различными способами.

Заключение

После того, как вы ознакомились с возможностями простого Яваскрипта работать с инверсией зависимостей и принципом «открытости-закрытости», можно применить полученные знания при создании кода.

  • При создании нового проекта не осуществляйте импорт – оставьте эту задачу интерфейсу;
  • Сторонитесь внешних библиотек, способных видоизменять ваши зависимости;
  • Используйте базовые интерфейсы;

Это способствует хоть и медленному, но верному созданию собственных интерфейсов. Good Luck!



Теги:
0

Оставить своё мнение

Ваш e-mail не будет опубликован. Обязательные поля помечены *