العمارة النقية مع الطباعة الطباعية: DDD والعمارة الطبقية

مرحبا يا هابر! لقد قمت مؤخرًا بإيلاء اهتمام كبير للهندسة المعمارية وقررت أن أطلع المجتمع على ترجمة للمقال Clean Architecture with Typescript: DDD، Onion by André Bazaglia .

المقدمة


لأكثر من 6 سنوات من خبرتي المهنية ، أتيحت لي الفرصة للعمل في شركات التكنولوجيا الرائعة التي تولي الكثير من الاهتمام للتوافر العالي وجودة الشفرة. اضطررت للتعامل مع المواقف الحرجة عندما كانت الأخطاء أو حتى فترة تعطل ثانية من النظام غير مقبولة.

الغرض من هذه المقالة ليس تغطية تفصيلية للمواضيع المعقدة على DDD والهندسة المعمارية ذات الطبقات ، ولكن مثال على تنفيذ هذين النهجين في الطباع. المشروع المستخدم أساسي ويمكن تنقيحه وتوسيعه ، على سبيل المثال ، باستخدام نهج CQRS .

لماذا DDD؟


يجب أن تنفذ منتجات البرمجيات التي تم إنشاؤها متطلبات الأعمال المحددة.

يبسط هذا النهج اختبار طبقة المجال ، مما يسمح لك بالتأكد من أن جميع متطلبات العمل تؤخذ في الاعتبار وكتابة كود طويل الأمد مقاوم للأخطاء.
DDD بالاشتراك مع بنية ذات طبقات تتجنب تأثير الدومينو عند تغيير الرمز في مكان واحد يؤدي إلى أخطاء لا حصر لها في أماكن مختلفة.

لماذا الهندسة المعمارية الطبقات؟


يتماشى هذا النهج بشكل جيد مع DDD ، حيث يتم بناء النظام بأكمله حول نموذج المجال ، وهو الدائرة المركزية في الصورة أدناه. لا تعتمد الطبقات الداخلية بشكل مباشر على الطبقات الخارجية وتستخدمها عن طريق الحقن. بالإضافة إلى ذلك ، نحصل على مرونة ممتازة ، بحيث يكون لكل طبقة منطقة مسؤوليتها الخاصة ويتم التحقق من البيانات الواردة من قبل كل طبقة وفقًا لاحتياجاتها. وبالتالي ، تتلقى الطبقات الداخلية دائمًا بيانات صالحة من الطبقات الخارجية. ناهيك عن الاختبار: يصبح اختبار الوحدة أبسط باستخدام حشود التبعية على أساس الواجهات ، مما يسمح لك بالتجريد من الأجزاء الخارجية للنظام ، مثل قواعد البيانات.



في حالة Typescript و Javascript ، انقلاب التحكم (عبر انقلاب التبعية)يعني تضمين (تمرير) التبعيات من خلال المعلمات بدلاً من الاستيراد الصريح. في مثال الكود التالي ، سنستخدم مكتبة Inversify ، التي تسمح لنا بوصف التبعيات باستخدام أدوات الديكور بحيث يمكن للفئات التي تم إنشاؤها لاحقًا إنشاء حاويات بشكل ديناميكي لحل التبعيات.

هندسة معمارية


لهذا التطبيق البسيط ...


سنقوم بإنشاء تطبيق عربة تسوق بسيط يمكنه التعامل مع إضافة المنتجات إلى سلة التسوق. قد تحتوي سلة التسوق على عدة قواعد عمل ، مثل الحد الأدنى أو الحد الأقصى من الكمية لكل عنصر.

دعونا نتعمق في طبقات التطبيق ، نبدأ بالطبقة الأعمق ونتحرك إلى الخارج بالتسلسل.

نطاق


طبقة المجال ، بحكم تعريفها ، هي موطن قواعد العمل. لا يعرف شيئًا عن أي طبقات خارجية ، وليس له تبعيات ويمكن اختباره بسهولة. حتى إذا قمت بتغيير التطبيق بأكمله حوله ، فسيظل المجال دون تغيير ، لأنه يحتوي فقط على قواعد العمل التي يمكن الوصول إليها لفهم الغرض منها لكل من يقرأ هذا الرمز. يجب تغطية هذه الطبقة قدر الإمكان بالاختبارات.

لذلك ، لدينا فئة أساسية لكيان ، والتي يمكن أن تحتوي على منطق معين مشترك لجميع فئات المجال. في هذا المثال ، كل المنطق الشائع هو إنشاء معرف مقاوم للتصادم مناسب للتحجيم الأفقي ، وستستخدم جميع الكائنات هذا النهج.

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...
}

بعد كتابة فئة الكيان ، يمكنك البدء في إنشاء فئة مجالنا المركزية التي تمتد إلى فئة خلاصة الكيان.

لا يوجد شيء معقد للغاية في هذا الفصل ، ولكن هناك بعض النقاط المثيرة للاهتمام التي يجب الانتباه إليها.

أولاً ، المُنشئ خاص ، مما يعني أن تنفيذ Cart () جديد سيؤدي إلى حدوث خطأ ، وهو ما نحتاجه. في 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. يمكن تنفيذ ذلك باستخدام طريقة unmarshal () .

لزيادة مرونة البنية ، يمكن أن تصبح طبقة المجال أيضًا مصدرًا لأحداث المجال. هنا ، يمكن تطبيق نهج مصادر الأحداث ، مع إنشاء الأحداث عندما تتغير كيانات المجال.

البرامج النصية للمستخدم


سنستخدم هنا أساليب المجال والكائنات المنفذة من مستوى البنية التحتية لتخزين البيانات.

نستخدم المكتبة العكسية لتطبيق نهج انعكاس الإدارة ، الذي يضخ المستودع من طبقة البنية التحتية في هذا السيناريو ، مما يمنحنا الفرصة للتعامل مع السلة باستخدام طرق النطاق وحفظ التغييرات في قاعدة البيانات بعد ذلك.

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)
  }
}

هذه الطبقة مسؤولة عن تشغيل التطبيق. لا تؤثر تغييرات التعليمات البرمجية لهذه الطبقة على كيانات المجال أو التبعيات الخارجية مثل قاعدة البيانات.

بنية تحتية


على الرغم من كل الوضوح ، تتفاعل طبقة البنية التحتية مع الأنظمة الخارجية ، مثل قاعدة البيانات.

لحفظ البيانات في قاعدة البيانات ، أستخدم نهج مخطط البيانات و Repository.

يمكن لمصمم الخرائط الحصول على البيانات المصدر من قاعدة البيانات وتحويلها إلى كائن المجال المقابل:

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)
  }
}

استنتاج


هناك مساحة كبيرة للتحسينات والتحسينات. تم إنشاء الكود لهذا العرض التوضيحي المرئي ، ويمكن أن يكون التحسين الأول في بنية الطبقات هو الإعلان عن إعلان واجهة المستودع في طبقة النطاق وتطبيقه في طبقة البنية التحتية ، والتي يمكننا مناقشتها في المرة القادمة.

كود المصدر متاح على جيثب .

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


All Articles