乐者为王

Do one thing, and do it well.

系统I/O地址表

PC机中仅使用A[0]-A[9]地址位来表示I/O地址,即可有1024个地址。前512个供系统电路使用,后512个供扩充插槽使用。当A[9]=0时表示为系统板上的I/O地址;A[9]=1时表示为扩充插槽接口卡上的地址。

系统I/O地址使用情况:

I/O地址范围 用途 I/O地址范围 用途
0000-001F 8237A DMA控制器1 00E0-00EF
0020-003F 8259A中断控制器1 00F0 重置协处理器总线
0040-005F 8253/8254定时/计数器(PIT) 00F1 设置协处理器总线
0060-006F 8042键盘控制器(AT) 00F2-00F7
0070-007F CMOS RAM与NMI屏蔽寄存器(AT) 00F8-00FF 协处理器
0080-009F DMA页寄存器 0100-01EF
00A0-00BF 8259A中断控制器2 01F0-01F7 硬盘
00C0-00DF 8237A DMA控制器2 01F8-01FF

扩充插槽I/O地址使用情况:

I/O地址范围 用途 I/O地址范围 用途
0200-0207 游戏卡I/O 0360-036F 保留
0208-020F 0370-0377
0210-0217 扩展部件(仅XT用) 0378-037F 并行口打印机1
0218-021F 0380-038F SDLC 通信及同步通信1
0220-024F 保留 0390-039F
0250-0277 03A0-03AF 同步通信2
0278-027F 并行口打印机2 03B0-03BF MDA 单色显示器
0280-02EF 03C0-03CF 保留
02F0-02F7 保留 03D0-03DF 彩色图形适配器
02F8-02FF 串行口2 03E0-03EF
0300-031F 试验卡 03F0-03F7 软盘适配器
0320-032F 硬盘适配器 03F8-03FF 串行口1
0330-035F

POSIX Conventions for Command Line Arguments

  1. An option is a hyphen followed by a single alphanumeric character, like this: -o.
  2. An option may require an argument (which must appear immediately after the option); for example, -oargument or -o argument.
  3. Options that do not require arguments can be grouped after a hyphen, so, for example, -lst is equivalent to -t -l -s.
  4. Options can appear in any order; thus -lst is equivalent to -tls.
  5. Options can appear multiple times.
  6. Options precede other nonoption arguments: -lst nonoption.
  7. The -- argument terminates options.
  8. The - option is typically used to represent one of the standard input streams.

如何编写完美的equals方法

Java语言规范要求equals方法具有下面的特点:

  1. 自反性(reflexive):对于任意非空引用x,x.equals(x)应返回true;
  2. 对称性(symmetric):对于任意引用x、y,当且仅当y.equals(x)返回true时,x.equals(y)返回true;
  3. 传递性(transitive):对于任意引用x、y、z,如果x.equals(y)返回true,且y.equals(z)也返回true,那么x.equals(z)应返回true;
  4. 一致性(consistent):如果x和y引用的对象没有改变,那么对x.equals(y)的重复调用应返回同一结果;
  5. 对任意非空引用x,x.equals(null)应返回false。

下面是编写完美equals方法的建议:

1
2
3
4
5
6
7
8
9
10
11
// 测试this和other是否是同一个对象
if (this == other) return true;
// 测试other是否为null,如果是就返回false
if (other == null) return false;
// 测试this和other是否属于同一个类
if (getClass() != other.getClass()) {
    return false;
}
// 将other对象的类型转换为你的类所属的类型
ClassName obj = (ClassName)other;
// 最后测试需要比较的字段。使用==比较基本类型字段,使用equals比较对象类型字段

如果遵循以上的规则定义了一个类的equals方法,定义其子类的equals方法时,首先调用父类的equals方法,如果这项测试能通过,那么再比较子类的字段。

1
if (!super.equals(other)) return false;

这里有个问题是如果this和other两个对象不是同一个类,equals方法该如何工作?有些程序员可能会使用instanceof代替getClass()来做判断,但这种方法是错误的,因为它违反了规则2所要求的对称性。

