paint-brush
Giới thiệu thực tế về thiết kế hướng tên miềntừ tác giả@tobi
1,435 lượt đọc
1,435 lượt đọc

Giới thiệu thực tế về thiết kế hướng tên miền

từ tác giả Piotr Tobiasz15m2023/07/22
Read on Terminal Reader

dài quá đọc không nổi

Thuật ngữ DDD, khối xây dựng và các khái niệm khác để bắt đầu.
featured image - Giới thiệu thực tế về thiết kế hướng tên miền
Piotr Tobiasz HackerNoon profile picture
0-item
1-item

Không nên đổi mới khi nói rằng viết phần mềm không chỉ đơn thuần là viết mã - mà là giải quyết một vấn đề cụ thể. Mặc dù các nhà phát triển là người cuối cùng triển khai giải pháp, nhưng không phải các nhà phát triển là người xác định vấn đề ngay từ đầu là gì. Nhiệm vụ đó được thực hiện bởi nhiều người kinh doanh, những người xem xét các quy trình, rủi ro và kết quả để mô tả vấn đề là gì, tại sao nó tồn tại và cách giải quyết vấn đề đó. Trong ngữ cảnh hướng miền, những người kinh doanh này được gọi là Chuyên gia miền .


Từ góc độ kỹ thuật, có vẻ như Chuyên gia miền nắm giữ một tài sản quý giá: kiến thức của họ về miền. Tuy nhiên, kiến thức này hiếm khi được chia sẻ ở dạng thô. Thay vào đó, nó thường được dịch thành các yêu cầu để các nhà phát triển có thể hiểu và thực hiện nó. Vấn đề với cách tiếp cận này là kiến thức miền của người kinh doanh và nhà phát triển có thể khác nhau. Điều này có nghĩa là quan điểm của những người xác định vấn đề và những người làm việc để giải quyết nó có thể không phù hợp, dẫn đến hiểu lầm và xung đột.


Vì vậy, lối thoát là gì? Đảm bảo rằng những người kinh doanh và kỹ thuật sử dụng cùng một ngôn ngữ và thuật ngữ.

Thiết kế hướng tên miền là gì?

Domain-Driven Design (DDD) là một phương pháp nhấn mạnh tầm quan trọng của việc tạo ra sự hiểu biết chung giữa các chuyên gia miền và các bên liên quan kỹ thuật và điều chỉnh giải pháp phần mềm với các yêu cầu kinh doanh cơ bản. Đây dường như là một định nghĩa cấp cao, phi kỹ thuật, nhưng nó cũng có thể được chia nhỏ thành một thứ gì đó thân thiện với nhà phát triển hơn:


DDD đang thể hiện các khái niệm trong thế giới thực trong mã và cấu trúc của nó bằng cách trau dồi và sử dụng ngôn ngữ phổ biến, được xây dựng bằng cách lập mô hình miền kinh doanh.


Vẫn còn một số thuật ngữ sẽ được giới thiệu, vì vậy nó có thể không rõ ràng 100% ngay bây giờ. Điều quan trọng nhất là DDD cung cấp các công cụ và hoạt động cho phép viết và cấu trúc mã phù hợp với tầm nhìn kinh doanh. Sau đó, không chỉ về giao tiếp mà còn về việc đưa ra các quyết định thiết kế thực sự định hình ngôn ngữ chung.

Thuật ngữ

Không có gì ngạc nhiên khi thuật ngữ quan trọng nhất trong thế giới DDD là Miền . Đây là cách Vlad Khononov, tác giả của cuốn sách "Học thiết kế dựa trên miền", mô tả nó:


Lĩnh vực kinh doanh xác định lĩnh vực hoạt động chính của công ty.


Điều đó có nghĩa là lĩnh vực kinh doanh cũng có thể được coi là:


  • Nguồn thu nhập chính của công ty,
  • Những gì công ty được biết đến nhiều nhất,
  • Bất cứ điều gì mà công ty làm tốt hơn đối thủ cạnh tranh.


