Define Errors Out Of Existence
- why exceptions contribute disproportionately to complexity
- it shows how to simplify exception handling.
- Goals: to reduce the number of places where exceptions must be handled
Exception handling is one of the worst sources of complexity in software systems.
Code that deals with special conditions is inherently harder to write than code that deals with normal cases, and developers often define exceptions without considering how they will be handled.
inherently: 固有地，内在地 用代码处理异常条件本身就比处理正常的场景更困难；与此同时，开发还要经常定义没有考虑会怎么处理的异常。 不负责任的使用异常，让系统变得更加的复杂。
The key overall lesson from this chapter is to reduce the number of places where exceptions must be handled;
in many cases the semantics of operations can be modified so that the normal behavior handles all situations and there is no exceptional condition to report
一切的核心是减少必须处理异常的地方 第二句话，前半段是理解的：操作的语义是可以修改的 后半句未能很好理解。没有理解是因为方法语义可变导致了需要代码处理所有的场景，还是通过改变方法语义来支持所有场景的处理。 留个疑问。 20220102：主要是希望通过改变语义的方式来减少异常场景。对于特殊场景，很难用一个语义全部覆盖掉。无法通过修改语义的方式去除的错误场景，可以通过隐藏和聚合的方式，将处理异常的地方减少到最小，从而降低系统复杂度。 主要是要多思考，是否可以通过变通一下，将可能会出现的错误定义在语义逻辑之外。
10.1 Why exceptions add complexity
exception - any uncommon condition that alters the normal flow of control in a program.
Many programming laguages include a formal exception machanism that allows exceptions to be thrown by lower-level code and caught by enclosing code.
However, exceptions can occur even without using a formal exception reporting mechanism, such as when a method returns a special value indicating that it didn't complete its normal behavior.
- A caller may provide bad arguments or configuration information.
- An invoked method may not be able to complete a requested operation.
- In a distributed system, network packets may be lost or deplayed, servers may not respond in a timely fashion, or peers may communicate in unexpected ways.
- The code may detect bugs, internal inconsistencies, or situations it is not prepared to handle
参数异常、不能完成请求、丢包、系统不可访问、代码有bug等等 timely: 及时地 peer: 端对端 detect: 检测出，查明 inconsistency: 易变，不一致性
Large systems have to deal with many exceptional conditions, particularly if they are distributed or need to be fault-tolerant. Exception handling can account for a significant fraction of all the code in a system.
Exception handling is more complicated
Exception handling code is inherently more difficult to write than normal-case code.
It usually means that something didn't work as expected.
When an exception occurs, the programmer can deal with it in two ways, each of which can be complicated.
Two ways to deal with
The first approach is to move forward and complete the work in progress in spite of the exception.
The second approach is to abort the operation in progress and report the exception upwards.
aborting can be complicated because the exception may have occurred at a point where system state is inconsistent (a data structure might have been partially initialized); the exception handling code must restore consistency, such as by unwinding any changes made before the exception occurred.
exception handling code creates opportunites for more exceptions.
- Consider the case of resending a lost network packet.
- consider the case of recovering lost data from a redundant copy: what if the redundant copy has also been lost.
- handled by aborting the operation in progress
方式一：重发失败的消息，这样会导致接受方会接收到重复的请求，接收方需要有新的异常场景需要处理。 方式二：通过冗余副本恢复丢失的数据，但是冗余副本也丢失了呢？ 恢复时出现的第二个异常通常更加隐晦和复杂。 方式三：终止操作，然后必须向调用方抛出其他的异常。
To prevent an unending cascade of exceptions, the developer must eventually find a way to handle exceptions without introducing more exceptions.
Larguage support for exceptions tends to be verbose and clunky, which makes exception handling code hard to read.
verbose: 冗长的，啰嗦的 clunky: 沉重的，笨重的
It's difficult to ensure that exception handling code really works.
such as I/O errors, can't easily be generated in a test environment, so it's hard to test the code that handles them.
code that hasn't been executed doesn't work
A recent study found that more than 90% of catastrophic failures in distributed data-intensive systems were caused by incorrect error handling.
When exception handling code fails, it's difficult to debug the problem, since it occurs so infrequently.
10.2 Too many exceptions
Programmers exacerbate the problems related to exception handling by defining unnecessary exceptions.
Most programmers are taught that it's important to detect and report errors;
It's tempting to use exceptions to avoid dealing with difficult situations: rather than figuring out a clean way to handle it, just throw an exception and punt the problem to the caller.
Some might argue that this approach empowers callers, since it allows wach caller to handle the exception in a different way.
if you are having trouble figuring out what to do for the particular situation, there's a good chance that the caller won't know what to do either.
在日常开发中，确实有这样的想法，通过异常声明，将不属于流程中的异常问题抛出，给上层调用方使用。 虽然这样可以让调用方自己决定异常处理的方式，但是无形之中增加了系统的复杂度。因为它只是把问题传递给了别人。而不是解决了问题。 如果你也不知道怎么处理特殊场景，那么调用方也可能不知道该做什么。
Generating an exception is a situation like this just passes the problem to someone else and adds to the system's complexity.
The exceptions thrown by a class are part of its interface; **classes with lots of exceptions have complex interfaces, and they are shallower than classes with fewer exceptions. **
It propagate up through several stack levels before being caught, so it affects not just the method's caller, but potentially also higher-level callers (and their interfaces).
一个抛出的异常影响的不止是调用方，还可能会影响更上层的调用方。 proapgate: 传播 potentially: 潜在地，可能地
Throwing exceptions is easy; handling them is hard
the complexity of exceptions comes from the exception handling code.
The best way to reduce the complexity damage caused by exception handling is to reduce the number of places where exceptions have to be handled.
10.3 Define errors out of existence
throwing an error when
unsetis asked to delte an unknown variable.
it is perfectly natural for
unsetto be invoked with the name of a variable that doesn't exist.
it should have simply returned without doing anything.
10.4 Example: file deletion in Windows
The Windows operating system does note permit a file to be deleted if it is open in a process.
if a file is open when it is deleted, Unix does not delete the file immediately, Instean, id marks the file for deletion, then the delete operation returns successfully.
Defines away two errors
The Unix approach defines away two different kinds of errors.
First, the delete operation no longer returns an error if the file is currently in use; the delete succeeds, and the file will eventually be deleted.
Second, deleting a file that's in use does not create exceptions for the processes using the file.
One possible approach to this problem would have been to delete the file immediately and mark all of the opens of the file to disable them; any attempts by other processes to read or write the deleted file would fail.
This approach would create new errors for those processes to handle.
10.5 Example: Java substring method
if either index is outside the range of the string, then
substringmethod would be easier to use if it performed this adjustment automatically, so that it implemented the following API: "returns the characters of the string (if any) with index greater than or equal to
beginIndexand less than
it defines the
IndexOutOfBoundsExceptionexception out of existence.
Many other llanguages have taken the error-free approach; for example, Python returns an empty result for out-of-range list slices.
error-free approach vs error-full approach
people sometimes counter that throwing errors will catch bugs; if errors are defined out of existence, won't that result in buggier software?
Perhaps this is why the Java developers decided that
substringshould throw exceptions.
The error-full approach may catch some bugs, but it also increases complexity, which results in other bugs.
Must write additional code to avoid or ignore the errors, and this increases the likelihood of bugs; or, they may forget to write the additional code, in which case unexcepted errors may be thrown at runtime.
In contrast, defining errors out of existence simplifies APIs and it reduces the amount of code that must be written.
OVerall, the best way to reduce bugs is to make siftware simpler.
10.6 Mask exceptions
The second technique for reducing the number of places where exceptions must be handled is exception masking.
With this approach, an exceptional condition is detected and handled at a low level in the system, so that higher levels of software need not be aware of the condition.
In a network transport protocol such as TCP, packets can be dropped for various reasons such as corruption and congestion.
TCP masks packet loss by resending lost packets within its implementation, so all data eventually gets through and clients are unarare of the dropped packets.
通过重发消息的方式，来隐藏消息因为错误或者拥挤导致丢失的问题。客户端并不需要感知未接收到的数据。 corruption: 错误 congestion: 拥挤 unaware: 不知道
NFS network file system
A more controversial example of masking .
If an NFS file server crashes or fails to respond for any reason, clients reissue their requests to the server over and over again until the problem is eventually resolved.
However, reporting exceptions would mak things worse, not better.
One possibility would be for the application to retry the file operation, but this ould still hang the application, and it's easier to perform the retry in one place in the NFS layer, rather than at every file system call in every application.
The other alternative is for applications to abort and return errors to their callers.
It's unlikely that the callers would know what to do either, so they would abort as well, resulting in a collapse of the user's working environment.
Exception masking doesn't work in all situations, but it is a powerful tool in the situations where it works.
It results in deeper classes, since it reduces the class's interface (fewer exceptions for users to be aeare of) and adds functionality in the form of the code that masks the exception.
Exception masking is an example of pulling complexity downward.
10.7 Exception aggregation
The third technique for reducing complexity related to exceptions is
To handle many exceptions with a single piece of code; rather than writing distinct handlers for many individual exceptions, handle them all in one place with a single handler.
Abatter approach is to aggregate the exceptions. Instead of catching the exceptions in the individual service methods, let them propagate up to the top-level dispach method for the Web server.
In each case, the error should result in an error response; the error differ only in the error message to include in the response
Thus, all conditions resulting in an error response can be handled with a single top-level exception handler.
The error message can be generated at the time the exception is thrown and included as a variable in the excepion record.
The aggregation described in the preceding paragraph has good properties from the standpoint of encapsulation and information hiding.
The top-level exception handler encapsulates knowledge about how to generate error responses, but it knows nothing about specific errors; it just uses the rror mesage provided in the excaption.
different subclases of the exception can be defined for different conditions.
Exception aggregation works best if an exception propagates several levels up the stack before it is handled; this allows more exceptions from more methods to be handled in the same place.
masking usually works best if an exception is handled in a low-level method.
Masking and aggregation are similar in that both approaches positon an exception handler where it can catch the most exceptions, eliminating many handlers that would otherwise need to be created.
one way of thinking about exception aggregation is that it replaces serveral special-purpose mechanisms, each tailored for a particular situation, with a single general-purpose mechanism that can handle multiple situations.
简单的来说异常聚合的作用是将多处特殊处理机制，针对每个特殊场景做了定制处理，都可以替换成通用的处理机制。 举了一个RAMCloud的例子。例子中说明RAMCloud对于崩溃恢复的处理。将异常的处理方式都统一成让服务器崩溃，然后进行恢复的方式。因为系统肯定需要服务器崩溃之后的恢复机制。这样就减少了异常处理的代码。 但觉得让服务器崩溃应该是一个下下策，而不是一个可以用来统一处理异常的方式。
10.8 Just crash?
The fourth technique for reducing complexity related to exception handling is to crash the application.
In most applications there will be certain errors that it's not worth trying to handle.
Typically, these errors are difficult to impossible to handle and don't occur very often. The simplest thing to do in response to these errors is to print diagnostic information and then abort the application
"Out of memory" errors
mallocfunction in C, which returns NULL if it cannot allocate the desired block of memory. This is an unfortunate behavior, because it assumes that every single caller of
mallocwill check the return value and take appropriate action if there is no memory.
A better approach is to define a new method
ckalloc, which calls
malloc, checks the result, and aborts the application with an error message if memory is exhausted.
In newer languages such as C++ and Java, the
newoperator throws an exception if memory is exhausted.
Dynamically allocated memory is such a fundamental element of any modern application that it doesn't make sense for the application to continue if memory is exhausted; it's better to crash as soon as the error is detected.
Other examples of erros where crashing the application maskes sense.
if an I/O error occurs while reading or writing an open file (such as a disk hard error), or if a network socket cannot be opened, there's not much the applicaiton can do to recover, so aborting with a clear error message is a sensible approach.
Aborting with an error message is also appropriate if an application encounters an internal error such as an inconsistent data structure.
Conditions like this probably indicate bugs in the program.
Whether or not it is acceptable to crash on a particular error depends on the application
10.9 Design special cases out of existence
Special cases can result in code that is riddled with
ifstatements, which make the code hard to understand and lead to bugs. Thus, special cases should be eliminated wherever possible.
The best way to do this is by designing the normal case in a way that automatically handles the special cases without any extra code.
10.10 Taking it too far
Defining away exceptions, or masking them inside a module, only masks sense if the exception information isn't needed outside the module.
In the rare situations where a caller cares about the special cases detected by the exceptions, there are other ways for it to get this information.
with exceptions, as with many other areas in software design, you must determine what is important and what is not important.
Things that are not important should be hidden, and the more of them the better. but when something is important, it must be exposed.
Special cases of any form make code harder to understand and increase the likelihood of bugs.
The best way to do this is by redefining semantics to eliminate error conditions
For exceptions that can't be defined away, you should look for opportunities to mask them at a low level, so their impact is limited, or aggregate several special-case handlers into a single more generic handler. Together, this techniques can have a significant impact on overall system complexity.
最好的方式是重新定义语义来减少错误的场景。 对于无法通过定义去除的异常，需要寻找机会将他们隐藏在底层，让他们的影响面受限。 或者聚合多个特殊场景的处理器到一个通用的处理器的地方。 总的来说，这些技术可以对整个系统的复杂性产生重大的影响。