异常处理最佳实践

英文原文:http://www.onjava.com/pub/a/onjava/2003/11/19/exceptions.html

异常处理的问题之一是知道何时以及如何使用它。在这篇文章中,我将讨论一些异常处理的最佳实践。我也会总结最近关于使用检查型异常的争论。

作为程序员,我们想要编写解决问题的高质量的代码。不幸的是,异常作为我们代码的意外结果出现。没有人喜欢意外结果,因此我们很快找到我们自己的方法去解决它们。我曾经见过一些聪明的程序员以下列方式处理异常:

1
2
3
4
5
6
7
public void consumeAndForgetAllExceptions() {
    try {
        // some code that throws exceptions
    } catch (Exception ex) {
        ex.printStacktrace();
    }
}

上面的代码有什么问题?

一旦异常被抛出,正常的程序执行被挂起,并且控制权转移到catch块。catch块捕获异常后什么也没做。在catch块后面的程序继续执行,就好象什么也没发生。

下面的代码怎么样?

1
2
public void someMethod() throws Exception {
}

这是一个空方法,它里面没有任何代码。空方法如何能抛出异常呢?Java不能阻止你这么做。最近我正好遇见过类似的代码:方法声明抛出异常,但方法里没有实际产生那个异常的代码。当我问那个程序员时,他回复说:“我知道,它会让API变得糟糕,但我一直都是这么做的,而且这样做很管用。”

C++社区花了好几年去确定如何使用异常。这种争论在Java社区刚刚开始。我曾经见过几个Java程序员与异常的使用作斗争。如果没有正确使用,异常会导致你的程序变慢,因为它需要内存和CPU去创建、抛出和捕获异常。如果过度使用,会使代码难以阅读,让使用API的程序员感到挫折。我们都知道挫折感(frustrations)会导致特殊技巧( hacks)和代码味道。客户端代码只要忽略异常或者抛出它们就可以规避这个问题,就像前面的两个示例。

异常的本质

大体上说,有3种不同的情况可以导致异常被抛出:

  • 由于编程错误的异常: 这种异常是由于编程错误产生的(比如,NullPointerException和IllegalArgumentException)。客户端代码通常不能做关于编程错误的任何事情。
  • 由于客户端代码错误的异常: 客户端代码尝试API不允许的东西,从而违反它的契约。如果异常有提供有用的信息,客户端可以采取一些替代的做法。例如:当分析一个非格式良好的XML文档时有异常被抛出。异常包含XML文档中引起问题的位置信息。客户端可以使用该信息去采取恢复措施。
  • 由于资源失败的异常: 当资源失败时生成的异常。例如:系统内存不足或者网络连接失败。资源失败的客户端响应是上下文驱动的。客户端可以过段时间重试操作或者仅仅记录资源失败和停止应用程序。

Java中的异常类型

Java定义了两类异常:

  • 检查型异常: 继承自Exception类的异常是检查型异常。客户端代码必须处理被API抛出的检查型异常,不是在catch子句中就是通过throw子句向外转发。
  • 非检查型异常: RuntimeException也是扩展自Exception。不过,所有从RuntimeException继承的异常享有特殊待遇。对客户端代码是否处理它们没有要求,因此被称为非检查型异常。

举例来说,下图显示了NullPointerException的层次结构。

nullpointer-exception-hierarchy

在这个图中,NullPointerException扩展自RuntimeException,因此是非检查型异常。

我曾经见过检查型异常的重度使用和非检查型异常的最小使用。最近,在Java社区有关于检查型异常和其真正价值的激烈争论。这场争论源于Java似乎是第一个带有检查型异常的主流OO语言这个事实。C++和C#根本没有检查型异常,在这些语言中所有的异常都是非检查型的。

由较低层抛出的检查型异常是强制性的,要求调用层必须捕获或者抛出它。如果客户端代码无法有效地处理异常,在API和它的客户端之间的检查型异常契约很快就会转变为不必要的负担。客户端代码的程序员可能会走捷径,通过使用一个空的catch块抑制异常或者仅仅抛出它。实际上,它只是把负担放到客户端的调用者身上。