Tên miền cũng có thể được chia thành các tên miền phụ - phạm vi hoạt động cụ thể hơn. Mặc dù có ba loại tên miền phụ khác nhau nhưng loại quan trọng nhất là tên miền chính . Nó mô tả cách công ty đạt được lợi thế kinh doanh. Hai vấn đề còn lại là về các vấn đề chung chung, phổ biến hơn, như hệ thống xác thực hoặc bảng quản trị nội bộ.


Có hiểu biết sâu sắc về lĩnh vực kinh doanh của công ty là vô cùng quan trọng để tận dụng đầy đủ các lợi ích của Thiết kế theo hướng miền. Nguồn tốt nhất của sự hiểu biết này không ai khác ngoài Chuyên gia miền . Đây là những cá nhân có vấn đề đang được giải quyết bằng phần mềm - các bên liên quan, các doanh nhân khác nhau và thậm chí cả người dùng. Không có nghĩa là các kỹ sư không hiểu rõ về miền mà họ đang làm việc, mà đúng hơn là các Chuyên gia là nguồn gốc của kiến thức về miền. Bằng cách làm việc cùng với Chuyên gia miền, nhà phát triển có thể đảm bảo rằng các mô hình của miền luôn chính xác và cập nhật.


Điều này dẫn đến một thuật ngữ quan trọng nhưng có khả năng mơ hồ khác: mô hình . Eric Evans, trong cuốn sách về DDD, mô tả mô hình như sau:


Đó là một cách giải thích thực tế trừu tượng hóa các khía cạnh liên quan đến việc giải quyết vấn đề hiện tại và bỏ qua các chi tiết không liên quan.


Vlad Khononov tiếp tục giải thích ý tưởng này bằng những thuật ngữ dễ hiểu hơn:


Một mô hình không phải là một bản sao của thế giới thực, mà là một cấu trúc của con người giúp chúng ta hiểu được các hệ thống trong thế giới thực.


Tóm lại, một mô hình là một đại diện của một khái niệm hoặc quy trình kinh doanh tạo điều kiện cho sự hiểu biết và giao tiếp về sự phức tạp cơ bản của miền.


Vlad đã sử dụng bản đồ để minh họa khái niệm mô hình miền một cách hiệu quả. Bản đồ là một ví dụ hoàn hảo về cách chúng chỉ hiển thị thông tin liên quan đến loại bản đồ, như địa hình, đường hoặc biên giới. Một bản đồ hiển thị tất cả các chi tiết cùng một lúc sẽ quá tải và khá vô dụng. Các mô hình miền cũng có thể được tìm thấy ở các dạng khác, chẳng hạn như:


  • Đơn đặt hàng của khách hàng, đại diện cho phiên bản đơn giản hóa của tất cả các quy trình diễn ra trong nền,
  • Thực đơn nhà hàng, nơi các món được liệt kê trong thực đơn là sản phẩm cuối cùng, thay vì liệt kê mọi thành phần và bước trong quy trình chuẩn bị,
  • Đặt chỗ du lịch, trong đó chuyến đi đã đặt chỉ làm nổi bật những chi tiết quan trọng nhất, mặc dù còn nhiều việc khác liên quan đến việc lập kế hoạch du lịch, khách sạn, v.v.


Phần cuối cùng của câu đố thuật ngữ Thiết kế theo hướng miền (DDD) là Ngôn ngữ phổ biến. Nó đề cập đến ngôn ngữ được chia sẻ bởi cả các bên liên quan về kỹ thuật và kinh doanh trong một dự án. Có một ngôn ngữ chung để mô tả lĩnh vực kinh doanh bắt nguồn từ mô hình miền là rất quan trọng trong DDD. Nó giúp đảm bảo rằng tất cả các thành viên trong nhóm đều hiểu rõ về không gian vấn đề, các khái niệm và mối quan hệ của chúng. Điều này dẫn đến sự liên kết tốt hơn và giảm nguy cơ hiểu lầm. Bằng cách sử dụng Ngôn ngữ phổ biến, giải pháp phần mềm có thể phản ánh chính xác các yêu cầu kinh doanh cơ bản, khiến nó trở thành một thành phần quan trọng của DDD.


