hệ thống
Sau đây là những đặc điểm chung mong muốn của một hệ thống:- Độ phức tạp tối thiểu. Các dự án quá phức tạp phải tránh. Điều quan trọng nhất là sự đơn giản và rõ ràng (đơn giản hơn = tốt hơn).
- Dễ dàng bảo trì. Khi tạo một ứng dụng, bạn phải nhớ rằng nó sẽ cần được bảo trì (ngay cả khi cá nhân bạn không chịu trách nhiệm bảo trì nó). Điều này có nghĩa là mã phải rõ ràng và dễ hiểu.
- Khớp nối lỏng lẻo. Điều này có nghĩa là chúng tôi giảm thiểu số lượng phụ thuộc giữa các phần khác nhau của chương trình (tối đa hóa việc tuân thủ các nguyên tắc OOP của chúng tôi).
- Khả năng tái sử dụng. Chúng tôi thiết kế hệ thống của mình với khả năng tái sử dụng các thành phần trong các ứng dụng khác.
- Tính di động. Nó sẽ dễ dàng thích ứng một hệ thống với môi trường khác.
- Phong cách thống nhất. Chúng tôi thiết kế hệ thống của mình bằng cách sử dụng một phong cách thống nhất trong các thành phần khác nhau của nó.
- Extensibility (khả năng mở rộng). Chúng ta có thể cải thiện hệ thống mà không vi phạm cấu trúc cơ bản của nó (thêm hoặc thay đổi một thành phần sẽ không ảnh hưởng đến tất cả các thành phần khác).
Các bước thiết kế hệ thống
- Hệ thống phần mềm. Thiết kế ứng dụng tổng thể.
- Phân chia thành các hệ thống con/gói. Xác định các phần khác biệt về mặt logic và xác định các quy tắc tương tác giữa chúng.
- Phân chia các hệ thống con thành các lớp. Chia các phần của hệ thống thành các lớp và giao diện cụ thể, đồng thời xác định sự tương tác giữa chúng.
- Phân chia các lớp thành các phương thức. Tạo một định nghĩa đầy đủ về các phương thức cần thiết cho một lớp, dựa trên trách nhiệm được giao.
- Phương pháp thiết kế. Tạo một định nghĩa chi tiết về chức năng của các phương pháp riêng lẻ.
Các nguyên tắc và khái niệm chung về thiết kế hệ thống
Khởi tạo lười biếng. Trong thành ngữ lập trình này, ứng dụng không lãng phí thời gian để tạo một đối tượng cho đến khi nó thực sự được sử dụng. Điều này tăng tốc quá trình khởi tạo và giảm tải cho bộ thu gom rác. Điều đó nói rằng, bạn không nên đi quá xa, bởi vì điều đó có thể vi phạm nguyên tắc mô đun. Có lẽ nên chuyển tất cả các trường hợp xây dựng sang một số phần cụ thể, chẳng hạn như phương thức chính hoặc sang lớp nhà máy . Một đặc điểm của mã tốt là không có mã soạn sẵn lặp đi lặp lại. Theo quy định, mã như vậy được đặt trong một lớp riêng để có thể gọi nó khi cần.AOP
Tôi cũng muốn lưu ý lập trình hướng khía cạnh. Mô hình lập trình này là tất cả về giới thiệu logic minh bạch. Nghĩa là, mã lặp đi lặp lại được đưa vào các lớp (khía cạnh) và được gọi khi các điều kiện nhất định được thỏa mãn. Ví dụ: khi gọi một phương thức có tên cụ thể hoặc truy cập một biến có kiểu cụ thể. Đôi khi các khía cạnh có thể gây nhầm lẫn, vì không rõ ngay lập tức mã được gọi từ đâu, nhưng đây vẫn là chức năng rất hữu ích. Đặc biệt là khi lưu trữ hoặc ghi nhật ký. Chúng tôi thêm chức năng này mà không cần thêm logic bổ sung vào các lớp thông thường. Bốn quy tắc của Kent Beck cho một kiến trúc đơn giản:- Tính biểu cảm — Mục đích của một lớp phải được thể hiện rõ ràng. Điều này đạt được thông qua việc đặt tên phù hợp, kích thước nhỏ và tuân thủ nguyên tắc chịu trách nhiệm duy nhất (chúng tôi sẽ xem xét chi tiết hơn bên dưới).
- Số lượng lớp học và phương pháp tối thiểu — Với mong muốn tạo ra các lớp học nhỏ và tập trung hẹp nhất có thể, bạn có thể đi quá xa (dẫn đến mô hình chống phẫu thuật shotgun). Nguyên tắc này yêu cầu giữ cho hệ thống nhỏ gọn và không đi quá xa, tạo ra một lớp riêng biệt cho mọi hành động có thể.
- Không trùng lặp — Mã trùng lặp, gây nhầm lẫn và là dấu hiệu của thiết kế hệ thống dưới mức tối ưu, được trích xuất và chuyển đến một vị trí riêng biệt.
- Chạy tất cả các bài kiểm tra — Có thể quản lý được một hệ thống vượt qua tất cả các bài kiểm tra. Bất kỳ thay đổi nào cũng có thể khiến thử nghiệm thất bại, cho chúng ta thấy rằng sự thay đổi của chúng ta đối với logic bên trong của một phương thức cũng đã thay đổi hành vi của hệ thống theo những cách không mong muốn.
CHẤT RẮN
Khi thiết kế một hệ thống, các nguyên tắc SOLID nổi tiếng đáng được xem xét:S (trách nhiệm duy nhất), O (đóng mở), L (thay thế Liskov), I (tách biệt giao diện), D (đảo ngược phụ thuộc).
Chúng tôi sẽ không tập trung vào từng nguyên tắc riêng lẻ. Đó sẽ là một chút ngoài phạm vi của bài viết này, nhưng bạn có thể đọc thêm ở đây .giao diện
Có lẽ một trong những bước quan trọng nhất trong việc tạo ra một lớp được thiết kế tốt là tạo ra một giao diện được thiết kế tốt thể hiện sự trừu tượng hóa tốt, ẩn các chi tiết triển khai của lớp và đồng thời trình bày một nhóm các phương thức nhất quán rõ ràng với nhau. Chúng ta hãy xem xét kỹ hơn một trong những nguyên tắc RẮN - phân biệt giao diện: các máy khách (các lớp) không nên triển khai các phương thức không cần thiết mà chúng sẽ không sử dụng. Nói cách khác, nếu chúng ta đang nói về việc tạo một giao diện có ít phương thức nhất nhằm thực hiện công việc duy nhất của giao diện (mà tôi nghĩ là rất giống với nguyên tắc trách nhiệm duy nhất), thì tốt hơn là tạo một vài giao diện nhỏ hơn thay thế của một giao diện cồng kềnh. May mắn thay, một lớp có thể thực hiện nhiều hơn một giao diện. Hãy nhớ đặt tên đúng cho giao diện của bạn: tên phải phản ánh nhiệm vụ được giao càng chính xác càng tốt. Và, tất nhiên, nó càng ngắn thì càng ít gây nhầm lẫn. Nhận xét tài liệu thường được viết ở cấp độ giao diện. Những nhận xét này cung cấp thông tin chi tiết về những gì mỗi phương thức nên thực hiện, những đối số mà nó nhận và những gì nó sẽ trả về.Lớp học
- hằng tĩnh công khai;
- hằng số tĩnh riêng tư;
- các biến thể hiện riêng.
Quy mô lớp học
Bây giờ tôi muốn nói về quy mô lớp học. Hãy nhớ lại một trong những nguyên tắc RẮN — nguyên tắc trách nhiệm duy nhất. Nó tuyên bố rằng mỗi đối tượng chỉ có một mục đích (trách nhiệm) và logic của tất cả các phương thức của nó nhằm hoàn thành nó. Điều này cho chúng ta biết tránh các lớp lớn, cồng kềnh (mà thực ra là đối tượng God object anti-pattern), và nếu chúng ta có nhiều phương thức với đủ loại logic khác nhau được nhồi nhét vào một lớp, chúng ta cần nghĩ đến việc chia nhỏ nó thành một lớp. vài phần logic (các lớp). Đổi lại, điều này sẽ làm tăng khả năng đọc mã, vì sẽ không mất nhiều thời gian để hiểu mục đích của từng phương thức nếu chúng ta biết mục đích gần đúng của bất kỳ lớp nào. Ngoài ra, hãy để ý đến tên lớp, tên này sẽ phản ánh logic mà nó chứa. Ví dụ: nếu chúng ta có một lớp có hơn 20 từ trong tên của nó, chúng ta cần suy nghĩ về việc tái cấu trúc. Bất kỳ lớp tự trọng nào cũng không nên có nhiều biến nội bộ như vậy. Trên thực tế, mỗi phương thức hoạt động với một hoặc một vài trong số chúng, tạo ra nhiều sự gắn kết trong lớp (điều này chính xác là như vậy, vì lớp phải là một thể thống nhất). Kết quả là, việc tăng tính gắn kết của một lớp dẫn đến việc giảm quy mô lớp học, và tất nhiên, số lượng lớp học sẽ tăng lên. Điều này gây khó chịu cho một số người, vì bạn cần nghiên cứu kỹ hơn về các tệp lớp để xem cách thức hoạt động của một tác vụ lớn cụ thể. Trên hết, mỗi lớp là một mô-đun nhỏ ít liên quan đến các mô-đun khác. Sự cô lập này làm giảm số lượng thay đổi chúng ta cần thực hiện khi thêm logic bổ sung vào một lớp. mỗi phương thức hoạt động với một hoặc một vài trong số chúng, tạo ra nhiều sự gắn kết trong lớp (điều này chính xác là như vậy, vì lớp phải là một thể thống nhất). Kết quả là, việc tăng tính gắn kết của một lớp dẫn đến việc giảm quy mô lớp học, và tất nhiên, số lượng lớp học sẽ tăng lên. Điều này gây khó chịu cho một số người, vì bạn cần nghiên cứu kỹ hơn về các tệp lớp để xem cách thức hoạt động của một tác vụ lớn cụ thể. Trên hết, mỗi lớp là một mô-đun nhỏ ít liên quan đến các mô-đun khác. Sự cô lập này làm giảm số lượng thay đổi chúng ta cần thực hiện khi thêm logic bổ sung vào một lớp. mỗi phương thức hoạt động với một hoặc một vài trong số chúng, tạo ra nhiều sự gắn kết trong lớp (điều này chính xác là như vậy, vì lớp phải là một thể thống nhất). Kết quả là, việc tăng tính gắn kết của một lớp dẫn đến việc giảm quy mô lớp học, và tất nhiên, số lượng lớp học sẽ tăng lên. Điều này gây khó chịu cho một số người, vì bạn cần nghiên cứu kỹ hơn về các tệp lớp để xem cách thức hoạt động của một tác vụ lớn cụ thể. Trên hết, mỗi lớp là một mô-đun nhỏ ít liên quan đến các mô-đun khác. Sự cô lập này làm giảm số lượng thay đổi chúng ta cần thực hiện khi thêm logic bổ sung vào một lớp. Sự gắn kết của nó dẫn đến việc giảm quy mô lớp học, và tất nhiên, số lượng lớp học tăng lên. Điều này gây khó chịu cho một số người, vì bạn cần nghiên cứu kỹ hơn về các tệp lớp để xem cách thức hoạt động của một tác vụ lớn cụ thể. Trên hết, mỗi lớp là một mô-đun nhỏ ít liên quan đến các mô-đun khác. Sự cô lập này làm giảm số lượng thay đổi chúng ta cần thực hiện khi thêm logic bổ sung vào một lớp. Sự gắn kết của nó dẫn đến việc giảm quy mô lớp học, và tất nhiên, số lượng lớp học tăng lên. Điều này gây khó chịu cho một số người, vì bạn cần nghiên cứu kỹ hơn về các tệp lớp để xem cách thức hoạt động của một tác vụ lớn cụ thể. Trên hết, mỗi lớp là một mô-đun nhỏ ít liên quan đến các mô-đun khác. Sự cô lập này làm giảm số lượng thay đổi chúng ta cần thực hiện khi thêm logic bổ sung vào một lớp.Các đối tượng
đóng gói
Ở đây, trước tiên chúng ta sẽ nói về một nguyên tắc OOP: đóng gói. Ẩn việc triển khai không đồng nghĩa với việc tạo ra một phương thức để cách ly các biến (hạn chế quyền truy cập một cách thiếu suy nghĩ thông qua các phương thức, getters và setters riêng lẻ, điều này là không tốt, vì toàn bộ điểm đóng gói bị mất). Ẩn quyền truy cập nhằm mục đích hình thành sự trừu tượng, nghĩa là lớp cung cấp các phương thức cụ thể được chia sẻ mà chúng tôi sử dụng để làm việc với dữ liệu của mình. Và người dùng không cần biết chính xác chúng tôi đang làm việc với dữ liệu này như thế nào — nó hoạt động và thế là đủ.luật Demeter
Chúng ta cũng có thể xem xét Luật Demeter: đó là một tập hợp nhỏ các quy tắc hỗ trợ quản lý độ phức tạp ở cấp độ lớp và phương thức. Giả sử chúng ta có một đối tượng Car và nó có một phương thức move(Object arg1, Object arg2) . Theo Luật Demeter, phương pháp này bị giới hạn trong việc gọi:- các phương thức của chính đối tượng Car (nói cách khác, đối tượng "this");
- các phương thức của các đối tượng được tạo trong phương thức di chuyển ;
- các phương thức của các đối tượng được truyền dưới dạng đối số ( arg1 , arg2 );
- các phương thức của các đối tượng Car bên trong (một lần nữa, "cái này").
Cấu trúc dữ liệu
Một cấu trúc dữ liệu là một tập hợp các phần tử có liên quan với nhau. Khi coi một đối tượng là một cấu trúc dữ liệu, có một tập hợp các phần tử dữ liệu mà các phương thức hoạt động trên đó. Sự tồn tại của các phương pháp này được ngầm giả định. Nghĩa là, cấu trúc dữ liệu là một đối tượng có mục đích lưu trữ và làm việc với (xử lý) dữ liệu được lưu trữ. Sự khác biệt chính của nó so với một đối tượng thông thường là một đối tượng thông thường là một tập hợp các phương thức hoạt động trên các phần tử dữ liệu được giả định là tồn tại. Bạn hiểu không? Khía cạnh chính của một đối tượng thông thường là các phương thức. Các biến nội bộ tạo điều kiện cho hoạt động chính xác của họ. Nhưng trong một cấu trúc dữ liệu, các phương thức luôn sẵn có để hỗ trợ công việc của bạn với các thành phần dữ liệu được lưu trữ, đây là điều tối quan trọng ở đây. Một loại cấu trúc dữ liệu là đối tượng truyền dữ liệu (DTO). Đây là lớp có biến công khai và không có phương thức (hoặc chỉ có phương thức đọc/ghi) được sử dụng để truyền dữ liệu khi làm việc với cơ sở dữ liệu, phân tích cú pháp tin nhắn từ ổ cắm, v.v. Dữ liệu thường không được lưu trữ trong các đối tượng như vậy trong một khoảng thời gian dài. Nó gần như ngay lập tức được chuyển đổi thành loại thực thể mà ứng dụng của chúng tôi hoạt động. Ngược lại, một thực thể cũng là một cấu trúc dữ liệu, nhưng mục đích của nó là tham gia vào logic nghiệp vụ ở các cấp độ khác nhau của ứng dụng. Mục đích của DTO là vận chuyển dữ liệu đến/từ ứng dụng. Ví dụ về DTO: cũng là một cấu trúc dữ liệu, nhưng mục đích của nó là tham gia vào logic nghiệp vụ ở các cấp độ khác nhau của ứng dụng. Mục đích của DTO là vận chuyển dữ liệu đến/từ ứng dụng. Ví dụ về DTO: cũng là một cấu trúc dữ liệu, nhưng mục đích của nó là tham gia vào logic nghiệp vụ ở các cấp độ khác nhau của ứng dụng. Mục đích của DTO là vận chuyển dữ liệu đến/từ ứng dụng. Ví dụ về DTO:
@Setter
@Getter
@NoArgsConstructor
public class UserDto {
private long id;
private String firstName;
private String lastName;
private String email;
private String password;
}
Mọi thứ dường như đủ rõ ràng, nhưng ở đây chúng ta tìm hiểu về sự tồn tại của các giống lai. Kết hợp là các đối tượng có các phương thức xử lý logic quan trọng, lưu trữ các phần tử bên trong và cũng bao gồm các phương thức truy cập (get/set). Các đối tượng như vậy rất lộn xộn và gây khó khăn cho việc thêm các phương thức mới. Bạn nên tránh chúng, vì không rõ chúng dùng để làm gì — lưu trữ các phần tử hay thực thi logic?
Nguyên tắc tạo biến
Hãy suy nghĩ một chút về các biến. Cụ thể hơn, hãy nghĩ về những nguyên tắc áp dụng khi tạo chúng:- Tốt nhất, bạn nên khai báo và khởi tạo một biến ngay trước khi sử dụng nó (đừng tạo một biến rồi quên nó đi).
- Bất cứ khi nào có thể, hãy khai báo các biến là cuối cùng để ngăn giá trị của chúng thay đổi sau khi khởi tạo.
- Đừng quên các biến đếm, mà chúng ta thường sử dụng trong một số loại vòng lặp for . Đó là, đừng quên loại bỏ chúng. Nếu không, tất cả logic của chúng tôi có thể bị hỏng.
- Bạn nên cố gắng khởi tạo các biến trong hàm tạo.
- Nếu có lựa chọn giữa việc sử dụng một đối tượng có hoặc không có tham chiếu ( new SomeObject() ), hãy chọn không sử dụng, vì sau khi đối tượng được sử dụng, nó sẽ bị xóa trong chu kỳ thu gom rác tiếp theo và tài nguyên của nó sẽ không bị lãng phí.
- Giữ thời gian tồn tại của một biến (khoảng cách giữa việc tạo biến và lần cuối cùng nó được tham chiếu) càng ngắn càng tốt.
- Khởi tạo các biến được sử dụng trong một vòng lặp ngay trước vòng lặp, không phải ở đầu phương thức chứa vòng lặp.
- Luôn bắt đầu với phạm vi hạn chế nhất và chỉ mở rộng khi cần thiết (bạn nên cố gắng tạo một biến cục bộ nhất có thể).
- Sử dụng mỗi biến cho một mục đích duy nhất.
- Tránh các biến có mục đích ẩn, ví dụ: một biến được phân chia giữa hai nhiệm vụ — điều này có nghĩa là loại của nó không phù hợp để giải quyết một trong số chúng.
phương pháp
từ bộ phim "Chiến tranh giữa các vì sao: Tập III - Sự trả thù của người Sith" (2005)
-
Quy tắc số 1 - Nhỏ gọn. Lý tưởng nhất là một phương thức không được vượt quá 20 dòng. Điều này có nghĩa là nếu một phương thức công khai "phồng lên" đáng kể, bạn cần nghĩ đến việc tách logic ra và chuyển nó thành các phương thức riêng tư riêng biệt.
-
Quy tắc #2 — if , other , while và các câu lệnh khác không được có nhiều khối lồng nhau: nhiều khối lồng nhau làm giảm đáng kể khả năng đọc mã. Lý tưởng nhất là bạn không nên có nhiều hơn hai khối {} lồng nhau .
Và cũng nên giữ mã trong các khối này nhỏ gọn và đơn giản.
-
Quy tắc #3 — Một phương thức chỉ nên thực hiện một thao tác. Nghĩa là, nếu một phương thức thực hiện tất cả các loại logic phức tạp, chúng ta sẽ chia nó thành các phương thức con. Kết quả là, bản thân phương thức sẽ là một mặt tiền có mục đích gọi tất cả các hoạt động khác theo đúng thứ tự.
Nhưng nếu thao tác có vẻ quá đơn giản để đưa vào một phương thức riêng biệt thì sao? Đúng là đôi khi có cảm giác như đang bắn đại bác vào chim sẻ, nhưng các phương pháp nhỏ mang lại một số lợi thế:
- Hiểu mã tốt hơn;
- Các phương thức có xu hướng trở nên phức tạp hơn khi tiến trình phát triển. Nếu một phương pháp bắt đầu đơn giản, thì sẽ dễ dàng hơn một chút để phức tạp hóa chức năng của nó;
- Chi tiết triển khai được ẩn;
- Tái sử dụng mã dễ dàng hơn;
- Mã đáng tin cậy hơn.
-
Quy tắc giảm dần - Mã nên được đọc từ trên xuống dưới: bạn đọc càng thấp, bạn càng hiểu sâu hơn về logic. Và ngược lại, càng lên cao, các phương pháp càng trừu tượng. Ví dụ: các câu lệnh chuyển đổi khá không nhỏ gọn và không mong muốn, nhưng nếu bạn không thể tránh sử dụng lệnh chuyển đổi, bạn nên cố gắng di chuyển nó xuống mức thấp nhất có thể, đến các phương thức cấp thấp nhất.
-
Đối số phương thức — Con số lý tưởng là gì? Lý tưởng nhất là không có gì cả :) Nhưng điều đó có thực sự xảy ra không? Điều đó nói rằng, bạn nên cố gắng có càng ít đối số càng tốt, bởi vì càng ít đối số thì càng dễ sử dụng một phương thức và càng dễ kiểm tra nó. Khi nghi ngờ, hãy thử dự đoán tất cả các tình huống sử dụng phương pháp với một số lượng lớn tham số đầu vào.
-
Ngoài ra, sẽ tốt hơn nếu tách các phương thức có cờ boolean làm tham số đầu vào, vì bản thân điều này hàm ý rằng phương thức thực hiện nhiều hơn một thao tác (nếu đúng, thì thực hiện một thao tác; nếu sai, thì thực hiện thao tác khác). Như tôi đã viết ở trên, điều này là không tốt và nên tránh nếu có thể.
-
Nếu một phương thức có một số lượng lớn các tham số đầu vào (cao nhất là 7, nhưng bạn thực sự nên bắt đầu suy nghĩ sau 2-3), một số đối số nên được nhóm thành một đối tượng riêng biệt.
-
Nếu có một số phương thức tương tự (đã quá tải), thì các tham số tương tự phải được truyền theo cùng một thứ tự: điều này cải thiện khả năng đọc và khả năng sử dụng.
-
Khi bạn truyền tham số cho một phương thức, bạn phải chắc chắn rằng tất cả chúng đều được sử dụng, nếu không thì tại sao bạn cần chúng? Cắt mọi tham số không sử dụng ra khỏi giao diện và hoàn thành nó.
- try/catch về bản chất trông không đẹp lắm, vì vậy sẽ là một ý tưởng hay nếu chuyển nó thành một phương thức trung gian riêng biệt (một phương thức để xử lý các ngoại lệ):
public void exceptionHandling(SomeObject obj) { try { someMethod(obj); } catch (IOException e) { e.printStackTrace(); } }
GO TO FULL VERSION