Tại sao cần tránh phụ thuộc vòng (Circular dependency)

Chào mọi người. Em đang làm 1 bài tập lớn, trong quá trình code em có 1 class rất lớn hơn 1000 dòng tên là ServerController nên em muốn tách ra thêm 1 class là ServerManager để 1 class không quá lớn. Nhưng do 2 class này đều dùng thuộc tính của nhau nên em đang thiết kế là: class ServerController có chứa đối tượng ServerManager và ServerManager cũng chứa ServerController. Như thế gặp trường hợp Phụ thuộc vòng (Circular dependency). Mọi người cho em hỏi là thiết kế như vậy thì có gặp vấn đề gì không ạ? Em xin cảm ơn.

1 Like

Bạn có thể tìm hiểu thêm về “friend class”

3 Likes

https://www.bettercodebytes.com/how-to-avoid-static-helper-classes/

4 Likes

Trường hợp này nến tách ra 1 thằng thứ 3 để 2 thằng cùng phụ thuộc vào nó.
Thiết kế của em đang như vầy:

A---------->B(A call B , A phụ thuộc vào B)
A<---------B(B call A, B phụ thuộc vào C)

Chuyển sang như thế này

A ---->C
B --->C

Tao thêm C để 2 thằng A,B phụ thuộc vào.

Khó maintain, dễ lỗi.Hình dung khi code em ngày càn phình to. nó sẽ giống hình bên trái.


Cố gắng làm cho giống hình bên phải.
Làm sao check cho toàn bộ project:
–> Dùng tools check: sonargraph

8 Likes

Thêm thông tin đi bạn, ngôn ngữ bạn đang dùng là gì? Back end mà bạn đang sử dụng framework nào?

Riêng câu trả lời trên của @nguyenhuuca là của bên Java.

7 Likes

Hi there,

Tớ không nghĩ đó là ý tưởng tốt đâu cậu :smile:

Như Nguyen Ca đã chỉ ra, việc phụ thuộc vòng không phải là dấu hiệu tốt trong design.
Tớ muốn đi sâu thêm vào việc cậu đang vướng mắc thật sự. Hi vọng comment này giúp cậu, như comment mà Ca chia sẻ :smile:

Here’s my two cents.
Trước tiên, tớ sẽ nói về vấn đề thực sự của cậu trước, rồi sẽ giải thích thêm về việc circular dependency.
Okay, vấn đề của cậu là cậu có 1 class lớn (ServerController), hơn 1000 dòng. Cậu đang muốn refactor nó (thao tác cậu tách ra 2 class được gọi là refactor), thành 2 hoặc nhiều class nhỏ hơn (điều này tốt, cậu sẽ cải thiện maintainability của hệ thống).
Khi cậu có 1 controller 1000 dòng, nó có nghĩa là:

  • Cậu đã để cho controller đó làm quá nhiều nhiệm vụ. Tớ đoán business logic của cậu cũng được đẩy hết vào controller.
  • Cậu đã để cho controller cover quá nhiều topic. ServerController có vẻ như khá generic, tớ đoán tất cả business, từ User, tới chợ, tới bầu cử tổng thống, tới thiên văn học… đều được để ở đây.

Để refactor controller đó, cậu cần:

  • Tách thành 2 layer: Controller để điều hướng, và Manager để chứa business logic. Đó là điều cậu đang cố gắng giải quyết.
  • Tách các topic từ controller này ra. Topic về bầu-cử-tổng-thống nên để ở Bầu-cử-tổng-thốngController, chứ không để ở ServerController.
  • Rename ServerController. Hm, nó hơi trừu tượng quá.

Đó là chiến lược tổng quát mà cậu nên thử. Tớ đoán cậu sẽ cần phối hợp cả 2 chiến lược, code hơn 1000 dòng, theo kinh nghiệm của tớ, sẽ có thể được tách thành chục class đó :smile:

Tiếp theo, về việc cậu design “2 class này đều dùng thuộc tính của nhau”, tớ đoán thuộc tính mà cậu nói tới là reference tới layer thấp hơn (như các đơn vị gọi tới CSDL). Hi vọng là như vậy, nếu không, cậu có vấn đề lớn hơn, khi lưu trữ trạng thái trong controller.
Vấn đề ở đây:

  • Controller là người điều hướng, tức là nó nên là người chuyển task cho các Manager (chứa business logic - người thực hiện nghiệp vụ) phù hợp. Controller hẳn nhiên không cần quan tâm tới những reference tới CSDL, phải không?
  • Manager, mặt khác, là người thực hiện nhiệm vụ, nên họ cần được hỗ trợ lấy dữ liệu. Vậy nên, Manager nên có các reference của đơn vị gọi tới CSDL hoặc 3rd party API.

Vậy nên, cậu có thể hình dung, ServerController chứa reference của ServerManager, và ServerManager chứa reference tới tầng thấp hơn. và ServerController muốn gì sẽ hỏi ServerManager để lấy. Việc đó sẽ khiến cho design của cậu trông tuyến tính hơn :smile:


