[Clean code] Chapter 10: Class

Cho đến nay trong cuốn sách này, chúng tôi đã tập trung vào cách viết tốt các dòng và các khối code. Chúng tôi đã đi sâu vào thành phần thích hợp của các chức năng và cách chúng tương tác với nhau. Nhưng đối với tất cả sự chú ý đến tính biểu đạt của các câu lệnh và các chức năng mà chúng bao gồm, chúng tôi vẫn chưa có mã rõ ràng cho đến khi chúng tôi chú ý đến các cấp tổ chức code cao hơn. Hãy nói về clean class!!

Class Organization

Theo quy ước chuẩn của Java, một class phải bắt đầu bằng một danh sách các biến. Các hằng số tĩnh piblic, nếu có, nên xuất hiện trước. Sau đó là các biến tĩnh private, tiếp theo là các biến private. Ít khi có lý do chính đáng để có một biến public.

Các public functions nên tuân theo danh sách các biến. Chúng tôi muốn đặt các private utilities được gọi bởi một public function ngay sau public function. Điều này tuân theo quy tắc stepdown và giúp chương trình đọc giống như một bài báo.

Đóng gói

Chúng tôi muốn giữ các biến và utility của mình ở chế độ private, nhưng chúng tôi không quá cuồng nhiệt về nó. Đôi khi chúng ta cần bảo vệ một biến hoặc một utility để nó có thể được truy cập bằng một bài test. Đối với chúng tôi, quy tắc test. Nếu một test trong cùng một package cần gọi một hàm hoặc truy cập một biến, chúng tôi sẽ đặt nó trong phạm vi protected hoặc package. Tuy nhiên, trước tiên, chúng tôi sẽ tìm cách duy trì private. Nới lỏng sự đóng gói luôn là phương sách cuối cùng.

Classes Should Be Small!

Nguyên tắc đầu tiên của các class là chúng nên nhỏ. Nguyên tắc thứ 2 là chúng nên nhỏ hơn nữa. Không, chúng ta không nhắc lại text từ chương Function. Nhưng cũng như với các hàm, nhỏ hơn là nguyên tắc chính khi thiết kế các lớp. Đối với các hàm, câu hỏi trước mắt của chúng tôi luôn là “Nhỏ như thế nào?”

Với hàm, chúng ta có thể đo bằng số dòng vật lý. Với class chúng ta sử dụng một thước đo khác, chúng ta đếm trách nhiệm (responsibilities)

Với class chúng ta sử dụng một thước đo khác, chúng ta đếm trách nhiệm (responsibilities)

Hình 10-1 là một class, SuperDashboard với khoảng 70 public methods. Đa số LTV đồng ý rằng nó quá lớn. Một vài LTV có thể coi SuperDasdboard là “God class”:

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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
// Figure 10-1
// Too Many Responsibilities
public class SuperDashboard extends JFrame implements MetaDataUser {
public String getCustomizerLanguagePath()
public void setSystemConfigPath(String systemConfigPath)
public String getSystemConfigDocument()
public void setSystemConfigDocument(String systemConfigDocument)
public boolean getGuruState()
public boolean getNoviceState()
public boolean getOpenSourceState()
public void showObject(MetaObject object)
public void showProgress(String s)
public boolean isMetadataDirty()
public void setIsMetadataDirty(boolean isMetadataDirty)
public Component getLastFocusedComponent()
public void setLastFocused(Component lastFocused)
public void setMouseSelectState(boolean isMouseSelected)
public boolean isMouseSelected()
public LanguageManager getLanguageManager()
public Project getProject()
public Project getFirstProject()
public Project getLastProject()
public String getNewProjectName()
public void setComponentSizes(Dimension dim)
public String getCurrentDir()
public void setCurrentDir(String newDir)
public void updateStatus(int dotPos, int markPos)
public Class[] getDataBaseClasses()
public MetadataFeeder getMetadataFeeder()
public void addProject(Project project)
public boolean setCurrentProject(Project project)
public boolean removeProject(Project project)
public MetaProjectHeader getProgramMetadata()
public void resetDashboard()
public Project loadProject(String fileName, String projectName)
public void setCanSaveMetadata(boolean canSave)
public MetaObject getSelectedObject()
public void deselectObjects()
public void setProject(Project project)
public void editorAction(String actionName, ActionEvent event)
public void setMode(int mode)
public FileManager getFileManager()
public void setFileManager(FileManager fileManager)
public ConfigManager getConfigManager()
public void setConfigManager(ConfigManager configManager)
public ClassLoader getClassLoader()
public void setClassLoader(ClassLoader classLoader)
public Properties getProps()
public String getUserHome()
public String getBaseDir()
public int getMajorVersionNumber()
public int getMinorVersionNumber()
public int getBuildNumber()
public MetaObject pasting(MetaObject target, MetaObject pasted, MetaProject project)
public void processMenuItems(MetaObject metaObject)
public void processMenuSeparators(MetaObject metaObject)
public void processTabPages(MetaObject metaObject)
public void processPlacement(MetaObject object)
public void processCreateLayout(MetaObject object)
public void updateDisplayLayer(MetaObject object, int layerIndex)
public void propertyEditedRepaint(MetaObject object)
public void processDeleteObject(MetaObject object)
public boolean getAttachedToDesigner()
public void processProjectChangedState(boolean hasProjectChanged)
public void processObjectNameChanged(MetaObject object)
public void runProject()
public void setAçowDragging(boolean allowDragging)
public boolean allowDragging()
public boolean isCustomizing()
public void setTitle(String title)
public IdeMenuBar getIdeMenuBar()
public void showHelper(MetaObject metaObject, String propertyName)
// ... many non-public methods follow ...
}

