Skip to content

Commit

Permalink
Merge pull request #116 from falsy/feature/115/updateExamples closed #…
Browse files Browse the repository at this point in the history
…115

Feature/115/update examples
  • Loading branch information
falsy authored Feb 18, 2025
2 parents f76c2cd + 33c28f1 commit e666612
Show file tree
Hide file tree
Showing 40 changed files with 701 additions and 234 deletions.
Binary file modified .yarn/install-state.gz
Binary file not shown.
113 changes: 102 additions & 11 deletions README-ko.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,11 +59,13 @@
│ ├─ dtos
│ └─ infrastructures
│ └─ interface
├─ client-a
├─ client-a(built with React)
│ └─ src
│ ├─ di
│ └─ ...
└─ client-b
└─ client-b(built with Next.js)
└─ src
├─ di
└─ ...
```

Expand Down Expand Up @@ -150,7 +152,7 @@ Presenters 레이어에서는 UI에서 필요로하는 메서드를 가지고
Vite, React, Jotai, Tailwind CSS, Jest, RTL, Cypress
```

client-a는 `Domains``Adapters` 레이어의 요소들을 그대로 사용해서 최종적으로 `DI`된 상위 레이어의 객체를 React의 Hooks와 전역 상태 라이브러리인 [Jotai](https://jotai.org/)를 활용하여 각 도메인의 메서드를 구현하고 이는 Presenters 레이어의 역할을 수행합니다.
Client-A는 `Domains``Adapters` 레이어의 요소들을 그대로 사용해서 최종적으로 `DI`된 상위 레이어의 객체를 React의 Hooks와 전역 상태 라이브러리인 [Jotai](https://jotai.org/)를 활용하여 각 도메인의 메서드를 구현하고 이는 Presenters 레이어의 역할을 수행합니다.

> 기존에 Adapters 패키지에서 Presenters 디렉토리로 명시적으로 Presenters 레이어를 나누었지만 이는 프레임워크에 의존하지 않은 범용적인 Presenters이며, 위 샘플 프로젝트처럼 React를 사용하는 서비스에서는 그에 부합하는 구성을 위해서 최종적으로 의존성을 주입한 Presenters 객체와 React Hooks을 활용하여 Presenters 영역을 확장 구성하였습니다.
Expand All @@ -174,31 +176,118 @@ export default function di() {
### Presenters

```tsx
import { useCallback, useMemo, useTransition } from "react"
import { useCallback, useMemo, useOptimistic, useState, useTransition } from "react"
import { atom, useAtom } from "jotai"
import IPost from "domains/aggregates/interfaces/IPost"
import Post from "domains/aggregates/Post"
import presenters from "../di"
import PostVM from "../vms/PostVM"
import IPostVM from "../vms/interfaces/IPostVM"

const PostsAtoms = atom<IPost[]>([])
const PostsAtoms = atom<IPostVM[]>([])

export default function usePosts() {
const di = useMemo(() => presenters(), [])

const [posts, setPosts] = useAtom<IPost[]>(PostsAtoms)
const [post, setPost] = useState<IPostVM>(null)
const [posts, setPosts] = useAtom<IPostVM[]>(PostsAtoms)
const [optimisticPost, setOptimisticPost] = useOptimistic(post)
const [optimisticPosts, setOptimisticPosts] = useOptimistic(posts)
const [isPending, startTransition] = useTransition()

const getPosts = useCallback(async () => {
startTransition(async () => {
const resPosts = await di.post.getPosts()
setPosts(resPosts)
const postVMs = resPosts.map((post) => new PostVM(post))
setPosts(postVMs)
})
}, [di.post, setPosts])

...
}
```

### View Models

Client-A에서는 프로젝트 레이어에서 React의 UI 상태 관리에 적합하도록 View Model을 구성하여 사용하였습니다.

```ts
import CryptoJS from "crypto-js"
import IUserInfoVO from "domains/vos/interfaces/IUserInfoVO"
import ICommentVM, { ICommentVMParams } from "./interfaces/ICommentVM"

export default class CommentVM implements ICommentVM {
readonly id: string
key: string
readonly postId: string
readonly author: IUserInfoVO
content: string
readonly createdAt: Date
updatedAt: Date

constructor(parmas: ICommentVMParams) {
this.id = parmas.id
this.postId = parmas.postId
this.author = parmas.author
this.content = parmas.content
this.createdAt = parmas.createdAt
this.updatedAt = parmas.updatedAt
this.key = this.generateKey(this.id, this.updatedAt)
}

updateContent(content: string): void {
this.content = content
this.updatedAt = new Date()
this.key = this.generateKey(this.id, this.updatedAt)
}

applyUpdatedAt(date: Date): void {
this.updatedAt = date
this.key = this.generateKey(this.id, this.updatedAt)
}

private generateKey(id: string, updatedAt: Date): string {
const base = `${id}-${updatedAt.getTime()}`
return CryptoJS.MD5(base).toString()
}
}
```

View Model에서는 위와 같이 값 변경에 따른 메서드를 제공하며(ex. updateContent) 모든 변경에는 updatedAt 값이 함께 변경하고, updatedAt 값과 ID 값을 활용하여 고유한 `Key` 값을 만들어 사용함으로써 React가 View의 변경을 감지하고 리렌더링 할 수 있도록 하였습니다.

```tsx
...

export default function usePosts() {
...

const deleteComment = useCallback(
async (commentId: string) => {
startTransition(async () => {
setOptimisticPost((prevPost) => {
prevPost.deleteComment(commentId)
return prevPost
})

try {
const isSucess = await di.post.deleteComment(commentId)
if (isSucess) {
const resPost = await di.post.getPost(optimisticPost.id)
const postVM = new PostVM(resPost)
setPost(postVM)
}
} catch (e) {
console.error(e)
}
})
},
[di.post, optimisticPost, setOptimisticPost, setPost]
)

...
}
```

Presenter 레이어의 Hooks에서도 위와 같이 Comment의 삭제 요청에 대한 간단한 예시로, VM에서 제공하는 메서드를 활용하여 낙관적 업데이트를 구현하고 요청이 성공하면 위 변경이 적용된 새로운 데이터를 요청하여 동기화 하도록 하였습니다.

## Client-B

### Use Stack
Expand All @@ -207,9 +296,11 @@ export default function usePosts() {
Next.js, Jotai, Tailwind CSS, Jest, RTL, Cypress
```

client-b 서비스는 client-a 서비스와 동일한 도메인을 활용한, 서비스 확장을 표현하는 서비스로 client-a 서비스와 유사하지만 client-a 서비스와 다르게 Next.js를 기반으로 기존의 client-a 서비스는 API 서버와의 HTTP 통신을 통해 데이터를 조작하지만 client-b는 HTTP 통신 없이 로컬 저장소(Local Storage)를 기반으로 설계하였습니다.
Client-B는 Client-A와 동일한 도메인을 활용한, 서비스 확장을 표현하는 서비스로 Client-A 서비스와 유사하지만 Client-A 서비스와 다르게 Next.js를 기반으로 하며 기존의 Client-A 서비스는 API 서버와의 HTTP 통신을 통해 데이터를 조작하지만 Client-b는 HTTP 통신 없이 로컬 저장소(Local Storage)를 기반으로 설계하였습니다.

그렇기 때문에 Client-A와 다르게 Client-B에서는 `Domains`에서 정의한 Repository의 인터페이스를 구체화한 새로운 Repository를 구성하고 이를 의존성 주입하여 사용함으로써 간단하게 기존의 서비스를 확장한 새로운 서비스를 구현할 수 있습니다.

그렇기 때문에 client-a와 다르게 client-b에서는 `Domains`에서 정의한 Repository의 인터페이스를 구체화한 새로운 Repository를 구성하고 이를 의존성 주입하여 사용함으로써 간단하게 기존의 서비스를 확장한 새로운 서비스를 구현할 수 있습니다.
> Client-B는 구제적인 기능 구현보다는 동일한 도메인을 활용한 다른 클라이언트 서비스 구성에 대한 간단한 예시입니다.
## Design System

Expand Down
119 changes: 105 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,17 +62,19 @@ In the monorepo structure, the Domains layer, Adapters layer, and Service layer
│ ├─ dtos
│ └─ infrastructures
│ └─ interface
├─ client-a
├─ client-a(built with React)
│ └─ src
│ ├─ di
│ └─ ...
└─ client-b
└─ client-b(built with Next.js)
└─ src
├─ di
└─ ...
```

## Tree Shaking

In this sample project, service packages use shared packages (`Domains`, `Adapters`, `and other potential packages`) through a `Source-to-Source` approach, rather than referencing pre-built outputs. This approach ensures that the services module bundler can effectively eliminate unused code during the final build. Therefore, all shared packages must be written using `ES Modules`.
In this sample project, service packages use shared packages (`Domains`, `Adapters`, `and other potential packages`) through a `Source-to-Source` approach, rather than referencing pre-built outputs. This approach ensures that the service's module bundler can effectively eliminate unused code during the final build. Therefore, all shared packages must be written using `ES Modules`.

> Most module bundlers natively support tree shaking for code written in ES Modules.
Expand All @@ -92,7 +94,7 @@ In the sample project, there are three entities: Post, Comment, and User.

Clean Architecture shares a common goal with DDD in pursuing domain-centric design. While Clean Architecture focuses on structural flexibility, maintainability, technological independence, and testability of software, DDD emphasizes solving complex business problems.

However, Clean Architecture adopts some of DDDs philosophy and principles, making it compatible with DDD and providing a framework to effectively implement DDD concepts. For example, Clean Architecture can leverage DDD concepts such as `Ubiquitous Language` and `Aggregate Root`.
However, Clean Architecture adopts some of DDD's philosophy and principles, making it compatible with DDD and providing a framework to effectively implement DDD concepts. For example, Clean Architecture can leverage DDD concepts such as `Ubiquitous Language` and `Aggregate Root`.

### Ubiquitous Language

Expand All @@ -108,7 +110,7 @@ Ubiquitous Language refers to a shared language used by all team members to main

An Aggregate is a consistency boundary that can include multiple entities and value objects. It encapsulates internal state and controls external access. All modifications must go through the Aggregate Root, which helps manage the complexity of relationships within the model and maintain consistency when services expand or transactions become more complex.

In the sample project, Post serves as an Aggregate, with the Comment entity having a dependent relationship on it. Therefore, adding or modifying a comment must be done through the Post entity. Additionally, while the Post entity requires information about the author (the User who wrote the post), the User is an independent entity. To maintain a loose relationship, only the Users id and name are included as a Value Object within Post.
In the sample project, Post serves as an Aggregate, with the Comment entity having a dependent relationship on it. Therefore, adding or modifying a comment must be done through the Post entity. Additionally, while the Post entity requires information about the author (the User who wrote the post), the User is an independent entity. To maintain a loose relationship, only the User's id and name are included as a Value Object within Post.

## Use Cases

Expand Down Expand Up @@ -153,7 +155,7 @@ The sample project's client services consist of two simple services: client-a an
Vite, React, Jotai, Tailwind CSS, Jest, RTL, Cypress
```

client-a directly utilizes elements from the `Domains` and `Adapters` layers and implements methods for each domain using React hooks and the global state management library [Jotai](https://jotai.org/). These methods act as the Presenters layer in the final service.
Client-A directly utilizes elements from the `Domains` and `Adapters` layers and implements methods for each domain using React hooks and the global state management library [Jotai](https://jotai.org/). These methods act as the Presenters layer in the final service.

> Previously, the Adapters package explicitly included a Presenters directory to represent a framework-agnostic Presenters layer. However, in services like this sample project that use React, we extend the Presenters layer by injecting dependencies into the final Presenters objects and utilizing React hooks to achieve a composition that aligns with the framework.
Expand All @@ -177,31 +179,118 @@ export default function di() {
### Presenters

```tsx
import { useCallback, useMemo, useTransition } from "react"
import { useCallback, useMemo, useOptimistic, useState, useTransition } from "react"
import { atom, useAtom } from "jotai"
import IPost from "domains/aggregates/interfaces/IPost"
import Post from "domains/aggregates/Post"
import presenters from "../di"
import PostVM from "../vms/PostVM"
import IPostVM from "../vms/interfaces/IPostVM"

const PostsAtoms = atom<IPost[]>([])
const PostsAtoms = atom<IPostVM[]>([])

export default function usePosts() {
const di = useMemo(() => presenters(), [])

const [posts, setPosts] = useAtom<IPost[]>(PostsAtoms)
const [post, setPost] = useState<IPostVM>(null)
const [posts, setPosts] = useAtom<IPostVM[]>(PostsAtoms)
const [optimisticPost, setOptimisticPost] = useOptimistic(post)
const [optimisticPosts, setOptimisticPosts] = useOptimistic(posts)
const [isPending, startTransition] = useTransition()

const getPosts = useCallback(async () => {
startTransition(async () => {
const resPosts = await di.post.getPosts()
setPosts(resPosts)
const postVMs = resPosts.map((post) => new PostVM(post))
setPosts(postVMs)
})
}, [di.post, setPosts])

...
}
```

### View Models

In Client-A, we structured the View Model in the project layer to effectively manage UI state in React.

```ts
import CryptoJS from "crypto-js"
import IUserInfoVO from "domains/vos/interfaces/IUserInfoVO"
import ICommentVM, { ICommentVMParams } from "./interfaces/ICommentVM"

export default class CommentVM implements ICommentVM {
readonly id: string
key: string
readonly postId: string
readonly author: IUserInfoVO
content: string
readonly createdAt: Date
updatedAt: Date

constructor(parmas: ICommentVMParams) {
this.id = parmas.id
this.postId = parmas.postId
this.author = parmas.author
this.content = parmas.content
this.createdAt = parmas.createdAt
this.updatedAt = parmas.updatedAt
this.key = this.generateKey(this.id, this.updatedAt)
}

updateContent(content: string): void {
this.content = content
this.updatedAt = new Date()
this.key = this.generateKey(this.id, this.updatedAt)
}

applyUpdatedAt(date: Date): void {
this.updatedAt = date
this.key = this.generateKey(this.id, this.updatedAt)
}

private generateKey(id: string, updatedAt: Date): string {
const base = `${id}-${updatedAt.getTime()}`
return CryptoJS.MD5(base).toString()
}
}
```

The View Model provides methods to handle value changes (e.g., updateContent). Whenever a value is updated, the updatedAt field is also modified. By using a combination of the updatedAt value and the ID, we generate a unique `key` that allows React to detect changes in the view and trigger re-renders as needed.

```tsx
...

export default function usePosts() {
...

const deleteComment = useCallback(
async (commentId: string) => {
startTransition(async () => {
setOptimisticPost((prevPost) => {
prevPost.deleteComment(commentId)
return prevPost
})

try {
const isSucess = await di.post.deleteComment(commentId)
if (isSucess) {
const resPost = await di.post.getPost(optimisticPost.id)
const postVM = new PostVM(resPost)
setPost(postVM)
}
} catch (e) {
console.error(e)
}
})
},
[di.post, optimisticPost, setOptimisticPost, setPost]
)

...
}
```

In the Presenter layer's hooks, we implemented optimistic updates using the methods provided by the View Model. For instance, when sending a delete request for a comment, we immediately apply the changes locally. After the request succeeds, we fetch the updated data to synchronize the state.

## Client-B

### Use Stack
Expand All @@ -210,9 +299,11 @@ export default function usePosts() {
Next.js, Jotai, Tailwind CSS, Jest, RTL, Cypress
```

The client-b service is an extension of client-a, utilizing the same domain model to demonstrate service scalability. While it shares similarities with client-a, the key difference is that client-b is built on Next.js. Unlike client-a, which manipulates data through HTTP communication with an API server, client-b is designed to operate without HTTP communication, relying instead on local storage.
The Client-B service is an extension of Client-A, utilizing the same domain model to demonstrate service scalability. While it shares similarities with Client-A, the key difference is that Client-B is built on Next.js. Unlike Client-A, which manipulates data through HTTP communication with an API server, Client-B is designed to operate without HTTP communication, relying instead on local storage.

Therefore, unlike Client-A, Client-B implements new repositories by concretely defining the repository interfaces from `Domains` and injecting these dependencies to create a new service that extends the existing functionality in a straightforward manner.

Therefore, unlike client-a, client-b implements new repositories by concretely defining the repository interfaces from `Domains` and injecting these dependencies to create a new service that extends the existing functionality in a straightforward manner.
> Client-B is a simple demonstration of another client service utilizing the same domain, focusing on the service structure rather than specific feature implementations.
## Design System

Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,8 @@
"typescript": "^5.6.3"
},
"resolutions": {
"path-to-regexp": "0.1.12"
"path-to-regexp": "0.1.12",
"esbuild": "0.25.0"
},
"packageManager": "[email protected]"
}
6 changes: 5 additions & 1 deletion packages/adapters/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,11 @@
},
"dependencies": {
"axios": "^1.7.7",
"crypto-js": "^4.2.0",
"domains": "workspace:*"
},
"packageManager": "[email protected]"
"packageManager": "[email protected]",
"devDependencies": {
"@types/crypto-js": "^4"
}
}
8 changes: 4 additions & 4 deletions packages/adapters/src/__test__/dtos/UserDTO.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,16 @@ describe("UserDTO", () => {
id: "12345",
name: "Falsy",
email: "[email protected]",
createdAt: new Date("2023-01-01T00:00:00Z"),
updatedAt: new Date("2023-01-02T00:00:00Z")
createdAt: "2023-01-01T00:00:00Z",
updatedAt: "2023-01-02T00:00:00Z"
}

const user = new UserDTO(params)

expect(user.id).toBe("12345")
expect(user.name).toBe("Falsy")
expect(user.email).toBe("[email protected]")
expect(user.createdAt).toEqual(new Date("2023-01-01T00:00:00Z"))
expect(user.updatedAt).toEqual(new Date("2023-01-02T00:00:00Z"))
expect(user.createdAt).toEqual("2023-01-01T00:00:00Z")
expect(user.updatedAt).toEqual("2023-01-02T00:00:00Z")
})
})
Loading

0 comments on commit e666612

Please sign in to comment.