टाइपस्क्रिप्ट के साथ शुद्ध वास्तुकला: डीडीडी और स्तरित वास्तुकला

हेलो, हेब्र! हाल ही में मैं वास्तुकला पर अधिक ध्यान दे रहा हूं और समुदाय के साथ लेख का अनुवाद ट्रांसफॉर्मेशन के साथ क्लीन आर्किटेक्चर: डीडीडी, प्याज और आंद्रे बाजाग्लिया के साथ साझा करने का फैसला किया है

परिचय


मेरे पेशेवर अनुभव के 6 वर्षों के लिए, मुझे शांत प्रौद्योगिकी कंपनियों में काम करने का अवसर मिला है जो उच्च उपलब्धता और कोड गुणवत्ता पर बहुत ध्यान देते हैं। मुझे महत्वपूर्ण परिस्थितियों से निपटना पड़ा जब बग या सिस्टम का एक दूसरा डाउनटाइम भी अस्वीकार्य था।

इस लेख का उद्देश्य डीडीडी और स्तरित वास्तुकला पर जटिल विषयों का एक विस्तृत कवरेज नहीं है, लेकिन टाइपस्क्रिप्ट में इन दोनों दृष्टिकोणों के कार्यान्वयन का एक उदाहरण है। उपयोग की गई परियोजना बुनियादी है और परिष्कृत और विस्तारित की जा सकती है, उदाहरण के लिए, CQRS दृष्टिकोण का उपयोग करके

DDD क्यों?


निर्मित सॉफ्टवेयर उत्पादों को व्यवसाय की आवश्यकताओं को लागू करना चाहिए।

यह दृष्टिकोण डोमेन परत के परीक्षण को सरल करता है, जो आपको यह सुनिश्चित करने की अनुमति देता है कि सभी व्यावसायिक आवश्यकताओं को ध्यान में रखा जाए और लंबे समय तक त्रुटि वाले प्रूफ कोड लिखे जाएं।
एक स्तरित वास्तुकला के साथ संयोजन में DDD डोमिनोज़ प्रभाव से बचा जाता है जब एक स्थान पर कोड बदलने से विभिन्न स्थानों में असंख्य कीड़े हो जाते हैं।

क्यों स्तरित वास्तुकला?


यह दृष्टिकोण DDD के साथ अच्छी तरह से चला जाता है, क्योंकि पूरे सिस्टम को एक डोमेन मॉडल के आसपास बनाया गया है, जो नीचे की छवि में केंद्र सर्कल है। आंतरिक परतें सीधे बाहरी लोगों पर निर्भर नहीं होती हैं और इंजेक्शन द्वारा उनका उपयोग करती हैं। साथ ही, हमें उत्कृष्ट लचीलापन मिलता है, इसलिए प्रत्येक परत की अपनी जिम्मेदारी का क्षेत्र होता है और आने वाले डेटा को प्रत्येक परत द्वारा अपनी आवश्यकताओं के अनुसार मान्य किया जाता है। और इसलिए, आंतरिक परतें हमेशा बाहरी परतों से वैध डेटा प्राप्त करती हैं। परीक्षण का उल्लेख नहीं करने के लिए: इकाई परीक्षण इंटरफेस के आधार पर निर्भरता नकली के उपयोग के साथ आसान हो जाता है, जो आपको सिस्टम के बाहरी हिस्सों जैसे डेटाबेस से अमूर्त करने की अनुमति देता है।



टाइपस्क्रिप्ट और जावास्क्रिप्ट के मामले में, नियंत्रण का उलटा (निर्भरता उलटा के माध्यम से)स्पष्ट आयात के बजाय मापदंडों के माध्यम से निर्भरता (गुजर) को एम्बेड करना। निम्नलिखित कोड उदाहरण में, हम इन्वर्साइज़ लाइब्रेरी का उपयोग करेंगे , जो हमें डेकोरेटर्स का उपयोग करके निर्भरता का वर्णन करने की अनुमति देता है ताकि बाद में बनाई गई कक्षाएं गतिशील रूप से निर्भरता को हल करने के लिए कंटेनर बना सकें।