Nhưng điều gì sẽ xảy ra nếu SuperDashboard chỉ chứa các methods:
1
2
3
4
5
6
7
8
9
// Figure 10-2
// Small Enough?
public class SuperDashboard extends JFrame implements MetaDataUser {
public Component getLastFocusedComponent()
public void setLastFocused(Component lastFocused)
public int getMajorVersionNumber()
public int getMinorVersionNumber()
public int getBuildNumber()
}

5 phương thức không phải nhiều, đúng không? Trong trường hợp này đó là vì mặc dù số lượng methods là nhỏ, SuperDashboard lại có quá nhiều trách nhiệm.

Tên của một class phải mô tả những trách nhiệm mà nó thực hiện. Trên thực tế, đặt tên có lẽ là cách đầu tiên giúp xác định quy mô class. Nếu chúng ta không thể tìm ra một tên ngắn gọn cho một class, thì có thể nó quá lớn. Tên lớp càng mơ hồ, thì càng có nhiều khả năng nó có quá nhiều trách nhiệm. Ví dụ: tên lớp bao gồm các từ chung như Processor hoặc Manager hoặc Super thường gợi ý về sự kết hợp đáng tiếc của các trách nhiệm.

Chúng ta cũng có thể viết mô tả ngắn gọn về class trong khoảng 25 từ, mà không sử dụng các từ “nếu”, “và”, “hoặc” hoặc “nhưng”. Chúng ta sẽ mô tả SuperDashboard như thế nào? “SuperDashboard cung cấp quyền truy cập vào thành phần giữ tiêu điểm cuối cùng và nó cũng cho phép chúng tôi theo dõi phiên bản và số lượng build.” Chữ “và” đầu tiên là một gợi ý rằng SuperDashboard có quá nhiều trách nhiệm.

The Single Responsibility Principle

The Single Responsibility Principle (SRP) có nghĩa là một class hoặc module nên có 1 và chỉ một lý do để thay đổi. Nguyên tắc này cung cấp cho chúng ta cả định nghĩa về trách nhiệm và hướng dẫn về quy mô class. Class nên có 1 trách nhiệm - một lý do để thay đổi

The Single Responsibility Principle (SRP) có nghĩa là một class hoặc module nên có 1 và chỉ một lý do để thay đổi

Lớp SuperDashboard có vẻ nhỏ trong Liệt kê 10-2 có hai lý do để thay đổi. Đầu tiên, nó theo dõi thông tin phiên bản mà dường như cần được cập nhật mỗi khi phần mềm được release. Thứ hai, nó quản lý các thành phần Java Swing (nó là một dẫn xuất của JFrame, đại diện Swing của một cửa sổ GUI cấp cao nhất). Không nghi ngờ gì nữa, chúng tôi sẽ muốn cập nhật số phiên bản nếu chúng tôi thay đổi bất kỳ mã Swing nào, nhưng cuộc trò chuyện không nhất thiết đúng: Chúng tôi có thể thay đổi thông tin phiên bản dựa trên những thay đổi đối với code khác trong hệ thống.

Cố gắng xác định trách nhiệm (lý do để thay đổi) thường giúp chúng ta nhận ra và tạo ra những điều trừu tượng tốt hơn trong code của chúng ta. Chúng ta có thể dễ dàng trích xuất cả ba phương thức SuperDashboard xử lý thông tin phiên bản vào một lớp riêng biệt có tên Version. (Xem Liệt kê 10-3.) Lớp Version là một cấu trúc có tiềm năng cao để sử dụng lại trong các ứng dụng khác!

1
2
3
4
5
6
7
// Figure 10-3
// A single-responsibility class
public class Version {
public int getMajorVersionNumber()
public int getMinorVersionNumber()
public int getBuildNumber()
}

SRP là một trong những khái niệm quan trọng hơn trong thiết kế OO. Đây cũng là một trong những khái niệm đơn giản hơn để hiểu và tuân thủ. Tuy nhiên, điều kỳ lạ là SRP thường là nguyên tắc thiết kế lớp bị lạm dụng nhiều nhất. Chúng tôi thường xuyên gặp phải các lớp học làm quá nhiều thứ. Tại sao?