检查型异常也被指责破坏封装。考虑以下代码:

1
2
public List getAllAccounts() throws FileNotFoundException, SQLException {
}

方法getAllAccounts()抛出两个检查型异常。该方法的客户端必须显式地处理这些特定实现的异常,即使它也不知道在getAllAccounts()中什么文件或数据库调用失败,或者没有业务提供文件系统或数据库逻辑。因此,异常处理在方法和它的调用者之间强制造成了一个不适当的紧密耦合。

设计API的最佳实践

说了这么多,现在让我们谈谈如何设计一个正确抛出异常的API。

1. 当确定使用检查型异常还是非检查型异常时,问问你自己:“当异常发生时客户端代码可以采取什么措施?”

如果客户端可以采取一些替代措施来从异常中恢复,把它设置为检查型异常。如果客户端不能做任何有用的事情,那么就把它设置为非检查型异常。这里的“有用的”的意思是采取措施从异常中恢复而不是仅仅记录异常。总的来说:

当异常发生时客户端的反应 异常类型
客户端代码将基于异常中的信息采取一些有用的恢复操作 把它设置为检查型异常
客户端代码不能做任何事情 把它设置为非检查型异常

此外,对于所有的编程错误尽量使用非检查型异常。非检查型异常有个好处就是不会强制客户端代码显式地处理它们。它们传播到你想捕获它们的地方,或者直接就是报告异常。Java的API有许多非检查型异常,像NullPointerException、IllegalArgumentException和IllegalStateException。我更倾向于使用Java提供的标准异常而不是创建自己的。这会让我的代码容易理解和避免增加代码的内存占用。

2. 保持封装性。

不要让特定实现的检查型异常逐步上升到更高的层。例如,不要把来自数据访问代码中的SQLException传播到业务对象层。业务对象层不需要了解SQLException。你有两个选择:

  • 如果客户端代码期望从异常中恢复,把SQLException转换成别的检查型异常。
  • 如果客户端代码不能做关于它的任何事情,把SQLException转换成一个非检查型异常。

大多数时候,客户端代码不能做关于SQLException的任何事情。不要犹豫去把它们转换成非检查型异常。考虑下面的代码片段:

1
2
3
4
5
6
7
public void dataAccessCode() {
    try {
        // some code that throws SQLException
    } catch (SQLException ex) {
        ex.printStacktrace();
    }
}

这里的catch块只是抑制异常而且什么也不做。理由是对于SQLException我的客户端没有什么能做的。以下列方式处理它怎么样?

1
2
3
4
5
6
7
public void dataAccessCode() {
    try {
        // some code that throws SQLException
    } catch (SQLException ex) {
        throw new RuntimeException(ex);
    }
}

它把SQLException转换成RuntimeException。如果SQLException发生,catch子句会抛出一个新的RuntimeException。执行线程挂起,报告异常。不管怎样,我没有用不必要的异常处理让我的业务对象层变得糟糕,特别是因为它不能做关于SQLException的任何事情。如果我的catch子句需要根异常的原因,我可以利用自JDK 1.4起就在所有异常类中有效的getCause()方法。

当SQLException发生时如果你确信业务层能采取一些恢复动作,你可以把它转换成一个更有意义的检查型异常。但我发现大多数时间仅仅抛出RuntimeException就足够。

3. 如果不能给客户端代码提供有用的信息就不要试图去创建新的定制异常。

下面的代码有什么问题?

1
2
public class DuplicateUsernameException extends Exception {
}

它没有给客户端代码提供任何有用的信息,除了一个指示的异常名字。不要忘记Java的异常类就像其它类,在其中你可以添加你认为客户端代码将会调用以便获得更多信息的方法。

我们可以给DuplicateUsernameException添加有用的方法,像:

1
2
3
4
5
public class DuplicateUsernameException extends Exception {
    public DuplicateUsernameException(String username) {}
    public String requestedUsername() {}
    public String[] availableNames() {}
}

