Giter VIP home page Giter VIP logo

ddd-sampling-faq's Introduction

Hello World

by: Vietnamese

I'm Anh, from 🇻🇳 and be a 👨‍💻, like to build a system by DDD.

Currently have interested in System Design 👷 🏗️ and Database.

Github statistic

GitHub stats

Tech Toolbox 🧰

Database 🗃️

MySQL MongoDB

Infrastructure

AWS

Programming Language 💻

JS NodeJS TS Dart Python

UI 🎨

React Redux MUI

Mobile 📱

Flutter

ORM ⚙️

Sequelize

Tool ⚒️

Git

Testing 🧪

Jest

Deep Learning 🤖

Tensorflow

Architecture 🏗️

Clean architeture + DDD

CQRS, Event Sourcing Screen Shot 2023-09-16 at 13 32 19

ddd-sampling-faq's People

Contributors

tuananhhedspibk avatar

Stargazers

 avatar  avatar

Watchers

 avatar

ddd-sampling-faq's Issues

Test Entity

Entity được tạo ra từ những methods dưới đây:

  1. Create method (constructor, factory method)
  2. Mutation method
  3. Reconstruct method

Do đó phía trên là 3 methods ta cần phải test khi tiến hành test entity.

Ví dụ về modeling

Lấy ví dụ về một hệ thống quản lí việc tuyển dụng cũng như các ứng viên ứng tuyển của một công ty.

Dưới đây là những thứ cần xem xét:

  1. Sơ đồ hệ thống
  2. Sơ đồ usecase
  3. Sơ đồ domain model
  4. Sơ đồ object

Liệu có thể truyền Value-Object vào creation method ?

Hoàn toàn được, tuy nhiên nếu truyền Value-Object vào creation method thì code sẽ dễ đọc hơn một chút. Cùng so sánh 2 pattern dưới đây

class MailAddress {
  constructor(value: string) {}
}

class User {
  createFromVO(mail: MailAddress) {
    const user = new User(mail); // Pattern-1
  }

  createFromPrimitive(mail: string) {
    const user = new User(new MailAddress(mail)); // Pattern-2
  }
}

Như thấy ở pattern-1 ta truyền vào đó MailAddress Value-Object nên sẽ không có trường hợp truyền các string không liên quan đến email vào cả, qua đó có thể nói rằng code sẽ dễ đọc hơn đôi chút.

Ngoài ra bắt buộc phải có sự kiểm tra tính chính xác của email truyền vào. Nếu điều này được thực thi trong User class thì sẽ giảm đi tính bảo trì của class cũng như sẽ gây sự khó hiểu cho người đọc code. Hơn nữa nếu class khác cũng sử dụng email thì logic kiểm tra email sẽ không thể tái sử dụng lại được. Do đó quá trình kiểm tra email nên được tiến hành trong MailAddress class để vừa đảm bảo tính tái sử dụng code cũng như đảm bảo được nghiệp vụ kiểm tra email.

Khi lấy dữ liệu từ Repository thì có nhất thiết phải kéo về cả những dữ liệu thừa ?

Pattern của DDD ở đây đó là sự đánh đổi hiệu suất khi truy xuất DB để có được tính bảo trì cao cho hệ thống.

Hãy thử lấy một ví dụ: khi cần update chỉ 2 cột trong DB, điều đó đồng nghĩa với việc ở các tầng domain, infrastructure ta cần các điều kiện if cũng như các xử lí validation khiến cho xử lí ở các tầng đó trở nên phức tạp và sẽ khó tiến hành mock các hàm ở tầng infrastructure cho mục đích thực hiện unit test.

Để tránh điều đó các method của entity / VO sẽ chỉ tập trung thực hiện domain logic, còn ở tầng infrastructure sẽ chỉ thực thi các truy xuất / update SQL đơn giản để từ đó có thể tăng tính tập trung logic theo từng tầng, giảm đi tính liên kết giữa các tầng cũng như dễ dàng thực thi unit test hơn.

Và do đó ở các repository ta sẽ kéo về toàn bộ các giá trị của các thuộc tính thuộc về entity / VO.

Điều cần cân nhắc ở đây đó là: HIỆU SUẤT DB vs TÍNH LIÊN TỤC VÀ DỄ TEST

Ngoài ra nếu không chấp nhận việc hiệu suất DB giảm với các xử lí truy vấn dữ liệu ta có thể tách ra thành hai loại model:

  • Domain Model: dùng khi cần UPDATE/ INSERT/ DELETE dữ liệu
  • Query Model: dùng khi truy vấn dữ liệu.

Với các xử lí batch (cập nhật một lượng lớn dữ liệu) ta có thể cân nhắc việc tăng hiệu suất xử lí và tính bảo trì của hệ thống. Khi đó ta có thể thực thi câu UPDATE QUERY cho nhiều bản ghi dữ liệu cùng một lúc.

Nên thực thi việc sắp xếp dữ liệu ở repository như thế nào ?

Ta hoàn toàn có thể truyền param dùng cho điều kiện sắp xếp vào repository bằng hình thức enum như sau:

enum UserOrderKey {
  NAME,
  USER_ID,
};

Thế nhưng có một vấn đề đó là OrderKey liệu có nên nằm ở tầng Domain. Nếu OrderKey được biểu thị bởi tên các cột trong DB thì điều đó là không nên thế nhưng nếu nó biểu thị bởi các thuộc tính của entity thì việc nằm ở tầng Domain hoàn toàn không có vấn đề gì cả. Việc thực thi sắp xếp (câu ORDER BY trong SQL) không nên để lộ lọt vào tầng Domain.

Nên định nghĩa Repository ở tầng nào, liệu có thể định nghĩa ở tầng usecase hay không ?

Nên định nghĩa ở tầng domain. Lí do là vì Repository biểu diễn phạm vi của Aggregate dưới dạng code. Còn nếu định nghĩa ở tầng use-case thì các thông tin liên quan tới Aggregate sẽ không còn nằm trong tầng domain nữa.

Ngoài ra nếu định nghĩa ở tầng use-case thì với một entity có thể cần tạo nhiều repositories khác nhau nên dù phạm vi của Aggregate có khác nhau đi chăng nữa thì tầng domain cũng khó có thể nhận diện được.

Phạm vi của Aggregate là rất quan trọng đối với tầng domain nên Repository biểu thị phạm vi của Aggregate cũng nên được định nghĩa ở tầng domain.

