Code Smells

Đôi khi cho dù bạn thiết kế code của mình tốt đến đâu, chúng vẫn sẽ luôn có những thay đổi cần thực hiện. Thật khó khăn, nếu không thể code được nó ngay lần đầu tiên, đó là nơi tái cấu trúc xuất hiện.

Tái cấu trúc (refactoring) là quá trình thực hiện thay đổi code của bạn để hành vi bên ngoài code không thay đổi, nhưng cấu trúc bên trong được cải thiện, điểu này được thực hiện bằng cách thực hiện các thay đổi nhỏ, tăng dần với cấu trúc code và kiểm tra thường xuyên để đảm bảo những hành vi này không làm thay đổi hành vi của code.

Lý tưởng nhất là bạn không muốn tiến hành tái cấu trúc khi code của bạn đã hoàn tất. Điều này có thể tổn thời gian và điều này có thể gây ra nhiều vấn đề hơn là đang sửa chữa. Bạn muốn thực hiện các thay đổi tái cấu trúc này khi bạn thêm tính năng, tái cấu trúc mã tại thời điểm này có thê làm cho việc bổ sung dễ dàng hơn để đạt được, và tiết kiệm cho bạn khỏi cần phải đại tu hoàn toàn.

Vì vậy, những thay đổi bạn cần phải thực hiện là gì ? Tương tự như cách chúng ta thấy các mẫu xuất hiện trong thiết kế, chúng ta cũng thấy các bad code xuất hiện. Chúng được gọi là Anti-Pattern. Nhiều trong số các mô hình này được giới thiệu trong cuốn sách Many of these anti-pattern are indentified bởi Martin Fowler, là một nhà phát triển, đây là một tài nguyên hữu ích để đọc. Cuốn sách không chỉ miêu tả những mẫu mã trống rỗng mà còn cung cấp các cấu trúc thiết kế lại để bạn có thể thay đổi.

Mục đích của code smell là để phát hiện ra những gì xấu trong mã, tương tự như một con chó đánh hơi ma túy mà bạn có thể thấy trong sân bay. Bạn sẽ học cách phát hiện ra những bad coding không được phép có ở đó.

Có lẽ ví dụ phổ biến nhất xuất hiện liên quan đến ý kiến. Thông thường, vấn đề là code không có ý kiến, đây có thể là một vấn đề vì nhiều lý do rõ ràng, không có bình luận làm cho ai đó khó hiểu được code đang làm gì hoặc nên làm gì. Trường hợp có thể có nhà phát triển khác, những người vừa tham gia dự án, hoặc có thể ngay cả bạn khi đã quên cách code hoạt động, không có bình luận là xẫu, nhưng có quá nhiều bình luận cũng là vấn đề của riêng nó. Họ có thể không đồng bộ khi mã thay đổi, bạn có thể nói rằng các bình luận có được coi là chất khử mùi cho code smell, rất có thể, nếu bạn có nhận xét mô tả cho một thiết kế phức tạp, bạn đang phải che đậy cho một thiết kế xấu.

Bạn có thể thấy các nhận xét nhắc nhở nói điều gì đó dọc như "không được quên điều này", "nếu bạn thay đổi điều này", hãy đảm bảo bạn cập nhật mã theo phương pháp khác này vì nó cũng là một bad code. Sử dụng các bình luận để giải thích một thiết kế đôi khi có thể chỉ ra rằng ngôn ngữ lập trình đó không phù hợp. Có lẽ ngôn ngữ lập trình không hỗ trợ nguyên tắc thiết kế mà bạn đang cố gắng áp dụng. Ví dụ, trong những năm dầu của Java, khái niệm generic chưa tồn tại, các nhà phát triển trước đây sẽ phải có ý kiến để giải thích những gì họ đang làm với mã khi truyền các loại. Cuối cùng, generic đã được tích hợp vào Java. Nhưng cho đến thời điểm đó, nhiều ý kiến đã được sử dụng. Đây là một sự xuất hiện phổ biến trong ngôn ngữ lập trình trẻ, bây giờ Java là ngôn ngữ trường thành hơn, bạn không có lý do gì mà không dùng. Nhưng đừng hiểu nhầm, tôi không ủng hộ không có ý kiến trong code, nhận xét rất hữu ích để ghi lại các API hoặc giao diện lập trình ứng dụng trong hệ thống của bạn. Và cũng để ghi lại lý do tại sao một sự lựa chọn cụ thể về cấu trúc dữ liệu hoặc thuật toán đã được thực hiện. Nó cũng cho phép code của bạn dễ dàng được sử dụng bởi người khác, giống như hầu hết mọi thứ trong lập trình, có một sự cân bằng cần tìm, nơi bạn phải nhận xét hiệu quả trong mã của mình.

