Tôi có nên đăng nhập lỗi trên các hàm tạo ngoại lệ không?


15

Tôi đã xây dựng một ứng dụng trong một vài tháng và tôi nhận ra một mô hình nổi lên:

logger.error(ERROR_MSG);
throw new Exception(ERROR_MSG);

Hoặc, khi bắt:

try { 
    // ...block that can throw something
} catch (Exception e) {
    logger.error(ERROR_MSG, e);
    throw new MyException(ERROR_MSG, e);
}

Vì vậy, bất cứ khi nào tôi ném hoặc bắt một ngoại lệ, tôi sẽ đăng nhập nó. Trên thực tế, đó gần như là tất cả các bản ghi mà tôi đã làm trên ứng dụng (bên cạnh thứ gì đó để khởi tạo ứng dụng).

Vì vậy, là một lập trình viên, tôi tránh xa sự lặp lại; Vì vậy, tôi quyết định chuyển các cuộc gọi logger sang xây dựng ngoại lệ, vì vậy, bất cứ khi nào tôi xây dựng một ngoại lệ, mọi thứ sẽ được ghi lại. Tôi cũng có thể, chắc chắn, tạo ra một ExceptionHelper đã ném ngoại lệ cho tôi, nhưng điều đó sẽ khiến mã của tôi khó diễn giải hơn và tệ hơn nữa, trình biên dịch sẽ không xử lý tốt, không nhận ra rằng một cuộc gọi đến thành viên đó sẽ không ném ngay lập tức.

Vì vậy, đây có phải là một mô hình chống? Nếu vậy, tại sao?


Điều gì nếu bạn tuần tự hóa và giải trừ ngoại lệ? Nó sẽ đăng nhập lỗi?
ngày tận thế

Câu trả lời:


18

Không chắc chắn nếu nó đủ điều kiện là một mô hình chống, nhưng IMO đó là một ý tưởng tồi: đó là khớp nối không cần thiết để đan xen một ngoại lệ với đăng nhập.

Bạn có thể không phải lúc nào cũng muốn ghi nhật ký tất cả các trường hợp của một ngoại lệ nhất định (có thể xảy ra trong quá trình xác thực đầu vào, việc ghi nhật ký có thể là không ổn định và không thú vị).

Thứ hai, bạn có thể quyết định ghi nhật ký các lỗi khác nhau với các mức ghi nhật ký khác nhau, tại thời điểm đó, bạn phải xác định rằng khi xây dựng ngoại lệ, một lần nữa thể hiện việc tạo ra ngoại lệ với hành vi ghi nhật ký.

Cuối cùng, nếu ngoại lệ xảy ra trong quá trình đăng nhập của một ngoại lệ khác thì sao? Bạn sẽ đăng nhập nó? Nó trở nên lộn xộn ...

Lựa chọn của bạn về cơ bản là:

  • Bắt, đăng nhập và (lại) ném, như bạn đã đưa ra trong ví dụ của mình
  • Tạo một lớp ExceptionHelper để làm cả hai cho bạn, nhưng Người trợ giúp có mùi mã và tôi cũng không khuyến nghị điều này.
  • Di chuyển xử lý ngoại lệ bắt tất cả lên cấp cao hơn
  • Cân nhắc AOP cho một giải pháp tinh vi hơn để mối quan tâm xuyên suốt như khai thác gỗ và xử lý ngoại lệ (nhưng phức tạp hơn chỉ có hai dòng trong khối catch của bạn;))

+1 cho "đồ sộ và không thú vị". AOP là gì?
Tulains Córdova

@ user61852 Lập trình hướng theo khía cạnh (Tôi đã thêm một liên kết). Đây câu hỏi cho thấy một ví dụ w / r / t AOP và khai thác gỗ trong Java: stackoverflow.com/questions/15746676/logging-with-aop-in-spring
Dan1701

11

Vì vậy, là một lập trình viên, tôi tránh xa sự lặp lại [...]

Có một mối nguy hiểm ở đây bất cứ khi nào khái niệm "don't repeat yourself"được thực hiện quá nghiêm trọng đến mức nó trở thành mùi.


2
Bây giờ tôi phải làm thế nào để chọn câu trả lời đúng khi tất cả đều tốt và dựa vào nhau? Giải thích tuyệt vời về cách DRY có thể trở thành vấn đề nếu tôi áp dụng cách tiếp cận cuồng tín với nó.
Bruno Brant