Sử dụng ID

Với auto increment ID ta cũng có thế áp dụng với DDD tuy nhiên có vài điều sau ta cần cân nhắc kĩ lưỡng:

Điều thứ nhất: khi sử dụng auto increment ID ta có thể nghĩ tới 2 cách xử lí sau đây:

  1. Ở trong Repository ta sẽ tiến hành tạo ID rồi lưu vào DB
  2. Thực hiện quá trình tạo ID từ DB, kéo ID từ DB về Repository rồi xử lí

Với cách xử lí thứ nhất ta cần phải check xem ID truyền vào có null hay không qua đó sẽ phát sinh xử lí if không cần thiết từ đó làm giảm đi tính bảo trì của code như ví dụ dưới đây

const Task = new Task({ content: "Test", userId });

// Ở đây cần check userId có null hay không trước khi truyền vào Task constructor

Do đó cách xử lí thứ 2 (đưa auto increment vào DB) là hợp lí hơn.
Ngoài việc sử dụng auto increment ID của DB ta cũng có thể sử dụng UUID hoặc ULID qua đó làm giảm đi sự phụ thuộc vào DB tăng tính bảo trì cho code.

Có nên update nhiều Aggregates trong cùng 1 transaction hay không ?

Cũng có nhiều luồng suy nghĩ về vấn đề này. Trên thực tế việc update nhiều Aggregates trong cùng 1 transaction sẽ khiến cho phạm vi của transaction sẽ lớn, từ đó dẫn tới việc nếu có lỗi sẽ gây ra ảnh hưởng trong một phạm vi lớn. Tuy nhiên nếu cần thiết phải rollback thì việc update nhiều Aggregates trong cùng 1 transaction sẽ phát huy hiệu quả trông thấy rõ.

Do đó ta có thể cân nhắc việc update 1 hay nhiều Aggregates trong cùng 1 transaction.

Nên thiết kế phạm vi của Aggregate như thế nào ?

Ta có thể xem xét ví dụ về "Trường học" và "Khoa" như dưới đây. Có cả thảy 2 pattern cho chúng ta có thể xem xét.

Screen Shot 2022-03-19 at 18 35 07

Với Pattern A ta thấy Aggregate có phạm vi rất lớn, nó có những ưu điểm sau:

  • Đảm bảo được tính đồng bộ
  • Việc thực thi cũng như test sẽ trở nên đơn giản hơn

Tuy nhiên Aggregate với phạm vi lớn cũng sẽ có những nhược điểm sau:

  • Lượng data cần xử lí cũng sẽ tăng theo
  • Cơ chế visualization cũng sẽ phức tạp hơn

Thực thi DDD với hệ thống sẵn có

Có 2 cách tiếp cận:

  • Bottom-Up: đi theo từng bước một, refactor từng entity, value-object. Ưu điểm ở đây đó là: chi phí thấp, hệ thống luôn trong trại thái có thể sử dụng được. Nhược điểm là có thể sẽ không cải thiện được vấn đề gì cả.
  • Top-Down: Viết lại nghiệp vụ cho từng layer một. Ưu điểm: do đã quyết định sẵn về cách cải thiện hệ thống nên hiệu quả sẽ cao hơn. Nhược điểm là việc kết hợp kiến trúc mới với kiến trúc hiện tại cần có sự đồng ý của team cũng như tốn khá nhiều chi phí để thực hiện. Nên về cơ bản cách tiếp cận này khó hơn so với cách tiếp cận Bottom-Up

Có thể sử dụng Pessimism Lock ở Repository

Khi đó nếu tầng domain không bị phụ thuộc vào bất kì một công nghệ nào thì điều đó là hoàn toàn có thể.

abstract class UserRepository {
  findByUserId(userId: UserId, exclusiveAccess: boolean);
}

Tham số exclusiveAccess không phải là việc lấy một khoá gì đó từ DB mà là một phần cấu trúc của Repository nên việc định nghĩa ở tầng domain là hoàn toàn có thể.

Khi transaction ở use-case bắt đầu thì ở tầng infra sẽ lấy về lock, nhờ đó ở cùng một thời điểm khi cùng truy cập vào cùng một tài nguyên thì xử lí của transaction nếu sẽ được ưu tiên thực thi trước so với xử lí của method findByUserId.

Ngoài ra ta cũng có thể tách riêng phần xử lí exclusive này thành một method riêng với tên là findByUserIdExclusively.

[Test] Mutation Method

Với mutation method (method thay đổi trạng thái của entity), ta thử test postpone method như sau:

class Task {
  // ...
  
  postpone() {
    if (this.postponeCount > MAX_POSTPONE_COUNT) {
      throw DomainException("Max postpone count is overflow");
    }
    this.dueDate = dueDate.plusDays(1);
    this.postponeCount += 1;
  }
}

Với những methods có phát sinh ngoại lệ như thế này, ta cần test 2 TH:

  • Normal case: khi không phát sinh ngoại lệ
  • Unusual case: phát sinh ngoại lệ

Với case phát sinh ngoại lệ ta có thể sử dụng reconstruct method để đưa giá trị cần kiểm tra tới một giới hạn nhất định. Tuy nhiên cách làm này có nhược điểm là reconstruct method thường sẽ không có validation nên nếu sử dụng sai mục đích sẽ dẫn tới những lỗi tiềm tàng sau này.

Có cần phải bảo trì các sơ đồ Model hay không ?

Sơ đồ hệ thống giúp ta có cái nhìn bao quát nhất về hệ thống nên việc bảo trì sơ đồ này là rất cần thiết.
Sơ đồ use-case không cần thiết phải bảo lưu, bảo trì vì khi dev, sơ đồ này sẽ hay thay đổi.
Sơ đồ objectSơ đồ DomainModel đều cần phải được bảo trì, cập nhật thường xuyên do ta phải đảm bảo code không quá khác xa so với sơ đồ DomainModel, vậy nên cần tuân theo trình tự Cập nhật sơ đồ DomainModel -> Chỉnh sửa code

Sơ đồ hệ thống

Biểu thị:

  • Hệ thống sẽ phát triển
  • Các actors liên quan
  • Các hệ thống liên kết bên ngoài

Screen Shot 2022-02-06 at 18 08 19