आर्किटेक्चर


इस सरल आवेदन के लिए ...


हम एक साधारण शॉपिंग कार्ट एप्लिकेशन बनाएंगे जो कार्ट में आइटम जोड़ने का काम कर सके। एक कार्ट में कई व्यावसायिक नियम हो सकते हैं, जैसे कि प्रत्येक आइटम के लिए न्यूनतम या अधिकतम मात्रा।

आइए आवेदन परतों की गहराई में गोता लगाएँ, अंतरतम परत से शुरू करें और क्रमिक रूप से आगे बढ़ें।

डोमेन


डोमेन परत, परिभाषा के अनुसार, व्यावसायिक नियमों का निवास स्थान है। यह किसी भी बाहरी परतों के बारे में कुछ नहीं जानता है, इसकी कोई निर्भरता नहीं है और आसानी से परीक्षण किया जा सकता है। यहां तक ​​कि अगर आप पूरे एप्लिकेशन को चारों ओर बदलते हैं, तो भी डोमेन अछूता रहेगा, क्योंकि इसमें केवल व्यावसायिक नियम हैं जो इस कोड को पढ़ने वाले सभी के लिए उनके उद्देश्य को समझने के लिए उपलब्ध हैं। इस परत को परीक्षणों से यथासंभव कवर किया जाना चाहिए।

इसलिए, हमारे पास एक इकाई आधार वर्ग है, जिसमें सभी डोमेन वर्गों के लिए कुछ निश्चित तर्क समाहित हो सकते हैं। इस उदाहरण में, सभी सामान्य तर्क क्षैतिज स्केलिंग के लिए उपयुक्त टकराव-प्रतिरोधी पहचानकर्ता उत्पन्न करना है, और सभी ऑब्जेक्ट इस दृष्टिकोण का उपयोग करेंगे।

export abstract class Entity<T> {
  protected readonly _id: string
  protected props: T

  constructor(props: T, id?: string) {
    this._id = id ? id : UniqueEntityID()
    this.props = props
  }

  // other common methods here...
}

इकाई वर्ग लिखने के बाद, आप हमारे केंद्रीय डोमेन वर्ग को बनाना शुरू कर सकते हैं जो इकाई सार वर्ग का विस्तार करता है।

इस वर्ग में बहुत जटिल कुछ भी नहीं है, लेकिन कुछ दिलचस्प बिंदु हैं जिन पर आपको ध्यान देना चाहिए।

सबसे पहले, कंस्ट्रक्टर निजी है, जिसका अर्थ है कि नई कार्ट () को निष्पादित करने से त्रुटि होगी, जो कि हमें चाहिए। DDD में, डोमेन ऑब्जेक्ट को हमेशा एक मान्य स्थिति में रखना अच्छा अभ्यास माना जाता है। खाली कार्ट ऑब्जेक्ट बनाने के बजाय, हम फ़ैक्टरी पैटर्न का उपयोग करते हैंजो कार्ट क्लास का तैयार उदाहरण देता है। यह सुनिश्चित करने के लिए कि निर्माण प्रक्रिया को सभी आवश्यक विशेषताएँ प्राप्त हुई हैं, उन्हें मान्य किया जा सकता है। इसी तरह, डोमेन के साथ बातचीत करने के लिए गेटर्स और सेटर का उपयोग किया जाता है, इसी कारण से कक्षा की आंतरिक स्थिति को एक निजी समर्थक ऑब्जेक्ट में संग्रहीत किया जाता है । गेटर्स उन विशेषताओं को रीड एक्सेस प्रदान करते हैं जो सार्वजनिक होनी चाहिए। इसी तरह, सार्वजनिक बसने वाले और अन्य तरीके आपको ऑब्जेक्ट की स्थिति की वैधता की निरंतर गारंटी के साथ डोमेन को बदलने की अनुमति देते हैं।

