[Clean code] Chapter 11: System

Complexity kills. It sucks the life out of developers, it makes products difficult to plan, build, and test - Ray Ozzie, CTO, Microsoft Corporation

How Would You Build a City?

Bạn có thể tự quản lý tất cả các chi tiết không? Chắc là không. Ngay cả việc quản lý một thành phố hiện tại cũng là quá sức đối với một người. Tuy nhiên, các thành phố hoạt động (hầu hết thời gian). Chúng hoạt động vì các thành phố có các nhóm người quản lý các khu vực cụ thể của thành phố, hệ thống nước, hệ thống điện, giao thông, thực thi pháp luật, quy tắc xây dựng, v.v. Một số người chịu trách nhiệm về big picture, trong khi những người khác tập trung vào các chi tiết.

Các thành phố cũng hoạt động vì chúng đã phát triển ở mức độ trừu tượng và tính mô đun thích hợp giúp cho các cá nhân và “các thành phần” mà họ quản lý có thể hoạt động hiệu quả, ngay cả khi không hiểu bức tranh toàn cảnh.

Mặc dù các team phần mềm cũng thường được tổ chức như vậy, nhưng các hệ thống mà họ làm việc thường không có sự tách biệt về mối quan tâm và mức độ trừu tượng giống nhau. Clean code giúp chúng tôi đạt được điều này ở mức độ trừu tượng thấp hơn. Trong chương này, chúng ta hãy xem xét làm thế nào để clean ở cấp độ trừu tượng cao hơn, cấp độ hệ thống.

In this chapter let us consider how to stay clean at higher levels of abstraction, the system level

Separate Constructing a System from Using It

Đầu tiên, hãy xem xét rằng xây dựng (construction) là một quá trình rất khác với sử dụng (use). Khi tôi viết bài này, có một khách sạn mới đang được xây dựng mà tôi nhìn thấy qua cửa sổ ở Chicago. Ngày nay nó là một hộp bê tông trần với cần cẩu xây dựng và thang máy được bắt vít ra bên ngoài. Những người bận rộn ở đó đều đội mũ cứng và mặc quần áo lao động. Trong một năm hoặc lâu hơn khách sạn sẽ được hoàn thành. Cần cẩu và thang máy sẽ biến mất. Tòa nhà sẽ sạch sẽ, được bao bọc bởi những bức tường cửa sổ bằng kính và màu sơn bắt mắt. Những người làm việc và ở lại đó cũng sẽ khác đi rất nhiều.

Software systems should separate the startup process, when the application objects are constructed and the dependencies are “wired” together, from the runtime logic that takes over after startup

Quá trình khởi động (startup), bắt đầu là mối quan tâm mà bất kỳ ứng dụng nào cũng phải giải quyết. Đó là mối quan tâm đầu tiên mà chúng ta sẽ xem xét trong chương này. Việc phân tách các mối quan tâm là một trong những kỹ thuật thiết kế lâu đời nhất và quan trọng nhất trong nghề thủ công của chúng tôi.

Thật không may, hầu hết các ứng dụng không tách rời mối quan tâm này. Mã cho quá trình khởi động là đặc biệt và nó được trộn lẫn với logic thời gian chạy. Đây là một ví dụ điển hình:

1
2
3
4
5
public Service getService() {
if (service == null)
service = new MyServiceImpl(...); // Good enough default for most cases?
return service;
}

Đây là thành ngữ KHỞI TẠO/ĐÁNH GIÁ LƯỜI (LAZY INITIALIZATION/EVALUATION), và nó có một số giá trị. Chúng tôi không phải chịu chi phí xây dựng trừ khi chúng tôi thực sự sử dụng đối tượng và kết quả là thời gian khởi động của chúng tôi có thể nhanh hơn. Chúng tôi cũng đảm bảo rằng null không bao giờ được trả lại.

Tuy nhiên, giờ đây chúng tôi có một phần phụ thuộc được mã hóa cứng trên MyServiceImpl và mọi thứ mà hàm tạo của nó yêu cầu (mà tôi đã trình bày). Chúng tôi không thể biên dịch mà không giải quyết các phụ thuộc này, ngay cả khi chúng tôi không bao giờ thực sự sử dụng một đối tượng thuộc loại này trong thời gian chạy!