Làm cho phần mềm hoạt động và làm cho phần mềm clean là hai hoạt động rất khác nhau. Hầu hết chúng ta đều có giới hạn trong đầu, vì vậy chúng ta tập trung vào việc code của mình hoạt động nhiều hơn là tổ chức và clean. Điều này hoàn toàn phù hợp. Duy trì sự tách biệt các mối quan tâm cũng quan trọng trong các hoạt động lập trình của chúng tôi cũng như trong các chương trình của chúng tôi.

Vấn đề là có quá nhiều người trong chúng ta nghĩ rằng chúng ta đã hoàn thành một khi chương trình vì nó hoạt động. Chúng tôi không thể chuyển sang mối quan tâm khác là tổ chức và clean. Chúng ta chuyển sang vấn đề tiếp theo thay vì quay lại và chia nhỏ các lớp được nhồi quá nhiều thành các đơn vị tách rời với các trách nhiệm duy nhất.

Đồng thời, nhiều nhà phát triển lo ngại rằng một số lượng lớn các lớp nhỏ, mục đích duy nhất khiến việc hiểu bức tranh lớn hơn trở nên khó khăn hơn. Họ lo ngại rằng họ phải điều hướng từ lớp này sang lớp khác để tìm ra cách hoàn thành một phần công việc lớn hơn.

Tuy nhiên, một hệ thống có nhiều lớp nhỏ không có nhiều bộ phận chuyển động hơn một hệ thống có một vài lớp lớn. Có nhiều thứ để học trong hệ thống với một vài lớp lớn. Vì vậy, câu hỏi đặt ra là: Bạn có muốn các công cụ của mình được tổ chức thành các hộp công cụ với nhiều ngăn kéo nhỏ, mỗi ngăn chứa các thành phần được xác định rõ ràng và được dán nhãn rõ ràng? Hay bạn muốn có một vài ngăn kéo mà bạn chỉ cần ném mọi thứ vào?

Bạn có muốn các công cụ của mình được tổ chức thành các hộp công cụ với nhiều ngăn kéo nhỏ, mỗi ngăn chứa các thành phần được xác định rõ ràng và được dán nhãn rõ ràng? Hay bạn muốn có một vài ngăn kéo mà bạn chỉ cần ném mọi thứ vào?

Mọi hệ thống khá lớn sẽ chứa một lượng lớn logic và độ phức tạp. Mục tiêu chính trong việc quản lý sự phức tạp đó là tổ chức nó để một nhà phát triển biết nơi cần tìm để tìm mọi thứ và chỉ cần hiểu sự phức tạp bị ảnh hưởng trực tiếp tại bất kỳ thời điểm nào. Ngược lại, một hệ thống với các lớp đa năng, lớn hơn luôn cản trở chúng ta bằng cách yêu cầu chúng ta phải vượt qua rất nhiều thứ mà chúng ta không cần biết ngay bây giờ.

Sửa các lớp nhỏ thì độ ảnh hưởng và thời gian tìm hiểu sẽ ít hơn

Nhắc lại những điểm trước đây để nhấn mạnh: Chúng tôi muốn hệ thống của mình bao gồm nhiều lớp nhỏ, không phải một vài lớp lớn. Mỗi lớp nhỏ đóng gói một khả năng đáp ứng duy nhất, có một lý do duy nhất để thay đổi và cộng tác với một vài người khác để đạt được các hành vi hệ thống mong muốn.

Cohesion: Sự gắn kết

Các lớp nên có một số lượng nhỏ các biến. Mỗi phương thức của một lớp nên thao tác với một hoặc nhiều biến này. Nói chung, một phương thức càng thao tác nhiều biến thì phương thức đó càng gắn kết với lớp của nó. Một lớp trong đó mỗi biến được sử dụng bởi mỗi phương thức là tối đa gắn kết.

Nói chung, không nên và cũng không thể tạo các lớp gắn kết tối đa như vậy; mặt khác, chúng tôi muốn sự gắn kết cao. Khi tính liên kết cao, có nghĩa là các phương thức và biến của lớp đồng phụ thuộc và gắn kết với nhau như một tổng thể logic.

Cùng ví dụ sự thực thi Stack trong 10-4. Đây là một lớp rất gắn kết. Trong ba phương thức, chỉ có size() không sử dụng được cả hai biến.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// Figure 10-4
public class Stack {
private int topOfStack = 0;
List<Integer> elements = new LinkedList<Integer>();

public int size() {
return topOfStack;
}
public void push(int element) {
topOfStack++;
elements.add(element);
}
public int pop() throws PoppedWhenEmpty {
if (topOfStack == 0)
throw new PoppedWhenEmpty();
int element = elements.get(--topOfStack); elements.remove(topOfStack);
return element;
}
}