1
Đó là một cú đâm thực sự tốt ở DRY, tôi phải thú nhận tôi là một DRYholic. Bây giờ tôi sẽ xem xét hai lần khi tôi nghĩ đến việc di chuyển 5 dòng mã đó sang một nơi khác vì lợi ích của DRY.
SZT

@SZaman Ban đầu tôi rất giống. Về mặt sáng sủa, tôi nghĩ rằng sẽ có nhiều hy vọng hơn cho những người trong chúng ta dựa quá nhiều vào khía cạnh dập tắt sự dư thừa so với những người, nói, viết một hàm 500 dòng bằng cách sử dụng bản sao và dán và thậm chí không nghĩ đến việc tái cấu trúc nó. Điều chính cần ghi nhớ IMO là mỗi khi bạn dập tắt một số trùng lặp nhỏ, bạn đang phân cấp mã và chuyển hướng một phụ thuộc ở nơi khác. Đó có thể là một điều tốt hoặc một điều xấu. Nó cung cấp cho bạn quyền kiểm soát trung tâm để thay đổi hành vi nhưng chia sẻ hành vi đó cũng có thể bắt đầu cắn bạn ...

@SZaman Nếu bạn muốn thực hiện một thay đổi như "Chỉ có chức năng này cần điều này, những người khác sử dụng chức năng trung tâm này thì không." Dù sao, đó là một hành động cân bằng như tôi thấy - một điều khó thực hiện hoàn hảo! Nhưng đôi khi một chút trùng lặp có thể giúp làm cho mã của bạn độc lập hơn và tách rời. Và nếu bạn kiểm tra một đoạn mã và nó hoạt động rất tốt, ngay cả khi nó sao chép một số logic cơ bản ở đây và đó, thì có thể có vài lý do để thay đổi. Trong khi đó một cái gì đó phụ thuộc vào rất nhiều thứ bên ngoài tìm thấy nhiều lý do bên ngoài hơn để thay đổi.

6

Để lặp lại @ Dan1701

Đây là về việc phân tách các mối quan tâm - chuyển việc đăng nhập vào ngoại lệ tạo ra sự kết hợp chặt chẽ giữa ngoại lệ và ghi nhật ký và cũng có nghĩa là bạn đã thêm một trách nhiệm bổ sung cho ngoại lệ đăng nhập có thể tạo ra sự phụ thuộc cho lớp ngoại lệ không cần.

Từ quan điểm bảo trì, bạn có thể (tôi sẽ) lập luận rằng bạn đang che giấu sự thật rằng ngoại lệ đang được ghi lại từ người bảo trì (ít nhất là trong bối cảnh của một cái gì đó giống như ví dụ), bạn cũng đang thay đổi bối cảnh ( từ vị trí của trình xử lý ngoại lệ đến hàm tạo của ngoại lệ) có thể không những gì bạn dự định.

Cuối cùng, bạn đang đưa ra một giả định rằng bạn luôn muốn ghi lại một ngoại lệ, theo cùng một kiểu, tại thời điểm mà nó được tạo / nâng lên - có lẽ không phải là trường hợp. Trừ khi bạn sẽ có các ngoại lệ đăng nhập và không đăng nhập, điều này sẽ gây khó chịu khá nhanh.

Vì vậy, trong trường hợp này, tôi nghĩ rằng "SRP" hơn hẳn "DRY".


1
[...]"SRP" trumps "DRY"- Tôi nghĩ rằng trích dẫn này khá nhiều tổng hợp hoàn hảo.

Như @Ike đã nói ... Đây là loại lý do tôi đang tìm kiếm.
Bruno Brant

+1 để chỉ ra rằng việc thay đổi bối cảnh ghi nhật ký sẽ ghi nhật ký như thể lớp ngoại lệ là nguồn gốc hoặc mục nhật ký, không phải vậy.
Tulains Córdova

2

Lỗi của bạn là ghi nhật ký một ngoại lệ trong đó bạn không thể xử lý nó và do đó không thể biết liệu đăng nhập có phải là một phần của việc xử lý đúng không.
Có rất ít trường hợp bạn có thể hoặc phải xử lý một phần lỗi, có thể bao gồm ghi nhật ký, nhưng vẫn phải báo hiệu lỗi cho người gọi. Các ví dụ là lỗi đọc không chính xác và tương tự, nếu bạn đọc mức thấp nhất, nhưng chúng thường có điểm chung là thông tin truyền đến người gọi được lọc nghiêm ngặt, để bảo mật và khả năng sử dụng.