Testing có thể là một vấn đề. Nếu MyServiceImpl là một đối tượng nặng ký, chúng tôi sẽ cần đảm bảo rằng một ĐỐI TƯỢNG TEST DOUBLE hoặc MOCK thích hợp được gán cho trường service trước khi phương thức này được gọi trong quá trình kiểm tra đơn vị. Bởi vì chúng tôi có logic xây dựng trộn lẫn với quá trình xử lý thời gian chạy thông thường, chúng tôi nên kiểm tra tất cả các đường dẫn thực thi (ví dụ: kiểm tra null và khối của nó). Có cả hai trách nhiệm này có nghĩa là phương pháp này đang làm nhiều hơn một việc, vì vậy chúng ta đang vi phạm Nguyên tắc Trách nhiệm Đơn lẻ (Single Responsibility Principle) theo một cách nhỏ.

Có lẽ tệ nhất là chúng ta không biết liệu MyServiceImpl có phải là đối tượng phù hợp trong mọi trường hợp hay không. Tôi ngụ ý rất nhiều trong bình luận. Tại sao class với method này phải biết bối cảnh toàn cầu? Chúng ta có thể thực sự biết đối tượng phù hợp để sử dụng ở đây không? Liệu một loại có thể phù hợp với tất cả các ngữ cảnh có thể không?

Tất nhiên, một lần xuất hiện LAZY-KHỞI TẠO không phải là một vấn đề nghiêm trọng. Tuy nhiên, thường có nhiều trường hợp thành ngữ thiết lập nhỏ như thế này trong các ứng dụng. Do đó, chiến lược thiết lập toàn cầu (nếu có) nằm rải rác trong ứng dụng, với ít tính mô-đun và thường trùng lặp đáng kể.

Nếu chúng ta siêng năng xây dựng các hệ thống mạnh mẽ và được hình thành tốt, thì chúng ta không bao giờ nên để những thành ngữ nhỏ, tiện lợi dẫn đến sự cố mô đun. Quá trình khởi động xây dựng đối tượng và nối dây cũng không ngoại lệ. Chúng ta nên mô đun hóa quy trình này tách biệt với logic thời gian chạy thông thường và chúng ta nên đảm bảo rằng chúng ta có một chiến lược global, nhất quán để giải quyết các phụ thuộc chính của mình.

Separation of Main

Một cách để tách việc xây dựng khỏi việc sử dụng đơn giản là chuyển tất cả các khía cạnh của việc xây dựng sang main hoặc các mô-đun được gọi bởi main và thiết kế phần còn lại của hệ thống với giả định rằng tất cả các đối tượng đã được xây dựng và nối dây một cách thích hợp. (Xem Hình 11-1.)

Dòng chảy của kiểm soát là dễ dàng để làm theo. main function xây dựng các đối tượng cần thiết cho hệ thống, sau đó chuyển chúng đến ứng dụng, ứng dụng này chỉ cần sử dụng chúng. Lưu ý hướng của các mũi tên phụ thuộc vượt qua rào cản giữa main và ứng dụng. Tất cả đều đi về một hướng, chỉ ra khỏi main. Điều này có nghĩa là ứng dụng không có kiến thức về main hoặc quá trình xây dựng. Nó chỉ đơn giản mong đợi rằng mọi thứ đã được xây dựng đúng cách.

Factories

Tất nhiên, đôi khi chúng ta cần làm cho ứng dụng chịu trách nhiệm khi một đối tượng được tạo. Ví dụ: trong một hệ thống xử lý đơn hàng, ứng dụng phải tạo các instance LineItem để thêm vào Order. Trong trường hợp này chúng ta có thể sử dụng ABSTRACT FACTORY pattern để cung cấp cho ứng dụng quyền kiểm soát thời điểm xây dựng LineItems, nhưng giữ các chi tiết của việc xây dựng đó tách biệt với mã ứng dụng. (Xem Hình 11-2.)

Một lần nữa lưu ý rằng tất cả các điểm phụ thuộc từ main hướng tới ứng dụng OrderProcessing. Điều này có nghĩa là ứng dụng được tách rời khỏi các chi tiết về cách xây dựng LineItem. Khả năng đó được giữ trong LineItemFactoryImplementation, nằm ở phía chính của dòng. Tuy nhiên, ứng dụng hoàn toàn kiểm soát khi các instance LineItem được xây dựng và thậm chí có thể cung cấp các đối số hàm tạo dành riêng cho ứng dụng.