Code smell tiếp theo mà chúng ta nói đến là code lặp lại. Bạn có thể thấy điều này trong code của bạn đặc biệt là trước khi bạn tìm hiểu về lập trình OOP hoặc các mẫu thiết kế. Code trùng lặp là khi bạn có các khối lệnh tương tự nhau, nhưng có những sự khác biệt nhỏ giữa chúng. Các khổi mã này xuất hiện ở nhiều nơi trong phần mềm của bạn. Giả sử bạn đang làm việc trên một trang web mạng xã hội, người dùng có thể tạo các bài đăng văn bản xuất hiện trên nguồn cấp dữ liệu mới, trên tường của một người bạn hoặc trên nhóm, trước tiên, bạn tạo khả năng đăng trên nguồn cấp tin tức, những bài đăng này được hiển thị bởi tất cả các bạn bè của người dùng trên nguồn cấp tin tức của họ. Khi tính năng đó hoàn tất, bạn sẽ phát triển code dễ dàng đăng lên tường của người bạn, vì bạn đã có chức năng được viết để đăng, bạn chỉ cấn sao chép và dán mã đó để sử dụng lại và đăng lên tường của một người bạn. Khi bạn tạo chức năng trong một nhóm, bạn cũng làm điều tương tự. Bây giờ, khi trang web mở rộng, bạn cũng muốn có khả năng đăng ảnh không chỉ là văn bản, bây giờ, để thêm chức năng này, bạn phải cập nhật mã ở 3 nơi. Điều gì nếu bạn có nhiều hơn 3 vị trí khác nơi mà người dùng có thể đăng ? Như bạn có thể thấy, nếu bạn chỉ làm đơn giản ngay từ đầu, thì khi hệ thống lớn lên không thể kiểm soát được. Với OOP và Design Pattern, bạn sẽ chỉ cần cập nhật nó ở một nơi, đừng lặp lại chính mình.

Bây giờ hãy nói về code smell của phương thức dài. Điều này tự giải thích tương đối, bạn không hề muốn có một phương thức dài.

Tương tự phương thức dài, các lớp lớn cũng là một vấn đề, các lớp lớn này thường đọc gọi là các lớp God, hay Black Hole. Nó sẽ chỉ ngày càng lớn hơn, thường bắt đầu như một kích thước thông thường, nhưng khi cần nhiều trách nhiệm hơn, các lớp này có vẻ lại là nơi thích hợp để đặt trách nhiệm. Cuối cùng nó sẽ cần một cái hố đen đúng nghĩa. Để tránh điều này, bạn cần phải rõ ràng về mục đích của một lớp và giữ cho lớp được gắn kết với lớp khác, vì vậy, nó chỉ phải làm tốt một việc. Nếu một chức năng không dành riêng cho trách nhiệm của lớp, bạn có thể đặt lớp ở một nơi khác, như đã nói, các lớp này có xu hướng tăng cao như cấp số nhân.

Ngược lại, có nhiều lớp nhỏ cũng là một vấn đề, đây thường là vấn đề với lớp dữ liệu (data). Các lớp dữ liệu là lớp chỉ chứa dữ liệu và không có chức năng thực sự, nói chung, các lớp này sẽ có phương thức getter() và setter(), nhưng không nhiểu. Ví dụ về một lớp dữ liệu sẽ là lớp điểm 2D chỉ chứa tọa độ X và tọa độ Y. Thay vì chỉ thao tác nó với các phương thức getter() và setter(), hãy suy nghĩ xem có cái gì khác có thể đặt trong lớp này hay không. Hãy xem xét việc các lớp khác đang thao túng lớp dữ liệu này, một số hành vi của họ có thể tốt hơn nếu đặt trong lớp dữ liệu không ? Ví dụ, lớp điểm 2D của chúng ta, bạn có thể có các hàm biến đổi khác nhau để di chuyển điểm, nếu một lớp chỉ chứa dữ liệu với các phương thức getter(), setter() thì nó có lẽ không phải là một lớp thực sự cần thiết. Một vấn đề liên quan nữa mà chúng ta thấy đó là cụm dữ liệu, các cụm dữ liệu là các nhóm dữ liệu xuất hiện cùng nhau trong các biến thể hiện một lớp hoặc tham số cho các phương thức. Giả sử chúng ta có phương thức thực hiện một cái gì đó với số nguyên x, y, và z. Phương pháp này có thể trông như thế này:


Nếu chúng ta có nhiều phương thức khác nhau thao tác trên cùng 3 biến đó, sẽ có ý nghĩa hơn khi có một đối tượng làm tham số, thay vì sử dụng các biến này làm tham số lặp đi lặp lại. Như thế này này:


Vì vậy, bây giờ, thay vì sử dụng ba giá trị dữ liệu làm tham số, giờ đây chúng được lưu giữ bên trong một đối tượng, và đối tượng đó có thể được sử dụng ở một vị trí của chúng như một tham số. Nhưng bạn cần phải cẩn thận bởi vì mặc dù như đã nói ở trên, bạn muốn các lớp này làm nhiều hơn chỉ là lưu dữ liệu, nên những thứ gì liên quan và thực sự hữu ích với nó, nên thuộc về lớp 3D.

Trong một lưu ý tương tự, có danh sách tham số dài cũng là một code smell.

Bây giờ, hãy chuyển đổi một chút để nói về code smells khác khi bạn thực hiện thay đổi code. Đầu tiên, trong số đó là dirvẻrgent change, xảy ra khi bạn phải thay đổi một lớp theo nhiều cách khác nhau vì nhiều lý do khác nhau. Điều này liên quan chặt chẽ đến lớp Black Hole đã nói. Khi bạn có một lớp lớn, nó sẽ có nhiều trách nhiệm khác nhau, vì vậy, sự phân tách mối quan tâm kém là một nguyên nhân phổ biến của dirvergent change. Sẽ thật tuyệt, nếu class của bạn chỉ có mục đích cụ thể như bình thường, điều này sẽ làm giảm số lượng lý do mà code cần phải được thay đổi. Và kết quả là, giảm sự thay đổi code cần thiết, nếu bạn thấy rằng bạn đang thay đổi một lớp theo nhiều cách thì đó có thể là một dấu hiệu tốt cho thấy trách nhiệm của lớp nên được phân chia thành các lớp riêng biệt và những trách nhiệm này nên được trích xuất riêng thành các trách nhiệm của riêng nó. Lớp lớn ban đầu sẽ ủy thác trách nhiệm cho những lớp được trích xuất này, bây giờ bạn đã giải quyết code smell.

Mặt khác, giả sử bạn chỉ muốn thực hiện một loại thay đổi, một yêu cầu nhỏ. Sẽ thật tuyệt, nếu sự thay đổi đó chỉ có ở một nơi. Nhưng nếu bạn thay đổi cho một yêu cầu và nó yêu cầu bạn phải chạm vào một loạt các lớp trên thiết kế của bạn chỉ để làm điều đó. Có lẽ đó không phải là một thiết kế tuyệt vời, code smell ở đây được gọi là shotgun surgecy. Đây là một lỗi thường xảy ra, kết quả là thay đổi ở một nơi đòi hỏi bạn phải sửa ở nhiều nơi trong thiết kế. Điều này xảy ra khi bạn đang cố gắng thêm một tính năng, điều chỉnh mã, sửa lỗi hoặc thay đổi thuật toán. Lý tưởng nhất, bạn muốn thay đổi của bạn được bản địa hóa. Nhưng điều đó không phải lúc nào cũng có thể, một thay đổi chỉ ảnh hưởng đến một hoặc hai lớp là cách tốt hơn. Nhưng đôi khi bạn phải dùng shotgun surgecy cho dù code của bạn có được thiết kế tốt đến đâu. Ví dụ, giả sử bạn cập nhật tuyên bố bản quyền hoặc cho phép, bạn sẽ phải thay đổi mọi tệp trong hệ thống của mình để thay đổi một khối văn bản bản quyền. Thông thường, điều đó có thể giải quyết bằng cách hợp nhất code vào một lớp cần thay đổi, nhưng điều đó không có nghĩa là bạn nên dùng để bạn chỉ thực hiện thay đổi trong một lớp.

Code smell tiếp theo của chúng ta sẽ là feature envy, xảy ra khi bạn có một phưong thức quan tâm nhiều hơn đến các chi tiết của một lớp khác với phương thức mà nó tham gia. Điều này nghe giống như một bộ phim lãng mạn. Những người yêu đã kết thúc mối quan hệ của họ nhiều năm trước vẫn còn yêu nhau mặc dù đang ở trong mối quan hệ mới. Họ quan tâm đến các mối quan hệ của người khác hơn là chính họ, bạn chỉ muốn hét vào phim rằng họ nên kết thúc mối quan hệ không tương xứng của họ và chỉ cần được bên nhau, họ sẽ không bao giờ hạnh phúc cho đến khi họ ở cùng một nơi. Nếu có vẻ hai phương thức hoặc lớp luôn nói chuyện với nhau và nên ở cùng nhau, rất có lẽ chúng nên như vậy. Nếu bạn có một phương thức trong một lớp dường như muốn kết nối nhiều đến một lớp khác để thực hiện công việc của nó, thì có lẽ nó thuộc về lớp kia tốt hơn.

Trong một lưu ý tương tự, chúng ta sẽ thảo luận về vấn đề khi mà hai lớp phụ thuộc quá nhiều vào nhau thông qua giao tiếp hai chiều. Code smell này được gọi là inappropriate intimacy (thân mật không phù hợp), nói rằng có hai lớp call thực sự gần gũi với nhau, vì vậy, một phương thức trong một lớp gọi các lớp kia và ngược lại, những class này có lẽ nên được kết hợp chặt chẽ như vậy ? Chắc là không, có khả năng, nên có một số cách để loại bỏ chu kỳ này, bạn có thể tính ra các phương thức mà cả hai lớp sử dụng vào một lớp khác, điều này có thể giải phóng một số giao tiếp chặt chẽ, để giao tiếp chỉ xảy ra một cách. Nhưng chu kỳ không nhất thiết là một điều xấu, bạn có thể nhận ra rằng Design Pattern có chu kỳ, sẽ có những tình huống trong đó các chu kỳ giao tiếp là cần thiết. Tuy nhiên, nếu có một cách để loại bỏ chu trình, đó sẽ là một giải pháp tốt để làm cho thiết kế của bạn đơn giản và dễ hiểu hơn.

Trước đây chúng ta đã nói về The Law Of Demeter, nguyên tắc đó định hướng phương thức nào bạn được phép gọi. Ngay cả khi đối tượng public, có các quy tắc cụ thể khi bạn không được phép gọi một phương thức. Code smell tiếp theo, messgae chains, có khả năng vi phạm The Law Of Demeter, giả sử bạn có một đối tượng A có phương thức getB() và điều này trả về một đối tượng B. Trên đối tượng B này, có một phương thức getC() và trả về đối tượng C. Cuối cùng, trên đối tượng C đó, bạn kêu gọi nó làm gì đó. Một chuỗi gọi quá dài là không nên, lý do tôi cũng chưa hiểu ...

Bây giờ, hãy nói về việc sử dụng các loại build-in-type quá nhiều. Những kiểu tích hợp nguyên thủy này là int, long, float, string. Rõ ràng, những thứ này phải được sử dụng, tuy nhiên, chúng chỉ nên tồn tại có thể ở mức thấp nhất trong  mã của bạn. Việc sử dụng quá mức các kiểu này xảy ra khi bạn không xác định vật cản và xác định các lớp phù hợp. Ví dụ, bạn có thể hình dung ra chỉ cần xác định hoặc mã hóa mọi thứ trong chuỗi hệ thống và đặt chúng vào mảng, dùng string ấy. Mã của bạn trông giống một cái gì đó được phát triển từ những năm 60, hồi đó là lúc bạn phải làm. Kể từ đó, các ngôn ngữ đã được phát triển cho phép chúng ta xác định các loại của riêng mình tốt hơn. Vì vậy, bạn nên sử dụng nó. Đây là một ví dụ cho bạn, ở Canada, chúng ta có mã bưu chính, chúng được gọi với cái tên khác nhau tùy thuộc nơi bạn sống, mã bưu điện, ZIP, PIN .v.v. Về cơ bản , đó là một loạt các chữ cái và số được bao gồm trong một địa chỉ bưu chính của chúng ta bao gồm 6 ký tự, chữ cái xen kẽ, số, chữ cái, số, chữ cái, số. Việt Nam mình là 20000. Nhưng ở Canada, chúng trông thế này: T6G2R3. Bạn có thể lưu chuỗi này dưới dạng String và xử lý như vậy trong toàn hệ thống của mình. Tuy nhiên, đó là những gì chúng ta muốn tránh ở đây, đó là vì vật cản chính sẽ bị chôn vùi trong các chi tiết không rõ ràng khi nhìn vào thiết kế của hệ thống, trong sơ đồ lớp UML. Sẽ không có sự tách biệt rõ ràng giữa một chuỗi mã bưu chính và các chuỗi khác trong hệ thống. Các bộ phận trong hệ thống của bạn sẽ quan tâm liệu mã bưu chính là chữ hoa hay khoảng trắng ? Nếu bạn định nghĩa một lớp PostalCode, thì bạn có thể lưu trữ các ký tự ở đó. Nếu như bạn đang sử dụng các mẫu nguyên thủy ở mức cao, thì hãy cẩn thận, đó có thể là một code smell.

Code smell tiếp theo mà chúng ta sẽ nói đến là switch statements. Bây giờ bạn có thể nghĩ, nó quá tuyệt vời ấy chứ, làm gì có gì sai với nó ? Chà, đôi khi cần phải có những câu lệnh lớn, dài nếu có nó trong mã của bạn. Tuy nhiên, đôi khi, switch có thể được xử lý tốt hơn, ví dụ: nếu điều kiện của bạn đang kiểm tra type của một cái gì đó, thì có một cách tốt hơn để xử lý các câu lệnh điều hướng này. Ở đây, trong ví dụ này, phương thức say() kiểm tra loại động vật và có các trường hợp khác nhau trong các tiếng kêu của nó, bạn có thể giảm các điều kiện này xuống bằng cách sử dụng đa hình.


Nếu không dùng đa hình:


Vì vậy, trước khi nghĩ về Switch key hãy nghĩ về đa hình trong OOP nhé.

Cuối cùng, code smell cuối cùng của chúng ta, refused request. Từ chối yêu cầu xảy ra khi một lớp con kế thừa một cái gì đó mà không cần nó. Những gì subclass nhận được lại là một gánh nặng nhiều hơn.

Tôi đoán code smell vẫn chưa dừng ở đây, nên tôi không muốn nói lời kết thúc, khi phát triển một phần mềm, thần chú code smell sẽ luôn đi theo chúng ta. Hẹn gặp lại các bạn.

Nhận xét

Bài đăng phổ biến từ blog này

Hiểu về Norm Regularization

Những thuật toán nền tảng trong lĩnh vực Trí tuệ nhân tạo (Artificial Intelligence I)