Tuy đơn giản nhưng ta vẫn cần đặt ra những câu hỏi sau nếu muốn điều chỉnh hệ thống:

  • Ai sẽ là người sử dụng hệ thống ?
  • Việc ứng tuyển sẽ được thực hiện trực tiếp đối với hệ thống ?

Quá trình trả lời các câu hỏi trên sẽ diễn ra khi tiến hành modeling, kết quả thảo luận sẽ được thể hiện thông qua sơ đồ domain model.

Hệ thống lần này sẽ chỉ do người phụ trách việc tuyển dụng sử dụng. Việc ứng tuyển sẽ thông qua form, dữ liệu thu thập từ form sẽ được nhập bằng tay vào hệ thống.

Tuy nhiên nếu sau này muốn phát triển hệ thống theo hướng có thể ứng tuyển trực tiếp thì phạm vi của hệ thống sẽ được mở rộng lên rất nhiều. Vậy nên hãy quyết định vấn đề này ngay từ đầu để có thể biểu thị nó một cách chính xác nhất trên sơ đồ hệ thống.

Có nên sử dụng Repository ở DomainService hay không ?

Cùng xem xét từ vai trò của DomainService, về cơ bản DomainService là nơi định nghĩa các rule hoặc nghiệp vụ mà khi cho vào Entity hay Value-Object trông sẽ rất là kì.

Ta lấy ví dụ với Booking Entity, "Khoảng thời gian này đã được đặt lịch từ trước" - nếu Booking Entity biết điều này thì sẽ rất là kì. Hơn nữa việc xem xét xem Booking có tồn tại hay không, ta cần sử dụng đến Repository bên trong DomainService. Nên ta hoàn toàn có thể sử dụng Repository bên trong DomainService.

Tuy nhiên cũng cần chú ý đảm bảo việc DomainService không bị quá phình to với nhiều nhiệm vụ đi kèm.

Sử dụng Repository để update một phần của Aggregate liệu có ổn ?

Không nên làm điều này, nguyên nhân là do khi update một phần của entity ta cần rất nhiều các điều kiện if hoặc các variation khác nên điều này sẽ làm cho nghiệp vụ ở tầng domain bị lộ lọt xuống tầng infrastructure.

Các logic update chỉ nên thực hiện ở tầng domain, còn repository sẽ chỉ ghi đè giá trị vào DB mà thôi. Điều này sẽ sẽ làm giảm đi sự ràng buộc giữa tầng domain và tầng infra qua đó tăng tính mở rộng cho hệ thống cũng như thực hiện test một cách dễ dàng.

Đối tượng test

Xét các sơ đồ usecase, domain-model, object như dưới đây.

Đầu tiên là sơ đồ use-case

Screen Shot 2022-03-27 at 16 53 52

User có các use-case như:

  • Tạo task
  • Trì hoãn task
  • Hoàn thành task
  • Thay đổi người thực hiện task

Sơ đồ Domain-Model, Object sẽ như sau:

Screen Shot 2022-03-27 at 16 54 47

Chúng ta sẽ thực hiện test với tất cả các tầng:

  • Domain
  • Usecase
  • Infrastructure

Modeling được thực hiện vào lúc nào ?

Sau khi thiết kế yêu cầu hệ thống và ở các phase dev. Nếu có vấn đề cần phải chỉnh sửa thì nhất thiết ta phải cập nhật các sơ đồ Model.

Trong thực tế sẽ không có phase Modeling mà quá trình này sẽ được tiến hành thông suốt trong toàn bộ quãng thời gian phát triển phần mềm.

Thông tin về version - Optimism lock được sử dụng như thế nào ?

Optimism lock thường được sử dụng để tránh tình trạng nhiều user cùng thay đổi tài nguyên cùng một lúc. Tuy nhiên việc thiết lập giá trị này (version) tại tầng domain thì lại vi phạm về quy tắc nghiệp vụ tại tầng domain.

Với thông tin về version, nó sẽ được điều khiển tại tầng infrastructure nên nếu thực thi hoặc điều khiển tại tầng domain thì sẽ là một sự vi phạm về quy tắc của layer. Tuy nhiên ta cũng có thể tư duy theo hướng Giảm đi tối đa sự vi phạm này do khi dùng Optimism Lock thì thông tin về version là rất cần thiết.

Có rất nhiều cách triển khai, sau đây là một cách trong số đó. Ta định nghĩa một OptimismLockable interface - interface này sẽ có thuộc tính là version. Đoạn code dưới đây sẽ triển khai OptimismLockable interfaceVersion class.

interface OptimismLockable {
  version: Version;
}

class Version {
  constructor () {
    return Version(0);   
  }  
}

class và interface trên sẽ được định nghĩa ở tầng domain tuy nhiên việc sử dụng nó hay không lại cần có sự cân nhắc nên có thể nói đây là cách giảm thiểu tối đa việc vi phạm quy tắc nghiệp vụ về layer. Entity class sử dụng Optimism Lock sẽ như sau

class Dog implements OptimismLockable {
  dogId: DogId;
  name: string;
  version: Version; // (1)

  create(name: string) {
    return new Dog(DogId(), name, Version.initial());
  }

  reconstruct(
    dogId: DogId,
    name: string,
    version: Version
  ) {
    return new Dog(dogId, name, version);
  }
}

Việc cập nhận version sẽ được triển khai ở tầng infrastructure. Câu SQL để update dữ liệu sẽ như sau:

UPDATE Dog
SET name = 'new_name', version = version + 1
WHERE version = 1 AND dogId = 1;

Câu update trên sẽ chỉ update những record có version = 1, vậy nên dù cùng được gọi vào 1 thời điểm nhưng sau khi version được increment thì số lượng các record thoả điều kiện version = 1 sẽ là 0. Xử lí sẽ thất bại và ném ra ngoại lệ.

Ngoài ra ta cũng có thể triển khai version ở abstract class (base class của các entities) nhưng nếu làm như vậy thì mọi entities sẽ phải xử lí phần thông tin version nên việc triển khai Optimism Lock thông qua interface vẫn hợp lí hơn cả.

[Test] Create Method

class Task {
  id: TaskId;
  name: TaskName;
  userId: UserId;
  status: TaskStatus;
  postponeCount: number;
  dueDate: Date;

  private constructor(id: TaskId, name: TaskName, userId: UserId, status: TaskStatus, postponeCount: number, dueDate: Date) {
    this.id = id;
    this.name = name;
    this.userId = userId;
    this.status = status;
    this.postponeCount = postponeCount;
    this.dueDate = dueDate;
  }

  create(name: TaskName, dueDate: Date, userId: UserId) {
    return new Task(
      TaskId(), name, userId, TaskStatus.UNDONE, 0, dueDate,
    );
  }
}

Phía trên ta để constructorprivate qua đó chỉ có trong class mới gọi được đến constructor mà thôi, ngoài ra ta chỉ public create method mà thôi.

Khi viết test code cần phải hiểu rõ rằng mình muốn xác nhận, kiểm tra điều gì để từ đó có cách triển khai test code phù hợp. Ngoài ra tiêu để của test case cũng nên phản ánh rõ mục đích của nó:

  • given: điều kiện tiên quyết (optional)
  • when: hành động, thao tác
  • then: giá trị kì vọng
    là 3 yếu tố nên có trong tiêu đề của test case.

Phạm vi ứng dụng của DDD

Domain: là cách ứng dụng phần mềm để giải quyết một vấn đề nào đó.
Domain Model: là một cách trừu tượng hoá các đối tượng nhằm giải quyết một bài toán (domain) nào đó.

Một phần mềm làm ra cần phải thoả mãn 2 yêu cầu sau:

  • Cung cấp một tính năng, dịch vụ nào đó cho người dùng (1)
  • Dễ dàng mở rộng, chỉnh sửa (2)

(1): Domain Model cần phản ánh đúng nghiệp vụ, vấn đề cần giải quyết.
(2): Áp dụng pattern sử dụng Entity, Repository để đáp ứng được việc thường xuyên thay đổi model.

Modeling - Liệu có cần thiết kế toàn bộ 4 loại sơ đồ Model hay không ?

Thực tế là không vì chỉ cần tạo những model cần thiết mà thôi.

4 loại sơ đồ ở đây đó là:

  • Sơ đồ hệ thống: có thể lược bỏ nếu không liên kết với các hệ thống bên ngoài quá nhiều
  • Sơ đồ Use-Case: cần thiết khi phát triển các tính năng mới. Tuy nhiên nếu không hiểu rõ được Actor là ai hoặc tính năng này có thể làm được những gì thì việc chỉnh sửa sẽ phát sinh. Nhưng nếu có thể biểu thị được các tính năng hoặc actor thông qua user story ta hoàn toàn có thể bỏ qua sơ đồ này và sử dụng một cách tiếp cận khác.
  • Sơ đồ DomainModel: cần thiết
  • Sơ đồ Object: cần thiết

Hai sơ đồ DomainModel và Object thực sự cần thiết là vì kể cả khi tính năng là rất nhỏ nhưng nếu thử phác thảo các sơ đồ Model sẽ có các vấn đề phát sinh khác cần phải xem xét.

Nguyên tắc khi thiết kế

Nhằm tăng tính mở rộng của code, khi thiết kế cần chú ý 2 điểm sau:

  1. Hiểu được nhiệm vụ của từng class, các thành viên của class (data, method) cần gắn kết chặt chẽ với nhau. Giảm tính phụ thuộc lẫn nhau giữa các classes.
  2. Có thể test hệ thống một cách dễ dàng.

[Test] Reconstruct method

Reconstruct method là method dùng để tạo entity dựa theo dữ liệu được lấy ra từ DB.

class Task {
  // ...
  
  reconstruct(id: TaskId, name: TaskName, userId: UserId, status: TaskStatus, dueDate: Date, postponeCount: number) {
    return new Task(
      id, name, userId, status, postpone, dueDate,
    );
  }
}

reconstruct method cũng gọi đến private method, vậy nên từ bên ngoài class nếu muốn tạo instance thì chỉ có cách gọi đến create hoặc reconstruct method.

Bản thân reconstruct method chỉ có nhiệm vụ lắp các giá trị từ tham số vào thuộc tính tương ứng nên xử lí của method này là khá đơn giản. Việc test nó có thể cân nhắc giản lược.

Với các trường hợp lưu file hoặc lưu dữ liệu ở service bên ngoài liệu có nên gom tất cả lại vào trong một repository ?

Tuỳ vào từng tính chất mà nên tách riêng thì hơn. Đặc biệt là với các trường hợp cần role-back thì việc có thể role-back lại hay không thì tuỳ vào từng use-case. Với các use-case có sử dụng transaction thì ta có thể ngầm hiểu rằng có xử lí ở đâu đó cần phải role-back lại.

Với những trường hợp lưu dữ liệu thì nếu quá trình role-back không rõ ràng sẽ dẫn tới các phát sinh ngoài ý muốn từ đó làm cho dữ liệu không đồng nhất.

Do đó nên đặt tên để có thể dễ phân biệt nhất. VD: với lưu file ta có thể đặt là XXXStorage còn với các Repo có tương tác với API ngoài thì ta đặt là XXXClient.

Còn với những trường hợp không thể role-back được thì nếu không có sự nhầm lẫn về mặt tính chất, ta có thể đặt chung vào một Repository cũng được.

Tạo Entity

  • Với dữ liệu lấy ra từ DB, ta cần những Factory Methods chuyên dụng để tái tạo lại entity đúng theo như yêu cầu của nghiệp vụ dựa trên dữ liệu lấy ra được.
  • Với những ngôn ngữ chỉ sử dụng được duy nhất một Primary Constructor trong khi yêu cầu cần có nhiều constructor thì ta có thể làm như sau:
  1. Tạo một private constructor
  2. Các factory methods tiếp theo (vd: create, reconstruct, ...) sẽ gọi đến private constructor đó để tạo instance
class Task {
  private constructor() {}

  static create() {
    const task = new Task();
    // ...
  }

  static reconstruct() {
    const task = new Task();
    // ...
  }
}

Vì ở đây ta định nghĩa private constructor nên các class bên ngoài không thể gọi đến constructor của class Task được. Nếu biến constructor trở thành public thì có thể dẫn đến việc khởi tạo các instance với giá trị sai lệch đi so với nghiệp vụ.

const wrongTask = new Task({ name: "wrong", status: TaskStatus.DONE });

reconstruct method được sử dụng trong class triển khai các repository để tái cấu trúc lại entity dựa theo dữ liệu lấy ra từ DB.

Sự khác biệt giữa constructor và factory method