Chiến lược giữ các hàm nhỏ và giữ cho danh sách tham số ngắn đôi khi có thể dẫn đến sự gia tăng các biến được sử dụng bởi một tập hợp con các phương thức. Khi điều này xảy ra, hầu như luôn luôn có nghĩa là có ít nhất một lớp khác đang cố gắng thoát ra khỏi lớp lớn hơn. Bạn nên cố gắng tách các biến và phương thức thành hai hoặc nhiều lớp hơn để các lớp mới gắn kết hơn.

Để ý đến các biến và methods, chúng đi cụm với nhau thì đó là dấu hiệu của việc, bạn nên tách class mới từ class lớn hiện tại =))

Duy trì kết quả gắn kết trong nhiều lớp nhỏ

Chỉ cần hành động phá vỡ các function lớn thành các function nhỏ hơn cũng gây ra sự gia tăng các lớp. Hãy xem xét một hàm lớn với nhiều biến được khai báo bên trong nó. Giả sử bạn muốn trích xuất một phần nhỏ của hàm đó thành một hàm riêng biệt. Tuy nhiên, code bạn muốn trích xuất sử dụng bốn trong số các biến được khai báo trong hàm. Bạn có phải chuyển tất cả bốn biến đó vào hàm mới dưới dạng đối số không?

Không có gì! Nếu chúng ta đã “thăng cấp” bốn biến đó thành các biến của lớp, thì chúng ta có thể trích xuất code mà không cần chuyển bất kỳ biến nào. Sẽ rất dễ dàng để phá vỡ function thành nhiều phần nhỏ.

Thật không may, điều này cũng có nghĩa là các lớp của chúng ta mất tính liên kết vì chúng tích lũy ngày càng nhiều biến chỉ tồn tại để cho phép một vài hàm chia sẻ chúng. Nhưng đợi đã! Nếu có một vài hàm muốn chia sẻ các biến nhất định, điều đó có phải khiến chúng trở thành một lớp theo đúng nghĩa của chúng không? Tất nhiên là thế. Khi các lớp mất tính liên kết, hãy chia tách chúng!

Vì vậy, việc chia một hàm lớn thành nhiều hàm nhỏ hơn thường cho chúng ta cơ hội để tách một số lớp nhỏ hơn ra. Điều này mang lại cho chương trình của chúng tôi một cơ quan hóa tốt hơn nhiều và một cấu trúc minh bạch hơn.

Hãy tách hàm lớn thành các hàm nhỏ, các hàm nhỏ không cần truyền đủ biến của hàm lớn mà hãy thăng cấp các biến này biến của class. Như vậy các hàm nhỏ chơi với nhau => tìm cách tách hàm nhỏ hày thành các class.

Để minh chứng cho ý tôi muốn nói, hãy sử dụng một ví dụ lâu đời được lấy từ cuốn sách tuyệt vời của Knuth về Literate Programming. Hình 10-5 hiển thị bản dịch sang Java của chương trình PrintPrimes của Knuth. Công bằng mà nói với Knuth, đây không phải là chương trình như anh ấy đã viết mà là nó được xuất ra bởi công cụ WEB của anh ấy. Tôi đang sử dụng nó vì nó là nơi khởi đầu tuyệt vời để chia một hàm lớn thành nhiều hàm và lớp nhỏ hơn.

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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
// Figure 10-5
// PrintPrimes.java
package literatePrimes;
public class PrintPrimes {
public static void main(String[] args) {
final int M = 1000; final int RR = 50;
final int CC = 4;
final int WW = 10;
final int ORDMAX = 30; int P[] = new int[M + 1]; int PAGENUMBER;
int PAGEOFFSET; int ROWOFFSET; int C;
int J;
int K;
boolean JPRIME;
int ORD;
int SQUARE;
int N;
int MULT[] = new int[ORDMAX + 1];
J = 1;
K = 1;
P[1] = 2;
ORD = 2;
SQUARE = 9;

while (K < M) {
do {
J = J + 2;
if (J == SQUARE) {
ORD = ORD + 1;
SQUARE = P[ORD] * P[ORD]; MULT[ORD - 1] = J;
}
N = 2;
JPRIME = true;
while (N < ORD && JPRIME) {
while (MULT[N] < J)
MULT[N] = MULT[N] +
if (MULT[N] == J) JPRIME = false;
N = N + 1;
}
} while (!JPRIME); K = K + 1;
P[K] = J;
}

{
PAGENUMBER = 1;
PAGEOFFSET = 1;
while (PAGEOFFSET <= M) {
System.out.println("The First " + M +
" Prime Numbers --- Page " + PAGENUMBER);
System.out.println("");
for (ROWOFFSET = PAGEOFFSET; ROWOFFSET < PAGEOFFSET + RR; ROWOFFSET++) {
for (C = 0; C < CC;C++)
if (ROWOFFSET + C * RR <= M)
System.out.format("%10d", P[ROWOFFSET + C * RR]);
System.out.println("");
}
System.out.println("\f"); PAGENUMBER = PAGENUMBER + 1; PAGEOFFSET = PAGEOFFSET + RR * CC;
PAGENUMBER = PAGENUMBER + 1;
PAGEOFFSET = PAGEOFFSET + RR * CC;
}
}
}
}