新版本提供两个有用的方法:requestedUsername()返回请求的名字;availableNames()返回与请求的名字相似的一组有效用户名。客户端可以使用这些方法去告知请求的用户名是无效的和其它的用户名是有效的。但如果你不打算添加额外的信息,那么就抛出一个标准的异常:

1
throw new Exception("Username already taken");

如果你认为当用户名已被占用时客户端代码除了日志记录不打算采取任何动作,那么抛出一个非检查型异常就更好:

1
throw new RuntimeException("Username already taken");

或者,你甚至可以提供一个方法检查是否用户名已被占用。

值得重复的是,检查型异常被用于那些客户端API可以基于异常中的信息采取一些富有成效的动作的情况中。对于所有的编程式错误尽量使用非检查型异常。它们让你的代码可读性更强。

4. 用文档说明异常。

你可以使用Javadoc的@throws标签来说明API抛出的检查型和非检查型异常。不过,我倾向于编写单元测试来说明异常。测试允许我看见动作中的异常,因此能被当作可以执行的文档。无论你做什么,都有一些办法让客户端代码可以通过它们了解你的API抛出的异常。这里是一个用于测试IndexOutOfBoundsException的示例单元测试:

1
2
3
4
5
6
7
8
public void testIndexOutOfBoundsException() {
    ArrayList blankList = new ArrayList();
    try {
        blankList.get(10);
        fail("Should raise an IndexOutOfBoundsException");
    } catch (IndexOutOfBoundsException success) {
    }
}

当blankList.get(10)被调用时上面的代码将会抛出IndexOutOfBoundsException。如果没有抛出异常,fail("Should raise an IndexOutOfBoundsException")语句会显式地让测试失败。通过为异常编写单元测试,你不仅说明异常是如何工作的,也通过测试特殊场景让你的代码更加健壮。

使用异常的最佳实践

下一组最佳实践显示客户端代码将如何处理抛出检查型异常的API。

1.总是要做些清理工作

如果你使用像数据库连接或网络连接这样的资源,确保你已经把它们清理干净。如果你调用的API只使用非检查型异常,你仍然应该在使用后用try-finally块清理资源。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public void dataAccessCode() {
    Connection conn = null;
    try {
        conn = getConnection();
        // some code that throws SQLException
    } catch (SQLException ex) {
        ex.printStacktrace();
    } finally {
        DBUtil.closeConnection(conn);
    }
}

class DBUtil {
    public static void closeConnection(Connection conn) {
        try {
            conn.close();
        } catch (SQLException ex) {
            logger.error("Cannot close connection");
            throw new RuntimeException(ex);
        }
    }
}

DBUtil是一个可以关闭连接的实用工具类。重点是finally块的使用,无论异常是否被捕获它都会执行。在这个例子中,finally关闭连接,如果关闭连接有问题就抛出一个RuntimeException。

2. 不要使用异常控制流程

生成栈跟踪是昂贵的,栈跟踪的价值是在调试。在一个流程控制的情况中,栈跟踪将被忽略,因为客户端仅仅想知道如何处理。

在下面的代码中,一个定制的异常MaximumCountReachedException被用来控制流程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public void useExceptionsForFlowControl() {
    try {
        while (true) {
            increaseCount();
        }
    } catch (MaximumCountReachedException ex) {
    }
    // continue execution
}

public void increaseCount()
    throws MaximumCountReachedException {
    if (count >= 5000)
        throw new MaximumCountReachedException();
}

useExceptionsForFlowControl()使用一个无限循环增加计数直到异常被抛出。这不仅使代码难以阅读,而且让代码变得更慢。仅在特殊情况下使用异常处理。

3. 不要抑制或忽略异常

当来自API的一个方法抛出检查型异常时,它试图告诉你应该采取一些反向动作。如果检查型异常对你毫无意义,不要犹豫去把它转换成一个非检查型异常并且再次抛出它,但是不要通过用{}捕获异常来忽略它,然后继续,好像什么也没发生过。

4. 不要捕获顶级异常

非检查型异常继承自RuntimeException类,RuntimeException又继承自Exception。通过捕获Exception类,你也捕获了RuntimeException,如以下代码所示:

1
2
3
try {
} catch (Exception ex) {
}

上面的代码同样忽略了非检查型异常。

5. 只记录异常一次

多次记录相同的异常栈跟踪可能会让程序员在检查关于异常原始源的栈跟踪时被迷惑。因此,只记录它一次。

总结

这些都是异常处理最佳实践的一些建议。我无意开始一场检查型异常与非检查型异常之间的宗教战争。你必须根据你的需求自定义设计和用法。我相信,假以时日,我们会找到更好的用异常编码的方法。

我要感谢Bruce Eckel、Joshua Kerievsky和Somik Raha他们对我在写这篇文章时的支持。

相关资源

如何解决含有中文的页面末尾内容会被SiteMesh截掉的问题

首先,我们要了解一下ServletResponse.setContentLength(int len)的含义。setContentLength是设置返回内容体长度的方法,len是内容体的长度。由于网络上传输内容是以字节(byte)为单位的,所以len就是指内容体有多少个字节。假设现在有长度为100个字节的数据,在输出数据到客户端前我们用setContentLength(90)设置内容体的长度为90个字节。那么在客户端接收到的数据长度就是90个字节而不是100。下面我们来做个试验:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class PageTruncateFilter implements Filter {

    public void init(FilterConfig config) throws ServletException {
    }

    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
                        throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest)req;
        HttpServletResponse response = (HttpServletResponse)res;

        String content = "<html><head><title>Truncated Page</title></head>";
               content += "<body>这个页面的内容will be truncated.</body></html>";
        response.setCharacterEncoding("gbk");
        response.setContentLength(content.length());
        PrintWriter out = response.getWriter();
        out.println(content);
        out.close();
    }

    public void destroy() {
    }
}

由于content字符串含有两个中文字符,每一个中文字符又是由两个字节组成,所以content.length()的值比content的字节数要少2。在客户端显示的内容也会因此少2个字节,即缺少“l>”这两个字符。

现在我们来浏览SiteMesh的源代码找出问题的原因。首先从PageFilter.java的doFilter方法开始。由于页面没有在decorators.xml中注册修饰,所以writeOriginal方法会被调用。下面是writeOriginal方法的源代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/** Write the original page data to the response. */
private void writeOriginal(HttpServletRequest request, HttpServletResponse response,
                          Page page) throws IOException {
    response.setContentLength(page.getContentLength());

    if (request.getAttribute(USING_STREAM).equals(Boolean.TRUE)) {
        PrintWriter writer = new PrintWriter(response.getOutputStream());
        page.writePage(writer);
        // flush writer to underlying outputStream
        writer.flush();
        response.getOutputStream().flush();
    } else {
        page.writePage(response.getWriter());
        response.getWriter().flush();
    }
}

首行代码就是设置内容体长度的,让我们追到page.getContentLength()里面去看看(Page接口的getContentLength方法是由AbstractPage.java实现的):

1
2
3
public int getContentLength() {
    return pageData.length;
}

它返回pageData数组的长度。但是pageData数组是char类型的,假如pageData中有中文内容时,设置的内容体长度就会小于pageData的字节数。这时就会出现输出到客户端的数据截掉的问题。所以只要修改getContentLength方法返回的值就可以了,下面是修改后的getContentLength方法:

1
2
3
4
public int getContentLength() {
    String content = new String(pageData);
    return content.getBytes().length;
}

在Eclipse 3中使用SSH2访问gro.clinux.org的CVS服务器

在Eclipse中生成公钥和私钥

打开Eclipse,进入Window -> Preferences -> Team -> CVS -> SSH2 Connection Method -> Key Management,点击“Generate RSA Key...”,然后填写Passphrase和Confirm Passphrase,接着点击“Save Private Key...”保存公钥和私钥(公钥和私钥文件保存在~/.ssh/目录下,id_rsa是私钥文件,id_rsa.pub 是公钥文件)。

将公钥上传至gro.clinux.org服务器

登陆gro服务器,进入[帐号维护],在页面底部你可以看到如下信息:

1
2
3
4
Shell帐号信息

Shell box:
CVS/SSH 共享认证Key:0 [编辑Keys]

点击[编辑Keys],然后把刚才生成的文件id_rsa.pub中的内容粘贴到文本框中(看看是不是符合它上面的要求,千万要仔细看看),如果没有问题的话,就更新吧。大约6个小时后Cron会自动更新,在服务器上你的目录下会自动添加文件夹/.ssh/,里面包含了名为authorized_keys2的文件,你的公钥就保存在这里。

在Eclipse中建立CVS连接

打开CVS Repository Exploring Perspective,进入New -> Repository Location,填入以下内容:

1
2
3
4
5
Host:cvs.unixname.gro.clinux.org
Repository Path:/cvsroot/unixname
User:你在gro.clinux.org上注册的帐户
Password:先前保存公钥和私钥时输入的Passphrase
Connection Type:extssh

点击Finish。如果你是第一次登录的话,将会弹出一个对话框,不用看那么仔细,直接点击OK就好了。

如何生成可执行的jar文件

首先用jar cvf executable.jar *.class命令生成jar文件,然后编辑jar文件中META-INF目录下的MAINFEST文件。在其尾部加上main-class: main-class-name就可以让它变成可执行的了。

为什么要软件建模

模型是对现实的简化。需要进行建模的原因有以下几点:

  1. 人们对复杂问题的理解能力是有限的;
  2. 我们要开发的软件系统是复杂的;
  3. 对系统的完整的理解有助于正确地实施工作(但不一定能使开发工作进展的更快);
  4. 我们不能完整的理解一个复杂的系统,为了能够更好地理解正在开发的系统,因此要对它进行建模。

那么,是不是任何情况下都要建模呢?未必尽然。是否要进行软件建模是由系统的复杂程度决定的。如果系统的复杂性在我们的控制范围之内,那么就不需要建模。不过,有一个自然趋势:随着时间的推移,几乎所有的应用系统变得越来越复杂。因此要记住的是:狗窝总有一天会膨胀成大厦,并且因为不堪承受其自身的重量而倒塌。

贫血领域模型

英文原文:https://martinfowler.com/bliki/AnemicDomainModel.html

贫血领域模型是那些已近存在了相当长时间的反模式中的一份子,然而目前似乎有一个特定的高潮。我和Eric Evans聊过这个,并且我们都注意到它们似乎越来越流行。作为真正的领域模型支持者,这不是一件好事。

贫血领域模型的基本症状是乍一看它就像真的领域模型一样。这些对象大多以领域空间中的名词命名,并且它们被真正的领域模型拥有的丰富的关系和结构连接。当你观察行为时问题来了,你意识到这些对象几乎没有任何行为,仅仅只是封装了getter和setter方法。这些模型的设计规则往往说不要把任何领域逻辑放到领域对象里。取而代之的是一组服务对象捕获了所有的领域逻辑。这些在领域对象上层的服务把领域对象当数据使用。

这种反模式最根本的恐怖在于它和面向对象设计的基本理念完全相反,它只是把数据和处理结合在一起。贫血领域模型其实只是一种过程式风格的设计,正是那种像我(和Eric)自Smalltalk早期以来一直在与之作斗争的顽固者。更糟糕的是,许多人认为贫血对象是真正的对象,因此完全没有抓住面向对象设计是怎么回事的要领。

现在面向对象的纯粹主义当然很好,但我意识到我需要更多的基本论据来反对这种贫血症。本质上贫血领域模型的问题是它们承担了领域模型所有的成本,没有产生任何的好处。主要成本是映射到数据库的笨拙,它通常会导致一整层的O/R映射。在而且只有在你使用强大的面向对象技术去组织复杂的逻辑时这才值得。通过把所有的行为拉取到服务中,不管用什么方法,基本上你最后得到的都是事务脚本,从而失去领域模型能带来的优势。正如我在《企业应用架构模式》中讨论过的,领域模型并不总是最好的工具。