संक्षेप में, हम एक महत्वपूर्ण बिंदु पर जोर दे सकते हैं: डोमेन लॉजिक को व्यवहार पर ध्यान केंद्रित किया जाना चाहिए, न कि गुणों पर।

export class Cart extends Entity<ICartProps> {
  private constructor({ id, ...data }: ICartProps) {
    super(data, id)
  }

  public static create(props: ICartProps): Cart {
    const instance = new Cart(props)
    return instance
  }

  public unmarshal(): UnmarshalledCart {
    return {
      id: this.id,
      products: this.products.map(product => ({
        item: product.item.unmarshal(),
        quantity: product.quantity
      })),
      totalPrice: this.totalPrice
    }
  }

  private static validQuantity(quantity: number) {
    return quantity >= 1 && quantity <= 1000
  }

  private setProducts(products: CartItem[]) {
    this.props.products = products
  }

  get id(): string {
    return this._id
  }

  get products(): CartItem[] {
    return this.props.products
  }

  get totalPrice(): number {
    const cartSum = (acc: number, product: CartItem) => {
      return acc + product.item.price * product.quantity
    }

    return this.products.reduce(cartSum, 0)
  }

  public add(item: Item, quantity: number) {
    if (!Cart.validQuantity(quantity)) {
      throw new ValidationError(
        'SKU needs to have a quantity between 1 and 1000'
      )
    }

    const index = this.products.findIndex(
      product => product.item.sku === item.sku
    )

    if (index > -1) {
      const product = {
        ...this.products[index],
        quantity: this.products[index].quantity + quantity
      }

      if (!Cart.validQuantity(product.quantity)) {
        throw new ValidationError('SKU exceeded allowed quantity')
      }

      const products = [
        ...this.products.slice(0, index),
        product,
        ...this.products.slice(index + 1)
      ]

      return this.setProducts(products)
    }

    const products = [...this.products, { item, quantity }]
    this.setProducts(products)
  }

  public remove(itemId: string) {
    const products = this.products.filter(product => product.item.id !== itemId)
    this.setProducts(products)
    this.emitCartMutation()
  }

  public empty() {
    this.setProducts([])
  }
}

जबकि हमारी कार्ट ऑब्जेक्ट को डोमेन विधियों का उपयोग करके एप्लिकेशन द्वारा उपयोग किया जा सकता है, कुछ मामलों में, हमें इसे एक साफ ऑब्जेक्ट में "तैनात" करने की आवश्यकता हो सकती है। उदाहरण के लिए, डेटाबेस को बचाने या क्लाइंट को JSON ऑब्जेक्ट के रूप में भेजने के लिए। इसे अनमरशाल () पद्धति का उपयोग करके लागू किया जा सकता है

वास्तुकला के लचीलेपन को बढ़ाने के लिए, डोमेन परत भी डोमेन घटनाओं का एक स्रोत बन सकती है। यहां, ईवेंट सोर्सिंग दृष्टिकोण लागू किया जा सकता है , जब ईवेंट्स के परिवर्तन के साथ डोमेन एंटिटीज़ बदलते हैं।

उपयोगकर्ता स्क्रिप्ट


यहां हम डेटा स्टोर करने के लिए बुनियादी ढांचे के स्तर से लागू डोमेन विधियों और वस्तुओं का उपयोग करेंगे।

हम व्युत्क्रम लाइब्रेरी का उपयोग प्रबंधन के दृष्टिकोण के व्युत्क्रम को लागू करने के लिए करते हैं, जो इस परिदृश्य में इन्फ्रास्ट्रक्चर लेयर से रिपॉजिटरी को इंजेक्ट करता है, जो हमें डोमेन विधियों का उपयोग करके टोकरी में हेरफेर करने की क्षमता प्रदान करता है और उसके बाद डेटाबेस में परिवर्तन को बचाता है।

import { inject, injectable } from 'inversify'