Về bản chất ở đây không có sự khác nhau nhiều nhưng về mặc method name thì constructor method chỉ có thể có tên là constructor hoặc cùng tên với class như ở một số ngôn ngữ như Java. Còn với factory method ta có thể để tên là create hoặc reconstruct.

Liệu có thể thực thi phân trang ở Repository ?

Nên trả về dưới dạng một object có biểu thị page như sau:

class Page<T> {
  items: T[];
  paging: Paging;
}

class Paging {
  totalCount: number;
  pageSize: number;
  pageNumber: number;
}

Câu request sẽ sử dụng object như sau:

class PagingCondition {
  pageSize: number;
  pageNumber: number;
}

abstract class UserRepository {
  fetchPageByName(name: string, pagingCondition: PagingCondition): Page<User>;
}

Xử lí paging sẽ được thực thi ở tầng infra.

Thế nhưng cũng sẽ xuất phát những câu hỏi cho rằng liệu thông tin về page có nên được định nghĩa ở tầng domain. Định nghĩa trừu tượng của page đó là là dữ liệu được trả về chứa các thông tin về số lượng các bản ghi liên quan trong một tập hợp, nên Repository trả về một phần dữ liệu của tập hợp dưới hình thức page là hoàn toàn hợp lệ.

Điều quan trọng hơn cả đó là việc che dấu đi phần triển khai paging nằm ở tầng infra.

Cập nhật nhiều aggregates cùng 1 lúc tại use-case

Với cách làm này ta sẽ tiến hành tạo nhiều aggregates tại use-case cùng một lúc, sau đó truyền vào repository một cách lần lượt

class Task {
  name: string;
}

abstract class ITaskRepository {
  insert(task: Task);
}

class ActivityHistory {
  constructor() {}
  static createFromTask(task: Task) {
    return ActivityHistory(`Create From ${task.name}`);
  }
}

abstract class IActivityHistoryRepository {
  insert(activityHistory: ActivityHistory);
}

// Usecase
class CreateTaskUseCase {
  execute(taskName: string) {
    const task = new Task(taskName);
    taskRepository.insert(task);

    const activityHistory = ActivityHistory.createFromTask(task);
    activityHistoryRepository.insert(activityHistory);
  }
}

Ưu điểm của cách làm này là khá đơn giản nên nó thường được áp dụng cho những người mới làm quen với DDD. Tuy nhiên nhược điểm của nó nằm ở chỗ nếu 1 use-case khác vô tình phá vỡ đi sự đồng bộ giữa TaskActivityHistory sẽ khiến cho người đọc code không nhận ra rằng mối liên hệ giữa Task và ``ActivityHistory`

class WrongCreateTaskUseCase {
  execute(taskName: string) {
    const task = new Task(taskName);
    taskRepository.insert(task);
  }
}

Nên đặt tên DomainService như thế nào ?

Không nên đặt tên theo kiểu XXXService mà nên đặt tên sát với nghiệp vụ nhất có thể ví dụ như TaxCalculation. Không những thế việc đặt tên theo format XXXService sẽ khiến cho tên rất dài, ngoài ra do không biết rõ được class sẽ đảm nhận nghiệp vụ gì nên việc thêm mới các methods vào class cũng sẽ rất khó khăn.

Nếu vô tình thêm quá nhiều methods vào cũng sẽ khiến cho class trở nên phình to hơn bình thường.

Hãy xem xét kĩ việc đặt tên dựa theo chức năng, nghiệp vụ mà service đảm nhiệm

DDD - hợp và không hợp

DDD rất thích hợp cho việc xử lí các domain có tính phức tạp cao

Tuy nhiên với các ứng dụng đơn thuần CRUD thì việc áp dụng DDD không hẳn đã là hợp lí vì những lí do sau:

  • Chi phí để hiểu DDD
  • Kể cả khi hiểu về DDD thì việc áp dụng cũng gặp rất nhiều khó khăn

Sơ đồ usecase

Sơ đồ này là chi tiết của hệ thống đối với từng actor (tác tử) có trong hệ thống.

Screen Shot 2022-02-23 at 16 42 26

Hơn nữa trong quá trình thiết kế sơ đồ usecase, nếu phát sinh vấn đề gì, ta hoàn toàn có thể chỉnh sửa lại sơ đồ thiết kế hệ thống.

Cách đảm bảo đồng bộ hoá giữa các Aggregate

Lấy một ví dụ về app quản lí task. Khi ta tiến hành tạo task thì Aggregate Task sẽ được tạo, đồng thời với đó là Aggregate ActivityHistory (lịch sử hoạt động) cũng sẽ được tạo theo. Nếu 2 aggregates này không đồng bộ hoá, thống nhất với nhau thì sẽ gây ra rất nhiều vấn đề.

Ngoài Task Aggregate ra ta còn có thể thấy rằng các Aggregates khác khi được tạo ra cũng sẽ kéo theo ActivityHistory được tạo. Như ví dụ dưới đây:

Screen Shot 2022-03-13 at 21 17 45

Có thể có 3 cách triển khai như sau:

  1. Cập nhật nhiều Aggregate tại use-case cùng 1 lúc
  2. Sử dụng domain-service
  3. Sử dụng domain-event

Sơ đồ DomainModel / Sơ đồ Object

2 sơ đồ này có mối liên hệ trừu tượng / cụ thể.

Sơ đồ DomainModel là sơ đồ đơn giản hoá từ sơ đồ class, cần tuân theo những quy định sau:

  • Cần phải ghi những thuộc tính tiêu biểu của object, có thể không cần phải ghi method.
  • Cần phải ghi rõ ràng các nghiệp vụ.
  • Biểu thị rõ tính liên quan giữa các objects.
  • Định nghĩa độ đa dạng
  • Định nghĩa phạm vi của Aggregate
  • Dịch nghĩa rõ ràng các thuật ngữ của nghiệp vụ.

Sơ đồ object là sơ đồ đưa ra ví dụ cụ thể của DomainModel. Cần có sự tham gia của người xây dựng mô hình.

Ví dụ:
Dưới đây là sơ đồ object khi sơ đồ usecase ở phần trước được triển khai. Gồm 2 trường hợp:

  • Trượt phỏng vấn server engineer từ vòng 1
  • Đang chuẩn bị phỏng vấn vòng 2

Screen Shot 2022-02-26 at 13 16 22

Từ sơ đồ object trên, ta có thể trừu tượng hoá để có sơ đồ Domain Model như sau:

Screen Shot 2022-02-26 at 13 20 25

Sơ đồ DomainModel luôn phải có

  • Entity, Value-Object
  • Các nghiệp vụ đi kèm

Ví dụ như: [Trạng Thái Tuyển Dụng] sẽ bao gồm những giá trị gì ? -> Đây chính là yếu tố cần phải cân nhắc.

Tuy nhiên khi tiến hành mô hình hoá có thể sẽ có những yếu tố chưa thể quyết định ngay được, hãy ghi chú lại trong bản thiết kế cho dù điều này sẽ khiến bản thiết kế của bạn trông không được chuẩn chỉnh cho lắm.

(4) chính là độ đa dạng - Liệu giữa Kết quả tuyển dụngVị trí tuyển dụng có cần một mối liên hệ nào đó hay không ? Đây là điều cần phải quyết định

(5) Quyết định phạm vi của Aggregate: với 1 Repository sẽ có 1 Aggregate tương ứng. Ta cần cân nhắc những:

  • Entity
  • Value-Object
  • Repository

nào sẽ được thực thi bên trong một Aggregate. Tuy vậy trong thực tế sẽ có những Aggregate nếu không thực thi trong thực tế sẽ không thấy rõ được tính hiệu quả của chúng. Vậy nên hãy quyết định Aggregate sớm nhất, thực thi nó sớm nhất và chỉnh sửa nó nếu cần thiết.

Trong sơ đồ DomainModel, tham chiếu bên trong Aggregate sẽ được coi là Composition, còn tham chiếu ra bên ngoài sẽ được biểu thị bởi dấu mũi tên như ở sơ đồ trên.

Hãy quyết định các thuật ngữ trong sơ đồ bằng tiếng Anh nhanh nhất có thể để từ đó làm cơ sở trong thiết kế:

  • Endpoint URL
  • Table Name
  • ...

Tầm quan trọng của sơ đồ object

Với những người mới bắt đầu (khi chưa có kiến thức về nghiệp vụ) thì việc đọc hiểu sơ đồ DomainModel là một điều rất khó khăn. Cách tiếp cận đi từ Cụ thể -> Trừu tượng sẽ dễ dàng hơn là đi từ Trừu tượng -> Cụ thể. Vậy nên sơ đồ object sẽ rất quan trọng trong những tình huống như vậy.

Trình tự thiết kế sơ đồ object và sơ đồ domain model

Lý thuyết sẽ nói rằng chúng sẽ được thiết kế cùng một lúc nhưng trong thực tế thì sẽ đi từ Cụ thể -> Trừu tượng từng bước một.

Update object con của Aggregate như thế nào ?

Lấy ví dụ với User Entity với mailAddresses là tập hợp các Email của user đó.

class User {
  name: string;
  mailAddresses: Email[];
}

abstract class UserRepository {
  insert(user: User);
  update(user: User);
}

Xét method update, ta truyền vào nó một user instance với nhiều EmailAddresses là object con. Vấn đề bây giờ là sẽ tạo mới MailAddress hay cập nhật hay xoá ?

Cách đơn giản nhất đó là xoá toàn bộ các bản ghi email_address gắn liền với user và insert mới toàn bộ các email mà user entity đang có. Cách làm này khó phát sinh bug và rất đơn giản để triển khai, xong vấn đề là nếu như một bảng khác cũng sử dụng khoá ngoại là email address hoặc giá trị created_at cần thiết cho xử lí logic thì cách làm này không phát huy được hiệu quả.

Ngoài ra còn một cách khác đó là so sánh các email hiện có trong DB với các email truyền vào, xử lí các điều kiện rẽ nhánh if để từ đó đưa ra kết luận cuối cùng đó là xoá, update hay tạo mới`. Tuy nhiên cách làm này khá phức tạp và cần phải viết test cẩn thận.