Với hầu hết các thuật ngữ được đề cập, sẽ dễ hiểu hơn thiết kế hướng miền là gì . Bây giờ là lúc đi sâu vào cách thức thực tế - các khối xây dựng của DDD.

Khu nhà

Các khối xây dựng DDD đóng vai trò là nền tảng để tạo Mô hình miền hiệu quả và hiệu quả. Vlad Khononov định nghĩa Mô hình miền theo cách sau:


Mô hình miền là mô hình đối tượng của miền kết hợp cả hành vi và dữ liệu.


Mô hình miền bao gồm các khối và cấu trúc xây dựng khác nhau. Những điều quan trọng nhất là:


  • Đối tượng giá trị,
  • thực thể,
  • uẩn,
  • Sự kiện tên miền,
  • kho lưu trữ,
  • Dịch vụ tên miền.

Đối tượng giá trị

Đối tượng Giá trị là các khối xây dựng cơ bản nhất hiện có. Đây là những đối tượng được xác định bởi một tập hợp các thuộc tính và giá trị. Họ không có mã định danh duy nhất - giá trị của họ xác định danh tính của họ. Chúng không thay đổi theo nghĩa là các giá trị khác nhau đã đại diện cho một đối tượng giá trị khác. Ví dụ về Đối tượng Giá trị bao gồm:


  • Số tiền,
  • Phạm vi ngày,
  • Địa chỉ bưu điện.


Đây là cách một Đối tượng Giá trị đơn giản có thể được triển khai trong Python:


 from pydantic import BaseModel class Address(BaseModel):  """Customer address."""  country: str  city: str  street: str  house_number: str  class Config:    frozen = True


Điều đó có nghĩa là việc sử dụng toán tử đẳng thức ( == ) để so sánh hai địa chỉ sẽ chỉ trả về True nếu cả hai đối tượng được gán chính xác các giá trị giống nhau.

thực thể

Các thực thể là loại khối xây dựng tiếp theo. Các thực thể đại diện cho các đối tượng riêng lẻ trong miền với một danh tính riêng biệt, chẳng hạn như một người hoặc một đơn đặt hàng. Chúng tương tự như Đối tượng Giá trị theo cách chúng cũng lưu trữ dữ liệu, nhưng các thuộc tính của chúng có thể và được mong đợi là thay đổi và do đó chúng cần một mã định danh duy nhất. Đơn đặt hàng và thông tin cá nhân chỉ là hai trường hợp đơn giản của Thực thể:


 import uuid from pydantic import BaseModel, Field from practical_ddd.building_blocks.value_objects import Address class Person(BaseModel):  """Personal data."""  id: uuid.UUID = Field(default_factory=uuid.uuid4)  first_name: str  last_name: str  address: Address class Order(BaseModel):  """Customer order."""  id: uuid.UUID = Field(default_factory=uuid.uuid4)  description: str  value: float


Vì giá trị của các phiên bản có thể sửa đổi được trong cả hai trường hợp, nên chúng cần một mã nhận dạng, có thể là UUID. Điều quan trọng hơn là, trong hầu hết các trường hợp, các thực thể không được quản lý trực tiếp mà thông qua một tập hợp .

tổng hợp

Tập hợp là một loại thực thể vì nó có thể thay đổi và yêu cầu một mã định danh duy nhất. Tuy nhiên, trách nhiệm chính của nó không phải là lưu trữ dữ liệu mà là nhóm một tập hợp các đối tượng có liên quan (Thực thể và Đối tượng Giá trị) lại với nhau thành một đơn vị nhất quán duy nhất. Tập hợp là đối tượng gốc, với một ranh giới được xác định rõ bao bọc trạng thái bên trong của nó và thực thi các bất biến để đảm bảo tính nhất quán của cả nhóm. Các tập hợp cho phép lý luận về miền theo cách tự nhiên và trực quan hơn bằng cách tập trung vào các mối quan hệ giữa các đối tượng hơn là bản thân các đối tượng.