@injectable()
export class CartService {
  @inject(TYPES.CartRepository) private repository: CartRepository

  private async _getCart(id: string): Promise<Cart> {
    try {
      const cart = await this.repository.getById(id)
      return cart
    } catch (e) {
      const emptyCart = Cart.create({ id, products: [] })
      return this.repository.create(emptyCart)
    }
  }

  public getById(id: string): Promise<Cart> {
    return this.repository.getById(id)
  }

  public async add(cartId: string, item: Item, sku: number): Promise<Cart> {
    const cart = await this._getCart(cartId)
    cart.add(item, sku)

    return this.repository.update(cart)
  }

  public async remove(cartId: string, itemId: string): Promise<Cart> {
    const cart = await this._getCart(cartId)
    cart.remove(itemId)

    return this.repository.update(cart)
  }
}

यह परत एप्लिकेशन के संचालन के लिए जिम्मेदार है। इस परत में कोड परिवर्तन डोमेन संस्थाओं या डेटाबेस जैसी बाहरी निर्भरता को प्रभावित नहीं करते हैं।

भूमिकारूप व्यवस्था


सभी स्पष्टता के बावजूद, बुनियादी ढांचे की परत बाहरी प्रणालियों के साथ सहभागिता करती है, जैसे कि डेटाबेस।

डेटाबेस में डेटा को बचाने के लिए, मैं डेटा मैपर और रिपॉजिटरी दृष्टिकोण का उपयोग करता हूं।

Mapper डेटाबेस से स्रोत डेटा प्राप्त कर सकता है और इसे संबंधित डोमेन ऑब्जेक्ट में बदल सकता है:

import { Cart, CartItem } from 'src/domain/cart'

const getProducts = (products: CartItem[]) => {
  return products.map(product => ({
    item: product.item,
    quantity: product.quantity
  }))
}

export class CartMapper {
  public static toDomain(raw: any): Cart {
    return Cart.create({
      id: raw.id,
      couponCode: raw.couponCode,
      products: getProducts(raw.products || [])
    })
  }
}

रिपॉजिटरी स्वयं किसी विशेष डेटाबेस के क्लाइंट लाइब्रेरी पर निर्भर हो सकती है। उदाहरण के लिए, रैम में स्टोरेज से, और डेटा को प्रबंधित करने के लिए इन विधियों का उपयोग करें:

import { injectable, inject } from 'inversify'
import { Cart } from 'src/domain/cart'
import { CartMapper } from '../mappers/cart'

interface ICartRepository {
  getById(id: string): Promise<Cart>
  create(cart: Cart): Promise<Cart>
  update(cart: Cart): Promise<Cart>
}

@injectable()
export class CartRepository implements ICartRepository {
  @inject(TYPES.Database) private _database: MemoryData

  async getById(id: string): Promise<Cart> {
    const cart = await this._database.cart.getById(id)
    if (!cart) {
      throw new ResourceNotFound('Cart', { id })
    }
    return CartMapper.toDomain(cart)
  }

  async create(cart: Cart): Promise<Cart> {
    const dtoCart = cart.unmarshal()
    const inserted = await this._database.cart.insert(dtoCart)
    return CartMapper.toDomain(inserted)
  }

  async update(cart: Cart): Promise<Cart> {
    const dtoCart = cart.unmarshal()
    const updated = await this._database.cart.update(cart.id, dtoCart)
    return CartMapper.toDomain(updated)
  }
}

निष्कर्ष


सुधार और सुधार के लिए बहुत जगह है। इस दृश्य प्रदर्शन के लिए कोड बनाया गया था, और स्तरित वास्तुकला में पहला सुधार डोमेन परत में रिपॉजिटरी इंटरफ़ेस की घोषणा और बुनियादी ढांचे की परत में इसके कार्यान्वयन की घोषणा हो सकती है, जिसे हम अगली बार चर्चा कर सकते हैं।

स्रोत कोड github पर उपलब्ध है

Source: https://habr.com/ru/post/undefined/


All Articles