Chương trình này, được viết dưới dạng một hàm duy nhất, là một mớ hỗn độn. Nó có cấu trúc thụt vào sâu, rất nhiều biến số lẻ và cấu trúc liên kết chặt chẽ. Ít nhất, một chức năng lớn nên được chia thành một vài chức năng nhỏ hơn.

Từ Liệt kê 10-6 đến Liệt kê 10-8 cho thấy kết quả của việc tách mã trong Liệt kê 10-5 thành các lớp và hàm nhỏ hơn, đồng thời chọn tên có ý nghĩa cho các lớp, chức năng và biến đó.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// Listing 10-6
// PrimePrinter.java (refactored)
package literatePrimes;

public class PrimePrinter {
public static void main(String[] args) {
final int NUMBER_OF_PRIMES = 1000;
int[] primes = PrimeGenerator.generate(NUMBER_OF_PRIMES);

final int ROWS_PER_PAGE = 50;
final int COLUMNS_PER_PAGE = 4;
RowColumnPagePrinter tablePrinter =
new RowColumnPagePrinter(ROWS_PER_PAGE, COLUMNS_PER_PAGE,
"The First " + NUMBER_OF_PRIMES + " Prime Numbers");

tablePrinter.print(primes);
}
}
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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
// Listing 10-7
// RowColumnPagePrinter.java
package literatePrimes;

import java.io.PrintStream;

public class RowColumnPagePrinter {
private int rowsPerPage;
private int columnsPerPage;
private int numbersPerPage;
private String pageHeader;
private PrintStream printStream;

public RowColumnPagePrinter(int rowsPerPage, int columnsPerPage,
String pageHeader) {
this.rowsPerPage = rowsPerPage;
this.columnsPerPage = columnsPerPage;
this.pageHeader = pageHeader;
numbersPerPage = rowsPerPage * columnsPerPage;
printStream = System.out;
}

public void print(int data[]) {
int pageNumber = 1;
for (int firstIndexOnPage = 0; firstIndexOnPage < data.length; firstIndexOnPage += numbersPerPage) {
int lastIndexOnPage =
Math.min(firstIndexOnPage + numbersPerPage - 1, data.length - 1);
printPageHeader(pageHeader, pageNumber);
printPage(firstIndexOnPage, lastIndexOnPage, data);
printStream.println("\f");
pageNumber++;
}
}

private void printPage(int firstIndexOnPage, int lastIndexOnPage,
int[] data) {
int firstIndexOfLastRowOnPage =
firstIndexOnPage + rowsPerPage - 1;
for (int firstIndexInRow = firstIndexOnPage; firstIndexInRow <= firstIndexOfLastRowOnPage; firstIndexInRow++) {
printRow(firstIndexInRow, lastIndexOnPage, data);
printStream.println("");
}
}

private void printRow(int firstIndexInRow, int lastIndexOnPage,
int[] data) {
for (int column = 0; column < columnsPerPage; column++) {
int index = firstIndexInRow + column * rowsPerPage;
if (index <= lastIndexOnPage)
printStream.format("%10d", data[index]);
}
}

private void printPageHeader(String pageHeader, int pageNumber) {
printStream.println(pageHeader + " --- Page " + pageNumber);
printStream.println("");
}

public void setOutput(PrintStream printStream) {
this.printStream = printStream;
}
}
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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
// Listing 10-8
// PrimeGenerator.java
package literatePrimes;

import java.util.ArrayList;

public class PrimeGenerator {
private static int[] primes;
private static ArrayList < Integer > multiplesOfPrimeFactors;

protected static int[] generate(int n) {
primes = new int[n];
multiplesOfPrimeFactors = new ArrayList < Integer > ();
set2AsFirstPrime();
checkOddNumbersForSubsequentPrimes();
return primes;
}

private static void set2AsFirstPrime() {
primes[0] = 2;
multiplesOfPrimeFactors.add(2);
}

private static void checkOddNumbersForSubsequentPrimes() {
int primeIndex = 1;
for (int candidate = 3; primeIndex < primes.length; candidate += 2) {
if (isPrime(candidate))
primes[primeIndex++] = candidate;
}
}

private static boolean isPrime(int candidate) {
if (isLeastRelevantMultipleOfNextLargerPrimeFactor(candidate)) {
multiplesOfPrimeFactors.add(candidate);
return false;
}
return isNotMultipleOfAnyPreviousPrimeFactor(candidate);
}

private static boolean isLeastRelevantMultipleOfNextLargerPrimeFactor(int candidate) {
int nextLargerPrimeFactor = primes[multiplesOfPrimeFactors.size()];
int leastRelevantMultiple = nextLargerPrimeFactor * nextLargerPrimeFactor;
return candidate == leastRelevantMultiple;
}

private static boolean isNotMultipleOfAnyPreviousPrimeFactor(int candidate) {
for (int n = 1; n < multiplesOfPrimeFactors.size(); n++) {
if (isMultipleOfNthPrimeFactor(candidate, n)) return false;
}
return true;
}

private static boolean isMultipleOfNthPrimeFactor(int candidate, int n) {
return
candidate == smallestOddNthMultipleNotLessThanCandidate(candidate, n);
}

private static int smallestOddNthMultipleNotLessThanCandidate(int candidate, int n) {
int multiple = multiplesOfPrimeFactors.get(n);
while (multiple < candidate)
multiple += 2 * primes[n];
multiplesOfPrimeFactors.set(n, multiple);
return multiple;
}
}