值得强调的是,把行为放进领域对象不应违背利用分层将领域逻辑从诸如持久和表现责任中分离的坚实方法。在领域对象中的逻辑应该是领域逻辑——验证、计算、业务规则——无论你喜欢叫它什么。(某些情况下,你会产生把数据源和表现逻辑放到领域对象里的争论,但它和我对贫血症的看法无关。)

这一切混乱的来源是许多OO专家强烈推荐在领域模型顶部放一层过程式服务,来形成一个服务层。但这不是让领域模型完全没有行为的一个论据,其实服务层主张服务层要和行为丰富的领域模型一起使用。

Eric Evans的精彩书籍《领域驱动设计》有如下的文字谈及这些层:

应用层【他对服务层的命名】:定义软件可以完成的工作,并且指挥具有丰富含义的领域对象来解决问题。这个层所负责的任务对业务影响深远,对跟其它系统的应用层进行交互非常必要。这个层要保持简练。它不包括处理业务规则或知识,只是给下一层中相互协作的领域对象协调任务、委托工作。在这个层次中不反映业务情况的状态,但反映用户或程序的任务进度的状态。

领域层(或者模型层):负责表示业务概念、业务状况的信息以及业务规则。并且保持这些内容的技术细节由基础结构层来完成,反映业务状况的状态在该层中被控制和使用。这一层是业务软件的核心。

这里的关键点是服务层是瘦的——所有的关键逻辑存在于领域层。他在服务模式中重申了这个观点:

现在,最常犯的错误就是太轻易地放弃把这种行为配置到合适的对象上,逐渐地滑向过程式编程。

我不知道为什么这个反模式如此常见。我猜想它是因为大多数人还没有真正地和正确的领域模型工作过,特别是如果他们有数据背景。有些技术鼓励正确的领域模型,例如J2EE的实体Bean,它是我偏爱POJO领域模型的理由之一。

总之,你在服务中发现越多的行为,你就越难体会到领域模型的好处。如果你所有的逻辑都在服务中,那你将得不到任何好处。

练习破解Andrnalin的Crackme

拿到这个程序,用PEiD查看这个程序,发现是用Microsoft Visual Basic 5.0编写的且没有加过壳,所以用W32Dasm反汇编。通过Functions/Imports查看它调用的函数,能够看见MSVBVM50!__vbaStrCmp字样。

  1. 启动SoftICE,然后运行Crackme程序;
  2. 在文本框中输入12345678;
  3. Ctrl+D来到SoftICE中,输入bpx __vbaStrCmp,按回车后用Ctrl+D命令返回Crackme;
  4. 点击OK,程序被SoftICE中断;
  5. 按F12回到调用__vbaStrCmp的地方;
  6. 按F6切换到代码窗口,移动光标,直到代码窗口中出现如下程序段:
1
2
3
4
001B:00401D70                MOV ECX, [EBP-28]
001B:00401D73                PUSH ECX
001B:00401D74                PUSH 00401A54
001B:00401D79                CALL [MSVBVM50!__vbaStrCmp]

因此可以知道__vbaStrCmp比较的是ECX和00401A54所指向的字符串。记下00401A54这个值,然后重复步骤1、2、3、4。接着查看ECX和00401a54指向的内容就可以知道正确的key了。

d ecx的显示如下:

1
2
0023:0013CC1C 31 00 32 00 33 00 34 00-35 00 36 00 37 00 38 00 1.2.3.4.5.6.7.8.
0023:0013CC2C 00 00 72 00 61 00 6D 00-46 00 69 00 79 00 05 00 ..r.a.m.F.i.y...

d 401a54的显示如下:

1
2
0023:00401A54 53 00 79 00 6E 00 54 00-61 00 58 00 20 00 32 00 S.y.n.T.a.X. .2.
0023:00401A64 6F 00 6F 00 31 00 00 00-4C 00 00 00 52 00 69 00 o.o.1...L...R.i.

12345678是我们输入的key,所以正确的key是SynTaX 2oo1(注意中间的空格)。

注意:如果你是在破解VB6程序,你应该在断点前加上msvbvm60!。