Dependency Injection

Một cơ chế mạnh mẽ để tách biệt xây dựng khỏi sử dụng là Dependency Injection (DI), ứng dụng của Inversion of Control (IoC) vào quản lý phụ thuộc.Inversion of Control di chuyển các trách nhiệm thứ cấp từ một đối tượng sang các đối tượng khác dành riêng cho mục đích đó, do đó hỗ trợ Nguyên tắc trách nhiệm duy nhất. Trong bối cảnh quản lý phụ thuộc, một đối tượng không nên chịu trách nhiệm cho việc tự khởi tạo các phụ thuộc. Thay vào đó, nó nên chuyển trách nhiệm này cho một cơ chế “có thẩm quyền” khác, do đó đảo ngược quyền kiểm soát. Vì thiết lập là mối quan tâm toàn cục, nên cơ chế có thẩm quyền này thường sẽ là một trong hai quy trình “chính” hoặc một container có mục đích đặc biệt.

Scaling Up

Các thành phố phát triển từ các thị trấn, phát triển từ các khu định cư. Lúc đầu, những con đường hẹp và thực tế không có, sau đó chúng được trải nhựa, rồi mở rộng theo thời gian. Các tòa nhà nhỏ và các mảnh đất trống được lấp đầy bằng các tòa nhà lớn hơn, một số trong số đó cuối cùng sẽ được thay thế bằng các tòa nhà chọc trời.

Lúc đầu, không có dịch vụ nào như điện, nước, nước thải và Internet (thở hổn hển!). Các dịch vụ này cũng được thêm vào khi mật độ dân số và tòa nhà tăng lên.

Sự tăng trưởng này không phải là không có đau đớn (pain). Đã bao nhiêu lần bạn lái xe qua một dự án “cải tạo” đường và tự hỏi: “Tại sao họ không xây đủ rộng ngay từ lần đầu tiên!?

Nhưng nó không thể xảy ra theo bất kỳ cách nào khác. Ai có thể biện minh cho chi phí của một đường cao tốc sáu làn chạy qua giữa một thị trấn nhỏ dự đoán sự phát triển? Ai sẽ muốn một con đường như vậy xuyên qua thị trấn của họ?

Có một huyền thoại rằng chúng ta có thể có được các hệ thống “ngay từ lần đầu tiên”. Thay vào đó, chúng ta chỉ nên triển khai các câu chuyện của ngày hôm nay, sau đó cấu trúc lại và mở rộng hệ thống để triển khai các câu chuyện mới vào ngày mai. Đây là bản chất của sự linh hoạt lặp đi lặp lại và gia tăng. Quá trình phát triển dựa trên thử nghiệm, tái cấu trúc và mã sạch mà họ tạo ra giúp việc này hoạt động ở cấp độ mã.

Nhưng còn ở cấp độ hệ thống thì sao? Không phải kiến trúc hệ thống yêu cầu lập kế hoạch trước sao? Chắc chắn, nó không thể tăng dần từ đơn giản đến phức tạp, phải không?

Hệ thống phần mềm là duy nhất so với các hệ thống vật lý. Kiến trúc của chúng có thể phát triển dần dần, nếu chúng ta duy trì sự phân tách hợp lý các mối quan tâm.

Bản chất phù du của các hệ thống phần mềm khiến điều này trở nên khả thi, như chúng ta sẽ thấy. Trước tiên chúng ta hãy xem xét một phản ví dụ về một kiến trúc không phân tách các mối quan tâm một cách thỏa đáng.

Các kiến trúc EJB1 và EJB2 ban đầu không phân tách các mối quan tâm một cách thích hợp và do đó đặt ra các rào cản không cần thiết đối với tăng trưởng hữu cơ. Hãy xem xét một Entity Bean cho một lớp Bank. Một bean thực thể là một biểu diễn trong bộ nhớ của dữ liệu quan hệ, nói cách khác, một hàng của bảng.