Điều duy nhất bạn có thể làm trong trường hợp của mình, và vì một số lý do phải làm, là dịch ngoại lệ, việc triển khai ném vào một người gọi mong đợi, xâu chuỗi bản gốc cho ngữ cảnh và để mọi thứ khác yên.

Tóm lại, mã của bạn kiêu ngạo về quyền và nghĩa vụ xử lý một phần ngoại lệ, do đó vi phạm SRP.
DRY không đi vào đó.


1

Lấy lại một ngoại lệ chỉ vì bạn đã quyết định đăng nhập nó bằng cách sử dụng một khối bắt (có nghĩa là ngoại lệ không thay đổi chút nào) là một ý tưởng tồi.

Một trong những lý do chúng tôi sử dụng các ngoại lệ, thông báo ngoại lệ và cách xử lý của nó là để chúng tôi biết những gì đã sai và các ngoại lệ được viết khéo léo có thể tăng tốc độ tìm ra lỗi bằng một biên độ lớn.

Cũng cần nhớ rằng, việc xử lý các trường hợp ngoại lệ tốn nhiều tài nguyên hơn so với giả sử if, vì vậy bạn không nên xử lý tất cả chúng thường chỉ vì bạn cảm thấy như vậy. Nó có tác động đến hiệu suất của ứng dụng của bạn.

Tuy nhiên, đó là cách tiếp cận tốt để sử dụng ngoại lệ như một phương tiện để đánh dấu lớp ứng dụng trong đó lỗi xuất hiện.

Hãy xem xét mã bán giả sau đây:

interface ICache<T, U>
{
    T GetValueByKey(U key); // may throw an CacheException
}

class FileCache<T, U> : ICache<T, U>
{
    T GetValueByKey(U key)
    {
        throw new CacheException("Could not retrieve object from FileCache::getvalueByKey. The File could not be opened. Key: " + key);
    }
}

class RedisCache<T, U> : ICache<T, U>
{
    T GetValueByKey(U key)
    {
        throw new CacheException("Could not retrieve object from RedisCache::getvalueByKey. Failed connecting to Redis server. Redis server timed out. Key: " + key);
    }
}

class CacheableInt
{
    ICache<int, int> cache;
    ILogger logger;

    public CacheableInt(ICache<int, int> cache, ILogger logger)
    {
        this.cache = cache;
        this.logger = logger;
    }

    public int GetNumber(int key) // may throw service exception
    {
        int result;

        try {
            result = this.cache.GetValueByKey(key);
        } catch (Exception e) {
            this.logger.Error(e);
            throw new ServiceException("CacheableInt::GetNumber failed, because the cache layer could not respond to request. Key: " + key);
        }

        return result;
    }
}

class CacheableIntService
{
    CacheableInt cacheableInt;
    ILogger logger;

    CacheableInt(CacheableInt cacheableInt, ILogger logger)
    {
        this.cacheableInt = cacheableInt;
        this.logger = logger;
    }

    int GetNumberAndReturnCode(int key)
    {
        int number;

        try {
            number = this.cacheableInt.GetNumber(key);
        } catch (Exception e) {
            this.logger.Error(e);
            return 500; // error code
        }

        return 200; // ok code
    }
}

Giả sử ai đó đã gọi GetNumberAndReturnCodevà nhận 500mã, báo hiệu lỗi. Anh ta sẽ gọi cho bộ phận hỗ trợ, người sẽ mở tệp nhật ký và thấy điều này:

ERROR: 12:23:27 - Could not retrieve object from RedisCache::getvalueByKey. Failed connecting to Redis server. Redis server timed out. Key: 28
ERROR: 12:23:27 - CacheableInt::GetNumber failed, because the cache layer could not respond to request. Key: 28

Sau đó, nhà phát triển biết ngay lớp phần mềm nào đã khiến quá trình hủy bỏ và có một cách dễ dàng để xác định vấn đề. Trong trường hợp này là rất quan trọng, vì Redis hết thời gian nên không bao giờ xảy ra.

Có lẽ một người dùng khác sẽ gọi phương thức tương tự, cũng nhận được 500mã, nhưng nhật ký sẽ hiển thị như sau:

INFO: 11:11:11- Could not retrieve object from RedisCache::getvalueByKey. Value does not exist for the key 28.
INFO: 11:11:11- CacheableInt::GetNumber failed, because the cache layer could not find any data for the key 28.

Trong trường hợp đó, bộ phận hỗ trợ có thể trả lời đơn giản cho người dùng rằng yêu cầu không hợp lệ vì anh ta đang yêu cầu một giá trị cho ID không tồn tại.


Tóm lược

Nếu bạn đang xử lý các trường hợp ngoại lệ, hãy đảm bảo xử lý chúng theo cách chính xác. Ngoài ra, hãy đảm bảo các ngoại lệ của bạn bao gồm dữ liệu / tin nhắn chính xác ở vị trí đầu tiên, theo sau các lớp kiến ​​trúc của bạn, vì vậy các thông báo sẽ giúp bạn xác định một vấn đề có thể xảy ra.


1

Tôi nghĩ vấn đề ở mức độ cơ bản hơn: bạn ghi lại lỗi và ném nó như một ngoại lệ ở cùng một nơi. Đó là mô hình chống. Điều này có nghĩa là cùng một lỗi được ghi lại nhiều lần nếu nó bị bắt, có thể được bọc vào một ngoại lệ khác và được ném lại.

Thay vì điều này tôi đề nghị ghi nhật ký lỗi không phải khi ngoại lệ được tạo, mà là khi nó bị bắt. (Tất nhiên, đối với điều này, bạn phải chắc chắn rằng nó luôn bị bắt ở đâu đó.) Khi một ngoại lệ bị bắt, tôi chỉ đăng nhập stacktrace của nó nếu nó không được ném lại hoặc bọc như một nguyên nhân khác. Các stacktraces và thông điệp của các ngoại lệ được bao bọc được ghi lại trong dấu vết ngăn xếp là "Nguyên nhân bởi ...", dù sao đi nữa. Và người bắt cũng có thể quyết định ví dụ thử lại mà không ghi lại lỗi trong lần thất bại đầu tiên, hoặc chỉ coi đó là một cảnh báo, hoặc bất cứ điều gì.


1

Tôi biết đó là một chủ đề cũ, nhưng tôi đã gặp một vấn đề tương tự và tìm ra một giải pháp tương tự để tôi thêm 2 xu của mình.

Tôi không mua đối số vi phạm SRP. Không hoàn toàn dù sao đi nữa. Chúng ta hãy giả sử 2 điều: 1. Bạn thực sự muốn ghi lại các trường hợp ngoại lệ khi chúng xảy ra (ở mức theo dõi để có thể tạo lại luồng chương trình). Điều này không có gì để làm với việc xử lý các trường hợp ngoại lệ. 2. Bạn không thể hoặc bạn sẽ không sử dụng AOP cho điều đó - Tôi đồng ý rằng đó sẽ là cách tốt nhất để đi nhưng thật không may, tôi bị mắc kẹt với một ngôn ngữ không cung cấp công cụ cho điều đó.

Theo cách tôi nhìn thấy, về cơ bản bạn đã bị kết án vi phạm SRP quy mô lớn, bởi vì bất kỳ lớp nào muốn ném ngoại lệ đều phải biết về nhật ký. Di chuyển đăng nhập vào lớp ngoại lệ thực sự làm giảm đáng kể vi phạm SRP vì bây giờ chỉ có ngoại lệ vi phạm nó và không phải mọi lớp trong cơ sở mã.


0

Đây là một mô hình chống.

Theo tôi, thực hiện một cuộc gọi đăng nhập trong hàm tạo của một ngoại lệ sẽ là một ví dụ về các vấn đề sau: Flaw: Con constructor does Real Work .

Tôi sẽ không bao giờ mong đợi (hoặc mong muốn) một nhà xây dựng thực hiện một số cuộc gọi dịch vụ bên ngoài. Đó là một tác dụng phụ rất không mong muốn, như Miško Hevery chỉ ra, buộc các lớp con và giả phải thừa hưởng hành vi không mong muốn.

Như vậy, nó cũng sẽ vi phạm nguyên tắc ít gây ngạc nhiên nhất .

Nếu bạn đang phát triển một ứng dụng với người khác, thì tác dụng phụ này có thể sẽ không rõ ràng với họ. Ngay cả khi bạn đang làm việc một mình, bạn có thể quên nó và tự làm mình ngạc nhiên.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.