Điều đầu tiên bạn có thể nhận thấy là chương trình dài hơn rất nhiều. Nó dài từ hơn một trang đến gần ba trang. Có một số lý do cho sự tăng trưởng này. Đầu tiên, chương trình được tái cấu trúc sử dụng các tên biến mô tả dài hơn. Thứ hai, chương trình được tái cấu trúc sử dụng khai báo hàm và lớp như một cách để thêm chú thích vào mã. Thứ ba, chúng tôi đã sử dụng các kỹ thuật định dạng và khoảng trắng để giữ cho chương trình có thể đọc được.

Lưu ý rằng chương trình đã được chia thành ba trách nhiệm chính như thế nào. Chương trình chính được chứa trong lớp PrimePrinter. Trách nhiệm của nó là xử lý môi trường thực thi. Nó sẽ thay đổi nếu phương thức gọi thay đổi. Ví dụ: nếu chương trình này được chuyển đổi thành dịch vụ SOAP, thì đây là lớp sẽ bị ảnh hưởng.

RowColumnPagePrinter biết tất cả về cách định dạng danh sách số thành các trang với một số hàng và cột nhất định. Nếu định dạng của đầu ra cần thay đổi, thì đây là lớp sẽ bị ảnh hưởng.

Lớp PrimeGenerator biết cách tạo một danh sách các số nguyên tố. Lưu ý rằng nó không có nghĩa là được khởi tạo như một đối tượng. Lớp chỉ là một phạm vi hữu ích trong đó các biến của nó có thể được khai báo và giữ ẩn. Lớp này sẽ thay đổi nếu thuật toán tính toán các số nguyên tố thay đổi.

Đây không phải là một bài viết lại! Chúng tôi đã không bắt đầu lại từ đầu và viết lại chương trình. Thật vậy, nếu bạn xem xét kỹ hai chương trình khác nhau, bạn sẽ thấy rằng chúng sử dụng cùng một thuật toán và cơ chế để hoàn thành công việc của mình.

Thay đổi được thực hiện bằng cách viết một bộ thử nghiệm xác minh hành vi chính xác của chương trình đầu tiên. Sau đó, vô số thay đổi nhỏ được thực hiện, mỗi lần một thay đổi. Sau mỗi lần thay đổi, chương trình được thực thi để đảm bảo rằng hành vi không thay đổi. Hết bước này đến bước khác, chương trình đầu tiên được dọn dẹp và chuyển thành chương trình thứ hai.

Organizing for Change

Đối với hầu hết các hệ thống, sự thay đổi là liên tục. Mọi thay đổi đều khiến chúng ta có nguy cơ phần còn lại của hệ thống không còn hoạt động như dự kiến. Trong một hệ thống sạch sẽ, chúng tôi tổ chức các lớp của mình để giảm nguy cơ thay đổi.

Lớp Sql trong Liệt kê 10-9 được sử dụng để tạo các chuỗi SQL được định dạng đúng với siêu dữ liệu thích hợp. Đây là một công việc đang được tiến hành và do đó, chưa hỗ trợ tính năng SQL như các câu lệnh UPDATE. Khi đến lúc lớp Sql hỗ trợ UPDATE cập nhật, chúng tôi sẽ phải “mở” lớp này để thực hiện sửa đổi. Vấn đề với việc mở một lớp là nó mang lại rủi ro. Bất kỳ sửa đổi nào đối với lớp đều có khả năng phá vỡ mã khác trong lớp. Nó phải được kiểm tra lại hoàn toàn.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// Figure 10-9
// A class that must be opened for change
public class Sql {
public Sql(String table, Column[] columns)
public String create()
public String insert(Object[] fields)
public String selectAll()
public String findByKey(String keyColumn, String keyValue)
public String select(Column column, String pattern)
public String select(Criteria criteria)
public String preparedInsert()
private String columnList(Column[] columns)
private String valuesList(Object[] fields, final Column[] columns) private String selectWithCriteria(String criteria)
private String placeholderList(Column[] columns)
}

Lớp Sql phải thay đổi khi chúng ta thêm một kiểu câu lệnh mới. Nó cũng phải thay đổi khi chúng tôi thay đổi chi tiết của một loại câu lệnh đơn lẻ — ví dụ: nếu chúng tôi cần sửa đổi hàm SELECT để hỗ trợ các lựa chọn con. Hai lý do để thay đổi này có nghĩa là lớp Sql vi phạm SRP.

