Разработка интерфейсов на javascript

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

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

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

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

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

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

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

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

Читайте также:  Kafka admin client java

Существует ещё один нежелательный момент – наличие блокировки. После замены библиотеки 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!

Источник

Implementing Interfaces in JavaScript

text

Object interfaces allow you to create code that specifies which methods a class must implement, without having to define how these methods are implemented. Interfaces share a namespace with classes and traits, so they may not use the same name.

In practice, interfaces serve two complementary purposes:

  • To allow developers to create objects of different classes that may be used interchangeably because they implement the same interface or interfaces. A common example is multiple database access services, multiple payment gateways, or different caching strategies. Different implementations may be swapped out without requiring any changes to the code that uses them.
  • To allow a function or method to accept and operate on a parameter that conforms to an interface, while not caring what else the object may do or how it is implemented. These interfaces are often named like Iterable , Cacheable , Renderable , or so on to describe the significance of the behavior.

Does JavaScript have interfaces?

There’s no notion of “this class must have these functions” (that is, no interfaces per se), because:

  1. JavaScript inheritance is based on objects, not classes. That’s not a big deal until you realize:
  2. JavaScript is an extremely dynamically typed language — you can create an object with the proper methods, which would make it conform to the interface, and then undefine all the stuff that made it conform. It’d be so easy to subvert the type system — even accidentally! — that it wouldn’t be worth it to try and make a type system in the first place.

Instead, JavaScript uses what’s called duck typing. (If it walks like a duck, and quacks like a duck, as far as JS cares, it’s a duck.) If your object has quack() , walk() , and fly() methods, code can use it wherever it expects an object that can walk() , quack() , and fly() , without requiring the implementation of some “Duckable” interface. The interface is exactly the set of functions that the code uses (and the return values from those functions), and with duck typing, you get that for free.

Note: JavaScript has the reserved word interface in case they ever would like to implement interfaces

Is Duck Typing enough to replace interfaces all together?

In my personal opinion, no.

Recently, in order to increase my practical JavaScript knowledge, I have been working on both JavaScript and TypeScript projects. The lack of interfaces made it difficult to implement things in JavaScript that I had architected with PHP in mind. I also began, as a practice project, to try to clone certain small PHP packages in JavaScript but regularly ran into the issue that JavaScript lacks interfaces where PHP does not.

This is when I came up with an idea to create my own “interface” in JavaScript.

Creating “interfaces” in JavaScript

The first thing I did was to create a new class with Interface as the suffix in the class name. This will help you differentiate what is an implementation and what is an interface when looking through files. In this case, let’s call our class MyInterface

module.exports = class MyInterface

I then created all the methods that I wished the implementers to implement but created them as empty methods.

module.exports = class MyInterface < /** * @param var1 * @param var2 * * @return */ firstMethod (var1, var2) <> /** * @return */ secondMethod () <> /** * @param var1 */ thirdMethod (var1) <> >

The next step is to “implement” the interface. What we are really doing is extending the MyInterface class, but if we try to use the methods of the implementation class and we do not implement them, it will cause issues because there will be no actions occurring on the method calls.

const MyInterface = require('./MyInterface') module.exports = class MyImplementation extends MyInterface < myVar constructor (theVar) < this.myVar = theVar >/** * @inheritDoc */ firstMethod (var1, var2) < return var1 + var2 >/** * @inheritDoc */ secondMethod () < return 'secondMethod' >/** * @inheritDoc */ thirdMethod (var1) < this.myVar = var1 >>

I know this isn’t a perfect solution, but I see it as good enough for solving the design implementation problems. This will allow you to also set the return type to the interface even when you are implementing. It will also not let you try to instantiate the interface class as there is no constructor defined.

/** * myObject */ const myFunction = () =>

I am perfectly aware that there are many ways to work around this and subvert the type system. I only wrote this post so that anyone who wanted to try to introduce interfaces into their JavaScript designs could have a way to do so.

Источник

Оцените статью