Trước tiên, bạn phải xác định Interface cục bộ (đang xử lý) hoặc từ xa (JVM riêng biệt) mà máy khách sẽ sử dụng. Liệt kê 11-1 cho thấy một Interface cục bộ khả thi:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package com.example.banking;
import java.util.Collections;
import javax.ejb.*;
public interface BankLocal extends java.ejb.EJBLocalObject {
String getStreetAddr1() throws EJBException;
String getStreetAddr2() throws EJBException;
String getCity() throws EJBException;
String getState() throws EJBException;
String getZipCode() throws EJBException;
void setStreetAddr1(String street1) throws EJBException;
void setStreetAddr2(String street2) throws EJBException;
void setCity(String city) throws EJBException;
void setState(String state) throws EJBException;
void setZipCode(String zip) throws EJBException;
Collection getAccounts() throws EJBException;
void setAccounts(Collection accounts) throws EJBException;
void addAccount(AccountDTO accountDTO) throws EJBException;
}

Tôi đã chỉ ra một số thuộc tính cho địa chỉ của Bank và tập hợp các tài khoản mà ngân hàng sở hữu, mỗi tài khoản sẽ có dữ liệu được xử lý bởi Account EJB riêng biệt.

Liệt kê 11-2 cho thấy Interface tương ứng cho Bank bean

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
package com.example.banking;
import java.util.Collections;
import javax.ejb.*;
public abstract class Bank implements javax.ejb.EntityBean {
// Business logic...
public abstract String getStreetAddr1();
public abstract String getStreetAddr2();
public abstract String getCity();
public abstract String getState();
public abstract String getZipCode();
public abstract void setStreetAddr1(String street1);
public abstract void setStreetAddr2(String street2);
public abstract void setCity(String city);
public abstract void setState(String state);
public abstract void setZipCode(String zip);
public abstract Collection getAccounts();
public abstract void setAccounts(Collection accounts);
public void addAccount(AccountDTO accountDTO) {
InitialContext context = new InitialContext();
AccountHomeLocal accountHome = context.lookup("AccountHomeLocal");
AccountLocal account = accountHome.create(accountDTO);
Collection accounts = getAccounts();
accounts.add(account);
}
// EJB container logic
public abstract void setId(Integer id);
public abstract Integer getId();
public Integer ejbCreate(Integer id) { ... }
public void ejbPostCreate(Integer id) { ... }
// The rest had to be implemented but were usually empty:
public void setEntityContext(EntityContext ctx) {}
public void unsetEntityContext() {}
public void ejbActivate() {}
public void ejbPassivate() {}
public void ejbLoad() {}
public void ejbStore() {}
public void ejbRemove() {}
}

Tôi chưa hiển thị interface LocalHome tương ứng, về cơ bản là một facoty được sử dụng để tạo các đối tượng, cũng như bất kỳ phương thức (truy vấn) công cụ tìm Bank nào mà bạn có thể thêm vào.

Cuối cùng, bạn phải viết một hoặc nhiều bộ mô tả triển khai XML chỉ định chi tiết ánh xạ quan hệ đối tượng cho kho lưu trữ bền vững, hành vi giao dịch mong muốn, các ràng buộc bảo mật, v.v.

Logic nghiệp vụ được liên kết chặt chẽ với “conatiner” ứng dụng EJB2. Bạn phải phân lớp các loại vùng chứa và bạn phải cung cấp nhiều phương thức vòng đời mà vùng chứa yêu cầu.

Do khớp nối này với hộp chứa nặng nên việc kiểm tra đơn vị bị cô lập gặp khó khăn. Cần phải mô phỏng vùng chứa, điều này rất khó hoặc lãng phí nhiều thời gian để triển khai EJB và kiểm tra máy chủ thực. Việc sử dụng lại bên ngoài kiến trúc EJB2 thực sự là không thể do khớp nối chặt chẽ.

Cuối cùng, ngay cả lập trình hướng đối tượng cũng bị phá hoại. Một bean không thể kế thừa từ một bean khác. Lưu ý logic để thêm một tài khoản mới. Thông thường trong EJB2 bean định nghĩa “đối tượng truyền dữ liệu” (DTO) về cơ bản là “cấu trúc” không có hành vi.
Điều này thường dẫn đến các loại dư thừa về cơ bản chứa cùng một dữ liệu và nó yêu cầu mã soạn sẵn để sao chép dữ liệu từ đối tượng này sang đối tượng khác.

Cross-Cutting Concerns (cần đọc lại)

