Реализация шаблона Builder во Vue.js, часть 1: Списки
Перевод: Markus Oberlehner — Implementing the Builder Pattern in Vue.js Part 1: Listings
Недавно я увидел отличный доклад Джейкоба Шаца Phenomenal Design Patterns in Vue. Одним из паттернов, которые он упомянул в своем выступлении, был паттерн Строителя (Builder). Я нашел его пример очень интересным, поэтому мне было ясно, что я должен сам поэкспериментировать с этим шаблоном.
В этой статье мы рассмотрим возможную реализацию шаблона Builder, специально адаптированного к тому, как я обычно структурирую свои приложения Vue.js. В первой части этой статьи состоящей из двух частей я использую этот шаблон для создания многоразовых представлений списков (представьте себе приложение с множеством списочных представлений для множества различных типов контента), а во второй части я рассмотрю возможное решение для создания множества форм с использованием этого подхода.
Шаблон Builder с компонентами Vue.js
Шаблон строителя (Builder) — это шаблон создания (Creational) в объектно-ориентированном программировании. «Creational» означает, что он обычно используется для упрощения процесса создания новых объектов. В приложениях Vue.js все строится на основе компонентов, и в нашем случае мы хотим, чтобы класс строителя создавал для нас новый компонент (так как на самом деле компонент не что иное, как объект).
Создание списочных представлений с помощью класса строителя
Прежде чем мы посмотрим на код, давайте начнем с определения проблемы, которую мы хотим решить. Подумайте о следующем сценарии: мы должны создать приложение, которое будет использоваться для управления множеством различных типов контента (например, пользователями, продуктами, сообщениями, статьями, тегами и т. д.). Для каждого из этих типов контента нам нужен список. Некоторые типы контента лучше подходят для табличных представлений, а другие должны отображаться в виде сетки. Опять же, нужна система фильтров, а большинству также нужна навигация по страницам.
В следующем фрагменте кода вы можете увидеть возможное решение для класса ListingBuilder, которое можно использовать для создания всех этих различных вариантов представлений списков для нас.
// src/builders/ListingBuilder.js import EntityListing from '../components/EntityListing.vue'; export default class ListingBuilder { constructor() { this.props = {}; } withProvider(provider) { this.provider = provider; return this; } withListingItem(item) { this.item = item; return this; } showFilter() { this.props.showFilter = true; return this; } showPagination() { this.props.showPagination = true; return this; } view(view) { this.props.view = view; return this; } build() { const Provider = this.provider; const Item = this.item; const props = this.props; return { render(h) { return h(Provider, [ h( EntityListing, { props, scopedSlots: { default: props => h(Item, { props }) }, }, [Item], ), ]); }, }; }, };
Как вы можете видеть выше, в нашем ListingBuilder есть несколько методов, которые мы можем использовать для управления тем, какой вид листингового представления будет создан для нас, как только мы вызовем метод build ().
<template> <div id="app"> <h2>Product Listing</h2> <ProductListing/> <h2>User Listing</h2> <UserListing/> </div> </template> <script> // src/App.vue import ListingBuilder from './builders/ListingBuilder'; import ProductListingItem from './components/ProductListingItem.vue'; import ProductProvider from './components/ProductProvider.vue'; import UserListingItem from './components/UserListingItem.vue'; import UserProvider from './components/UserProvider.vue'; export default { name: 'App', components: { ProductListing: new ListingBuilder() .withProvider(ProductProvider) .withListingItem(ProductListingItem) .showFilter() .showPagination() .view('grid') .build(), UserListing: new ListingBuilder() .withProvider(UserProvider) .withListingItem(UserListingItem) .showPagination() .view('table') .build(), }, }; </script>
В этом примере вы можете увидеть, как мы можем использовать класс ListingBuilder для быстрого создания двух разных списочных компонентов. Компонент ProductListing имеет собственные реализации ProductProvider и ProductListingItem, имеет фильтры и разбивку на страницы. Кроме того, он использует сетку общего компонента EntityListing. Компонент UserListing, с другой стороны, использует немного другую конфигурацию.
В следующем фрагменте кода вы можете увидеть, как мы могли бы достичь того же результата, используя вместо этого отдельный компонент ProductListing.
<template> <ProductProvider> <EntityListing view="grid" show-filter show-pagination v-slot="{ entity }" > <ProductListingItem :entity="entity"/> </EntityListing> </ProductProvider> </template> <script> // src/components/ProductListing.vue import EntityListing from './EntityListing.vue'; import ProductListingItem from './ProductListingItem.vue'; import ProductProvider from './ProductProvider.vue'; export default { name: 'ProductListing', components: { EntityListing, ProductListingItem, ProductProvider, }, }; </script>
Результат кода, который вы видите выше, точно такой же, как и при использовании нашего класса ListingBuilder для создания компонента ProductListing. Если вам интересен остальной код, вы можете взглянуть на следующий CodeSandbox.
Улучшение возможности повторного использования с помощью шаблона Director
Хотя мы увидели, что мы можем уменьшить большую часть работы на начальном этапе, позволив классу ListingBuilder выполнять тяжелую работу, этот шаблон может привести к увеличению объема кода в конце (в момент использования), если нам придется повторно использовать компонент ProductListing в нескольких местах. С отдельным компонентом мы можем просто импортировать и повторно использовать компонент, но когда мы используем ListingBuilder, мы должны повторять логику инициализации снова и снова везде, где нам нужен компонент.
Решение этой проблемы: шаблон Director.
// src/builders/ListingDirector.js import ProductListingItem from '../components/ProductListingItem.vue'; import ProductProvider from '../components/ProductProvider.vue'; import UserListingItem from '../components/UserListingItem.vue'; import UserProvider from '../components/UserProvider.vue'; export default class ListingDirector { constructor(builder) { this.builder = builder; } makeProductListing() { return this.builder .withProvider(ProductProvider) .withListingItem(ProductListingItem) .showFilter() .showPagination() .view('grid') .build(); } makeUserListing() { return this.builder .withProvider(UserProvider) .withListingItem(UserListingItem) .showPagination() .view('table') .build(); } }
<script> // src/App.vue import ListingBuilder from './builders/ListingBuilder'; import ListingDirector from './builders/ListingDirector'; export default { name: 'App', components: { ProductListing: new ListingDirector( new ListingBuilder() ).makeProductListing(), UserListing: new ListingDirector( new ListingBuilder() ).makeUserListing(), }, }; </script>
Выше вы можете увидеть, как мы можем использовать ListingDirector для повторного использования существующих конфигураций компонентов списка.
Заключение
Хотя я определенно вижу сильные стороны этого шаблона, он ощущается как инородное тело в типичном приложении Vue.js. Я не думаю, что это тот шаблон, который вы будете часто использовать в каждом приложении, которое вы создаете, а скорее шаблон, который вы используете только в очень определенных ситуациях.
Но это была только первая часть этой серии из двух частей. Прочитайте вторую часть этой серии, чтобы узнать больше о том, как использовать шаблон Builder для создания переиспользуемых форм.