Giờ, tớ sẽ giải thích việc design có phụ thuộc vòng, và design “tuyến tính” (tớ gọi nó là design tuyến tính, vì mỗi flow trông giống như 1 sợi dây vậy).
Lý do cậu nên yêu thích design tuyến tính hơn (tớ đang viết lại thứ có trong comment của @nguyenhuuca thôi :smile: cậu nên đọc comment của cậu ấy, nó có nhiều ý tưởng tốt lắm!):

  • Độ phức tạp của code sẽ giảm đi. Code dễ hiểu hơn => dễ maintain hơn
    Thử tưởng tượng nếu code của cậu có circular dependency, khi cậu track 1 chức năng ở controller, cậu thấy chức năng đó gọi 1 method khác ở manager, và khi vào đó, cậu phát hiện method đó lại gọi 1 method khác ở controller. Nếu code của cậu là 1 bộ phim, Nolan sẽ phải gọi cậu là “cụ”.
  • Cậu không thể viết unit test, trong đa số các TH, vì class của cậu phụ thuộc lẫn nhau, nên không có cách nào cô lập chúng cho unit test.
  • Nếu cậu có 1 team làm việc trên cùng controller đó, cậu sẽ có rất nhiều khoảnh khắc “Opps!” khi merge code.

Hi vọng câu trả lời này giúp cậu hiểu được vấn đề ở code của cậu, cách refactor code của cậu (ở mức chiến lược tổng quát), và lý do sao cậu nên refactor nó thành design không có phụ thuộc vòng.

10 Likes

Em có cố gắng tách nhỏ class ra và chia thành các class chức năng như sơ đồ class này ạ. Từ việc 2 class to là GameServer và GameManager phụ thuộc vòng thì em tách nhỏ thành 6 class nhỏ hơn là GameServer, PlayerController, ResourceController, MapController, TurnController và AutoPlayController. Như sơ đồ này em thấy vấn đề về phụ thuộc vòng và class quá lớn đã được giải quyết khi đưa về các class chức năng.

Code GameServer kiểu như này:
Demo1

Nhưng em gặp 1 VẤN ĐỀ là đối tượng là thể hiện của class PlayerController đều được các đối tượng là thể hiện của các class khác sử dụng. Vì dữ liệu người chơi thì được sử dụng ở rất nhiều logic khác nhau trong game mà. Em có 2 hướng giải quyết:

Thứ nhất là là trong các class còn lại để 1 tham chiếu đến PlayerController, sau đó khi khởi tạo sẽ truyền đối tượng PlayerController này vào. Như thế trong các thể hiện của class khác mà muốn sử dụng PlayerController đã có tham chiếu đến PlayerController nên có thể lấy để sứ dụng. Nhưng các này có vấn đề là phải khai báo PlayerController ở nhiều class. Em thấy thiết kế như vậy không hay, nó lại gây rối. Code sẽ kiểu như này:

**Thứ hai là: ** Do các đối tượng thể hiện của các class đều được khai báo khi khởi tạo đối tượng GameServer. Sau đó thì khi các đối tượng khác gọi hàm thực thi thì phải truyền đối tượng PlayerController vào nếu hàm đó muốn sử dụng PlayerController. Nhưng vấn đề các là như thế em sẽ phải thêm PlayerController vào nhiều param trong các hàm. Và khi code thực khi nhiều tầng gọi hàm thì PlayerController cũng sẽ phải truyền vào nhiều param của nhiều tầng. Cách thiết kế này em thấy cũng rối và không hay lắm. Code sẽ kiểu như này:

Em rất mong nhận được phản hồi và ý kiến của mọi người.
anh @library, @nguyenhuuca có cao kiến gì chỉ giáo cho em với ạ.

Hm,
Cậu có thể sử dụng singleton pattern cho playerController.

public class GameServer {
  private PlayerController playerController; // Cậu không có lý do gì để public `playerController`, trừ khi cậu code Unity :P
  //...

  public GameServer {
    this.playerController = PlayerController.getInstance();
    // ...
  }
}

Ngoài ra, tớ muốn hỏi tại sao cậu cần giữ playerController đồng nhất trong tất cả các class vậy?

3 Likes

Trường hợp của em không dùng Singleton được. Tại vì Class PlayerController sẽ có rất nhiều thể hiện. Còn lý do em muốn giữ PlayerController đồng nhất là vì em muốn class PlayerController sẽ quản lý các dữ liệu liên quan đến người chơi, tất cả các logic muốn truy cập hay thay đổi dữ liệu người chơi chỉ được thông qua bằng cách sử dụng các method (API) của PlayerController. Như thế sẽ bảo vệ được dữ liệu người chơi không thể bị thay đổi tuỳ tiện ở nhiều nơi mà được quản lý tập trung ở PlayerController.