Tiếp theo từ các ví dụ trước, một tập hợp có thể được biểu diễn dưới dạng một khách hàng:


 import uuid from pydantic import BaseModel, Field from practical_ddd.building_blocks.entities import Person, Order from practical_ddd.building_blocks.value_objects import Address class Customer(BaseModel):  """Customer aggregate.  Manages personal information as well as orders.  """  id: uuid.UUID = Field(default_factory=uuid.uuid4)  person: Person  orders: list[Order] = Field(default_factory=list)  def change_address(self, new_address: Address) -> None:    self.person.address = new_address  def add_order(self, order: Order) -> None:    if self.total_value + order.value > 10000:      raise ValueError("Order cannot have value higher than 10000")    self.orders.append(order)  def remove_order(self, order_id: uuid.UUID) -> None:    order = next((order for order in self.orders if order.id == order_id), None)    if order is None:      raise IndexError("Order not found")    self.orders.remove(order)  @property  def total_value(self) -> float:    return sum(order.value for order in self.orders)


Khách hàng được liên kết trực tiếp với dữ liệu cá nhân và nó lưu trữ tất cả các đơn đặt hàng. Trên hết, phần tổng hợp hiển thị giao diện để quản lý địa chỉ của người đó cũng như thêm và xóa đơn đặt hàng. Điều này là do thực tế là chỉ có thể thay đổi trạng thái của tập hợp bằng cách thực hiện các phương thức tương ứng.


Mặc dù ví dụ trước tương đối đơn giản, chỉ với một ràng buộc (giá trị đơn hàng không được lớn hơn 10000), nhưng nó sẽ chứng minh việc sử dụng các khối dựng DDD và mối quan hệ của chúng. Trong các hệ thống thực tế, các tập hợp thường phức tạp hơn, với nhiều ràng buộc, ranh giới hơn và có thể có nhiều mối quan hệ hơn. Rốt cuộc, chính sự tồn tại của họ là để quản lý sự phức tạp này. Ngoài ra, trong thế giới thực, các tập hợp thường sẽ được duy trì trong kho lưu trữ dữ liệu, chẳng hạn như cơ sở dữ liệu. Đây là nơi mẫu kho lưu trữ phát huy tác dụng.

Kho

Tất cả các thay đổi trạng thái của tổng hợp phải được thực hiện theo giao dịch trong một hoạt động nguyên tử duy nhất. Tuy nhiên, tổng thể không có trách nhiệm "tự tồn tại". Mẫu kho lưu trữ cho phép trừu tượng hóa các chi tiết của việc lưu trữ và truy xuất dữ liệu và thay vào đó, làm việc với các tập hợp ở mức độ trừu tượng cao hơn. Nói một cách đơn giản, một kho lưu trữ có thể được coi là một lớp giữa lưu trữ tổng hợp và lưu trữ dữ liệu. Một tệp JSON là một ví dụ khá đơn giản về một cửa hàng như vậy. Tập hợp khách hàng có thể có một kho lưu trữ hoạt động trên các tệp JSON:


 import json import uuid from practical_ddd.building_blocks.aggregates import Customer class CustomerJSONRepository:  """Customer repository operating on JSON files."""  def __init__(self, path: str) -> None:    self.path = path  def get(self, customer_id: uuid.UUID) -> Customer:    with open(self.path, "r") as file:      database = json.load(file)      customer = database["customers"].get(str(customer_id))      if customer is None:        raise IndexError("Customer not found")      person = database["persons"][str(customer["person"])]      orders = [database["orders"][order_id] for order_id in customer["orders"]]    return Customer(      id=customer["id"],      person=person,      orders=orders,    )  def save(self, customer: Customer) -> None:    with open(self.path, "r+") as file:      database = json.load(file)      # Save customer      database["customers"][str(customer.id)] = {        "id": customer.id,        "person": customer.person.id,        "orders": [o.id for o in customer.orders],      }      # Save person      database["persons"][str(customer.person.id)] = customer.person.dict()      # Save orders      for order in customer.orders:        database["orders"][str(order.id)] = order.dict()      file.seek(0)      json.dump(database, file, indent=4, default=str)


Tất nhiên, lớp này có thể (và có lẽ nên) làm được nhiều hơn thế, nhưng nó không nhằm mục đích trở thành một ORM đa chức năng, hoàn hảo. Nó sẽ đưa ra ý tưởng về trách nhiệm của kho lưu trữ, trong trường hợp này là lưu trữ và truy xuất tổng hợp của khách hàng trong tệp JSON . Cũng cần lưu ý cách kho lưu trữ xử lý các thực thể được liên kết với tập hợp. Vì dữ liệu cá nhân và đơn đặt hàng được liên kết chặt chẽ với vòng đời của khách hàng nên chúng phải được quản lý một cách chính xác khi dữ liệu tổng hợp đang được xử lý.

Dịch vụ tên miền

Một trường hợp khác cần xem xét là khi có logic nghiệp vụ đơn giản là không phù hợp với tổng hợp hoặc bất kỳ thực thể hoặc đối tượng giá trị nào của nó. Nó có thể là logic phụ thuộc vào nhiều tập hợp hoặc trạng thái của kho lưu trữ dữ liệu. Trong những trường hợp như vậy, một cấu trúc được gọi là Dịch vụ miền có thể hữu ích. Dịch vụ Miền phải có khả năng quản lý các tập hợp, chẳng hạn như bằng cách sử dụng kho lưu trữ, sau đó Dịch vụ này có thể lưu trữ logic miền không thuộc về tập hợp. Ví dụ: một khách hàng có thể yêu cầu logic để tránh mất quá nhiều đơn hàng:


 import uuid from typing import Protocol from practical_ddd.building_blocks.aggregates import Customer class CustomerRepository(Protocol):  """Customer repository interface."""  def get(self, customer_id: uuid.UUID) -> Customer:    ...  def save(self, customer: Customer) -> None:    ... class CustomerService:  """Customer service."""  def __init__(self, repository: CustomerRepository) -> None:    self.repository = repository  def get_customer(self, customer_id: uuid.UUID) -> Customer | None:    try:      return self.repository.get(customer_id)    except IndexError:      return None  def save_customer(self, customer: Customer) -> None:    existing_customer = self.get_customer(customer.id)    # If customer is already in the database and has more than 2 orders,    # he cannot end up with half of them after a single save.    if (      existing_customer is not None      and len(existing_customer.orders) > 2      and len(customer.orders) < (len(existing_customer.orders) / 2)    ):      raise ValueError(        "Customer cannot lose more than half of his orders upon single save!"      )    self.repository.save(customer)


Tổng hợp không thể đảm bảo trạng thái của nó khác với trạng thái trong tệp JSON như thế nào vì nó không có kiến thức về tệp JSON ngay từ đầu. Đó là lý do tại sao logic so sánh phải được đưa vào Dịch vụ miền. Cũng cần lưu ý rằng Dịch vụ miền sẽ hoạt động với sự trừu tượng hóa kho lưu trữ. Điều này làm cho việc hoán đổi triển khai cụ thể bằng một triển khai thay thế bằng cách sử dụng phép nội xạ phụ thuộc trở nên đơn giản.

Để tất cả chúng cùng nhau

Với tất cả các phần hiện đã được đề cập, giờ đây chúng có thể được coi là một chương trình đang hoạt động:


 import uuid from practical_ddd.building_blocks import aggregates, entities, value_objects from practical_ddd.database.repository import CustomerJSONRepository from practical_ddd.service import CustomerService # Initialize domain service with json repository srv = CustomerService(repository=CustomerJSONRepository("test.json")) # Create a new customer customer = aggregates.Customer(  person=entities.Person(    first_name="Peter",    last_name="Tobias",    address=value_objects.Address(      country="Germany",      city="Berlin",      street="Postdamer Platz",      house_number="2/3",    ),  ), ) srv.save_customer(customer) # Add orders to existing customer customer = srv.get_customer(uuid.UUID("a32dd73a-6c1b-4581-b1d3-2a1247320938")) assert customer is not None customer.add_order(entities.Order(description="Order 1", value=10)) customer.add_order(entities.Order(description="Order 2", value=210)) customer.add_order(entities.Order(description="Order 3", value=3210)) srv.save_customer(customer) # Remove orders from existing customer # If there are only 3 orders, it's gonna fail customer = srv.get_customer(uuid.UUID("a32dd73a-6c1b-4581-b1d3-2a1247320938")) assert customer is not None customer.remove_order(uuid.UUID("0f3c0a7f-67fd-4309-8ca2-d007ac003b69")) customer.remove_order(uuid.UUID("a4fd7648-4ea3-414a-a344-56082e00d2f9")) srv.save_customer(customer)


