Mục lục
ToggleTiếp nối phần một về Biến, Tham số bài viết hôm nay sẽ trình bày về việc làm sao để clean code trong method. Mời các bạn đón đọc nhé.
Các function chúng ta tạo ra với mục đích chỉ thực hiện 1 điều duy nhất, nhưng đôi khi chúng lại chứa những thứ ẩn bên trong đó. Đôi khi nó làm thay đổi thuộc tính của class hiện tại, thay đổi giá trị tham số đầu vào hay thậm chí là biến global của hệ thống.
Xem ví dụ dưới dây:
Class UserValidator public class UserValidator { private Cryptographer cryptographer; public boolean checkPassword(String userName, String password) { User user = UserGateway.findByName(userName); if (user != User.NULL) { String codedPhrase = user.getPhraseEncodedByPassword(); String phrase = cryptographer.decrypt(codedPhrase, password); if ("Valid Password".equals(phrase)) { Session.initialize(); return true; } } return false; } }
Trong đoạn code trên có 1 đoạn mã không phù hợp với hàm checkPassword đó là đoạn Session.initialize().
Như tên gọi của hàm, thì hàm này với mục đích là kiểm tra mật khẩu, không có đoạn nào nói rằng sẽ khởi tạo lại Session. Việc này có thể tạo ra rủi ro khi dùng hàm này. Ví dụ như 1 số lúc chúng ta vô tình dùng hàm này để kiểm tra mật khẩu, ví dụ nhập lại mật khẩu trước khi đổi mật khẩu hay thực hiện tác vụ khác. Hành động này có thể vô tình làm mất đi các thông tin đã lưu trong Session trước đó.
Trong trường hợp này có thể đổi tên hàm thành checkPasswordAndInitializeSession
Xem ví dụ sau:
appendFooter(s);
Hàm này có ý nghĩa là gì? nối thêm s vào Footer hay là nối Footer vào s? s là tham số đầu vào hay là tham số đầu ra?
public void appendFooter(StringBuffer report).
Với những hàm như vậy chúng ta sẽ phải tốn công sức hơn để kiểm tra lại chữ ký function ( các function phân biệt nhau bởi param và kiểu param, kiểu trả về, exception ném ra, độ đóng của hàm – tham khảo https://developer.mozilla.org/en-US/docs/Glossary/Signature/Function).
Để giải quyết việc này, thì chúng ta cần hạn chế sử dụng các tham số đầu ra thay vào đó sử dụng dạng đối tượng như sau:
report.appendFooter();
Một Function chỉ nên làm 1 cái gì đó hoặc trả lời 1 cái gì đó, nhưng không bao giờ nên gồm cả 2. Một function chỉ nên thay đổi trạng thái của đối tượng, hoặc nó trả về 1 vài thông tin về đối tượng.
Ví dụ:
public boolean set(String attribute, String value);
Hàm này sẽ set giá trị value cho attribute, trả về true nếu thành công, false nếu thất bại (có thể do thuộc tính attribute không tồn tại).
Điều này sẽ dẫn đến 1 số câu lệnh như sau:
if (set(“username”, “unclebob”))…
Với đoạn mã trên chúng ta có thể hiểu là:
Rất khó để hiểu được điều này vì nó không rõ ràng, set không rõ là động từ hay tính từ.
Người viết với mục đích set là 1 động từu nhưng với ngữ cảnh sử dụng if dường như nó giống tính từ hơn.
Giải pháp đưa ra là đặt lại tên và phân tích đoạn mã trở nên dễ hiểu hơn:
if (attributeExists("username")) { setAttribute("username", "unclebob"); ... }
Trả về mã lỗi là hình thức vi phạm lách luật với việc Tách lệnh truy vấn. Điều này sẽ làm gia tăng việc sử dụng câu lệnh if.
if (deletePage(page) == E_OK)
Điều này sẽ có thể tạo ra việc lồng code nhiều tầng. Bởi lẽ khi bạn trả về mã lôi, bạn tạo ra vấn đề mà hàm gọi phải xử lý lỗi ngay lập tức.
if (deletePage(page) == E_OK) { if (registry.deleteReference(page.name) == E_OK) { if (configKeys.deleteKey(page.name.makeKey()) == E_OK) { logger.log("page deleted"); } else { logger.log("configKey not deleted"); } } else { logger.log("deleteReference from registry failed"); } } else { logger.log("delete failed"); return E_ERROR; }
Nếu sử dụng Exception thay vì trả về error codes, đoạn mã sẽ nhìn sạch sẽ và gọn hơn nhiều.
try { deletePage(page); registry.deleteReference(page.name); configKeys.deleteKey(page.name.makeKey()); } catch (Exception e) { logger.log(e.getMessage()); }
Thật xấu xí khi trộn lẫn code xử lý lỗi và code xử lý logic thông thường. Việc cần làm lúc này là tách hàm trong phần try và catch thành các hàm riêng biệt.
public void delete(Page page) { try { deletePageAndAllReferences(page); } catch (Exception e) { logError(e); } } private void deletePageAndAllReferences(Page page) throws Exception { deletePage(page); registry.deleteReference(page.name); configKeys.deleteKey(page.name.makeKey()); } private void logError(Exception e) { logger.log(e.getMessage()); }
Function chỉ nên thực hiện 1 mục đích (one thing), Việc xử lý lỗi là 1 việc (one thing). Do vậy nếu function dùng để xử lý lỗi , thì nó chỉ nên làm việc đó, không gì khác cả.
Nói cách khác, nếu lênh try có trong function thì nó nên nằm đầu tiên trong hàm, và không có xử lý bất kì gì khác sau khối try/catch.
Thông thường việc trả về mã lỗi sẽ sử dụng trong 1 class hoặc enum.
public enum Error { OK, INVALID, NO_SUCH, LOCKED, OUT_OF_RESOURCES, WAITING_FOR_EVENT; }
Có rất nhiều class khác sẽ import và sử dụng class này. Do vậy khi class/enum này thay đổi thì tất cả các class phụ thuộc phải biên dịch và triển khai lại (recompiled và redeployed ). Các lập trình viên không muốn thêm mới các mã lỗi sẽ tận dụng lại mã lỗi cũ để tránh việc tái biên dịch.
Nhưng khi chúng ta sử dụng Exception thay vì mã lỗi, các Exception mới đều là dẫn xuất của class Exception. Chúng ta có thể thêm vào mà không phải thực hiện hành động biên dịch cũng như triển khai lại.
Việc trùng lặp code làm cho mã nguồn của chúng ta nặng nề hơn. Thứ nữa nếu có lỗi xảy ra, việc lặp code sẽ tạo ra vô số các bản sao của lỗi đó.
Trùng lặp là gốc rễ mọi tội lỗi trong phần mềm. Nhiều nguyên tắc và thông lệ được tạo ra để kiểm soát và hạn chế sự trùng lặp đó như.
Lập trình khai báo:
Lập trình mệnh lệnh:
Nguyên tắc Dijkstra trong lập trình cấu trúc nói rằng: mọi hàm và mọi block trong function nên có 1 đầu vào 1 và 1 đầu ra. Theo quy tắc này thì chỉ nên có 1 lệnh return trong hàm, không có break, continue trong vòng lặp, và không bao giờ sử dụng goto.
Những nguyên tắc này không thực sự mang nhiều lợi ích cho các hàm nhỏ, nhưng mang ý nghĩa lớn với các hàm lớn hơn.
Do vậy, nếu bạn giữ function của bạn nhỏ, thì việc sử dụng nhiều lệnh return, break hay continue không ảnh hưởng gì cả. Mặt khác lệnh goto cũng chỉ nên dùng cho những hàm lớn mà thôi.
Viết phần mềm cũng giống như viết văn vậy. Khi bạn viết xuống 1 bài báo, bạn bắt đầu viết những ý tưởng xuống sau đó hiệu chỉnh nó để trở nên tốt hơn. Bản nháp đầu tiên có thể vụng về và vô tổ chức. Sau chỉnh sửa bạn có thể chỉnh lại câu từ, tái cấu trúc hay lọc lại nó cho tới khi đúng như ý bạn muốn.
Khí viết 1 hàm chức năng, thường nó sẽ dài và phức tạp. Nó cũng bao gồm rất nhiều cấp thụt dòng và nhiều vòng lặp lồng nhau. Thậm chí nó có list dài tham số truyền vào. Tên cũng chẳng tối ưu, hay code cũng bị trùng lặp.
Việc này là hoàn toàn bình thường, bởi lẽ sau đó mới là việc quan trọng. Chúng ta sẽ hiệu chỉnh, lọc lại đoạn code, tách nhỏ hàm, đổi tên, loại bỏ trùng lặp, thu nhỏ các function và tái sử dụng chúng. Đôi khi chúng ta sẽ cần tách nhỏ các class. Nhưng tất cả điều này vẫn phải đảm bảo đoạn mã thỏa mãn test case đưa ra.
Có lẽ rất ít người có thể việc tốt, chuẩn chỉnh ngay từ đầu. Thay vào đó chúng ta thường bắt đầu với các function rườm rà, chưa đúng chuẩn sau đó tối ưu lại.
Nhớ rằng Function là động từu, class là danh từ. Đây không phải là 1 quy định cứng nhắc cổ hủ nào đó. Mà đây là nghệ thuật lập trình, và luôn luôn là nghệ thuật của ngôn ngữ thiết kế.
Những lập trình viên lão làng nghĩ về các hệ thống như những câu truyện được kể hơn là những chương trình được viết.
Nói ngắn gọn thì giống như là :”Viết code như làm thơ”. Thi sĩ thì có thơ để bộc bạch, coder thì thổ lộ qua từng dòng code.
SetupTeardownIncluder.java package fitnesse.html; import fitnesse.responders.run.SuiteResponder; import fitnesse.wiki. * ; public class SetupTeardownIncluder { private PageData pageData; private boolean isSuite; private WikiPage testPage; private StringBuffer newPageContent; private PageCrawler pageCrawler; public static String render(PageData pageData) throws Exception { return render(pageData, false); } public static String render(PageData pageData, boolean isSuite) throws Exception { return new SetupTeardownIncluder(pageData).render(isSuite); } private SetupTeardownIncluder(PageData pageData) { this.pageData = pageData; testPage = pageData.getWikiPage(); pageCrawler = testPage.getPageCrawler(); newPageContent = new StringBuffer(); } private String render(boolean isSuite) throws Exception { this.isSuite = isSuite; if (isTestPage()) includeSetupAndTeardownPages(); return pageData.getHtml(); } private boolean isTestPage() throws Exception { return pageData.hasAttribute("Test"); } private void includeSetupAndTeardownPages() throws Exception { includeSetupPages(); includePageContent(); includeTeardownPages(); updatePageContent(); } private void includeSetupPages() throws Exception { if (isSuite) includeSuiteSetupPage(); includeSetupPage(); } private void includeSuiteSetupPage() throws Exception { include(SuiteResponder.SUITE_SETUP_NAME, "-setup"); } private void includeSetupPage() throws Exception { include("SetUp", "-setup"); } private void includePageContent() throws Exception { newPageContent.append(pageData.getContent()); } private void includeTeardownPages() throws Exception { includeTeardownPage(); if (isSuite) includeSuiteTeardownPage(); } private void includeTeardownPage() throws Exception { include("TearDown", "-teardown"); } private void includeSuiteTeardownPage() throws Exception { include(SuiteResponder.SUITE_TEARDOWN_NAME, "-teardown"); } private void updatePageContent() throws Exception { pageData.setContent(newPageContent.toString()); } private void include(String pageName, String arg) throws Exception { WikiPage inheritedPage = findInheritedPage(pageName); if (inheritedPage != null) { String pagePathName = getPathNameForPage(inheritedPage); buildIncludeDirective(pagePathName, arg); } } private WikiPage findInheritedPage(String pageName) throws Exception { return PageCrawlerImpl.getInheritedPage(pageName, testPage); } private String getPathNameForPage(WikiPage page) throws Exception { WikiPagePath pagePath = pageCrawler.getFullPath(page); return PathParser.render(pagePath); } private void buildIncludeDirective(String pagePathName, String arg) { newPageContent.append("\n!include ").append(arg).append(" .").append(pagePathName).append("\n"); } }
Bình luận: