Principle trong Programing, Các nguyên tắc lập trình cơ bản
Mã của bạn phải rõ ràng và dễ bảo trì.Thật dễ dàng để viết mã nhưng Thật khó để viết mã tốt.
Mã xấu có nhiều dạng. Mã lộn xộn, chuỗi if-else khổng lồ, chương trình bị hỏng chỉ với một lần điều chỉnh, các biến số không có ý nghĩa—chương trình có thể hoạt động một lần, nhưng nếu được đưa vào thử nghiệm, sẽ không bao giờ có thể đứng vững.
Làm thế nào để bạn viết mã hiệu quả? Bằng cách viết có kỷ luật và có mục đích. Dưới đây mình sẽ chia sẻ các nguyên tắc lập trình quan trọng
1. Tại sao Nguyên tắc lập trình lại quan trọng?
Nguyên tắc lập trình (Programming principles) là một tập hợp các quy tắc và hướng dẫn được áp dụng trong quá trình viết code, nhằm đảm bảo rằng code sẽ đáp ứng được yêu cầu chức năng, dễ dàng bảo trì và tái sử dụng.
Bằng cách học một số nguyên tắc lập trình và sử dụng chúng trong mã của bạn, bạn sẽ trở thành một nhà phát triển tốt hơn. Thực hiện theo các phương pháp này sẽ cải thiện chất lượng tổng thể của mã và giúp bạn hoặc người khác dễ dàng thực hiện các thay đổi hoặc thêm chức năng trong tương lai.
Các nguyên tắc lập trình giúp bạn tránh các vấn đề như mã lộn xộn hoặc quá phức tạp, chương trình bị hỏng sau một lần điều chỉnh và các biến vô nghĩa. Bám sát chúng cũng sẽ cho phép bạn cộng tác thành công hơn bằng cách đảm bảo các lập trình viên đồng nghiệp có thể hiểu mã của bạn.
2. Các nguyên tắc lập trình phổ biến
2.1 KISS - "Keep It Simple, Stupid"
Có nghĩa là bạn nên viết mã càng đơn giản càng tốt.
Nguyên lý KISS đề cập đến việc tính đơn giản (simple) nên được đặt là mục tiêu của việc thiết kế hệ thống, và hầu hết các hệ thống sẽ làm việc tốt nhất khi nó được giữ ở trạng thái đơn giản, thay vì bị phức tạp hóa vấn đề hơn. Những sự phức tạp hóa vấn đề một cách không cần thiết luôn cần được loại bỏ.
-> sử dụng các tên biến rõ ràng
-> dùng các thư viện mã hóa và sử dụng các công cụ hiện có
Ví dụ: Trong file HTML template, chúng ta có thể sử dụng ngFor để lặp lại các phần tử trên giao diện người dùng:
<ul>
<li *ngFor="let item of items">{{ item.name }}</li>
</ul>
Trong file component, chúng ta có thể khai báo các biến và các hàm xử lý logic:
import { Component } from '@angular/core';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
title = 'test';
items = [
{ name: 'Item 1' },
{ name: 'Item 2' },
{ name: 'Item 3' },
];
addItem() {
this.items.push({ name: 'New Item' });
}
}
Với cách thức này, chúng ta sử dụng Angular cơ bản để hiển thị danh sách các mục và cho phép người dùng thêm mới một mục vào danh sách. Code rất đơn giản và dễ hiểu, đảm bảo tuân thủ nguyên tắc KISS.
2.2 Keep things DRY - Don't Repeat Yourself (DRY)
DRY có nghĩa là không lặp lại mã, tránh lặp lại. Đó là một lỗi mã hóa phổ biến . Khi viết mã, tránh trùng lặp dữ liệu hoặc logic. Nếu bạn đã từng sao chép và dán mã trong chương trình của mình, thì đó không phải là mã DRY. Thay vì sao chép các dòng, hãy thử tìm một thuật toán sử dụng vòng lặp.
Mã DRY rất dễ bảo trì. Việc gỡ lỗi một vòng lặp xử lý 50 lần lặp lại sẽ dễ dàng hơn so với 50 khối mã xử lý một lần lặp lại. Việc thực hiện tốt DRY sẽ giúp bạn maintain code tốt hơn, hay giải quyết vấn đề thay đổi logic code của một đoạn xử lý dễ dàng hơn sau này.
2.3 YAGNI - You Aren't Gonna Need It
YAGNI có nghĩa là không thực hiện một cái gì đó cho đến khi nó là cần thiết.
Một trong những nguyên tắc quan trọng nhất của việc học lập trình máy tính là bạn không nên cố gắng giải quyết một vấn đề không tồn tại.
Trong bài giới thiệu về YAGNI, Martin Fowler có đề cập đến 4 vấn đề khi vi phạm YAGNI:
-
Cost of building: Khi bạn làm chức năng mà cuối cùng không cần đến nó. Nó khiến bạn tốn nhiều effort trong việc lên thiết kế, code, test ...
-
Cost of repair: Khi chức năng mà bạn hướng đến là cần thiết, nhưng bạn lại implement theo một cách không hợp lý. Nó sẽ khiến bạn tốn effort để lên kế hoạch lại, code lại, và test lại chức năng đã làm, bởi nó không thực sự là những gì bạn cần
-
Cost of delay: Dù trong bất kỳ trường hợp nào, bạn cũng sẽ gặp phải vấn đề này. Bạn đang mất thời gian vào một chức năng mà mình chưa cần đến ở thời điểm hiện tại, nó kéo theo việc những chức năng cần thiết ở thời điểm hiện tại không thể được hoàn thiện và release sớm
-
Cost of carry: Dù trong bất kỳ trường hợp nào, bạn cũng sẽ gặp phải vấn đề này. Bạn đang thêm một lượng code mới vào trong project của mình, khiến cho hệ thống phức tạp hơn và sẽ mất công để maintain, modify, debug hơn
Ví dụ:
import { Component } from '@angular/core';
import { DataService } from './data.service';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
title = 'My App';
data;
constructor(private dataService: DataService) {}
ngOnInit() {
this.getData();
}
getData() {
this.dataService.fetchData().subscribe(data => {
this.data = data;
});
}
}
Trong ví dụ này, chúng ta chỉ định nghĩa một thành phần AppComponent
và sử dụng DataService
để lấy dữ liệu từ một API. Chúng ta không cần thiết phải tạo ra các thành phần khác hoặc các hàm xử lý dữ liệu phức tạp, bởi vì chúng ta không biết chính xác những gì mà chúng ta sẽ cần trong tương lai.
Theo nguyên tắc YAGNI, chúng ta nên viết code dựa trên những yêu cầu cụ thể hiện tại, thay vì viết code dựa trên những giả định về những yêu cầu có thể xảy ra trong tương lai. Viết code đơn giản và chỉ thực hiện các tính năng cần thiết là rất quan trọng để giảm thiểu sự phức tạp và đảm bảo tính ổn định của ứng dụng.
Thực tế cũng rất khó để định nghĩa ranh rới của YAGNI. Có rất nhiều tính năng, hay đoạn xử lý logic bạn sẽ phải đắn đo cân nhắc xem có phải YAGNI hay không. Các lập trình viên đôi khi vi phạm nguyên tắc này khi cố gắng tuân thủ các phương pháp mã hóa DRY, vì vậy hãy ghi nhớ cả hai.
2.4 SOLID
Đây là một tập hợp các nguyên tắc lập trình được giới thiệu bởi Robert C. Martin (hay còn gọi là Uncle Bob) để thiết kế và viết code hiệu quả và dễ bảo trì.
SOLID bao gồm 5 nguyên tắc: Single Responsibility (SRP), Open-Closed (OCP), Liskov Substitution (LSP), Interface Segregation (ISP), Dependency Inversion (DIP).
Nguyên tắc Single Responsibility (SRP):
Một lớp hoặc đối tượng nên chỉ có một trách nhiệm duy nhất và không vượt quá phạm vi của nó. Single Responsibility (SRP): Mỗi thành phần (component, service, directive, pipe...) chỉ nên có một trách nhiệm duy nhất trong hệ thống.
Ví dụ:
Nếu có một component để hiển thị danh sách sản phẩm và cũng đang sử dụng để thêm, sửa và xóa sản phẩm thì không đúng với nguyên tắc này, vì nó làm quá nhiều việc.
Dưới đây là một ví dụ về việc áp dụng nguyên tắc này trong một service của Angular:
Trong ví dụ này, ProductListService
chỉ chịu trách nhiệm giao tiếp với API để lấy thông tin, thêm mới, xóa người dùng. Không có logic xử lý phức tạp nào được thực hiện trong service này. Điều này giúp cho service trở nên dễ bảo trì và tái sử dụng hơn.
Nguyên tắc Open/Closed (OCP):
Mã nguồn nên được thiết kế để có thể mở rộng, nhưng đóng cho việc sửa đổi, tức là bạn không cần phải sửa đổi mã hiện có mỗi khi muốn thêm chức năng mới.
Trong nguyên tắc Open/Closed (OCP), chúng ta cần thiết kế các class và module sao cho chúng có thể mở rộng (open) để thêm các tính năng mới, nhưng đóng lại (closed) để sửa đổi các tính năng hiện có.
Ví dụ:
Ta có một trang web bán hàng online, và trên trang web này có một danh sách sản phẩm hiển thị ra cho người dùng. Trong trường hợp này, ta có thể tạo một component Angular để quản lý danh sách sản phẩm và hiển thị chúng ra trang web.
Để áp dụng nguyên tắc OCP trong ví dụ này, ta có thể thiết kế component để cho phép mở rộng cho các tính năng liên quan đến hiển thị danh sách sản phẩm mới, nhưng đóng lại để không thay đổi các tính năng hiện có. Cụ thể, ta có thể tạo một abstract class
là ProductListComponent
để định nghĩa các phương thức chung cho component hiển thị danh sách sản phẩm, như sau:
export abstract class ProductListComponent {
products: Product[];
abstract loadProducts(): void;
abstract displayProducts(): void;
}
Sau đó, ta có thể tạo các class con để kế thừa từ ProductListComponent
và triển khai các phương thức loadProducts()
và displayProducts()
dựa trên các yêu cầu cụ thể của tính năng mới sẽ được thêm vào. Ví dụ, trong trường hợp chúng ta muốn thêm tính năng lọc sản phẩm theo giá, ta có thể tạo một class con là PriceFilteredProductListComponent
như sau:
export class PriceFilteredProductListComponent extends ProductListComponent {
minPrice: number;
maxPrice: number;
loadProducts(): void {
// Load products from API filtered by price
}
displayProducts(): void {
// Display products with price filter applied
}
}
Như vậy, khi chúng ta muốn thêm tính năng lọc sản phẩm theo giá, ta chỉ cần tạo ra một class mới kế thừa từ ProductListComponent
, triển khai các phương thức cần thiết và sử dụng nó để hiển thị danh sách sản phẩm. Trong trường hợp này, việc thêm tính năng không ảnh hưởng đến các tính năng hiện có của component.
Nguyên tắc Liskov Substitution (LSP):
Các đối tượng con nên có thể thay thế các đối tượng cha của nó mà không ảnh hưởng đến tính năng của chương trình.
Nguyên tắc Interface Segregation (ISP):
Một giao diện nên chỉ chứa các phương thức cần thiết cho một đối tượng cụ thể và không chứa các phương thức không cần thiết.
Nguyên tắc Dependency Inversion (DIP):
Mã nguồn nên phụ thuộc vào các giao diện, không phải các lớp cụ thể. Điều này đảm bảo tính linh hoạt và dễ bảo trì cho mã nguồn.
2.5 Separation of Concerns
Separation of concerns là một nguyên tắc trong lập trình, có nghĩa là chia tách các công việc vào các phần khác nhau để giảm thiểu sự phụ thuộc giữa chúng. Nguyên tắc này cho phép phát triển và bảo trì mã dễ dàng hơn bằng cách chia các chức năng của hệ thống thành các phần riêng biệt có tính độc lập cao.
Ví dụ, khi xây dựng một ứng dụng web, ta có thể sử dụng mô hình MVC (Model-View-Controller) để chia các chức năng của ứng dụng thành các phần khác nhau. Model chuẩn bị dữ liệu, View hiển thị giao diện cho người dùng và Controller xử lý yêu cầu từ người dùng. Bằng cách chia tách các chức năng này thành các phần khác nhau, ta có thể chỉnh sửa và bảo trì mã dễ dàng hơn và giảm thiểu các vấn đề liên quan đến tính đúng đắn và bảo mật của ứng dụng.
Kết quả là mã dễ gỡ lỗi. Nếu bạn cần viết lại mã kết xuất, bạn có thể làm như vậy mà không cần lo lắng về cách dữ liệu được lưu hoặc logic được xử lý.
~~Lời kết: ~~
Các nguyên tắc lập trình này giúp đảm bảo rằng mã của bạn được thiết kế một cách chặt chẽ, dễ bảo trì và dễ mở rộng trong tương lai. Việc áp dụng các nguyên tắc này sẽ giúp cho mã nguồn của bạn trở nên có tính tái sử dụng cao và giảm thiểu các lỗi liên quan đến việc phát triển và bảo trì mã nguồn.
Tham khảo:
- 10 Essential Programming Principles
- Programming Principles
- Tổng hợp nhiều nguồn khác
All rights reserved