Mọi thứ đều có trách nhiệm và ranh giới của nó. Aggregate chịu trách nhiệm quản lý các thực thể và đối tượng giá trị của nó, cũng như thực thi các ràng buộc của nó. Dịch vụ miền sử dụng kho lưu trữ JSON được đưa vào để duy trì dữ liệu trong tệp JSON và thực thi các ranh giới miền bổ sung. Cuối cùng, mỗi thành phần có một chức năng và ý nghĩa riêng biệt trong miền được chỉ định.

Bản tóm tắt

Không còn nghi ngờ gì nữa, Thiết kế hướng miền là một ý tưởng phức tạp cần nắm bắt. Nó cung cấp các phương pháp, mẫu và công cụ để giúp các nhóm phần mềm giải quyết các vấn đề kinh doanh khó khăn nhất bằng cách nhấn mạnh vào lĩnh vực kinh doanh. Tuy nhiên, DDD không chỉ là một tập hợp các khối xây dựng. Đó là một tư duy đòi hỏi sự hợp tác và giao tiếp giữa các bên liên quan về kỹ thuật và kinh doanh. Một sự hiểu biết chung về miền, được thể hiện thông qua ngôn ngữ phổ biến, là rất quan trọng đối với sự thành công của dự án DDD. Khi được thực hiện tốt, DDD có thể dẫn đến phần mềm phù hợp hơn với nhu cầu của doanh nghiệp và hiệu quả hơn trong việc giải quyết các vấn đề phức tạp.

Lời bạt và các bước tiếp theo

Bài viết này không bao giờ có ý định giống như "DDD: From Zero To Hero", mà là để giới thiệu về vũ trụ DDD. Tôi muốn trình bày các khái niệm quan trọng nhất của Thiết kế hướng miền theo cách rất đơn giản và thực tế. Tôi tin rằng học Thiết kế theo hướng miền là một cách tuyệt vời để nâng cao kiến thức chuyên môn về lập trình. Tuy nhiên, bạn không nghe về nó quá thường xuyên - ít nhất là không nhiều như "11 mẹo và thủ thuật JavaScript ĐIÊN RỒ - một chủ đề 🧵".


Trong mọi trường hợp, nếu bạn thấy điều này thú vị, bạn có thể xem qua phần nguồn để biết sách và bài báo đã truyền cảm hứng cho tôi viết bài này ngay từ đầu. Có một số khái niệm tôi không trình bày vì tôi nghĩ chúng nằm ngoài phạm vi của phần giới thiệu này, nhưng chúng rất đáng để nghiên cứu:


  • bối cảnh giới hạn
  • Sự kiện tên miền
  • Bão sự kiện


Bạn chắc chắn sẽ tìm thấy chúng trong các nguồn được liệt kê dưới đây.

nguồn

Các ví dụ mã được sử dụng trong bài viết có thể được tìm thấy ở đây: liên kết .


Học thiết kế dựa trên tên miền của Vlad Khononov. Một cuốn sách tuyệt vời đã từng là nguồn cảm hứng chính cho tôi. Giải thích sâu hơn tất cả các khái niệm được thảo luận trong bài viết này.


Các mẫu kiến trúc trong Python của Harry Percival và Bob Gregory. Tôi đã đọc cuốn sách gần hai năm trước và nó đã có tác động đáng kể đến tôi với tư cách là một nhà phát triển. Tôi đã quay lại với nó khi viết bài này, và nó đã giúp tôi một lần nữa.


DDD bằng Python của Przemysław Górecki. Tôi phát hiện ra blog này ở gần cuối bài viết, nhưng nó đã thu hút sự quan tâm của tôi vì nó cực kỳ chuyên nghiệp. Sự thật thú vị: Tôi đã làm việc trong cùng một công ty với Przemysław và tôi hoàn toàn không biết về điều đó.


Cũng được xuất bản ở đây .