Kiến trúc EJB2 tiến gần đến sự tách biệt thực sự của các mối quan tâm trong một số lĩnh vực. Ví dụ: giao dịch mong muốn, bảo mật và một số hành vi kiên trì được khai báo trong bộ mô tả triển khai, độc lập với mã nguồn.

Lưu ý rằng các mối quan tâm như tính bền bỉ có xu hướng vượt qua ranh giới đối tượng tự nhiên của một miền. Bạn muốn duy trì tất cả các đối tượng của mình bằng cách sử dụng cùng một chiến lược, ví dụ: sử dụng một DBMS6 cụ thể so với các tệp phẳng, tuân theo các quy ước đặt tên nhất định cho bảng và cột, sử dụng ngữ nghĩa giao dịch nhất quán, v.v.

Về nguyên tắc, bạn có thể lập luận về chiến lược kiên trì của mình theo cách mô-đun, được gói gọn. Tuy nhiên, trên thực tế, về cơ bản, bạn phải trải rộng cùng một mã thực hiện chiến lược kiên trì trên nhiều đối tượng. Chúng tôi sử dụng thuật ngữ mối quan tâm xuyên suốt cho những mối quan tâm như thế này. Một lần nữa, khung kiên trì có thể là mô-đun và logic miền của chúng tôi, trong sự cô lập, có thể là mô-đun. Vấn đề là giao điểm chi tiết của các lĩnh vực này.

Trên thực tế, cách kiến trúc EJB xử lý tính bền bỉ, bảo mật và giao dịch, lập trình hướng khía cạnh - aspect-oriented programming (AOP) đã “dự đoán” là một cách tiếp cận có mục đích chung để khôi phục tính mô đun cho các mối quan tâm xuyên suốt.

Trong AOP, các cấu trúc mô-đun được gọi là các khía cạnh chỉ định những điểm nào trong hệ thống sẽ được sửa đổi hành vi của chúng theo một cách nhất quán nào đó để hỗ trợ một mối quan tâm cụ thể. Thông số kỹ thuật này được thực hiện bằng cách sử dụng cơ chế lập trình hoặc khai báo ngắn gọn.

Sử dụng tính kiên trì làm ví dụ, bạn sẽ khai báo những đối tượng và thuộc tính nào (hoặc mẫu của chúng) sẽ được duy trì và sau đó ủy quyền các tác vụ cần kiên trì cho khung kiên trì của bạn. Các sửa đổi hành vi được thực hiện không xâm lấn đối với mã mục tiêu bằng khung AOP. Chúng ta hãy xem xét ba khía cạnh hoặc các cơ chế giống như khía cạnh trong Java.

Java Proxies

Pure Java AOP Frameworks

AspectJ Aspects

Test Drive the System Architecture

Không thể cường điệu hóa sức mạnh của việc phân tách các mối quan tâm thông qua các cách tiếp cận giống như khía cạnh. Nếu bạn có thể viết logic miền của ứng dụng bằng cách sử dụng POJO, được tách rời khỏi mọi mối quan tâm về kiến trúc ở cấp mã, thì bạn có thể thực sự kiểm tra - test drive kiến trúc của mình. Bạn có thể phát triển nó từ đơn giản đến phức tạp, khi cần, bằng cách áp dụng các công nghệ mới theo yêu cầu. Không cần thiết phải thực hiện Big Design Up Front (BDUF). Trên thực tế, BDUF thậm chí còn có hại vì nó cản trở việc thích nghi với sự thay đổi, do tâm lý chống lại việc loại bỏ nỗ lực trước đó và do cách các lựa chọn kiến trúc ảnh hưởng đến suy nghĩ tiếp theo về thiết kế.

Các kiến trúc sư tòa nhà phải thực hiện BDUF vì không khả thi để thực hiện các thay đổi kiến trúc triệt để đối với một cấu trúc vật lý lớn một khi việc xây dựng đang được tiến hành tốt. Mặc dù phần mềm có vật lý riêng, nhưng việc thực hiện thay đổi triệt để là khả thi về mặt kinh tế, nếu cấu trúc của phần mềm phân tách mối quan tâm của nó một cách hiệu quả.