Nên bắt đầu từ đâu ?

Có 2 cách tiếp cận:

  • Domain modeling: Với những thứ được làm mới, ngay từ đầu cần có sự hiểu rõ vấn đề, đối sách phù hợp cũng như có được phương án cải thiện những cái đã có sẵn.
  • Làm theo các pattern: chú trọng vào tính mở rộng của hệ thống, không quan trọng làm mới hay cải thiện những cái đã có

Sự gắn kết giữa việc triển khai và Modeling

Sau khi hoàn thành sơ đồ DomainModel sẽ là quá trình triển khai. Trong quá trình dev, sẽ phát sinh nhiều vấn đề mà ta cần phải cập nhật DomainModel cũng như cập nhật source code.

Tuy nhiên khi model thay đổi thì việc cập nhật lại source code không hẳn đã đơn giản vì cần phải có sự cân nhắc về việc thay đổi phần nào sao cho phù hợp nhất.

Do đó cần phải làm cho Model và Code gần nhau nhất có thể. Để có thể làm được điều đó ta có thể áp dụng Best Practice đó là Entity và Value-Object pattern.

Tuy nhiên khi thay đổi code, ta cần phải thực thi Regression Test bằng tay một cách thường xuyên, dẫn đến việc nếu thực thi test không đầy đủ có thể xảy ra trường hợp code không phản ánh đúng nghiệp vụ hoặc phát sinh bug. Để tránh điều đó xảy ra với DDD thì việc thực thi test tự động là một điều vô cùng quan trọng.

Cách sử dụng ORM Class

Như phần trước đã nói, ORM Class chỉ nên được sử dụng trong tầng infrastructure chứ không nên được sử dụng ở tầng domain hoặc tầng usecase ngoài ra cũng không nên sử dụng ORM Class như là Entity hoặc VO.

Mà nên sử dụng bằng cách truyền các entity hoặc VO vào trong một ORM Class dưới dạng tham số truyền vào các hàm insert hoặc update của Repository.

Còn với các method liên quan đến tham chiếu, ta sẽ sử dụng ORM Class để lấy dữ liệu từ DB rồi tạo các instance của entity hoặc VO dựa theo kết quả từ DB.

Sử dụng domain service

Việc gắn thêm chữ Service cho DomainService class name là một việc làm không được khuyến khích, nguyên nhân là vì nó khiến cho class đó không biểu thị rõ ràng nghiệp vụ mà nó đảm nhận.

class CreateTaskUseCase {
  constructor(taskCreator: TaskCreator) {}
}

