Đây là một tình huống phổ biến và có nhiều cách phổ biến để đối phó với nó. Đây là nỗ lực của tôi tại một câu trả lời kinh điển. Hãy bình luận nếu tôi bỏ lỡ bất cứ điều gì và tôi sẽ cập nhật bài viết này.
Đây là một mũi tên
Những gì bạn đang thảo luận được gọi là mô hình chống mũi tên . Nó được gọi là một mũi tên vì chuỗi ifs lồng nhau tạo thành các khối mã mở rộng ra xa hơn và sang phải rồi quay lại bên trái, tạo thành một mũi tên trực quan "chỉ" sang bên phải của khung soạn thảo mã.
Làm phẳng mũi tên với người bảo vệ
Một số cách phổ biến để tránh Mũi tên được thảo luận ở đây . Phương thức phổ biến nhất là sử dụng mẫu bảo vệ , trong đó mã xử lý các luồng ngoại lệ trước rồi xử lý luồng cơ bản, ví dụ thay vì
if (ok)
{
DoSomething();
}
else
{
_log.Error("oops");
return;
}
... bạn sẽ sử dụng ....
if (!ok)
{
_log.Error("oops");
return;
}
DoSomething(); //notice how this is already farther to the left than the example above
Khi có một loạt các vệ sĩ, điều này sẽ làm phẳng mã đáng kể vì tất cả các vệ sĩ đều xuất hiện ở bên trái và if của bạn không được lồng vào nhau. Ngoài ra, bạn đang ghép nối trực quan điều kiện logic với lỗi liên quan của nó, điều này giúp dễ dàng hơn nhiều để biết điều gì đang xảy ra:
Mũi tên:
ok = DoSomething1();
if (ok)
{
ok = DoSomething2();
if (ok)
{
ok = DoSomething3();
if (!ok)
{
_log.Error("oops"); //Tip of the Arrow
return;
}
}
else
{
_log.Error("oops");
return;
}
}
else
{
_log.Error("oops");
return;
}
Bảo vệ:
ok = DoSomething1();
if (!ok)
{
_log.Error("oops");
return;
}
ok = DoSomething2();
if (!ok)
{
_log.Error("oops");
return;
}
ok = DoSomething3();
if (!ok)
{
_log.Error("oops");
return;
}
ok = DoSomething4();
if (!ok)
{
_log.Error("oops");
return;
}
Điều này là khách quan và dễ định lượng hơn để đọc bởi vì
- Các ký tự {và} cho một khối logic đã cho gần nhau hơn
- Số lượng bối cảnh tinh thần cần thiết để hiểu một dòng cụ thể là nhỏ hơn
- Toàn bộ logic liên quan đến một điều kiện if có nhiều khả năng nằm trên một trang
- Nhu cầu lập trình viên để cuộn trang / mắt theo dõi được giảm đi rất nhiều
Cách thêm mã chung vào cuối
Vấn đề với mô hình bảo vệ là nó dựa vào cái được gọi là "lợi nhuận cơ hội" hay "lối thoát cơ hội". Nói cách khác, nó phá vỡ mô hình rằng mỗi và mọi chức năng nên có chính xác một điểm thoát. Đây là một vấn đề vì hai lý do:
- Nó chà xát một số người sai cách, ví dụ những người đã học viết mã trên Pascal đã học được rằng một hàm = một điểm thoát.
- Nó không cung cấp một phần mã thực thi khi thoát bất kể là gì , đó là chủ đề trong tay.
Dưới đây tôi đã cung cấp một số tùy chọn để khắc phục giới hạn này bằng cách sử dụng các tính năng ngôn ngữ hoặc bằng cách tránh hoàn toàn vấn đề.
Tùy chọn 1. Bạn không thể làm điều này: sử dụng finally
Thật không may, là một nhà phát triển c ++, bạn không thể làm điều này. Nhưng đây là câu trả lời số một cho các ngôn ngữ có chứa từ khóa cuối cùng, vì đây chính xác là những gì nó dành cho.
try
{
if (!ok)
{
_log.Error("oops");
return;
}
DoSomething(); //notice how this is already farther to the left than the example above
}
finally
{
DoSomethingNoMatterWhat();
}
Tùy chọn 2. Tránh vấn đề: Tái cấu trúc các chức năng của bạn
Bạn có thể tránh vấn đề bằng cách chia mã thành hai chức năng. Giải pháp này có lợi ích làm việc cho bất kỳ ngôn ngữ nào, và ngoài ra, nó có thể làm giảm độ phức tạp theo chu kỳ , đây là một cách đã được chứng minh để giảm tỷ lệ lỗi của bạn và cải thiện tính đặc hiệu của bất kỳ bài kiểm tra đơn vị tự động nào.
Đây là một ví dụ:
void OuterFunction()
{
DoSomethingIfPossible();
DoSomethingNoMatterWhat();
}
void DoSomethingIfPossible()
{
if (!ok)
{
_log.Error("Oops");
return;
}
DoSomething();
}
Tùy chọn 3. Thủ thuật ngôn ngữ: Sử dụng vòng lặp giả
Một mẹo phổ biến khác mà tôi thấy là sử dụng while (true) và break, như thể hiện trong các câu trả lời khác.
while(true)
{
if (!ok) break;
DoSomething();
break; //important
}
DoSomethingNoMatterWhat();
Mặc dù điều này ít "trung thực" hơn so với sử dụng goto
, nhưng nó sẽ ít bị rối hơn khi tái cấu trúc, vì nó đánh dấu rõ ràng ranh giới của phạm vi logic. Một lập trình viên ngây thơ cắt và dán nhãn của bạn hoặc goto
tuyên bố của bạn có thể gây ra vấn đề lớn! (Và thẳng thắn, mô hình rất phổ biến bây giờ tôi nghĩ rằng nó truyền đạt rõ ràng ý định, và do đó không "không trung thực" chút nào).
Có các biến thể khác của tùy chọn này. Ví dụ, người ta có thể sử dụng switch
thay vì while
. Bất kỳ ngôn ngữ xây dựng với một break
từ khóa có thể sẽ làm việc.
Tùy chọn 4. Tận dụng vòng đời của đối tượng
Một cách tiếp cận khác thúc đẩy vòng đời đối tượng. Sử dụng một đối tượng ngữ cảnh để mang theo các tham số của bạn (thứ mà ví dụ ngây thơ của chúng tôi thiếu một cách đáng ngờ) và loại bỏ nó khi bạn hoàn thành.
class MyContext
{
~MyContext()
{
DoSomethingNoMatterWhat();
}
}
void MainMethod()
{
MyContext myContext;
ok = DoSomething(myContext);
if (!ok)
{
_log.Error("Oops");
return;
}
ok = DoSomethingElse(myContext);
if (!ok)
{
_log.Error("Oops");
return;
}
ok = DoSomethingMore(myContext);
if (!ok)
{
_log.Error("Oops");
}
//DoSomethingNoMatterWhat will be called when myContext goes out of scope
}
Lưu ý: Hãy chắc chắn rằng bạn hiểu vòng đời đối tượng của ngôn ngữ bạn chọn. Bạn cần một số loại bộ sưu tập rác xác định để làm việc này, tức là bạn phải biết khi nào hàm hủy sẽ được gọi. Trong một số ngôn ngữ, bạn sẽ cần sử dụng Dispose
thay vì hàm hủy.
Tùy chọn 4.1. Tận dụng vòng đời đối tượng (mẫu bao bọc)
Nếu bạn sẽ sử dụng một cách tiếp cận hướng đối tượng, cũng có thể làm điều đó đúng. Tùy chọn này sử dụng một lớp để "bọc" các tài nguyên cần dọn dẹp, cũng như các hoạt động khác của nó.
class MyWrapper
{
bool DoSomething() {...};
bool DoSomethingElse() {...}
void ~MyWapper()
{
DoSomethingNoMatterWhat();
}
}
void MainMethod()
{
bool ok = myWrapper.DoSomething();
if (!ok)
_log.Error("Oops");
return;
}
ok = myWrapper.DoSomethingElse();
if (!ok)
_log.Error("Oops");
return;
}
}
//DoSomethingNoMatterWhat will be called when myWrapper is destroyed
Một lần nữa, hãy chắc chắn bạn hiểu vòng đời đối tượng của bạn.
Tùy chọn 5. Thủ thuật ngôn ngữ: Sử dụng đánh giá ngắn mạch
Một kỹ thuật khác là tận dụng đánh giá ngắn mạch .
if (DoSomething1() && DoSomething2() && DoSomething3())
{
DoSomething4();
}
DoSomethingNoMatterWhat();
Giải pháp này tận dụng cách thức hoạt động của toán tử &&. Khi phía bên trái của && ước tính là sai, phía bên phải không bao giờ được đánh giá.
Thủ thuật này hữu ích nhất khi cần có mã compact và khi mã không có khả năng bảo trì nhiều, ví dụ: bạn đang thực hiện một thuật toán nổi tiếng. Đối với mã hóa tổng quát hơn, cấu trúc của mã này quá dễ vỡ; ngay cả một thay đổi nhỏ đối với logic cũng có thể kích hoạt việc viết lại hoàn toàn.