Điều này có nghĩa là chúng ta có thể bắt đầu một dự án phần mềm với kiến trúc “đơn giản ngây thơ” nhưng tách biệt tốt, cung cấp các câu chuyện người dùng đang hoạt động nhanh chóng, sau đó thêm cơ sở hạ tầng khi chúng ta mở rộng quy mô. Một số trang web lớn nhất thế giới đã đạt được tính khả dụng và hiệu suất rất cao, sử dụng bộ nhớ đệm dữ liệu tinh vi, bảo mật, ảo hóa, v.v., tất cả đều được thực hiện hiệu quả và linh hoạt vì các thiết kế được ghép nối tối thiểu có độ đơn giản phù hợp ở mọi cấp độ trừu tượng và phạm vi.

Tất nhiên, điều này không có nghĩa là chúng ta tham gia vào một dự án “không có định hướng”. Chúng ta có một số kỳ vọng về phạm vi chung, mục tiêu và lịch trình cho dự án, cũng như cấu trúc chung của hệ thống kết quả. Tuy nhiên, chúng ta phải duy trì khả năng thay đổi hướng đi để ứng phó với các hoàn cảnh đang thay đổi.

Kiến trúc EJB ban đầu chỉ là một trong số nhiều API nổi tiếng được thiết kế quá mức và làm ảnh hưởng đến việc tách biệt các mối quan tâm. Ngay cả các API được thiết kế tốt cũng có thể bị quá mức khi chúng không thực sự cần thiết. Một API tốt phần lớn phải biến mất khỏi tầm nhìn hầu hết thời gian, do đó nhóm dành phần lớn nỗ lực sáng tạo của mình tập trung vào các câu chuyện người dùng đang được triển khai. Nếu không, các ràng buộc về kiến trúc sẽ cản trở việc cung cấp hiệu quả giá trị tối ưu cho khách hàng.

Để tóm tắt lại cuộc thảo luận dài này,

An optimal system architecture consists of modularized domains of concern, each of which is implemented with Plain Old Java (or other) Objects. The different domains are inte- grated together with minimally invasive Aspects or Aspect-like tools. This architecture can be test-driven, just like the code.

Kiến trúc hệ thống tối ưu bao gồm các miền quan tâm được mô-đun hóa, mỗi miền được triển khai bằng Plain Old Java (hoặc các đối tượng khác). Các miền khác nhau được tích hợp với nhau bằng các công cụ Aspects hoặc Aspect-like ít xâm lấn. Kiến trúc này có thể được kiểm thử, giống như mã.

Optimize Decision Making

Tính mô-đun và sự tách biệt các mối quan tâm làm cho việc quản lý và ra quyết định phi tập trung trở nên khả thi. Trong một hệ thống đủ lớn, dù là một thành phố hay một dự án phần mềm, không một cá nhân nào có thể đưa ra tất cả các quyết định.

Chúng ta đều biết rằng tốt nhất là giao trách nhiệm cho những người có trình độ cao nhất. Chúng ta thường quên rằng tốt nhất là hoãn quyết định cho đến phút cuối cùng có thể. Điều này không phải là lười biếng hay vô trách nhiệm; nó cho phép chúng ta đưa ra những lựa chọn sáng suốt với thông tin tốt nhất có thể. Một quyết định vội vàng là một quyết định được đưa ra với kiến thức không tối ưu. Chúng ta sẽ có ít phản hồi từ khách hàng, suy ngẫm về dự án và kinh nghiệm với các lựa chọn triển khai của mình hơn nếu chúng ta quyết định quá sớm.

The agility provided by a POJO system with modularized concerns allows us to make opti- mal, just-in-time decisions, based on the most recent knowledge. The complexity of these decisions is also reduced.

Tính linh hoạt do hệ thống POJO cung cấp với các mối quan tâm được mô-đun hóa cho phép chúng ta đưa ra các quyết định tối ưu, kịp thời dựa trên kiến ​​thức mới nhất. Độ phức tạp của các quyết định này cũng được giảm bớt.

Use Standards Wisely, When They Add Demonstrable Value

Xây dựng công trình là một điều kỳ diệu đáng xem vì tốc độ xây dựng các tòa nhà mới (kể cả vào giữa mùa đông) và vì những thiết kế phi thường có thể thực hiện được với công nghệ ngày nay. Xây dựng là một ngành công nghiệp trưởng thành với các bộ phận, phương pháp và tiêu chuẩn được tối ưu hóa cao đã phát triển dưới áp lực trong nhiều thế kỷ.