Chúng tôi có thể phát hiện vi phạm SRP này từ quan điểm tổ chức đơn giản. Sơ lược phương thức của Sql cho thấy rằng có các phương thức riêng, chẳng hạn như selectWithCriteria, dường như chỉ liên quan đến các câu lệnh select.

Private method chỉ áp dụng cho một tập hợp con nhỏ của một lớp có thể là một phát hiện hữu ích để thấy các khu vực tiềm năng để cải thiện. Tuy nhiên, động lực chính cho hành động tìm kiếm phải là bản thân sự thay đổi của hệ thống. Nếu lớp Sql được coi là hoàn chỉnh về mặt logic, thì chúng ta không cần lo lắng về việc tách các trách nhiệm. Nếu chúng ta không cần chức năng UPDATE trong tương lai gần, thì chúng ta nên để Sql một mình. Nhưng ngay sau khi chúng tôi nhận thấy mình đang mở một lớp, chúng tôi nên xem xét việc sửa chữa thiết kế của mình.

Điều gì sẽ xảy ra nếu chúng ta xem xét một giải pháp như vậy trong Liệt kê 10-10? Mỗi public interface method được định nghĩa trong Sql trước đó từ Liệt kê 10-9 được cấu trúc lại thành dẫn xuất riêng của lớp Sql. Lưu ý rằng các phương thức private, chẳng hạn như valueList, di chuyển trực tiếp đến nơi chúng cần thiết. Hành vi riêng tư chung được tách biệt với một cặp lớp tiện ích, WhereColumnList

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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
// Figure 10-10
// A set of closed classes
abstract public class Sql {
public Sql(String table, Column[] columns)
abstract public String generate();
}

public class CreateSql extends Sql {
public CreateSql(String table, Column[] columns)
@Override public String generate()
}

public class SelectSql extends Sql {
public SelectSql(String table, Column[] columns)
@Override public String generate()
}

public class InsertSql extends Sql {
public InsertSql(String table, Column[] columns, Object[] fields)
@Override public String generate()
private String valuesList(Object[] fields, final Column[] columns)
}

public class SelectWithCriteriaSql extends Sql {
public SelectWithCriteriaSql(String table, Column[] columns, Criteria criteria)
@Override public String generate()
}

public class SelectWithMatchSql extends Sql {
public SelectWithMatchSql(String table, Column[] columns, Column column, String pattern)
@Override public String generate()
}

public class FindByKeySql extends Sql {
public FindByKeySql(String table, Column[] columns, String keyColumn, String keyValue)
@Override public String generate()
}

public class PreparedInsertSql extends Sql {
public PreparedInsertSql(String table, Column[] columns)
@Override public String generate()
private String placeholderList(Column[] columns)
}

public class Where {
public Where(String criteria)
public String generate()
}

public class ColumnList {
public ColumnList(Column[] columns)
public String generate()
}

Mã trong mỗi lớp trở nên cực kỳ đơn giản. Thời gian hiểu cần thiết của chúng tôi để hiểu bất kỳ lớp nào giảm xuống gần như không có. Rủi ro mà một chức năng này có thể phá vỡ một chức năng khác trở nên rất nhỏ. Từ quan điểm kiểm tra, việc chứng minh tất cả các bit logic trong giải pháp này trở thành một nhiệm vụ dễ dàng hơn, vì các lớp đều được cách ly với nhau.

Quan trọng không kém, khi đã đến lúc thêm các câu lệnh UPDATE, không có lớp nào trong số các lớp hiện có cần thay đổi! Chúng tôi viết mã logic để xây dựng các câu lệnh UPDATE trong một lớp con mới của Sql có tên là UpdateSql. Không có mã nào khác trong hệ thống sẽ bị hỏng do thay đổi này.

Logic Sql được cấu trúc lại của chúng tôi đại diện cho điều tốt nhất của tất cả các thế giới. Nó hỗ trợ SRP. Nó cũng hỗ trợ một nguyên tắc thiết kế lớp OO quan trọng khác được gọi là Open-Closed Principle, hoặc OCP Lớp nên mở để mở rộng nhưng đóng để sửa đổi. Lớp Sql được cấu trúc lại của chúng tôi mở để cho phép chức năng mới thông qua lớp con, nhưng chúng tôi có thể thực hiện thay đổi này trong khi vẫn đóng mọi lớp khác. Chúng tôi chỉ cần đặt lớp UpdateSql của chúng tôi tại chỗ.

Chúng tôi muốn cấu trúc hệ thống của mình để chúng tôi sử dụng ít nhất có thể khi chúng tôi cập nhật chúng với các tính năng mới hoặc thay đổi. Trong một hệ thống lý tưởng, chúng tôi kết hợp các tính năng mới bằng cách mở rộng hệ thống, không phải bằng cách thực hiện các sửa đổi đối với mã hiện có.