hm, thực ra ý tớ muốn hỏi là tại sao cậu cần sử dụng 1 player controller duy nhất trong toàn bộ các controller và game server?
Có phải 1 instance PlayerController chỉ chứa dữ liệu của 1 người dùng nhất định, thay vì có thể access dữ liệu của 1 loạt người dùng không cậu?

3 Likes

Game mà em đang thiết kế là game online muti-player, game có nhiều phòng chơi hay map chơi. Mỗi map chơi sẽ có từ 2 đến 10 player. Thiết kế của em thì mỗi GameServer đại diện cho 1 phòng chơi hay map chơi. Các controller sẽ làm các nhiệm vụ cho mỗi phòng chơi hay map chơi đó. Vì vậy sẽ có nhiều thể hiện của GameServer, GameServer thì chức các controller. Vì PlayerController chứa toàn bộ dữ liệu của tất cả người chơi trong game đó. Là nơi tất cả dữ liệu của tất cả người chơi được quản lý tập trung. Nên các controller khác đều cần sử dụng PlayerController. Ý tưởng của em hiện giờ là như vậy.

Hm.
Thiết kế này của cậu sẽ hơi khó cho cậu sau này đấy.
Thông thường, nguyên tắc thiết kế 1 hệ thống nên luôn tách biệt class chứa dữ liệu và class thực hiện logic (như validation, cách tạo 1 phòng chơi, etc.). Các class chứa dữ liệu là model/domain của hệ thống, các class chứa logic chứa cách sử dụng dữ liệu để làm gì đó.
Có nhiều lý do tốt cho việc này. Một trong số đó là giúp class logic của cậu không có trạng thái - state (ví dụ như cậu lưu dữ liệu người chơi trong PlayerController, tức là PlayerController có trạng thái rồi. Khi cậu tạo 1 ván mới, cậu phải có PlayerController mới).
GameServer như cậu nói là phòng chơi, thì nó nên là nơi chứa dữ liệu, nhưng có reference của class chứa logic (các controller của cậu có vẻ như chứa logic). Đó là dấu hiệu của bad design, khi dữ liệu và logic bị coupling với nhau. Cậu sẽ gặp vấn đề lớn khi mở rộng phần mềm của cậu, cả về mặt tính năng lẫn khía cạnh quy mô.
Tớ nghĩ cậu sẽ cần rework lại kiến trúc của hệ thống này. Nó sẽ tốn thời gian, nhưng sẽ là bài học tốt.

Trong TH của cậu, cách đơn giản nhất là cậu có setter của PlayerController cho các controller khác. Khi khởi tạo GameServer, cậu sẽ khởi tạo các controller khác, và update PlayerController reference vào các controller khác. Tất cả nên được thực hiện ở 1 chỗ, như thế này:

public class GameServer {
  private PlayerController playerController;
  private AutoplayController autoplayController;
  // ...

  public GameServer() {
    playerController = new PlayerController();
    autoplayController = new AutoplayController();
    autoplayController.setPlayerController(playerController);
    // ...
  }
}

Với bài tập lớn, tớ nghĩ cách làm này reasonable. Tuy nhiên, vấn đề của cậu vẫn sẽ còn ở đó, và khi đi làm, cậu nên tránh lặp lại thiết kế này :smile:

4 Likes

Nhưng những class controller kiểu gì cũng phải chỉnh sửa dữ liệu người chơi. Vậy thiết kế kiểu gì để tách biệt class chứa dữ liệu (PlayerController) và các class logic (Các class controller khác) ạ?

1 Like

Hm.
Trước tiên, tớ nghĩ cậu cần thử phân biệt model/domain với logic khác nhau như thế nào trước :smile: Tớ nghĩ khi cậu phân biệt được 2 concept này, cậu có thể hiểu cách tách biệt chúng rồi, vì nó khá straight forward. Thử xem nhé!

Cậu đúng về phần class controller (trong TH của cậu, controller là class nắm logic) cần sửa dữ liệu người chơi, nhưng controller không cần lưu dữ liệu người chơi. Nếu cậu lưu dữ liệu người chơi ở controller (tức là cậu có property trong controller, nơi giữ dữ liệu người chơi), cậu đang lưu giữ trạng thái rồi :smile:
Đó là điều tớ muốn nói tới, khi đề cập việc tách logic với model/domain/dữ liệu.

Một điểm nữa, PlayerController có lẽ là tên tồi cho 1 class chứa dữ liệu. Controller nên là “người làm gì đó”, chứ không phải “người nắm dữ liệu” (tốt hơn có thể là PlayerDataHolder hoặc PlayerDataManager, hoặc thậm chí PlayerRepository nếu cậu muốn ứng dụng repository pattern vào hệ thống của cậu). Tên class thực ra quan trọng lắm đấy! :smile:

6 Likes

Em mới đọc qua chưa hiểu kỹ nhưng thấy hay. Em đang xem cái DataMapper design pattern. Thấy có vẻ xử lý được vấn đề của em.

1 Like
83% thành viên diễn đàn không hỏi bài tập, còn bạn thì sao?