Ở ví dụ trên thì TaskCreator là Domain Service. Việc đảm bảo tính đồng bộ giữa 2 Aggregates sẽ được chuyển qua TaskCreator chứ không phải thực hiện ở UseCase.

class TaskCreator {
  create(taskName: string) {
    const task = new Task(taskName);
    taskRepository.insert(task);

    const activityHistory = ActivityHistory.createFromTask(task);
    activityRepository.insert(task);
  }
}

Thế nhưng việc làm này cũng không thể phòng tránh được việc các use-case khác phá vỡ đi tính đồng bộ. Để giải quyết vấn đề này ta cần xem xét đến visualization của các class, interface (public / private) - việc điều chỉnh này sẽ tuỳ vào từng ngôn ngữ mà có sự khác biệt nhất định. Phần triển khai dưới đây là dành cho typescript.

class TaskEntity {
  constructor(taskName: string) {
    this.taskName = taskName;
  }

  create(param: TaskCreateParameter): TaskEntity {
    return TaskEntity(param.taskName);
  }
}

create method của TaskEntity class sử dụng TaskCreateParameter cho tham số đầu vào. Định nghĩa TaskCreateParameter sẽ nằm trong DomainService.

public interface TaskCreateParameter {
  taskName: string;
}

private class TaskCreateParameterImpl implements TaskCreateParameter {
  taskName: string;
}

class TaskCreator {
  constructor(
    taskRepository: taskRepository,
    activityHistoryRepository: ActivityHistoryRepository
  ) {}

  create(taskName: string) {
    const task = Task.create(TaskCreateParameterImpl(taskName));
    taskRepository.insert(task);

    const activityHistory = ActivityHistory(task);
    activityHistoryRepository.insert(task);
  }
}

class TaskCreateParameterImpl dùng để truyền vào hàm create của TaskEntity class, nhưng nó được định nghĩa là private class do đó ở use-case khác nếu sử dụng TaskCreateParameterImpl sẽ dẫn tới lỗi biên dịch.

class CreateWrongTaskUsecase {
  execute(taskName: string) {
    const task = Task.create(TaskCreateParameterImpl(taskName));
    taskRepository.insert(task);
  }
}

Lúc này việc tạo task chỉ có thể được thực hiện trong TaskCreator mà thôi, qua đó tránh được vấn đề bất đồng bộ giữa các Aggregates.

Tuy nhiên cách làm này có nhược điểm là sẽ làm cho DomainService ngày một phình to trong khi đó DomainEntity và Value-Object sẽ ngày càng thu nhỏ lại (không còn chứa nhiều domain logic nữa). Qua đó sẽ dẫn tới việc cách viết code của chúng ta sẽ quay trở về thời kì "tiền DDD".

Để tránh điều này xảy ra có 2 biện pháp sau đây:

  • Đầu tiên là giảm thiểu tối đa việc sử dụng DomainService, chỉ dùng nó khi thực sự cần thiết. Kể cả khi sử dụng nó, ta cũng không nên dồn quá nhiều domain logic vào đây.
  • Ngoài ra không nên đặt tên theo format ABCXYZService, hãy đặt một cái tên phản ánh đúng nghiệp vụ mà nó đảm nhận, qua đó giúp ta có thể tránh việc thêm quá nhiều các methods thừa thãi khác vào class.

Các giá trị được lưu trong entity

Sử dụng các cột được tạo tự động trong ORM - với các thuộc tính created_at hoặc updated_at thường được tạo tự động bở ORM thì sử dụng chúng ở màn hình liệu có được không ?

Nếu trong trường hợp có hiển thị trên màn hình thì nên định nghĩa các thuộc tính này trong các class Entity hoặc VO và sử dụng các setter method được định nghĩa trong các class đó. Có 2 lí do cho điều đó:

  1. Thời điểm thiết lập giá trị cho chúng - Vì từ khi entity được tạo cho đến khi truyền vào Repository (do ORM chỉ có thể được sử dụng trong Repository mà thôi) thì các giá trị này sẽ không được thiết lập dẫn đến việc tồn tại các entity instance với created_atupdated_at = null . Đây là điều không nên có.

  2. Giá trị được thiết lập - Do thời gian được chuyển xuống dưới ORM và thời gian logic ở tầng domain diễn ra là hoàn toàn khác nhau cũng như phía ORM không thể biết chính xác thời điểm logic ở tầng domain diễn ra nên nếu muốn chính xác thời gian diễn ra logic thì nên thiết lập giá trị created_atupdated_at tại tầng domain.

Sample Code

Với các Factory class, ta nên để các method create, reconstruct dưới dạng static method. Factory class sẽ làm nhiệm vụ tạo các entity instance dựa trên dữ liệu lấy ra từ DB.

Với Value-Object ta có thể triển khai dưới dạng enum như sau:

enum ScreeningStatus {
  IN_PROGRESS = 'IN_PROGRESS',
  ADOPTED = 'ADOPTED',
  REJECTED = 'REJECTED',
}

hoặc dạng class như sau:

class ScreeningId {
  value: string;

  constructor(value: string) {
    this.value = value;
  }
}

Sử dụng DomainEvent

Chúng ta sẽ thử thêm DomainEvent vào sơ đồ domain model.

Screen Shot 2022-03-19 at 12 32 14

ActivityHistory sẽ không phụ thuộc vào Task nữa mà sẽ phụ thuộc vào TaskCreatedEvent. Nếu đi theo hướng này, flow thực thi sẽ như sau:

  1. Tạo DomainEvent tại một thời điểm nhất định trong method xử lí của entity, lưu event vào tập hợp các events của entity
  2. Sau khi method insert/ update của Repository thành công, sẽ Publish event.
  3. EventListener sẽ bắt các event này và tiến hành xử lí các Aggregates khác. (việc bắt event sẽ phụ thuộc vào từng framework mà có cách viết, triển khai khác nhau).
class CreateTaskUseCase {
  execute() {
    const task = TaskEntity.create(taskName); // tại đây sẽ tạo domain event và lưu vào tập các events của entity
    taskRepository.insert(task, domainEventPublisher); // sau khi insert thành công, sẽ tiến hành publish event nhờ vào domainEventPublisher
  }
}

abstract class DomainEventPublisher {
  publish(event: DomainEvent);
}