Isolating from Change

Nhu cầu sẽ thay đổi, do đó mã sẽ thay đổi. Chúng tôi đã học được trong OO 101 rằng có các lớp cụ thể, chứa các chi tiết triển khai (code) và các lớp trừu tượng, chỉ đại diện cho các khái niệm. Một lớp khách hàng phụ thuộc vào các chi tiết cụ thể sẽ gặp rủi ro khi các chi tiết đó thay đổi. Chúng tôi có thể giới thiệu các interfaces và abstract classes để giúp cô lập tác động của các chi tiết đó.

Sự phụ thuộc vào các chi tiết cụ thể tạo ra thách thức cho việc test hệ thống. Nếu chúng tôi đang xây dựng một lớp Portfolio và nó phụ thuộc vào API TokyoStockExchange bên ngoài để lấy giá trị của danh mục đầu tư, thì các trường hợp thử nghiệm của chúng tôi sẽ bị ảnh hưởng bởi sự biến động của việc tra cứu như vậy. Thật khó để viết một test khi cứ sau năm phút, chúng tôi lại nhận được một câu trả lời khác nhau!

Thay vì thiết kế Portfolio để nó phụ thuộc trực tiếp vào TokyoStockExchange, chúng tôi tạo một interface, StockExchange, khai báo một method duy nhất:

1
2
3
public interface StockExchange {
Money currentPrice(String symbol);
}

Chúng tôi thiết kế TokyoStockExchange để implement interface này. Chúng tôi cũng đảm bảo rằng constructor của Portfolio lấy tham chiếu StockExchange làm đối số:

1
2
3
4
5
6
7
public Portfolio {
private StockExchange exchange;
public Portfolio(StockExchange exchange) {
this.exchange = exchange;
}
// ...
}

Bây giờ test có thể tạo ra một interface có thể kiểm tra của interface StockExchange mô phỏng TokyoStockExchange. Việc triển khai thử nghiệm này sẽ cố định giá trị hiện tại cho bất kỳ ký hiệu nào mà chúng tôi sử dụng trong test. Nếu test của chúng tôi cho thấy việc mua năm cổ phiếu của Microsoft cho danh mục đầu tư của chúng tôi, thì chúng tôi lập mã việc triển khai thử nghiệm để luôn trả lại 100 đô la cho mỗi cổ phiếu của Microsoft. Việc triển khai thử nghiệm của chúng tôi đối với giao diện StockExchange giảm xuống một tra cứu bảng đơn giản. Sau đó, chúng tôi có thể viết một bài kiểm tra dự kiến $ 500 cho giá trị danh mục đầu tư tổng thể của chúng tôi.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class PortfolioTest {
private FixedStockExchangeStub exchange;
private Portfolio portfolio;

@Before
protected void setUp() throws Exception {
exchange = new FixedStockExchangeStub();
exchange.fix("MSFT", 100);
portfolio = new Portfolio(exchange);
}

@Test
public void GivenFiveMSFTTotalShouldBe500() throws Exception {
portfolio.add(5, "MSFT");
Assert.assertEquals(500,portfolio.value());
}
}

Nếu một hệ thống được tách rời đủ để được test theo cách này, nó cũng sẽ linh hoạt hơn và thúc đẩy việc tái sử dụng nhiều hơn. Việc thiếu khớp nối có nghĩa là các yếu tố của hệ thống của chúng tôi được cách ly tốt hơn với nhau và khỏi sự thay đổi. Sự cô lập này giúp bạn hiểu rõ hơn về từng phần tử của hệ thống dễ dàng hơn.

Bằng cách giảm thiểu sự ghép nối theo cách này, các lớp của chúng ta tuân theo một nguyên tắc thiết kế lớp khác được gọi là Nguyên tắc đảo ngược phụ thuộc (DIP - Dependency Inversion Principle).

Thay vì phụ thuộc vào chi tiết triển khai của lớp TokyoStockExchange, lớp Portfolio của chúng tôi giờ phụ thuộc vào StockExchange interface. StockExchange interface đại diện cho khái niệm trừu tượng về việc yêu cầu giá hiện tại của một biểu tượng. Sự trừu tượng này cô lập tất cả các chi tiết cụ thể của việc nhận được một mức giá như vậy, bao gồm cả việc lấy mức giá đó từ đâu.

Bibliography

[RDD]: Object Design: Roles, Responsibilities, and Collaborations, Rebecca Wirfs-
Brock et al., Addison-Wesley, 2002.
[PPP]: Agile Software Development: Principles, Patterns, and Practices, Robert C. Martin, Prentice Hall, 2002.
[Knuth92]: Literate Programming, Donald E. Knuth, Center for the Study of language and Information, Leland Stanford Junior University, 1992.

Author

Ming

Posted on

2024-08-17

Updated on

2024-08-27

Licensed under

Comments