Nhiều nhóm đã sử dụng kiến trúc EJB2 vì đây là một tiêu chuẩn, ngay cả khi các thiết kế nhẹ hơn và đơn giản hơn đã đủ. Tôi đã thấy các nhóm trở nên ám ảnh với nhiều tiêu chuẩn được thổi phồng mạnh mẽ và mất tập trung vào việc triển khai giá trị cho khách hàng của họ.

Standards make it easier to reuse ideas and components, recruit people with relevant expe- rience, encapsulate good ideas, and wire components together. However, the process of creating standards can sometimes take too long for industry to wait, and some standards lose touch with the real needs of the adopters they are intended to serve.

Tiêu chuẩn giúp tái sử dụng ý tưởng và thành phần dễ dàng hơn, tuyển dụng những người có kinh nghiệm liên quan, gói gọn những ý tưởng hay và kết nối các thành phần lại với nhau. Tuy nhiên, quá trình tạo ra tiêu chuẩn đôi khi có thể mất quá nhiều thời gian để ngành công nghiệp chờ đợi và một số tiêu chuẩn không đáp ứng được nhu cầu thực sự của những người áp dụng mà chúng dự định phục vụ.

Systems Need Domain-Specific Languages

Xây dựng công trình, giống như hầu hết các lĩnh vực khác, đã phát triển một ngôn ngữ phong phú với vốn từ vựng, thành ngữ và mẫu (patterns) truyền tải thông tin cần thiết một cách rõ ràng và súc tích. Trong phần mềm, gần đây đã có sự quan tâm mới trong việc tạo ra Ngôn ngữ dành riêng cho lĩnh vực (DSL), là các ngôn ngữ kịch bản hoặc API riêng biệt, nhỏ trong các ngôn ngữ chuẩn cho phép viết mã sao cho đọc giống như một dạng văn xuôi có cấu trúc mà một chuyên gia trong lĩnh vực có thể viết.

Một DSL tốt sẽ giảm thiểu “khoảng cách giao tiếp” giữa khái niệm lĩnh vực và mã triển khai khái niệm đó, giống như các hoạt động linh hoạt tối ưu hóa giao tiếp trong nhóm và với các bên liên quan của dự án. Nếu bạn đang triển khai logic lĩnh vực bằng cùng ngôn ngữ mà một chuyên gia trong lĩnh vực sử dụng, thì rủi ro bạn dịch sai miền thành triển khai sẽ ít hơn.

Khi được sử dụng hiệu quả, DSL sẽ nâng mức độ trừu tượng lên trên các thành ngữ mã và mẫu thiết kế. Chúng cho phép nhà phát triển tiết lộ ý định của mã ở mức độ trừu tượng phù hợp.

Domain-Specific Languages allow all levels of abstraction and all domains in the applica- tion to be expressed as POJOs, from high-level policy to low-level details.

Ngôn ngữ dành riêng cho miền cho phép mọi cấp độ trừu tượng và mọi miền trong ứng dụng được thể hiện dưới dạng POJO, từ chính sách cấp cao đến chi tiết cấp thấp.

Conclusion

Hệ thống cũng phải sạch. Một kiến trúc xâm lấn sẽ làm quá tải logic miền và ảnh hưởng đến tính linh hoạt. Khi logic miền bị che khuất, chất lượng sẽ bị ảnh hưởng vì lỗi dễ ẩn hơn và các câu chuyện trở nên khó triển khai hơn. Nếu tính linh hoạt bị tổn hại, năng suất sẽ bị ảnh hưởng và lợi ích của TDD sẽ bị mất.

Ở mọi cấp độ trừu tượng, mục đích phải rõ ràng. Điều này sẽ chỉ xảy ra nếu bạn viết POJO và sử dụng các cơ chế giống như khía cạnh để kết hợp các mối quan tâm triển khai khác một cách không xâm lấn.

Cho dù bạn đang thiết kế hệ thống hay các mô-đun riêng lẻ, đừng bao giờ quên sử dụng thứ đơn giản nhất có thể hoạt động.

Author

Ming

Posted on

2024-08-27

Updated on

2024-08-27

Licensed under

Comments