class ActivityHistoryEventListener {
  @OnEvent()
  createActivityHistory(event: TaskCreatedEvent) {
    const activityHistory = ActivityHistory.fromTaskCreatedEvent(event);
    activityHistoryRepository.insert(activityHistory);
  }
}

Sử dụng DomainEvent sẽ có 3 ưu điểm sau:

  1. Đảm bảo được tính đồng bộ giữa các Aggregates.
  2. Usecase, EventListner có thể được triển khai một cách đơn giản hơn
  3. Mối liên hệ giữa các Aggregates cũng sẽ được thấy rõ ràng hơn khi thông qua event.

Về ưu điểm thứ nhất ta thấy rằng khi tạo Task xong, việc tạo ActivityHistory bắt buộc phải thông qua ActivityHistoryEventListener nên sẽ tránh tình trạng use-case phá vỡ đi sự đồng bộ giữa các Aggregates.

Về ưu điểm thứ 2 ta thấy việc không phải giới hạn truy cập (public / private) cũng khiến việc triển khai đơn giản đi rất nhiều.

Tuy nhiên nhược điểm của cách này là ta cần phải giải thích chi tiết về nó cho các thành viên khác trong team.

Quá trình publish event được triển khai ở tầng infra sẽ trông như sau

class TaskRepository {
  insert(task: TaskEntity, domainEventPublisher: DomainEventPublisher) {
    task.getDomainEvents().forEach(event => domainEventPublisher.publish(event));
    task.clearEvents(); // Xoá mọi events sau khi đã publish xong
  }
}

Ta thấy ở đây cần truyền domainEventPublisher param vào insert method. Đây là một ngoại lệ vì về lí thuyết ta chỉ nên truyền entity vào repository mà thôi. Tuy nhiên nếu không thể hiện rõ thông qua việc truyền param thì sẽ rất khó truy vết được event. Ngoài ra còn có một sự lựa chọn khác đó là DI.

Tuy nhiên có một vấn đề đó là khi xem code của use-case ta không thể biết được DomainEvent được tạo khi nào. Dưới đây là một giải pháp:

class CreateTaskUseCase {
  constructor(
    private taskRepository: TaskRepository,
    private domainEventPublisher: DomainEventPublisher,
    private domainEventSeedFactory: DomainEventSeedFactory,
  ) {}

  execute(taskName: string) {
    const seed = domainEventSeedFactory.createSeed();
    const task = Task.create(taskName, seed);
    taskRepository.insert(task, domainEventPublisher);
  }
}

Ta cần truyền DomainEventSeed vào create method của TaskEntity. Sẽ có câu hỏi đặt ra là "Liệu việc truyền DomainEventSeed vào có ảnh hưởng đến các Aggregate khác hay không ?". Câu trả lời là chỉ cần xem các DomainEvent được tạo ra trong method là có thể biết được.

class DomainEvent {
  seed: DomainEventSeed;
}

public interface DomainEventSeed {}

private class DomainEventSeedImpl {}

class DomainEventSeedFactory {
  createSeed(): DomainEventSeed {
    return DomainEventSeedImpl();
  }
}

Việc đặt DomainEventSeedImpl là private class sẽ giúp hạn chế việc tạo DomainEventSeed lung tung ở nhiều chỗ, khi đó việc tạo DomainEventSeed bắt buộc phải thông qua DomainEventSeedFactory.

Phân biệt giữa Entity / Value-Object với Data Model / ORM Class

Entity và Value-Object (VO) là các class được sử dụng để biểu thị DomainModel. Trong quá trình triển khai, ta nên tránh việc các yếu tố như RDB Table hoặc ORM ảnh hưởng tới Entity và VO.

Ngược lại thì Data Model dùng để lưu trữ dữ liệu vào DB, với RDB sẽ được biểu thị thông qua các Table. ORM Class là các class dùng để Mapping giữa Data của chương trình với Database.

Xét ví dụ với Domain Model như sau:

Screen Shot 2022-02-26 at 17 43 06

Nếu sử dụng Entity / VO ta sẽ có source code như sau:

class User {
  userId: String;
  name: String;
  mailAddresses: MailAddress[];
}

class MailAddress {
  value: string;

  constructor() {
    // ...
  }
}

Với DataModel ta sẽ có như sau:

Screen Shot 2022-02-26 at 17 43 06

class User {
  userId: string;
  name: string;
  mailAddresses: UserMailAddress[];
}

class UserMailAddress {
  user: User;
  mailAddress: string;
}

User và UserAddress có quan hệ thông qua userId. userId là khoá ngoại được kéo từ bảng User.
Ta thấy rằng với Data Model ta cần phải truyền vào UserMailAddress một user instance. Điều này sẽ gây ra khó khăn cho quá trình viết unit test vì ngoài việc test UserMailAddress ta còn phải quan tâm đến mối liên hệ giữa UserMailAddressUser. Với Enttiy / VO ta chỉ cần quan tâm đến User là đủ.

Liệu có thể triển khai insert và update cùng một lúc ?

Hoàn toàn được. Tuy nhiên nên cân nhắc giữa ưu, nhược điểm của phương pháp này.
Ưu điểm đó là việc chỉ cần gọi đến một method duy nhất mà không cần phân biệt đâu là save, đâu là update.
Nhược điểm đó là phần triển khai trong repository sẽ rất phức tạp do việc phân biệt rõ ràng giữa saveupdate. Từ đó dẫn đến tính mở rộng, bảo trì của code sẽ bị ảnh hưởng.

Trên thực tế việc chia ra save, update ngay từ tầng domain cũng không phải là ít.

Có thể update nhiều entities cùng một lúc ?

Hoàn toàn có thể. Ngoài việc update riêng lẻ từng entity một, ta hoàn toàn có thể gom nhóm để update nhiều entities cùng một lúc.

abstract class UserRepository {
  insert(user: User);
  insert(users: User[]);
}

Hàm insert 1 user duy nhất được gọi từ use-case tuy nhiêu nếu được gọi đi gọi lại nhiều lần với 1 use-case sẽ gây ảnh hưởng đến performance của hệ thống nên bạn có thể cân nhắc việc sử dụng hàm batch-insert. Tuy nhiên độ phức tạp khi sử dụng hàm batch-insert sẽ tăng lên đôi chút, số lượng test-case cũng sẽ tăng theo nên nếu không có vấn đề đáng kể với performance thì việc sử dụng batch-insert cũng không quá cần thiết.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.