Taogen's Blog

Stay hungry stay foolish.

This post will discuss content in Java IO streams. It is based Java SE 8.

Input Stream

Input Stream Methods

  • int available(): return available to read the remaining byte size.
  • void close(): close the input stream.
  • void mark(int readlimit): marks the current position in this input stream.
    • readlimit: the number of bytes that can be read from the stream after setting the mark before the mark becomes invalid. Note the parameter for some subclass of InputStream has no meaning.
  • boolean markSupported(): return if this input stream supports the mark and reset methods.
  • int read(): reads the next byte of data.
  • int read(byte[] b): reads some bytes from input stream and stores them into buffer array b.
  • int read(byte[] b, int off, int len): reads up to len bytes of data from input stream into array of bytes.
  • void reset(): repositions this stream to the position at mark (default 0)
  • long skip(long n): skips bytes of data.

What does Input Stream actually do?

InputStream only reads data from front to back. It can skip some data to read. It can back to specified position to read again.

InputStream can read bytes one by one or read some bytes into byte array once.

If there is no available data in InputStream, the calling one of the read methods will return -1.

int read(byte b[]) => int read(byte b[], int offset, int len), abstract int read()

read() methods can implement directly (e.g ByteArrayInputStream) or implement by calling the native method (e.g FileInputStream).

InputStream Types

  • ByteArrayInputStream: Byte array as data
    • ByteArrayInputStream(byte[] b)
  • FileInputStream: File as data
    • FileInputStream(File file)
  • ObjectInputStream: Deserializes primitive data and objects previously written using an ObjectOutputStream.
    • ObjectInputStream(InputStream in)
    • readObject()
  • PipedInputStream: A piped input stream should be connected to a piped output stream; the piped input stream then provides whatever data bytes are written to the piped output stream.
    • PipedInputStream()
    • connect(PipedOutputStream src)
  • StringBufferStream(Deprecated): String as data.
    • StringBufferInputStream(String s)
  • SequenceInputStream: Logical concatenation of other input streams.
    • SequenceInputStream(InputStream s1, InputStream s2)
  • FilterInputStream: It contains some other input stream as data, and transforming data or providing additional functionality.
    • BufferedInputStream: read data from the internal buffer array rather than read data from input stream every time.
      • BufferedInputStream(InputStream in)
    • PushbackInputStream: unread one byte.
      • PushbackInputStream(InputStream in)
      • unread(int byteValue)
    • DataInputStream: read primitive Java data types.
      • DataInputStream(InputStream in)
      • readInt()
    • LineNumberInputStream(Deprecated): provides the added functionality of keeping track of the current line number.
      • LineNumberInputStream(InputStream in)
      • getLineNumber()

OutputStream

OutputStream Methods

  • void close(): close output stream and release resources.
  • void flush(): Flushes output stream and forces buffered bytes to be written out.
  • void write(byte[] b): Writes byte array.
  • void write(byte[] b, int offset, int len): Writes len bytes from the byte array starting at offset.
  • void write(int b): Writes the specified byte.

What does Output Stream actually do?

Write one byte or some bytes data to output stream.

If output stream doesn’t call the flush or close method, some data written in buffer array may don’t write to the stream.

Some output streams have the buffer array, others not.

OutputStream Types

  • ByteArrayOutputStream: Writes data to a byte array. retrieve data using toByteArray() and toString().
    • toByteArray()
    • toString()
  • FileOutputStream: Writes data to a File or FileDescriptor.
    • FileOutputStream(File file)
  • ObjectOutputStream: Writes primitive data types and java objects to an OuputStream.
    • ObjectOutputStream(OutputStream out)
    • writeObject(Object obj)
    • writeInt(int val)
  • PipedOutputStream: Sending data to pipe.
  • FilterOutputStream: Sit on top of an already existing output stream. It transforming the data or providing additional functionality.
    • BufferedOutputStream: Writes data to buffer array. Avoid write data to stream every time.
      • BufferedOutputStream(OutputStream out)
    • DataOutputStream: Write primitive Java data types to output stream.
      • writeChars(String s)
      • writeInt(int val)
    • PrintStream: Print various values to output stream.
      • println(String s)
      • write(int b)

Notes

if not implements Serializable interface, calling writeObject() will throw java.io.NotSerializableException

Reader

Reader Methods

  • abstract void close()
  • void mark(int readAheadLimit)
  • boolean markSupported()
  • int read()
  • int read(char[] cbuf)
  • abstract int read(char[] cbuf, int off, int len)
  • int read(CharBuffer target)
    • java.nio.CharBuffer target is using for read characters.
  • boolean ready(): return whether this stream is ready to be read. It’s same with InputStream.available()
  • void reset()
  • long skip(long n)

What does Reader actually do?

Read one character to return or read some characters into char array from somewhere, e.g char array, file, pipe.

Reader Types

Description like InputStream

  • BufferedReader
    • BufferedReader(Reader in)
    • readLine()
  • LineNumberReader
    • LineNumberReader(Reader in)
    • readLine()
    • getLineNumber()
  • CharArrayReader
    • CharArrayReader(char c[])
  • Abstract FilterReader
  • PushbackReader
    • PushbackReader(Reader in)
    • unread(int val)
  • InputStreamReader
    • InputStreamReader(InputStream in)
  • FileReader
    • FileReader(File file)
  • PipedReader
    • connect(PipedWriter src)
  • StringReader
    • StringReader(String s)

Writer

Writer Methods

  • Writer append(char c)
  • Writer append(CharSequence csq)
    • Same with write(String str), but it can chained call, and String parameter can be null.
  • Writer append(CharSequence csq, int start, int end)
  • abstract void close()
  • abstract void flush()
  • void write(char[] cbuf)
  • abstract void write(char cbuf, int offset, int len)
  • void write(int c)
  • void write(String str)
  • void write(String str, int offset, int len)

What does Writer actually do?

Writes one character or write some characters to somewhere, e.g char array, file, pipe.

Writer Types

  • BufferedWriter
    • BufferedWriter(Writer out)
    • void newLine()
  • OutputStreamWriter
    • OutputStreamWriter(OutputStream out, Charset cs)
  • FileWriter
    • FileWriter(File file)
  • StringWriter
    • StringBuffer getBuffer()
  • CharArrayWriter
    • String toString()
    • char[] toCharArray()
  • abstract FilterWriter
  • PipedWriter
  • PrintWriter
    • PrintWriter(OutputStream out)
    • PrintWriter(Writer out)
    • void println(String s)

Standard IO Streams

java.lang.System

  • PrintStream out
  • InputStream in
  • PrintStream err

Interfaces

java.lang.AutoClosable: Auto close resources.

  • void close(): This method is invoked automatically on objects managed by the try-with-resources statement.

Closeable: resources can be closed.

  • void close()

DataInput: provide reading data in any of the Java primitive types.

  • boolean readBoolean()
  • int readInt()

DataOutput: provide writing data in any of the Java primitive types.

  • void writeBoolean(int b)
  • void writeInt(int val)

Externalizable: extends Serializable interface. It using for object serialization.

  • void readExternal(ObjectInput in)
  • void writeExternal(ObjectOutput out)

FileFilter: It is a functional interface and can be used for a lambda expression or method reference. Filter files.

  • boolean accpet(File pathname)

FileNameFilter: It is a functional interface and can be used for a lambda expression or method reference. Filter file names.

  • boolean accept(File dir, String name)

Flushable: data can be flushed.

  • void flush(): Flushes buffered output to the underlying stream.

ObjectInput: extends DataInput interface. Allows reading object.

  • Object readObject()

ObjectInputValidation: Callback interface to allow validation of objects within a graph. It has no implemented subclasses.

ObjectOutput: extends DataOutput interface. Allows writing object.

  • void writeObject(Object obj)

ObjectStreamConstants: Constants written into the Object Serialization Stream.

Serializable: Allows class serializability. It has no methods.

File

File

  • Fields
    • static String pathSeparator: system dependent path separator, e.g ;.
    • static String separator: system dependent name separator, e.g /.
  • Constructors
    • File(File parent, String child), File(String pathname), File(String parent, Sring child), File(URI uri)
  • File Permission
    • boolean canExecute(), boolean canRead(), boolean canWrite()
    • boolean setExecutable(boolean executable), boolean setWritable(boolean writable)
  • File Handling
    • static File createNewFile() , static File createTempFile(String prefix, String suffix, File directory), boolean mkdir(), boolean mkdirs(), renameTo(File dest)
    • boolea delete(), boolean deleteOnExit()
    • int compareTo(File pathname): compares two abstract pathnames lexicographically.
    • String[] list(), String[] list(FilenameFilter filter), File[] listRoots()
    • Path toPath(), URI toURI(), URL toURL()
  • File Information
    • boolean exists()
    • isAbsolute(), isDirectory(), isFile(), isHidden()
    • String getAbsolutePath(), String getCanonicalPath(), String getName(), File getParentFile(), String getPath()
    • long getFreeSpace(), long getTotalSpace(), long getUsableSpace(), long lastModified(), long length()

FileDescriptor

Whenever a file is opened, the operating system creates an entry to represent this file and stores its information. Each entry is represented by an integer value and this entry is termed as file descriptor. [2]

Basically, Java class FileDescriptor provides a handle to the underlying machine-specific structure representing an open file, an open socket, or another source or sink of bytes. [2]

The applications should not create FileDescriptor objects, they are mainly used in creation of FileInputStream or FileOutputStream objects to contain it. [2]

File Paths

Get file, absolute filepath and inputStream in classpath, e.g src/resources/test.txt

You can get InputStream by using ClassLoader.getResourceAsStream() method.

InputStream in = getClass().getClassLoader().getResourceAsStream("test.txt");

You can get file object and file path by using ClassLoader get URL, then get them.

URL url = getClass().getClassLoader().getResource("test.txt");
File file = new File(url.getFile());
String filepath = file.getAbsolutePath();

Difference between getResource() methods:

getClass().getClassLoader().getResource("test.txt") //relative path
getClass().getResource("/test.txt")); //note the slash at the beginning

Get file, absolute file path and inputStream in JAR, e.g src/resources/test.txt

This is deliberate. The contents of the “file” may not be available as a file. Remember you are dealing with classes and resources that may be part of a JAR file or other kind of resource. The classloader does not have to provide a file handle to the resource, for example the jar file may not have been expanded into individual files in the file system. [3]

Anything you can do by getting a java.io.File could be done by copying the stream out into a temporary file and doing the same, if a java.io.File is absolutely necessary. [3]

Result: You can get input stream and File by ClassLoader, but you can’t get right usable absolute file path. If you want get a usable absolute path in JAR, you can copy resource file stream to create a new temporary file, then get absolute file path of the temporary file.

File file = null;
String resource = "/test.txt";
URL res = getClass().getResource(resource);
if (res.getProtocol().equals("jar")) {
try {
InputStream input = getClass().getResourceAsStream(resource);
file = File.createTempFile("tempfile", ".tmp");
OutputStream out = new FileOutputStream(file);
int read;
byte[] bytes = new byte[1024];

while ((read = input.read(bytes)) != -1) {
out.write(bytes, 0, read);
}
out.close();
file.deleteOnExit();
} catch (IOException ex) {
Exceptions.printStackTrace(ex);
}
} else {
//this will probably work in your IDE, but not from a JAR
file = new File(res.getFile());
}

System.out.println(file.getAbsolutePath());

Summary

IO Streams Functionality

InputStream or Reader it read data from somewhere, e.g byte or char array, file, pipe. It can read single-unit data from the stream to return or read multiple units data from the stream to store into an array every time. read() methods can implement by java code or implements by calling native method. Filter streams use the Decorator Pattern to add additional functionality (e.g unread) that implements by internal buffer array to other stream objects.

OutputStream or Writer it write data to somewhere, e.g byte or char array, file, pipe.

IO Stream Types

  • Byte or Char Array
  • File
  • Object
  • Piped
  • Sequence:
  • Filter
    • Buffered: Operating with buffer array.
    • DataInput, DataOutput: Operating with primitive Java types
    • Pushback: Add unread functionality.
    • Print:
    • LineNumber

Others

class java.nio.CharBuffer using in method int read(CharBuffer target)

interface java.lang.CharSequence using in method Writer append(CharSequence csq)

Questions

InputStream vs Reader? What the real difference between them? Who has more efficient?

Stream is operating bytes, and reader or writer is operating characters. Writing streams of raw bytes such as image data using OutputStream. Writing streams of characters using Writer.

CharSequence vs String

A CharSequence is a readable sequence of char values. You can call chars() method get the InputStream.

References

[1] Java SE 8 API Document

[2] Java File Descriptor Example

[3] How to get a path to a resource in a Java JAR file - Stack Overflow

Content

  • Introduction to Servlet
  • The Servlet Interface
  • The Request
  • Servlet Context
  • The Response
  • Filtering
  • Sessions
  • Dispatching Requests
  • Web Applications
  • Application Lifecycle Events
  • Mapping Requests to Servlets
  • Security

Introduction to Servlet

What is a Servlet

Servlet 是基于 Java 的 Web component,它是被 servlet container 管理的,它可以用来生成动态的内容。Servlet 是平台独立的 java class,可以运行在支持 servlet container 的 Web Server 中。Servlet 通过 Servlet 容器实现的 request/response 范例与 Web 客户端进行交互。

What is a Servlet Container

Servlet container (有时也叫做 servlet engine)它是 Web server 或 application server 的一部分,它提供了发送 request 和 response 的网络服务,解析基于 MIME 的 requests,以及格式化基于 MIME 的 responses。Servlet Container 还管理 Servlets 的整个 lifecycle。

Servlet container 可以内置到 Web server 中,也可以通过 Web server 的扩展 API 作为附加组件安装到 Web server 中。Servlet container 也可以内置或安装在 application servers。

所有 servlet containers 必须支持 HTTP 作为请求和响应的协议。其它基于 request/response 的协议也可能支持如 HTTPS。

Why do we need Servlet

实现基于 HTTP 协议的 Java Web 应用程序,我们需要使用 servlet 技术来生成动态的响应内容。

Why do we need Servlet Container

Servlet 只是一个 Java 类,它接收 request 对象和响应 response 对象。然而,一个应用中有很多 servlets,我们需要一个容器来管理这些 servlets 的创建和销毁,以及去解析和生成网络协议的报文。

How they work

  • Client 发出一个 HTTP 请求访问 Web server。
  • Web server 接收到请求,将请求交给 servlet container。servlet container 可以在与主机 Web server 相同的进程中运行,可以在同一主机上的不同进程中运行,也可以在与其处理请求的 Web server 不同的主机上运行。
  • Servlet container 根据配置决定调用哪个 servlet,并将表示 request 和 response 的对象传递给 servlet。
  • Servlet 利用 request 对象处理逻辑,生成响应的数据。
  • 一旦 servlet 处理完了请求,servlet container 确保 response 正确地 flushed,并且将控制权返回给 Web server。

Servlet History

Servlet versions history

Servlet API version Released Specification Platform Important Changes
Servlet 4.0 Sep 2017 JSR 369 Java EE 8 HTTP/2
Servlet 3.1 May 2013 JSR 340 Java EE 7 Non-blocking I/O, HTTP protocol upgrade mechanism (WebSocket)[14]
Servlet 3.0 December 2009 JSR 315 Java EE 6, Java SE 6 Pluggability, Ease of development, Async Servlet, Security, File Uploading
Servlet 2.5 September 2005 JSR 154 Java EE 5, Java SE 5 Requires Java SE 5, supports annotation
Servlet 2.4 November 2003 JSR 154 J2EE 1.4, J2SE 1.3 web.xml uses XML Schema
Servlet 2.3 August 2001 JSR 53 J2EE 1.3, J2SE 1.2 Addition of Filter
Servlet 2.2 August 1999 JSR 902, JSR 903 J2EE 1.2, J2SE 1.2 Becomes part of J2EE, introduced independent web applications in .war files
Servlet 2.1 November 1998 2.1a Unspecified First official specification, added RequestDispatcher, ServletContext
Servlet 2.0 December 1997 N/A JDK 1.1 Part of April 1998 Java Servlet Development Kit 2.0[15]
Servlet 1.0 December 1996 N/A Part of June 1997 Java Servlet Development Kit (JSDK) 1.0[9]

Servlet versions and apache tomcat versions

Servlet Spec JSP Spec EL Spec WebSocket Spec JASPIC Spec Apache Tomcat Version Latest Released Version Supported Java Versions
4.0 2.3 3.0 1.1 1.1 9.0.x 9.0.31 8 and later
3.1 2.3 3.0 1.1 1.1 8.5.x 8.5.51 7 and later
3.1 2.3 3.0 1.1 N/A 8.0.x (superseded) 8.0.53 (superseded) 7 and later
3.0 2.2 2.2 1.1 N/A 7.0.x 7.0.100 6 and later (7 and later for WebSocket)
2.5 2.1 2.1 N/A N/A 6.0.x (archived) 6.0.53 (archived) 5 and later
2.4 2.0 N/A N/A N/A 5.5.x (archived) 5.5.36 (archived) 1.4 and later
2.3 1.2 N/A N/A N/A 4.1.x (archived) 4.1.40 (archived) 1.3 and later
2.2 1.1 N/A N/A N/A 3.3.x (archived) 3.3.2 (archived) 1.1 and later

Servlet Example: HelloWorld

1.Generating maven project

$ mvn archetype:generate -DgroupId=com.taogen.example -DartifactId=servlet-helloworld -DarchetypeArtifactId=maven-archetype-webapp -DinteractiveMode=false

2.Add servlet-api dependencies in pom.xml

<dependency>
<groupId>javax.servlet</groupId>
<artifactId>servlet-api</artifactId>
<version>2.5</version>
<scope>provided</scope>
</dependency>

3.Add HelloWroldServlet.java

package com.taogen.example;

import java.io.*;
import java.util.Date;
import javax.servlet.*;
import javax.servlet.http.*;

// Extend HttpServlet class
public class HelloWorldServlet extends HttpServlet {

private String message;

public void init() throws ServletException {
// Do required initialization
message = "Hello World! ";
}

public void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {

// Set response content type
response.setContentType("text/html");

// Actual logic goes here.
PrintWriter out = response.getWriter();
out.println("<h3>" + this.message + "</h3>");
}

public void destroy() {
// do nothing.
}
}

4.Configuring servlet in web.xml

<servlet>
<servlet-name>HelloWorld</servlet-name>
<servlet-class>com.taogen.example.HelloWorldServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>HelloWorld</servlet-name>
<url-pattern>/HelloWorld</url-pattern>
</servlet-mapping>

5.Running project

Package project by mvn package, move war file to Apache Tomcat /webapps directory, starting Apache Tomcat server.

Visiting the URL http://localhost:8080/{your-servlet-context}/HelloWrold

The Servlet Interface

Servlet interface 是 Java Servlet API 的核心抽象。所有的 servlets 直接或间接的实现了这个接口。Java Servlet API 中有两个实现了 Servlet interface 的类 GenericServletHttpServlet。一般开发人员通过 extends HttpServlet 来实现它们 Servlets。

Request Handing Methods

基础的 Servlet interface 定义了 service 方法来处理客户端请求。对于 servlet container 路由到 servlet 实例的每个请求,都会调用此方法。

处理 Web 应用程序的并发请求,通常需要 Web Developer 设计可以处理多个线程执行 service 方法的servlets。

通常 Web container 通过在不同的线程上并发执行 service 方法来处理对同一 servlet 的并发请求。

HTTP Specific Request Handling Methods

Servlet 接口的抽象子类 HttpServlet 添加了额外的方法来帮助处理基于 HTTP 的请求。这些方法是:

  • doGet
  • doPost
  • doPut
  • doDelete
  • doHead
  • doOptions
  • doTrace

Number of Instances

在非分布式环境,servlet container 中每个 servlet 仅能有一个实例。如果 servlet 实现 SingleThreadModel 接口,servlet container 可能会实例化多个 servlet 实例。

SingleThreadModel 保证仅有一个线程执行 servlet 实例的 service 方法,它避免并发地访问一个 servlet 实例的 service 方法,然而,我们可以通过其它方法达到这个目的,SingleThreadModel 是不推荐使用的。

Servlet Life Cycle

Servlet 是通过定义明确的生命周期进行管理的,该生命周期定义了如何加载和实例化,如何初始化,如何处理来时客户端的请求,和如何退出服务。API 中的生命周期由 javax.servlet.Servlet 接口的 initservicedestroy 方法表示,所有的 servlet 必须直接或者通过 GenericServlet 或 HttpServlet 抽象类间接地实现这个接口。

Loading and Instantiation

Servlet container 负责加载和实例化 servlet。加载和实例化可以在 container 启动的时候,或者延迟到 container 需要 servlet 来处理请求。Servlet 默认是懒加载。

Initialization

在 servlet 对象实例化后,在它能处理客户端请求之前,container 必须初始化 servlet。初始化方便 servlet 可以读取持久性配置数据,初始化昂贵的资源以及执行其它一次性的活动。container 通过实现 ServletConfig 接口唯一(每个 Servlet 声明)对象调用 Servlet 接口的 init 方法开初始化 servlet。配置对象允许 servlet 从 Web 应用配置信息中访问 name-value 初始化参数。

Request Handling

在一个 servlet 正确初始化后,servlet container 可能使用它来处理客户端的请求。请求通过 ServletRequest 对象来表示,servlet 通过调用 ServletResponse 对象提供的方法来响应请求。这两个对象作为参数传递给Servlet 接口的 service 方法。

对于 HTTP 请求,container 提供的对象类型是 HttpServletRquest 和 HttpServletResponse。

Multithreading Issues

Servlet container 可能通过 servlet 的 service 方法发送并发请求。为了处理这些请求,Servlet 开发者必须为 service 方法中的多线程并发处理做好充分的准备。

Servlet 的 service 方法不建议使用 synchronized 关键字,因为那将使得 container 不能使用 instance pool,而是顺序执行请求,这会严重影响 servlet 的性能。

Exception During Request Handling

Servlet 在请求 service 时,可能 throw ServletException 或 UnavailableException,其中 ServletException 表示在处理请求过程中由错误发生,并且 container 应该采取适当措施来清理请求。UnavailableException 表示这个 servlet 临时或永久地不能处理请求,container 必须从服务中移除这个 servlet,调用它的 destroy 方法,并且释放 servlet 实例。

Thread Safety

request 和 response 对象的实现没有保证线程安全,这意味着它们应该在请求处理线程范围内使用。request 和 response 对象的引用不应该执行在其它线程。

End of Service

servlet container 不需要在任何特定时间内都保持 servlet 的加载。Servlet 实例在 servlet 容器中的生命周期可能是几天,几个月或者几年。

当 servlet container 决定把一个 servlet 从 service 中移除,它调用 Servlet 接口的 destroy 方法去允许这个 servlet 去释放所有它使用的和保存的任何持久状态的资源。

在 servlet container 调用destroy 方法,它必须允许任何正在执行这个 servlet 的 service 方法的线程执行完毕,或者执行超时。

一旦一个 servlet 实例的 destroy 方法被调用了,这个实例将不再接收请求。如果 container 需要这个servlet,它必须重新创建一个新的实例。在 destroy 方法完成后,servlet container 必须释放 servlet 实例,让它能够进行垃圾收集。

The Request

Request object 封装了来自客户端请求的所有信息。对于 HTTP 协议,这个信息是从客户端传递到服务器的 HTTP 请求的 headers 和 message body。

HTTP Protocol Parameters

Parameters 是由一组 name-value pair 存储的。ServletRequest 接口中获取参数的方法有:

  • getParameter
  • getParameterNames
  • getParameterValues
  • getParameterMap

来自 query string 和 post body 的数据是聚合在 request parameter set 中的。query string 数据表示在post body 数据之前。如:一个请求的 query string 是 http://xxx?a=hello,它的 post body 是 a=goodbye&a=world,参数集的结果将是 a=(hello, goodbye, world)。

Post 表单数据转换为 parameter set 的条件:

  1. 它是一个 HTTP or HTTPS 请求。
  2. HTTP method 是 POST。
  3. content type 是 application/x-www-form-urlencoded
  4. Servlet 已对请求对象上的任何 getParameter 方法族进行了初始调用。

如果上面的条件没有全部满足,post 请求的 form data 不会包含在 parameter set中,但 post data 依然可以通过 request object 的 input stream 中获取。如果所有条件都满足,post form data 将不再能从 request object 的 input stream 中读取。parameter 只能表现为 form data 和 input stream 两种方式之一。

Attributes

Attributes 是关联一个请求的对象。Container 可以设置 attributes 以表示无法通过 API 表示的信息,或者可以由 servlet 设置 attributes 以将信息传递给另一个 servlet (通过 RequestDispatcher)。ServletRequest 接口操作 attributes 的方法有:

  • getAttribute
  • getAttributeNames
  • setAttribute

一个 attribute value 只能与一个 attribute name 关联。

以 “java.” 和 “javax.” 为前缀的 attributes 是 servlet specification 定义的。

Headers

HttpServletRequest 接口获取 header 的方法有:

  • getHeader
  • getHeaders
  • getHeaderNames

可能存在多个 headers 是相同的名称,如果有多个 header 是相同的名称,getHeader 方法返回第一个 header,getHeaders 方法返回所有 headers 的 Enumeration 对象。

Headers 可能 string 表示的 int 和 Date 数据,HttpServletRequest 接口提供了直接获取 int 和 Date 类型的 header 的方法:

  • getIntHeader
  • getDateHeader

getIntHeader 方法可能会 throw NumberFormatException,getDateHeader 方法可能 throw IllegalArgumentException。

Request Path Elements

request URI = Context Path + Servlet Path + Path Info + Query String

e.g. http://myserver.com/myproject/myservlet/remove?id=1

Configuration

  • <servlet-mapping>myservlet/*</servlet-mapping>

Result

  • Context Path: /myproject
  • Servlet Path: /myservlet
  • Path Info: /remove
  • Query String: ?id=1

Path Translation Methods

Servlet API 中允许开发者获取 Java Web 应用的文件在文件系统中的绝对文件路径。这些方法是:

  • ServletContext.getRealPath
  • HttpServletRequest.getPathTranslated

Cookies

HttpServletRquest 接口提供了 getCookies 方法去获取请求中的一组 cookies。这些 cookies 是从客户端每次发送到服务端的请求中的数据。

服务端可以添加和删除 Cookie, 以及设置 cookie 的属性,如有效期等。

Internationalization

客户端可以选择向 Web server 指示它们希望以哪种语言给出响应。客户端可以使用 header 中的 Accept-Language 属性来传达这个信息。ServletRequest 接口提供了获取客户端的偏好语言的方法:

  • getLocale
  • getLocales

getLocale 方法返回客户端最希望的语言的 locale 对象。getLocales 方法返回 locale 对象的 Enumeration,以降序的方式表示客户端所有偏好的语言。

如果客户端没有指明偏好的语言,那么 getLocale 将返回 servlet container 默认的 locale,getLocales 将返回只包含 默认 locale 的 enumeration。

Request data encoding

如果客户端没有通过 Content-Type header 指明 HTTP request 的字符编码,HttpServletRequest.getCharacterEncoding 方法将返回 null。

Container 读取 request 的数据的默认编码为 ISO-8859-1。开发者可以通过 setCharacterEncoding 方法来设置 request 的字符编码。设置 request 字符编码一定要在读取 request 数据之前,一旦数据被读取了,字符编码的设置将不会生效。

Lifetime of the Request Object

每一个 request 对象仅仅在 servlet 的 service 方法或者 filter 的 doFilter 方法范围内有效。Container 为了减少创建 request 对象的性能花费,通常会循环利用 request 对象。开发者必须注意,在非有效范围之外维持 request 对象的引用是不推荐的,它可能导致意外的结果。

Servlet Context

SevletContext interface 定义了一组方法让 servlet 与 它的 servlet container 进行交流。例如,获取一个文件的 MIME type,dispatch request,写日志,以及设置和存储所有 servlet 可以访问的属性等。

Scope of ServletContext Interface

每一个 Java 虚拟机的每一个 Web 应用程序只有一个 ServletContext 对象。

Initialization Parameters

ServletContext 接口允许 servlet 访问在 deployment descriptor 定义的 context 初始化参数的方法:

  • getInitParamenter
  • getInitParamenterNames

应用程序开发人员使用初始化参数来表示设置信息。如网站管理员的电子邮件地址,或者系统的关键配置数据。

Context Attributes

Servlet 可以通过 name 把对象属性绑定到 context 中。任何绑定到 context 中的 attribute Web 应用程序中任何其他 servlet 都可以使用。ServletContext 接口操作 attributes 的方法:

  • setAttribute
  • getAttribute
  • getAttributeNames
  • removeAttribute

Resources

ServletContext interface 提供了直接访问静态类型文档,如 HTML,GIF,和JPEG 等文件的方法:

  • getResource
  • getResourceAsStream

静态文件的 path 是以‘/’开始的 context 根目录的相对路径。上面的方法不能获取动态文件如 getResource(“/index.jsp”)

Temporary Working Directories

Servlet containers 必须为 每一个 servlet context 提供一个私有的临时目录,并且可以通过 javax.servlet.context.tempdir context attribute 来访问这个目录。

The Response

Response object 封装了 server 返回给 client 的所有信息。在 HTTP 协议中,这个信息通过 HTTP headers 或 message body 从 server 传输到 client。

Buffering

出于效率目的,允许(但不是必需)servlet container 来缓冲输出到客户端的数据。一般服务器是默认缓冲的,允许 servlet 去指定 buffering 的参数。

ServletResponse interface 允许 servlet 访问和设置 buffering 信息的方法:

  • getBufferSize
  • setBufferSize
  • isCommitted
  • reset
  • resetBuffer
  • flushBuffer

ServletResponse interface 提供这些方法去执行缓冲操作,无论 servlet 使用 ServletOutput 还是 Writer。

Headers

Servlet 可以通过 HttpServletResponse interface 的方法去设置 HTTP repsose 的 headers:

  • setHeader
  • addHeader

HttpServletResponse interface 也提供了添加具体的数据类型的 headers 的方法:

  • setIntHeader
  • setDateHeader
  • addIntHeader
  • addDateHeader

Convenience Methods

HttpServletReponse interface 中的 convenience 方法有:

  • sendRedirect
  • sendError

sendRedirect 是完整的 request URI,它包含 context path 即 /contextPath/servletPath/pathInfo,而 RequsetDispather 的 forward 和 include 是相对于 context path 路径的相对目录,即 /servletPath/pathInfo

Internationalization

Servlet 可以设置 response 的 locale 和 character encoding。

Servlet 使用 ServletResponse 的 setLocale 方法可以设置 response 的 locale。如果 response 已经 committed 则 setLocale 方法是无作用的。如果 servlet 在 response committed 之前没有设置 locale,container 的默认的 locale 用于确定 response 的 locale。

Servlet 可以使用 locale encoding mapping 来设置使用特定 locale 时也使用对应的 character encoding。

<locale-encoding-mapping-list>
<locale-encoding-mapping>
<locale>zh-CN</locale>
<encoding>UTF-8</encoding>
</locale-encoding-mapping>
</locale-encoding-mapping-list>

ServletResponse 提供了设置 character encoding 的方法:

  • setCharacterEncoding
  • setContentType

和 setLocale 方法一样 set character encoding 在 response committed 之后是无作用的。如果 servlet 在 ServletResponse 的 getWriter 方法调用之前或 response committed 之后,没有设置 character encoding,则默认使用 ISO-8859-1 编码。

Closure of Response Object

当 response 关闭时,container 必须立刻 flush 在 response buffer 中所有剩余的内容,返回给 client。

关闭 request 和 response 对象的 events:

  • servlet 的 service 方法结束时。
  • 在 response 的 setContentLength 方法指定的内容量大于零,,并已写入 response 中。
  • 调用了 sendError 方法时。
  • 调用了 sendRedirect 方法时。

Lifetime of Response Object

每个 response 对象仅仅在 servlet 的 service 方法,或者在 filter 的 doFilter 方法范围内有效。Container 通常会循环利用 response 对象来减少创建 response 对象的性能消耗。开发者必须注意在 response 对象的有效范围之外维护 response 对象的参考可能会导致意外的行为。

Filtering

Filter 是 Java Servlet 组件,它允许在接受和响应请求的过程中访问和修改请求的 header 和 payload。

What is a Filter

Filter 是一段重用的代码,是一个 java class,它可以转换 HTTP request, response, 和 header 的内容。Filter 通常不会像 servlet 那样创建 response 或 响应请求,而是修改和调整对资源的请求,并修改或调整对资源的响应。

Filter 可以作用在动态或静态的内容上。动态和静态的内容一般指的是 Web resources。

开发者使用 filter 功能的类型有:

  • 在请求调用之前访问资源。
  • 在请求调用之前处理请求。
  • 通过用自定义的 request 对象 wrapping request 来修改 request 的 headers 和 data。
  • 通过用自定义的 response 对象来修改 response 的 headers 和 data。
  • 调用资源后对其进行拦截。
  • 对一个或一组 servlet 按顺序执行多个 filter 的操作。

使用 Filter components 常见的例子:

  • Authentication filters
  • Logging and auditing filters
  • Image conversion filters
  • Data compression filters
  • Encryption filters
  • Tokenizing filters
  • Filters that trigger resource access events
  • XSL/T filters that transform XML content
  • MIME-type chain filters
  • Caching filters

Main Concepts

开发者通过 implement javax.servlet.Filter interface 创建 filter,并且提供一个无参的 constructor。Filter 在 deployment descriptor 中使用 <filter> 进行声明。一个或一组 filter 可以通过在 deployment descriptor 中定义 <filter-mapping> 元素配置它的调用。这个配置通过 servlet 的 logic name 或者 resources URL 来实现。

Filter Lifecycle

在 Web 应用程序部署之后,container 接收到请求之前,container 必须找到应用于 Web 资源的 filter 列表。container 必须确保每个 filter 实例化,并且调用 init(FilterConfig config) 方法进行初始化。

每一个 filter 只有一个实例。当 container 接收请求,它传递 ServletRequestServletResponse 参数调用第一个 filter 实例的 doFilter 方法,并且 FilterChain 对象将被用于传递请求到下一个 filter。

Wrapping Requests and Responses

Filter 概念的核心是 wrapping 请求和响应,以便它可以重写行为来执行过滤任务。开发者不仅可以重写 request 和 response 对象存在的方法,还可以提供新的 API 去满足特定的过滤任务。

当一个 filter doFilter 方法被调用时,container 必须保证传递给下一个 filter 或者目标 web resource 的 request 和 response 对象与传递给当前 doFilter 方法的 request 和 response 对象是相同的。 另外,wrapper object 相同的要求也应用于从 servlet 或 filter 到 RequestDispatcher.forward or include 的调用。

Filter Environment

Filter 的初始化参数在 deployment descriptor 中<filter> 内的 <init-params> 元素中定义。Filter 通过 FilterConfiggetInitParameter 方法访问参数。另外,FilterConfig 为了加载资源,logging,存储状态到 ServletContext attribute 中等功能,它可以访问 ServletContext 对象。

Configuration of Filters in a Web Application

For example:

<filter>
<filter-name>My Filter</filter-name>
<filter-class>com.example.MyFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>My Filter</filter-name>
<servlet-name>MyServlet1</servlet-name>
<url-pattern>/foo/*</url-pattern>
</filter-mapping>

Filters and the RequestDispatcher

Java Servlet 2.4 之后可以配置 filter 在调用 request dispatcher forward() and include() 方法时过滤。使用 <dispatcher> 元素在指出过滤请求的条件,它的值有:

  • REQUEST:表示请求直接来自 client 时过滤。
  • FORWARD:表示请求在使用 RequestDispatcher forward() 时过滤。
  • INCLUDE:请求在使用 RequestDispatcher include() 时过滤。
  • ERROR:请求转到 error resource 时过滤。
  • 以上多个值的组合

<dispatcher> 元素的使用示例,如下:

<filter-mapping>
<filter-name>Logging Filter</filter-name>
<url-pattern>/products/*</url-pattern>
<dispatcher>FORWARD</dispatcher>
<dispatcher>REQUEST</dispatcher>
</filter-mapping>

Sessions

HTTP 是一种 stateless 协议。为了构建有效的 Web 应用程序,将来自特定 client 的 request 彼此关联是必要的。Servlet specification 定义了简单的 HttpSession interface,它允许 servlet container 使用多种方法中的一种来 track 用户的 session,而无需让开发者介入方法之间的细微差别。

Session Tracking Mechanisms

Cookies

Session 通过 HTTP cookies 来跟踪会话是最常用使用的一种 session tracking mechanism 。Container 发送 cookie 给 client,client 将在接下来的访问 server 请求中发送这个 cookie,明确地将请求与会话关联。session tracking 的 cookie 的名称必须是 JSESSIONID。

URL Rewriting

URL Rewriting 是 session tracking 的另一种方法。当 client 不接受 cookie,可以使用 URL rewriting 的方式去实现 session tracking。URL rewriting 涉及到添加 session ID 到 URL path 中,container 解析该 URL,并将这个 request 与一个 session 进行关联。URL rewriting 的 URL 例子如下:

http://www.myserver.com/index.html;jsessionid=1234

只有当 client 不接受 cookie 时,URL rewriting 才会有效果。当 client 可以接受 cookie 时,URL rewiring 的 URL 不会被 container 解析,以及不会将 request 与 session 关联。

Creating Session

如果一个 session 只是一个预期的 session 而尚未建立,则认为该 session 是新的。因为 HTTP 是基于request-response 的协议,所以 HTTP session 被认为是新的,直到客户端 “join” 它为止。当 session 跟踪信息已返回到服务器以表明已建立会话时,客户端将 join 该 session。 在客户端 join session 之前,不能假定客户端的下一个请求将被识别为 session 的一部分。

Session Scope

HttpSession 对象必须是 application (or servlet context) level。不同 context 可以有相同的建立 session 的 cookie,但是 container 不能在不同的 context 之间共享这些 session 对象。

Binding Attributes into a Session

Servlet 可以通过一个 name 将一个 object attribute 绑定到 HttpSession 中。绑定在 session 中的 object 对于任何其他属于相同的 ServletContext 的 servlet 并且属于同一个会话的请求都是可以使用的 。

Session Timeouts

在 HTTP protocol 中,当 client 不在活跃时没有明确的结束信号。这意味着只有 timeout period mechanism 可以表明 client 不在活跃。

Session 的默认 timeout period 在 servlet container 中定义,它可以通过 HttpSession 接口的 getMaxInactiveInterval 方法获取。开发者可以通过 HttpSession 的 setMaxInactiveInterval 方法改变这个 timeout。这些方法的 timeout period 是以秒为单位来定义的。如果 session 的 timeout period 设置为-1,这个 session 将永远不会过期。

Important Session Semantics

Threading Issues

多个 servlet 执行请求线程可以同时访问同一个 session object。访问 session object 应该是 synchronized,开发者负责适当地 synchronizing 访问 session resources。

Client Semantics

由于 cookie 或 SSL certificates 一般是被 Web browser 控制的,它们没有与特定的浏览器窗口关联的,来自所有从 client 浏览器窗口到 servlet container 的请求可能是同一个 session。为了获得最大的可移植性,开发者应该始终假定 client 的所有窗口都参与同一个 session。

Dispatching Requests

当构建一个 Web 应用程序,将请求的处理 forward 到另一个 servlet 或 include 另一个 servlet 在 response 中的输出通常很有用。RequestDispatcher interface 提供了一种机制去实现它。

Obtaining a RequestDispatcher

获取 RequestDispatcher 接口的对象可以通过 ServletContext 接口的以下方法:

  • getReqeustDispatcher
  • getNameDispatcher

getRequestDispathcer 方法使用一个在 ServletContext 范围的描述路径的 String 参数。这个路径是相对 ServletContext 根目录的和以 “/” 开头的相对路径。这个方法使用这个 path 去查询一个 servlet。

Using a Request Dispatcher

使用 request dispatcher,servlet 调用 ReqeustDispatcher 接口的 include 或 forward 方法。这些方法的参数可以是传递给 javax.servlet 接口 service 方法的 request 和 response 参数或者是 request 和 response wrapper classes 的子类的实例。container 应该确保将 request 分发到目标 servlet 发生在和原始请求相同的 JVM 虚拟的同一线程。

The Include Method

RequestDispatcher 接口的 include 方法可能在任何时候被调用。include 方法的目标 servlet 可以访问 request 对象的所有方面,但是它使用 response 对象是有很多限制的。

include 的目标 servlet 仅仅可以把信息写入 response 对象的 ServletOutputStream 或者 Writer。它不能设置 header 或调用任何影响 header 的方法,调用 HttpServletRequest.getSession() 方法会抛出 IllegalStateException 异常。

Included Request Parameters

Servlet 使用 requestDispathcer 的 include 方法,以下 request attributes 是必须设置的:

  • javax.servlet.include.request_uri
  • javax.servlet.include.context_path
  • javax.servlet.include.servlet_path
  • javax.servlet.include.path_info
  • javax.servlet.include.query_string

The Forward Method

调用 RequestDispatcher 的 forward 方法必须仅仅在 server 没有内容提交给给 client。如果在 response buffer 中有没有提交的输出数据,在目标的 servlet 的 service 方法调用之前,这些内容必须是清空的。如果 response 已经提交了,必须抛出 IllegalStateException。

Servlet 使用 requestDispathcer 的 forward 方法,以下 request attributes 是必须设置的:

  • javax.servlet.forward.request_uri
  • javax.servlet.forward.context_path
  • javax.servlet.forward.servlet_path
  • javax.servlet.forward.path_info
  • javax.servlet.forward.query_string

Error Handling

如果 request dispatcher 的目标 servlet 抛出 runtime exception 或者 Servlet Exception or IOException checked exception,它应该传播到 calling servlet。所有其他的异常应该包装成 ServletException,并且 root cause of exception 设置为 original exception,因为不应该传播该异常。

Web Applications

Web application 是由 servlets,HTML pages,classes 和 其他资源的集合。Web 应用程序可以在不同供应商的 container 中运行。

Web Applications Within Web Servers

Web 应用程序根植于 Web server 的特定路径。例如,catalog 应用程序应该通过 http://www.mycrop.com/catalog 定位到。所有以这个前缀开头的请求都将被路由到表示 catalog 应用程序的 ServletContext。

Relationship to ServletContext

Servlet container 必须强制一个 Web 应用程序与一个 ServletContext 一一对应。ServletContext 对象为 servlet 提供了其应用程序视角。

Elements of a Web Application

一个 Web application 可能有以下内容组成:

  • Servlets
  • JSP Pages
  • Utility Classes
  • Static documents(HTML,images,sounds,etc)
  • Client side Java applets,beans,and classes
  • Descriptive meta information that ties all of the above elements together

Directory Structure

Web 应用程序是作为结构化目录的层次结构存在的。在应用程序的层次结构中有一个特殊的目录为 WEB-INF,这个目录包含与应用程序相关的不在应用程序的文档 root 目录中的文件。WEB-INF 节点不是应用程序的公开文档树中的一部分。容器不能将 WEB-INF 目录中包含的文件直接提供给 client。WEB-INF 目录的内容是对 ServletContext getResource 和 getResourceAsStream 方法是可见的,以及使用 RequestDispatcher 调用。如果应用开发者需要访问如应用的配置信息文件,但不希望把它直接暴露给 Client,可以把它们放到 WEB-INF 目录下。任何访问 WEB-INF 目录资源的请求将返回 404。WEB-INF 目录的内容有:

  • /WEB-INF/web.xml: deployment descriptor
  • /WEB-INF/classes/: servlet 和 uitlity classes.
  • /WEB-INF/lib/*.jar: Java Archive files. These files contains servlets, beans, and other utility classes.

Web Application Archive File

可以使用标准的 Java archive tools 将 Web 应用程序打包并签名为 Web ARchive format (WAR) file。WAR 文件中的 META-INF 目录包含了对 Java archive tools 有用的信息。这个目录不能直接被 client 访问。

Web Application Deployment Descriptor

Web 应用程序 deployment descriptor 包含一下配置和部署信息的类型:

  • ServletContext Init Paramenters
  • Session Configuration
  • Servlet/JSP Definitions
  • Servlet/JSP Mappings
  • Welcome File list
  • Error Pages
  • Security

Dependencies On Extensions

当大量的应用程序使用相同的 code 或 resources,它们通常将作为库文件安装在 servlet container 中。这些文件一般是常用的或标准的 API,使用它们不会牺牲可移植性。Servlet container 必须为这些 libraries 提供一个目录。位于这个目录的文件必须可用被所有 Web 应用程序使用。该目录位置是特定于 container 的。

应用程序开发者依赖的扩展必须在 WAR 文件中提供 META-INF/MANIFEST.MF entry,其中列出了 WAR 需要的所有扩展。Manifest entry 的格式应该遵循标准的 JAR manifest 格式。

Web container 必须也能够识别在 WAR 中 WEB-INF/lib 条目下的任何 library JARs 的 manifest entry 中表达的声明的依赖。

Error Handling

当异常发生在 servlet 或 JSP page,下面的属性必须设置的:

  • javax.servlet.error.status_code (java.lang.Integer)
  • javax.servlet.error.exception_type (java.lang.Class)
  • javax.servlet.error.message (java.lang.String)
  • javax.servlet.error.exception (java.lang.Throwable)
  • javax.servlet.error.request_uri (java.lang.String)
  • javax.servlet.error.servlet_name (java.lang.String)

这些属性允许 servlet 去生成特定的内容。

Error Pages

为了允许开发者可以去自定义 servlet 出现 error 时返回给 Web client 的内容,deployment descriptor 定义了 error page 描述的列表。这个语法允许当 servlet 或 filter 调用 HttpResponse.sendError 返回特定的 status code,或者 servlet 产生的 exception 或 error 传播到了 container 时,container 将返回资源配置。

如果在 response 上调用了 sendError 方法,container 查询使用 status-code 语法声明的 Web 应用程序的 error page 列表,并尝试进行匹配。如果匹配成功,container 返回通过 location entry 指定的资源。error page 声明的例子:

<error-page>
<error-code>404</error-code>
<!-- /src/main/webapp/error-404.html-->
<location>/error-404.html</location>
</error-page>
<error-page>
<exception-type>java.lang.Exception</exception-type>
<location>/errorHandler</location>
</error-page>

Web Application Deployment

当一个 Web 应用程序部署到 container 中,在 Web 应用程序开始处理 client 请求之前,接下来的步骤是必须执行的:

  • 实例化每一个在 deployment descriptor 中 <listener> 元素定义的 listener 的实例。
  • 为了实例化实现了 ServletContextListener 的 Listener 实例,调用它的 contextInitialized() 方法。
  • 实例化每个在 deployment descript 中的 <filter> 元素定义的 filter 的实例,并且调用它的 init() 方法进行初始化。
  • 实例化每个包含 <load-on-startup><servlet> 元素定义的 servlet 实例,并且调用它的 init() 方法进行初始化。

Inclusion of a web.xml Deployment Descriptor

如果 Web 应用程序不包含任何 servlet,filter,或 listener 组件,这个应用程序不需要包含 web.xml。换句话说,一个仅仅包含静态文件或 JSP page 的Web 应用程序不需要出现 web.xml。

Application Lifecycle Events

应用程序 event 功能使开发者能够更好地控制 ServletContext,HttpSession 和 ServletReqeust 的生命周期,实现更好的代码分解,并提高管理 Web 应用程序使用的资源的效率。

Event Listener

应用程序的 event listener 是实现一个或多个 servlet event listener 接口的 class。在部署 Web 应用程序时,将实例化它们并将其注册在 Web container 中。

Servlet event listener 支持 ServletContext,HttpSession 和 ServletRequest 对象中状态改变的事件通知。每种事件类型有多种 listener class。开发者可以指定 container 对每种事件类型调用 listener bean 的顺序。

Event Types and Listener Interfaces

  • ServletContextListener
  • ServletContextAttributeListener
  • HttpSessionListener
  • HttpSessionAttributeListener
  • HttpSessionActivationListener
  • HttpSessionBindingListener
  • ServletRequestListener
  • ServletRequestAttributeListener

Deployment Descriptor Example

<web-app>
<display-name>MyListeningApplication</display-name>
<listener>
<listener-class>com.acme.MyConnectionManager</listener-class>
</listener>
<listener>
<listener-class>com.acme.MyLoggingModule</listener-class>
</listener>
<servlet>
<display-name>RegistrationServlet</display-name>
...etc
</servlet>
</web-app>

Mapping Requests to Servlets

Use of URL Paths

URL path 映射规则使用下面的顺序,当成功匹配后不再继续往下匹配:

  • Container 尝试查找请求路径与 servlet path 的精准匹配。
  • Container 尝试循环的匹配 longest path-prefix。这是通过使用 '/' 字符作为路径分隔符来一次降低目录树的路径来完成的。
  • 如果 URL path 最后部分包含扩展名,如 .jsp,servlet container 将尝试匹配处理该扩展名请求的 servlet。
  • 如果上面的三个规则都没有匹配到 servlet,container 将尝试提供适合于所请求资源的内容。如果应用程序定义了默认的 servlet,则将使用它。

Specification of Mappings

在 Web 应用程序的 deployment descriptor 中,使用以下语法去定义 mappings:

  • / 字符开头,以 /* 字符结尾的字符串。
  • *. 开头作为扩展映射的字符串
  • 仅包含 / 字符表明应用程序的默认 servlet。
  • 其它仅仅精准匹配的字符串。

Mapping Set Example

  • /foo/bar/*
  • /baz/*
  • /catalog
  • *.bop

Security

Servlet 的 deployment descriptor 中的声明可以设置应用程序的安全。Web 应用程序包含许多用户可以访问的资源。这些资源通常是暴露在不受保护的开放网络,例如 Internet 环境中,大量的 Web 应用程序有安全性需求。Servlet container 具有满足这些要求的机制和基础结构,它们具有一下这些特征:

  • Authentication: 通信实体互相证明其代表授权访问的特定身份。
  • Access control for resources: 与资源进行交互的手段仅限于用户或程序的集合,以加强完整性,机密性或可用性约束。
  • Data Integrity: 信息在传输的过程中不能被第三方修改。
  • Confidentiality or Data Privacy: 信息仅仅对授权访问的用户可用。

Implementing Security in Servlet

在 Web 应用程序的 Deployment Descriptor 中配置 security:

  • <security-role>: Defining roles.
  • <login-config>: Defining how to authenticate user. e.g. login by username and password in login page.
  • <security-constraint>: Defining constrained resources URI, HTTP methods, constraint role, data constraint type.

在 Apache Tomcat server 的 conf/tomcat-user.xml 文件中配置合法的角色、用户和密码。

  • <role> and <user>

使用基于自定义表单或者默认的弹框方式进行安全认证。

References

[1] Java Servlet 2.5 Specification

[2] Java servlet - Wikipedia

本篇是将介绍如何更好地编程,如何编写整洁的代码,以及如何将 bad code 转换为 good code。

Clean Code

What is Clean Code

clean code 应该具有以下特点:

  • 整洁性、可读性。1)简单明了,清晰地表达代码的意图。2)便于阅读,有意义的命名。3)统一的良好代码格式。
  • 可维护性、可扩展性。1)单一职责,每个类和方法做好一件事。2)开闭原则。3)没有重复。4)最小化依赖。
  • 健壮性。1)错误处理。2)单元测试完全覆盖,无潜在 bug。
  • 高效性。性能尽可能最优。

Why do We Need Clean Code

代码是需求的最终表达形式,实现一个软件我们需要编写正确、整洁的代码,使其高效地在机器上运行,以及便于扩展和维护。

编写 bad code 可能会导致严重的后果。bad code 会使得团队的生产力持续降低。bad code 可能导致软件无法维护,以及存在大量 bug,最终导致软件无法正常运行。最后,不得不重新设计代码,建立一个新的团队重构旧的系统,以及跟进旧系统的改变。

产生 bad code 的原因

产生 bad code 的原因有很多,如:1)需求的改变。2)时间规划太紧,没有足够的时间去做得更好。3)对一个程序很疲倦,想要早点结束。4)手上堆积了很多其它的事,想要赶紧做完它,然后做其它事情。5)管理者的管理不当。…

作为程序开发者,我们不能一味地抱怨外部原因,我们更应该反思我们自己,我们能否做得更好。1)当软件的时间规划和安排不合理时,我们应该及时的反馈我们的想法。大部分管理者想要看到事实,以及想要 good code。2)在开发过程中,我们应该保持专业的态度,持续保证代码的整洁,就像医生做手术前要洗手一样,多花一点时间保持代码的整洁,让保证代码不会变成 bad code。唯一保证 deadline 的方法就是在任何时候尽可能地保持代码地整洁,为了加快速度编写混乱的代码最终会拖垮你的进度。

Meaningful Names

命名在软件开发中无处不在。我们需要为变量、方法、参数、类、包和项目文件目录等命名。我们需要大量的命名,因此我们应该把它做得更好。这一部分,我们将介绍好的命名规则,以及尽力避免的不好的命名规则。

Rules for Creating Good Names

选择一个好的名称是需要时间的,但它节省的时间大于它的花费。持续关注代码中名称,当你发现更好的名称时立刻改变它。

Use Intention-Revealing Names

一个好的名称应该告诉我们它为什么存在,它能做什么,它如何使用。如果一个名称需要注释说明,然么这个名称没有展现它的意图。使用揭示意图的名称更容易理解和改变代码。示例如下:

错误写法

int d; // elapsed time in days

正确写法

int elapsedTimeInDays;
int daysSinceCretation;

揭示意图常用的方法:

  • 使用揭示意图的名称。
  • 使用静态变量代替直接量。
  • 使用对象封装一组数据。

Make Meaningful Distinction

如果名称必须不同,那么它们一定存在某些不同。有意义的区分不同的名称,可以让读者清晰地知道它们之间的不同。

常见无意义区分:

  • 使用数字序列命令 a1, a2, .. aN。如 copyChars(char a1[], char a2[]) => copyChars(char source[], char destination[])
  • 使用同义词。如 1)info 和 data,ProductInfo 和 Productdata。2)a, an, the,zork 和 theZork。

Use Pronounceable Names

我们大脑更习惯处理可发音的语言。可发音的名称更容易与他人交流和讨论。命名时尽量使用完整的单词的组合。示例如下:

错误的写法

class DtaRcrd102{
private Date genymdhms;
private Date modymdhms;
private final String pszqint = "102";
}

正确的写法

class Customer {
private Date generationTimestamp;
private Date modificationTimestamp;
private final String recordId = "102";
}

Use Searchable Names

单字母的名称和数字常量等名称很难在文本中定位。尽量不要使用单字母命名和直接使用常量。

Class Names

Classes 和 objects 的名称应该为名词或名词短语。如 Customer,WikiPage,Account,AddressParser 等。

Method Names

方法名称应该为动词或者动词短语。如 postPayment,deletePage,save 等。

Pick One Word per Concept

为一个抽象概念选择一个词,并且坚持使用它。例如,fetch,retrieve,get 等相似含义的名称用在不同的类中功能相似的方法中,会使人感到困惑。类似还有 controller,manager 和 driver。同一个概念应该坚持使用同一个单词。

Use Solution Domain Names and Problem Domain Names

阅读代码的人一定是程序员,我们应该坚持使用计算机术语。计算机术语大部分程序员都比较熟悉,遇到不清楚的术语,可以在网上快速查询得到结果,不必与作者沟通。

Add Meaningful Context

增加有意义的 Context 让读者能清晰直观地知道这个变量是某个类的一部分。

如 firstName,lastName,street,houseNumber,city,state,zipcode 等变量是 Address 类中的成员。其中 state 不能直观的看出它是 adderss 的一部分。我们可以添加前缀来增加 context。state => addressState。

Avoid Creating Bad Names

Avoid Disinformation

我们应该避免留下误解代码含义的错误线索。

  • 避免单词缩写。如, hypotenuse 缩写成 hp。
  • 集合对象不确定数据结构时,应避免添加固定的类型后缀。如,accountList => accountGroup, bunchOfAccounts, or accounts.

Avoid Encodings

编码的名称增加解码的负担。编码名称一般很少是可发音的,以及它容易误解类型。尽量避免使用编码的名称。

常见的编码命名方式:

  • 匈牙利表示法(Hungarian Notation)变量名以一个或多个小写字母开头,代表变量的类型。后面附以变量的名字。这种方案允许每个变量都附有表示变量类型的信息。如 int[] aNamesboolean bDeleted
  • 成员前缀 m_。
  • 接口和实现。使用 Ixxx 表示接口,如 IShapeFactory。

Avoid Mental Mapping

避免思维映射,即避免使读者将名称翻译为它们已经知道的概念。如设计模式名称、算法名称等等。在命名时我们使用问题领域术语和解决方案领域术语。

Don’t Be Cute

如果名称太机灵,它们将仅能被作者分享过它的含义的人记住。如 HolyHandGrenade 实际上表示 DeleteItems,whack() 实际表示 kill(),eatMyShorts() 表示 abort()。 尽量避免这种命名方式。

Don’t Pun

避免使用相同的词表示两个目的。使用相同的术语表示两个不同的想法是一语双关的,让人不易理解,产生误解。如,一个 add 方法用于将两个值相加或连接。然而,另一个 add 方法则表示将元素添加到集合对象中。我们不应该一词双关,我们可以使用 insert 或 append 代替 add。

Don’t Add Gratuitous Context

不要增加不必要的 context。如,为某个模块的所有 classes 添加前缀标识。

有意义的命名总结

单个命名应该:揭示意图,可发音,可搜索,一致性,使用专业术语,增加有意义的 context。

多个命名之间应该:可区分。

有意义的命名的实现目标:1)整洁性、可维护性。名称揭示意图,代码解释一切,无注释。2)可读性。代码命名简单、明确,让人更轻松、更快地阅读和理解代码。

Functions

Small

function 的第一规则:它们应该非常小。

  1. Blocks and Indenting。if..else,while 等代码块应该占一行,作为方法调用。代码块作为方法调用可以有一个很好的描述名称。

  2. indent level 不应该超过一层或两层。这使得 functions 更容易阅读和理解。

Do One Thing

Functions 应该做一件事,它们应该做好,它们应该只做一件事。

如何知道是否是只做一件事:

  • function 只有一层抽象,即不包含多个不同的抽象层级。
  • function 不能被划分多个 sections。

Switch Statements

让 switch 语句只做一件事是困难的,根据它的性质,switch 语句总是做 N 件事。我们可以将 switch 的每一个分支封装成一个方法。

Use Descriptive Names

functions 的名称应该描述 function 做了什么。不要害怕使得名称很长,一个长的 descriptive name 比短的神秘的名称更好。一个长的 descriptive name 比一个长的 comment 更好。

Function Arguments

理想的 function 参数的个数是0个,其次是1个和2个,3个参数是尽量避免的。

多个参数的问题:

  • 参数需要占用额外的概念理解的精力,每次都需要解释它。
  • 参数越多越难测试。

其它参数问题

  1. Flag 参数。传递一个 boolean 参数给方法是很糟糕的,你明显的表现出这个 function 不是做一件事。
  2. 参数对象。超过两个或三个参数应该封装成一个 class。
  3. 动词和关键词。方法名应该是动词+名词对。如使用 writeField() 代替 write()
  4. 输出参数。输出参数比输入参数是更难理解的。我们既要关注输入是什么,也要关注输出是什么,它使我们花费双倍的精力。如果你的 function 必须改变某些状态,让它改变其拥有对象的状态。如 appendFooter(String report) => report.appendFooter()

Have No Side Effects

你的 function 保证了做一件事,但可能存在其它隐藏的事情。示例如下:

public boolean checkPassword(String userName, String password){
User user = UserGateway.findByName(userName);
if (user != User.NULL){
if (user.getPassword.equals(password)){
Session.initialize();
return ture;
}
}
return false;
}

上面的 checkPassword() 方法中的 Session.initialize() 是隐藏的事情。因为初始化会话这件事不属于检查密码。可以将方法改为 checkPasswordAndInitializeSession(),其实现如下:

public boolean checkPasswordAndInitializeSession(String userName, String password){
boolean passwordRight = checkPassword(userName, password);
if (passwordRight){
Session.initialize();
return true;
}
return false;
}

Command Query Separation

Functions 应该是做某些事或者回答某些事,而不是两者都做。你的 function 应该改变一个对象的状态或者返回一个对象的一些信息。一个错误的例子如下:

public boolean set(String attribute, String value);

上面这个方法即设置了对象的属性,又返回操作是否成功和属性是否存在。这会导致出现奇怪的语句,如 if (set("username", "unclebob"))

上面的方法应该划分为两个方法,一个执行操作,一个查询状态。如下:

public void set(String attribute, String value);
public boolean attributeExists(String attribute);

Prefer Exception to Returning Error Codes

执行命令的方法返回 error codes 轻微地违反了 command query separation 原则。当你返回一个 error code,调用者必须立刻处理 error,才能继续执行其它语句,这将导致代码很多层嵌套判断。示例代码如下:

if (deletePage(page) == E_OK){
if (registry.deleteReference(page.name) == E_OK){
if (configKeys.deleteKey(page.name.makeKey()) == E_OK){
...
} else {

}
}
}

你可以使用 exception 代替返回 error codes,这样的话,不需要立即处理错误情况,代码没有太多的嵌套,是代码更整洁。代码如下:

try {
deletePage(page);
registry.deleteReference(page.name);
configKeys.deleteKey(page.name.makeKey());
}
catch (Exception e) {
logger.log(e.getMessage());
}

Extract Try/Catch Blocks

Try/catch 代码块是丑陋的,它混合了正常代码和错误处理代码。更好的办法是将 try/catch 代码块封装到一个独立的方法。上面的代码可以改为:

public void delete(Page page){
try {
deletePageAndAllReference(page);
} catch (Exception e){
logError(e);
}
}
public void deletePageAndAllReferences(Page page) throws Exception {
deletePage(page);
registry.deleteReference(page.name);
configKeys.deleteKey(page.name.makeKey());
}

private void logError(Exception e){
logger.log(e.getMessage());
}

Error Handling Is One Thing

Functions 应该做一件事,Error handling 它是一件事,所以,一个处理 errors 的 function 不应该做其它的事。即在 catch/finally 代码块后面不应有其它的语句,或一个处理 error 的 function 仅包含 try/catch/finally 代码块。

Don’t Repeat Yourself

在软件中重复是所有罪恶的根源。我们应该尽可能的消除重复的代码。

Clean functions 的总结

一个 function 应该尽量小,只做一件事,尽量少的参数,和使用描述性的名称。

如何只做一件事:只有一层抽象,不能划分为多个 sections,没有隐藏功能,命令和查询分离。

其它问题:错误处理,消除重复。

一个专业的程序员把系统当作故事来讲诉,而不是编写程序。

Comments

Don’t comment bad code–rewrite it.

注释的正确用法是为了弥补我们在代码中失败地表达自己。当你需要写注释时,你需要思考它有没有方式在代码中表达。每次你写了一个注释,你应该感到你表达能力的失败。

为什么尽量不写注释,因为程序员很少去维护注释的正确性。

  • 注释不能弥补差的代码。与其花时间去写注释解释混乱的代码,不如花时间使代码整洁。

  • 在代码中解释你自己。在很多时候可以创建一个方法来代替你注释要说明的。如下:

    // Check to see if the employee is eligible for full benefits
    if ((employee.flog & HOURLY_FLAG) && (employee.age > 65))

    代替为

    if (employee.isEligibleForFullBenefits())

Good Comments

有些注释时有必要的或有益的。始终记住真正好的注释是找到一种方式不写注释。

常见的有必要的注释

  • Legal Comments。版权和作者声明是有必要的和合理的。如 // Copyright (C) 2003,2004,2005 by Object Mentor, Inc. All rights reserved.
  • Explanation of Intent。解释为什么这么做。如 // we are greater because we are the right type.
  • Warning of Consequence。警告注释。如 // Don't run unless you, have some time to kill
  • TODO Comments。TODO 一般用于此刻没时间做或者暂时做不了的事。它解释代码的问题和未来应该做什么。如 //TODO-MdM these are not needed // We expect this to go away when we do the checkout model
  • Amplification。详述注释。用于详述某些重要的事情,其他人可能认为不重要的事。// the trim is real important. It removes the starting spaces that could cause the item to be recognized as another list.

Formatting

Code formatting 是重要的,编码风格和可读性会持续影响软件的维护性和扩展性。

Vertical Formatting

  1. 行数。一个源码文件不应该超过 500 行,一般最好 200 行左右。小文件通常比大文件更容易理解。
  2. 自顶向下阅读。最上面的方法提供高层次的概念和算法。细节应该是从上到下递增的。
  3. 垂直的间隔。不同概念的一组代码应该用空行分隔。
  4. 垂直的密度。垂直密度暗示了紧密的关系,相关性高得代码应该紧密排布在一起。
  5. 垂直的距离。紧密关联的概念应该保持垂直方向接近的。1)变量声明语句应该尽可能接近它们使用的地方。2)实例变量应该声明在 class 的顶端。3)依赖的方法。一个方法调用另一个方法,它们应该在垂直方向相互接近的,调用者应该在被调者的上面。4)概念上类似。相似的内容应该在垂直方向相互接近的。如方法的重载,相同的方法名,不同的参数的方法,它们的功能是类似的。
  6. 垂直的顺序。方法的调用依赖时指向下面方向的。

Horizontal Formatting

  1. 水平的字符数应该不超过 120 个字符。
  2. 水平的间隔和密度。操作符与操作数之间有一个空格,将它们分成左边和右边两个部分,看起来更清晰。
  3. 缩进。使用缩进来表示代码语句范围的层级结构。

Team Rules

每一个程序员都有自己最喜欢的格式规则,但是如果在一个团队中工作,我们需要按照团队的规则去编写代码。一个团队的所有开发者应该同意一个编码风格,然后每个团队成员应该使用这个风格。我们需要一个统一的编码风格。

Error Handling

错误处理时必须要做的一件事。输入可能异常,设备可能失败,总的来说,事情可能发生错误,程序员有责任确保当发生异常时系统可以正常工作。

  1. 使用异常处理而不是返回值。使用异常处理比返回错误码更整洁。返回错误码需要立刻处理,会导致多层嵌套的复杂结构。
  2. 尝试编写强制异常的测试。
  3. 使用 Unchecked Exceptions。Checked exceptions 违反了 Open/Closed 原则。如果你抛出一个 checked exception,你需要在你 catch 和 throw 之间的所有方法上声明异常。
  4. 根据调用者的需求定义异常类。我们定义异常类最重要是它们是怎么被捕获的。通常单个异常是较好的。我们可以简化 API 将多个异常包装成一个异常。
  5. 不要用异常处理 try/catch 来做业务流程处理。即 catch 代码块中不应该出现业务代码。
  6. 不要返回 Null。返回 null,给调用者造成额外的负担,null 会导致很多 null 检查代码。如果尝试返回 null 时,可以考虑抛出异常或者返回特殊对象代替。如 Collections.emptyList()
  7. 不要传递 Null 作为参数。

Unit Test

单元测试保证了代码的每一个功能都是我们所期待的结果。

Keeping Tests Clean

混乱的测试代码比没有测试更糟糕。测试代码必须随着生产代码的发展而改变,混乱的测试代码很难去改变,测试代码越来越多,越来越混乱,最终整个测试代码会被弃用。

没有测试代码,不能保证代码的改变是正常工作的,不能保证改变系统的一部分没有破环系统的其他部分。所以系统的缺陷率开始上升。他们停止清洁和整理生产代码,因为他们害怕改变会影响系统正常工作。他们的生产代码开始腐烂变坏。

测试代码和生产代码是一样重要的。它需要思考,设计和关注。它必须保持和生产代码一样整洁。

单元测试让我们的代码是灵活的、可维护的和可重用的。因为如果你有测试,你就不害怕改变代码,你可以不断地优化你的代码。

Clean Tests

使测试代码整洁的方法:可读性。让测试方法和正常方法一样整洁,使它尽量的小,只做一件事,只有一层抽象,和使用描述性的名称等。

Single Concept per Test

每个测试方法应该只测试单个概念。

F.I.R.S.T

编写整洁测试代码的 5 个规则:

  • Fast。测试应该是快的。如果测试运行很慢,你不想频繁地运行它们。如果你没有频繁地运行测试,你不会更早地发现问题,以及更轻松地修复问题。
  • Indenpent。测试方法应该不依靠其它代码。你应该能够独立地测试,以及以任何顺序运行测试。
  • Repeatable。测试应该在任何环境重复执行。如生产环境、QA 环境,和本地环境等。
  • Self-Validating。测试应该有一个布尔输出。你可以不通过日志文件知道测试是否通过。
  • Timely 。测试代码应该在业务代码之前写。

Classes

Class Organization

标准的 Java convention,一个 class 由一组变量开始,首先是 pubilc static constants,然后是 private static 变量,最后是 instance 变量。变量后面是方法,方法按照抽象层次由高往下。

Class Should Be Samll

class 的第一规则是 class should be small。class 的名称应该说明其应履行的职责。我们应该能够不使用 “if”, “and”, “or”, “but” 等单词情况下,用 25 words 来写出 class 的简单的描述。

Single Responsibility Principle 表示一个 class 或 module 应该只有一个发生改变的原因。这个原则帮助我们进行职责的定义和对 class size 的指导。尝试识别职责(reasons to change)常常能帮助我们在代码中找到和创建更好的抽象。

Cohension。classes 应该有少量的实例变量。一般更多的变量被一个方法操作,这个方法对 class 来说越内聚。当一个 classes 不内聚,应该分离它们。

Organizing for Change

大部分系统是不断改变的。每一个改变都可能使得系统的其它部分不能正常工作的风险。我们可以阻止我们的 classes 减少改变的风险。下面这些原则可以帮助我们更好地组织 classes 以减少代码改变带来的风险:SRP(Single Responsibility Principle),Open-Closed Principle(OCP),Dependency Inversion Principle(DIP)

Summary

Most Important

  • Meaningful Names
  • SRP(Single Responsibility Principle)

References

[1] Clean Code by Robert C. Martin

设计模式的总结

Design Pattern What Why Examples How Functions
(Creational)
Abstract Factory 提供了一个接口来创建相关的对象家族,而不用指定它们具体的 classes。 整体的改变多个 objects。 应用切换不同的外观(scroll bar,window,button) Client 通过 FactoryProvider 获取一个 AbstractFactory 的子类实例,使用这个 Factory 去生产产品对象。 一致性
Builder 将复杂对象的构造与其表现分开,因此相同的构造过程可以创建不同的表现。 不改变过程,构造不同的对象。 阅读器需要一个格式转换处理器对象,不同处理器可以转换不同的格式。不改变转换逻辑,增加新的转换处理器。 Client 通过将不同的 Builder 子类实例传给 Director,然后通过调用 director 的construct()方法得到产品对象。 扩展性
Factory Method 让一个类的实例化延迟到子类。 使用抽象对象维持关系,一个抽象类型的具体子类决定另一个抽象类型的具体子类。 文本编辑器不同的子应用创建不同的文档。只有当具体的子应用创建后,才知道要创建哪个文档类型实例。 不同的 Creator 子类生产不同的 Product 子类。Client 通过创建不同的 Creator 子类来创建不同的 Product 子类。 灵活性
Prototype 通过拷贝这个 prototype 去创建新的对象。 想要重复创建类似的对象,且这个对象的创建过程比较复杂。 编辑音乐乐谱的应用,需要重复添加多个音符对象。 Client 创建 Prototype 子类实例,然后调用它的 clone() 方法,得到这个类的对象拷贝。 复用性
Singleton 确保一个 class 仅有一个实例,并且提供一个全局的访问它的方法。 有些 class 必须只有一个实例。 一个系统应该仅有一个文件系统和一个窗口管理器。 方法一:(饥饿式)直接创建静态的实例的成员变量。方法二:(懒惰式)定义一个同步的静态方法获取实例。方法三:(懒惰式)在内部类中定义一个静态的实例的成员变量。 唯一性
(Structural)
Adapter 让不兼容的 classes 一起工作。Adapter 让 Adaptee 适应 Target。 一个不能兼容另一个接口的接口,希望他能够兼容。 画图编辑器有 lines, polygons, text 等元素。定义抽象的图形化接口 Shape,text 与 lines, polygons 有额外的操作,不同同时兼容一个接口。 方法一:(Object Adapter)Adapter 组合 Adaptee 。方法二:(Class Adapter)Adapter 实现 AdapteeImpl。 复用性
Bridge 解耦抽象与它的实现,使它们可以独立地改变。 想要独立地修改、扩展,以及重用 abstraction 和 implementation。 Window 有对应不同平台的子类,有不同特性的子类。都使用子类实现的话,需要多维度组合表示,难以添加新的子类。 Client 通过将 implementation 子类实例作为参数,创建 abstraction 的子类实例。调用 abstract 对象的方法实际上是调用 implementation 的方法。 扩展性
Composite 将对象组成树形结构以表示部分-整体层次结构。Composite 让 client 统一地对待单一对象和组合对象。 将多个 components 组成更大的 components,每个节点可以是单个组件,也可以是多个组件的组合,每个节点用统一的接口对象表示。 画图编辑器可以插入线、多边形和文字,可以插入图文。图文是图形和文字的组合。想把组合元素和单一元素统一对待,减少代码的复杂性。 Composite 是 Component 的子类,它有一个 Component list 成员。Client 通过 new Composite() 来创建 Component 对象。 易用性,扩展性
Decorator 动态地给一个 object 附加额外的职责。 有时我们想为 objects 添加职责,而不是整个 class。 为文本阅读器添加 border 或者 scroll 等。 Client 创建一个 ConcreteComponent 对象,然后将对象作为参数,创建 Decorator 子类,调用 ConreteDecorator 对象是操作间接的调用 ConreteComponent 对象的操作。 扩展性
Facade 为一个 subsystem 中的一组接口提供一个统一的接口,使得 subsystem 更容易使用。 将系统构建为子系统降低它的复杂性,并且,想要最小化系统之间的交流和依赖。 Client 创建 Facade 实例,调用它的方法,它去调用子系统中的指定对象的方法。 易用性,松耦合
Flyweight 通过共享可以有效地支持大量细粒度的对象。 一个应用存在大量的相同的元素,如果每个元素都使用一个对象去表示,会造成空间的浪费。 文档编辑器中的相同的字符可以共享一个字符对象。 Client 创建一个 FlyweightFactory,通过它来存储和去除共享的 Flyweight 对象。 复用性
Proxy Proxy 为另一个对象提供了一个代理,去控制对这个对象的访问。 推迟对象的创建和初始化的全部花费,直到我们真正需要使用它。 文档中的大图片需要很长时间去加载,我们可以控制文档的加载,先加载所有文字,再加载图片。 Client 通过 new ConcreteProxy() 创建 Subject 实例。Proxy 是 Subject 的子接口。ConcreteProxy 对象存在一个对 Subject 对象的引用。 灵活性
(Behavioral)
Chain of Responsibility 通过给多个对象处理请求的机会,避免耦合请求发送者与接收者。 避免将请求者与具体的接收者绑定。 Client 创建多个handler 将它们连接起来,把所有请求发送给第一个 handler。 松耦合、可扩展性
Command 将一个请求封装为一个对象,因此让你参数化客户端的不同请求。 有时需要发送一个请求给对象,但不知道任何关于请求的操作或者请求的接收者。 Client 创建 Invoker,Receiver 对象和 Command 对象。receiver 对象作为参数构建 command 对象。执行命令先调用 invoker 的 setCommand() ,再调用requestExecute() 方法。 松耦合、可扩展性
Interpreter 给定一种语言,定义其语法表示形式,以及使用该表示形式来解释语言中的句子。 如果一个种特殊的问题经常发生,它可能值得用简单的语言将问题的实例表达为句子。我们可以构建 interpreter 通过解释这些句子来解决问题。 Context 对象表示句子,TerminalExpression 对象可以解释 context。NonterminalExpression 对象可以连接 TerminalExpression 对象进行解释。 可扩展性
Iterator 提供以一种方法去顺序地访问聚合对象的元素,而不暴露它的底层表示。 访问它的元素而不暴露它的内部结构,提供统一的接口去遍历不同类型的聚合数据结构。 Aggreate 子类可以创建对应 Iterator 子类,Iterator 实现顺序遍历。 灵活性
Mediator 定义一个对象去封装一组对象是如何交互。 大量的相互连接让系统的行为很难去改变。Mediator 可以用来控制和协调一组对象之间的交互。 Mediator 作为参数创建不同的 ConcreteColleague 对象,colleague 之间的调用时通过调用 mediator 对象的方法实现。 松耦合、可扩展性
Memento 在不违反封装和外部化一个对象的内部状态等情况下,使得该对象可以在以后恢复之前的状态。 有时需要记录一个对象的内部状态。实现检查点和 undo 功能,让用户在发生错误时恢复状态记录的对象状态。 caretaker 对象可以存储和删除 memento 对象,memento 对象保存了 originator 对象的状态,caretaker 可以设置不同的 memento 来恢复 originator 对象的状态。 可恢复
Observer 在对象中定义一对多的依赖,因此当一个对象改变状态时,所有它的依赖者是自动通知和更新的。 将系统划分为一组合作的 classes 常见的辅作用是需要维护相关对象之间的一致性。你不想通过使 classes 紧耦合来实现一致性,因为它降低了代码的可复用性。 Subject 对象可以 attach Observer 对象,可以 detach Observer 对象。Subject 中存储了所有 attach 的 Observer对象的list,当 Subject 状态发送改变时,自动通知所有 attach 的 Observer 对象。 松耦合、可扩展性
State 当一个对象的内部状态改变时允许改变它的行为。 一个对象需要在不同的状态表现不同的行为。 TCPConnection class 他表示一个网络连接。一个 TCPConnection object 可可能是多个不同状态中的一个,如:Established,Listening,Closed。当一个 TCPConnection 对象接收到请求时,它可以根据当前的状态进行响应。 Context 对象设置不同的 State 对象,执行 request() 方法得到不同的结果。 可扩展性
Strategy 定义一组算法,封装每一个,以及让它们是可互换的。 一个行为可能切换不同的实现方式。 Context 配置不同的 Strategy 对象,执行不同的算法。 扩展性
Template Method 在操作中定义算法的骨架,将某些步骤推迟到子类。 通过使用抽象操作定义算法的某些步骤,模板方法可以固定其顺序,但可以让子类更改这些步骤以适合其需求。 Client 通过 new ConcreteClass() 来创建 AbstractClass,ConcreteClass 实现了父类抽象步骤的方法。 复用性
Visitor Visitor 可以让你定义新的的操作,而无需更改其所操作的元素的类。 抽象父类定义了一组操作,不同的子类不一定需要实现所有的操作。所有操作放在所有子类中,会让人感到疑惑,以及难以维护。 Client 定义一组 Element 对象,然后循环调用每个 Element 对象的 accept(Visitor visitor) 方法,可以对所有 Element 对象执行一个指定操作。 松耦合、可扩展性

设计模式的用途

设计模式常见的用途如下:

  • 可复用性:减少冗余代码。
  • 可扩展性:隔离职责,松耦合。很容易添加新类型的 class,容易添加新的功能。
  • 灵活性:方便修改功能,少改动,独立修改。方便控制管理对象。
  • 易用性:使得 Client 操作简单。
可复用性 可扩展性 灵活性 易用性 其他
Prototype
Adapter
Flyweight
Template Method
Builder
Bridge
Decorator
Chain of Responsibility
Command
Interpreter
Mediator
Observer
State
Strategy
Visitor
Factory Method
Proxy
Iterator
Composite
Facade
Abstract Factory
Singleton
Memento

本篇将介绍设计模式中常见的11种行为型模式,其中设计模式的实现代码使用 Java 语言描述。

Behavioral Patterns

行为型模式它关注对象之间的算法和职责分配。它不仅是描述对象和类的模式,也是描述它们之间交流的模式。

Chain of Responsibility

What

通过给多个对象处理请求的机会,避免耦合请求发送者与接收者。Chain 接收请求,并沿着 chain 传递请求直到有一个对象能够处理该请求为止。

Why

Motivation

避免将请求者与具体的接收者绑定。使用一个类统一接收所有请求,接收者连接成一条链来处理请求。

Applicability

  • 超过一个对象去处理请求。
  • 你想发送一个请求给一些对象中的一个,并且不明确指定接收者。
  • 动态指定一组对象处理一个请求。

Solution

Structure

类结构

对象结构

Participants

  • Handler:定义一个接口去处理请求。
  • ConcreteHandler:处理请求。接收继任者(successor)。如果它能处理一个请求,则处理它;如果不能,则将这个请求转发给它的继任者。
  • Client:初始化一个请求,发送给 chain 上的一个 ConcreteHandler 对象。

Collaborations

  • 当 Client 发送一个请求,这个请求在 Chain 上传播,直到有一个 ConcreteHandler 对象能够处理它。

Implementations

Click to expand!
public abstract class Handler{
private Handler nextHandler;

public abstract void handleRequest();

public void setNext(Handler handler){
this.nextHandler = handler;
}
}

public class ConcreteHandler1 extends Handler{
public void handleRequest(int request){
if (request == 1){
System.out.println("handle by ConcreteHandler1");
}else{
if (nextHandler != null)
nextHandler.process(request);
}
}
}

public class ConcreteHandler2 extends Handler{
public void handleRequest(int request){
if (request == 2){
System.out.println("handle by ConcreteHandler2");
}else{
if (nextHandler != null)
nextHandler.process(request);
}
}
}

public class Client{
public static void main(String[] args){
Handler handler = new ConcreteHandler1();
Handler nextHandler = new ConcreteHandler2();
handler.setNext(nextHandler);
int request1 = 1, request2 = 2;
handler.handleRequest(request1);
handler.handleRequest(request2);
}
}

Consequences

Benefits

  • 减少了耦合。
  • 增加了给对象分配职责的灵活性。

Drawbacks

  • 请求的接收不能保证。因为一个请求没有明确指定接收者,所有不能保证它能被处理。

Command

What

将一个请求封装为一个对象,因此让你参数化客户端的不同请求。

Why

Motivation

有时需要发送一个请求给对象,但不知道任何关于请求的操作或者请求的接收者。

Applicability

  • 参数化执行动作,将其封装为对象。
  • 在不同的时间指定,排队和执行请求。Command 对象的生命周期可以独立于原始请求。
  • 支持 undo。Command 执行操作可以存储状态用来反转影响 command 本身。
  • 支持 logging changes。所以可以在系统崩溃后可以重新正确运行。
  • 围绕基于 primitive 操作的 high-level 操作来构建系统。

Solution

Structure

Participants

  • Command:声明用于执行操作的接口。
  • ConcreteCommand:定义 Receiver object 和 action 之间的绑定。通过调用接收者相应的操作来实现 Execute。
  • Cient:创建一个 ConcreteCommnd 对象,以及设置它的 receiver。
  • Invoker:请求 command 得到请求结果。
  • Receiver:知道如何执行与请求相关的操作。

Collaborations

  • Client 创建一个 ConcreteCommand 以及指定它的 receiver。
  • Invoker 对象存储 ConcreteCommand 对象。
  • Invoker 通过调用 command 对象的 execute() 方法来发出请求。
  • ConcreteCommand 对象调用接收者的操作来完成请求。

Implementations

Click to expand!
public interface Command{
void execute();
}
public class Receiver{
public void action1(){
System.out.println("action 1 executing...");
}
public void action2(){
System.out.println("action 2 executing...");
}
}
public class ConcreteCommand1 implements Command{
public Receiver receiver;
public ConcreteCommand1(Receiver receiver){
this.receiver = receiver;
}
public void execute(){
receiver.action1();
}
}
public class ConcreteCommand2 implements Command{
public Receiver receiver;
public ConcreteCommand1(Receiver receiver){
this.receiver = receiver;
}
public void execute(){
receiver.action2();
}
}
public class Invoker{
Command slot;
public void setCommand(Command command){
this.slot = command;
}
public void requestExecute(){
this.slot.execute();
}
}
public class Clinet{
Invoker invoker = new Invoker();
Receiver receiver = new Receiver();
invoker.setCommnad(new ConcreteComand1(receiver));
invoker.requestExecute();
invoker.setCommnad(new ConcreteComand2(receiver));
invoker.requestExecute();
}

Consequences

Benefits

  • Command 将调用操作的对象与知道如何执行该操作的对象分离。
  • 你可以装配命令作为 composite 命令。
  • 很容易添加新的 Command。因为你不需要该改变存在的 classes。

Interpreter

What

给定一种语言,定义其语法表示形式,以及使用该表示形式来解释语言中的句子。

Why

Motivation

如果一个种特殊的问题经常发生,它可能值得用简单的语言将问题的实例表达为句子。然后,你可以构建 interpreter 通过解释这些句子来解决问题。

Applicability

  • 语法是简单的。
  • 效率不是一个关键问题。

Solution

Structure

Participants

  • AbstractExpression:定义一个抽象 interpret 操作,它存在于所有 abstract syntax tree 中的节点。
  • TerminalExpression:实现与 terminal symbols 有关的 interpret 操作。
  • NonterminalExpression:实现 nonterminal symbols 相关的 interpret 操作。
  • Context:包含给 interpreter 的全部信息。
  • Client:构建一个抽象的 syntax tree 表示一个符合语法规定的特定的句子。调用 interpret 操作。

Collaborations

  • Client 构建一个句子作为 NonterminalExpression 和 TerminalExpression 实例的abstract syntax tree 。然后,client 初始化 context,调用 interpret 操作。
  • 每个 NonterminalExpression node 定义了 interpret 对每个子表达式上的 interpret。
  • 每个 node 的 interpret 操作使用 context 去存储和访问 interpreter 的状态。

Implementations

Click to expand!
public interface Expression{
boolean interpret(String context);
}

public class TerminalExpression implements AbstractExpression{
private String data;
public TerminalExpression(String data){
this.data = data;
}
public boolean interpret(Context context){
if (data.contains(Context.data)){
return true;
}else{
return false;
}
}
}
public class NonterminalExpression implements AbstractExpression{
private Expression expression1;
private Expression expression2;
public NonterminalExpression(Expression expression1, Expression expression2){
this.expression1 = expression1;
this.expression2 = expression2;
}
public boolean interpret(Context context){
return expression1.interpret(context) && expression2.interpret(context);
}
}
public class Context{
private String data;
public Context(String data){
this.data = data;
}
}

public class Client{
Context context1 = new Context("Tom");
TerminalExpression terminalExp1 = new TerminalExpression("Tom");
TerminalExpression terminalExp2 = new TerminalExpression("Jack");
terminalExp1.interpret(context1);
terminalExp2.interpret(context1);
NonterminalExpression nonterminalExp = new NonterminalExpression(terminalExp1, terminalExp2);
nonterminalExp.interpret(context1);
}

Consequences

Benefits

  • 它很容易去改变和扩展语法。
  • 实现语法是容易的。
  • 可以增加新的方式去 interpret 表达式。

Drawbacks

  • 复杂的语法很难去管理和维护。

Iterator

What

提供以一种方法去顺序地访问聚合对象的元素,而不暴露它的底层表示。

Why

Motivation

一个聚合对象如 list,应该有一种方式去访问它的元素而不暴露它的内部结构。你可能想要用不同的方式去遍历集合,让它取决于你想要的实现。Iterator 模式可以帮你实现以上功能。

Applicability

  • 访问一个聚合对象的内容,而不暴露它的内部表示。
  • 支持多种对聚合对象的遍历方式。
  • 提供统一的接口去遍历不同类型的聚合数据结构。

Solution

Structure

Participants

  • Iterator:定义一个接口去访问和遍历元素。
  • ConcreteIterator:实现 Iterator 接口。保持追踪遍历聚合元素的位置。
  • Aggregate:定义创建 Iterator 对象的接口。
  • ConcreteAggregate:实现创建 Iterator 对象接口,返回合适的 ConcreteIterator 对象。

Collaborations

  • ConcreteIterator 保持聚合元素对象的轨迹,能够计算在遍历中接下的元素对象。

Implementations

Click to expand!
public interface Aggregate{
Iterator createIterator();
}
public class ConcreteAggregate implements Aggregate{
private int[] data = new int[32];
private int size;
private int currentSize;

public void add(int number){
data[currentSize] = number;
currentSize++;
}

public Iterator createIterator(){
return new ConcreteIterator(data, currentSize);
}
}

public interface Iterator{
int first();
void next();
boolean isDone();
int currentItem();
}
public class ConcreteIterator implements Iterator{
private int[] data;
private int cursor = 0;

public ConcreteIterator(int[] data, int currentSize){
data = new int[currentSize];
for (int i = 0; i < currentSize; i++){
this.data[i] = data[i];
}
}

public int first(){
// ignored. not important
return null;
}
public int next(){
if (cursor < data.length){
return data[cursor++];
}else{
throw new ArrayIndexOutOfBoundExcpetion();
}
}
public boolean isDone(){
return cursor >= data.length -1;
}
public int currentItem(){
// ignored. not important
return null;
}
}

public class Client{
public static void main(String[] args){
Aggregate aggregate = new ConcreteAggregate();
aggregate.add(1);
Iterator iterator = aggregate.createIterator();
while(! iterator.isDone()){
System.out.println(iterator.next());
}
}
}

Consequences

Benefits

  • 它支持聚合结构的遍历中的变化。
  • Iterator 简化了 Aggregate 接口。
  • 一个聚合对象可以有多个遍历。

Mediator

What

定义一个对象去封装一组对象是如何交互。Mediator 通过防止对象之间显式地互相引用来促进松耦合,并且它让你可以独立地更改它们之间的交互。

Why

Motivation

面向对象的设计鼓励在对象之间分配行为。这种分配可能导致一个对象与很多对象有关联。大量的相互连接让系统的行为很难去改变。你可以使用 Mediator 去解决这类问题。Mediator 可以用来控制和协调一组对象之间的交互。Mediator 充当了中介,可以防止一组对象明确地相互引用。对象只知道 Mediator,从而减少对象相互连接的数量。

Applicability

  • 一组对象交流十分复杂。
  • 重用一个对象是复杂的,因为它引用了很多其他的类,以及与很多其他类存在交流。
  • 在多个类之间分布的行为应可自定义,而无需大量子类化。

Solution

Structure

类结构

对象结构

Participants

  • Mediator:为 Colleague 对象交流定义一个接口。
  • ConcreteMediator:通过协调 Colleague 对象来实现合作行为。维护它的 colleagues 对象。
  • Colleague classes:每一个 Collegue 类知道它的 Mediator 对象。每个 colleague 与它的 mediator 交流。

Collaborations

  • Colleagues 发送和接收请求来自 Mediator 对象。Mediator 通过在适当的 Colleagues 之间路由请求来实现协作行为。

Implementations

Click to expand!
public interface Mediator{

}
public class ConcreteMediator implements Mediator{
private ConcreteColleague1 concreteColleague1;
private ConcreteColleague2 concreteColleague2;
public void setConcreteColleague1(ConcreteColleague1 concreteColleague1){
this.concreteColleague1 = concreteColleague1;
}
public void setConcreteColleague2(ConcreteColleague2 concreteColleague2){
this.concreteColleague2 = concreteColleague2;
}
public void callHelloToColleague2FromColleague1(){
concreteColleague2.hello(concreteColleague1);
}
}
public interface Colleague{

}
public class ConcreteColleague1 implements Colleague{
private Mediator mediator;
public ConcreteColleague1(){}
public ConcreteColleague1(Mediator mediator){
this.mediator = mediator;
}
public void sayHelloToColleague2(){
mediator.sayHelloToColleague2FromColleague1();
}
}
public class ConcreteColleague2 implements Colleague{
private Mediator mediator;
public ConcreteColleague2(){}
public ConcreteColleague2(Mediator mediator){
this.mediator = mediator;
}
public void hello(Colleague colleague){
System.out.println("hello, response to " + colleague);
}
}
public class Client{
public static void main(String[] args){
// config mediator
Mediator mediator = new ConcreteMediator();
ConcreteColleague1 colleague1 = new ConcreteColleague1(mediator);
ConcreteColleague2 colleague2 = new ConcreteColleague2(mediator);
mediator.setConcreteColleague1(colleague1);
mediator.setConcreteColleague2(colleague2);
// send request among colleagues by call mediator methods
colleague1.sayHelloToColleague2();
}
}

Consequences

Benefits

  • 它限制了子类。
  • 它解耦了 colleagues。
  • 它简化对象通信协议。
  • 它将对象的协作抽象化。
  • 它中心控制对象的交互。

Drawbacks

  • 由于它中心控制对象的交互,Mediator 会变得很庞大,它自身变得很难维护。

Memento

What

在不违反封装和外部化一个对象的内部状态等情况下,使得该对象可以在以后恢复之前的状态。

Why

Motivation

有时需要记录一个对象的内部状态。实现检查点和 undo 功能,让用户在发生错误时恢复状态记录的对象状态。但是对象一般是封装了一些或全部状态,使它不能被其他对象访问,以及不可能在外部保存。暴露对象的内部状态违反了封装,这会损害应用程序的可靠性和可扩展性。

我们可以使用 Memento 模式解决这个问题。memento 是一个对象,它可以存储对象内部状态的快照(snapshot)。

Applicability

  • 一个对象的状态的快照必须保存,因此它可以在以后恢复之前的状态。
  • 一个接口直接地获取状态将暴露实现细节和打破对象的封装。

Solution

Structure

Participants

  • Memento:1)存储 originator 对象的内部状态。2)防止非 originator 对象访问。3)它是一个 POJO 类。
  • Originator:1)创建一个 包含当前内部状态快照的 memento。2)使用 memento 去恢复它的内部状态。
  • Caretaker:1)负责 memento 的保管。2)不操作或检查 memento 的内容。3)保持多个 memento 的轨迹,维护保存点。

Collaborations

  • caretaker 从 originator 请求一个 memento,保持一段时间,以及把它传回 originator。它们的交互如下图所示。
  • Memento 是被动的。只有 orginator 能够创建 memento 指派或取回它的状态。

Implementations

Click to expand!
public class Memento{
private int state;

public Memento(){}
public Memento(int state){
this.state = state;
}
public int getState(){
return state;
}
public void setState(int state){
this.state = state;
}
}
public class Originator{
private int state;
public void setState(int state){
this.state = state;
}
public int getState(){
return this.state;
}
public void createMemento(){
return new Memento(this.state);
}
public setMemento(Memento memento){
this.state = memento.getState();
}
}
pubilc class Caretaker{
private List<Memento> mementos = new ArrayList<>();
private Originatro orginator;
public Caretacker(Originator orginator){
this.originator = orginator;
}
pubilc void addMemento(){
Memento newMemento = this.originator.createMemento();
this.mementos.add(newMemento);
return newMemento;
}
public void setMemento(Memento memento){
for (m : mementos){
if (m.state == memento.state){
this.originator.setMementor(m);
}
}
}
}

public class Client{
public static void main(String[] args){
Originatro originator = new Originator();
Careracker caretacker = new Caretacker(originator);
originator.setState(1);
System.out.println("state one: " + originator.getState());
Memento memento1 = caretacker.addMemento();
originator.setState(2);
System.out.println("state two: " + originator.getState());
Memento memento2 = caretacker.addMemento();
caretacker.setMemento(memento1);
System.out.println("restore state one: " + originator.getState());
}
}

Consequences

Benefits

  • 保持封装边界。
  • 简化 originator。把 originator 内部状态的版本保留放到了其它类中。

Drawbacks

  • 使用 memento 可能是昂贵的。如果 Originator 拷贝大量的信息存储在 memento,使用 memento 可以导致很大的花费。
  • 保管 mementos 的隐性成本。caretaker 负责删除它保管的 mementos。然而 caretaker 不知道在 memento 中有多少 state。因此,caretaker 可以能导致大量的存储 mementos 的花费。

Observer

What

在对象中定义一对多的依赖,因此当一个对象改变状态时,所有它的依赖者是自动通知和更新的。

Why

Motivation

将系统划分为一组合作的 classes 常见的辅作用是需要维护相关对象之间的一致性。你不想通过使 classes 紧耦合来实现一致性,因为它降低了代码的可重用性。

Applicability

  • 当一个抽象有两个方面,一个依赖另一个。在具体的对象中封装这些方面,让你独立地改变和重用它们。
  • 当你改变一个对象需要改变其他对象,并且你不知道有多少对象需要改变时。
  • 一个对象可以通知其他对象不需要关心这些对象是什么。

Solution

Structure

Participants

  • Subject:1)知道它的 observers。无数个 Observer 对象可能观察一个 subject。2)提供一个接口关联和脱离 Observer 对象。
  • Observer:为接收 subject 改变通知的对象定义一个更新的接口。
  • ConcreteSubject:1)存储 ConcreteObserver 对象的信息。2)当状态改变时发送通知给它的 observers。
  • ConcreteObserver:1)维护一个 ConcreteSubject 的引用。2)存储与 subject 一致的状态。3)实现 Observer 更新接口,保持它的状态与 subject 一致。

Collaborations

  • 当改变发生时,ConcreteSubject 通知它的 observers,让 observers 的状态和自己的保持一致。
  • 当改变通知之后, ConcreteObserver 对象可能查询 subject 的信息。ConcreteObserver 使用这个信息使它的状态与 subject 保持一致。

Implementations

Click to expand!
public interface Subject{
void attach(Observer observer);
void detach(Observer observer);
void notify();
}
public class ConcreteSubject implements Subject{
private int state;
List<Observer> observers = new ArrayList<>();

public void setState(int state){
this.state = state;
notify();
}
public int getState(){
return this.state;
}
public void attach(Observer observer){
observers.add(observer);
}
public void detach(Observer observer){
observers.remove(observer);
}
public void notify(){
for (Observer observer : observers){
observer.update();
}
}
}
public interface Observer{
void update();
}
public class ConcreteObserver1 implements Observer{
private int state;
private Subject subject;

public ConcreteObserver1(){}
public ConcreteObserver1(Subject subject){
this.subject = subject;
this.state = subject.getState();
}
public int getState(){
return this.state;
}
public void update(){
this.state = subject.getState();
}
}
public class ConcreteObserver2 implements Observer{
private int state;
private Subject subject;

public ConcreteObserver2(){}
public ConcreteObserver2(Subject subject){
this.subject = subject;
this.state = subject.getState();
}
public int getState(){
return this.state;
}
public void update(){
this.state = subject.getState();
}
}
public class Cilent{
public static void main(String[] args){
int state = 1;
Subject subject = new ConcreteSubject(state);
Observer observer1 = new ConcreteObserver1(subject);
Observer observer2 = new ConcreteObserver2(subject);
System.out.println("observer1 state is " + observer1.getState());
System.out.println("observer2 state is " + observer2.getState());
subject.attach(observer1);
subject.attach(observer2);
// automatically notify and update observers
subject.setState(2);
System.out.println("observer1 state update to " + observer1.getState());
System.out.println("observer2 state update to " + observer2.getState());
}
}

Consequences

Benefits

  • 抽象地耦合 Subject 和 Observer。subjecct 不知道它有一组 observers,不知道 observer 具体的类。
  • 支持广播通信。

Drawbacks

  • 意外的更新。可能会导致 observers 很难追踪的虚假更新。

State

What

当一个对象的内部状态改变时允许改变它的行为。这个对象好像更改了它的 classs。

Why

Motivation

一个对象需要在不同的状态表现不同的行为。

例子:TCPConnection class 他表示一个网络连接。一个 TCPConnection object 可可能是多个不同状态中的一个,如:Established,Listening,Closed。当一个 TCPConnection 对象接收到请求时,它可以根据当前的状态进行响应。

Applicability

  • 一个对象的行为取决于它的状态,并且它必须根据它的状态在运行时改变它的行为。
  • 操作有大量的多条件语句,这些语句取决于对象的状态。

Solution

Structure

Participants

  • Context:1)定义 Client 想要的接口。2)维护一个 ConcreteState 子类的实例,它定义了当前状态。
  • State:定义一个接口去封装与 Context 的特殊状态相关的行为。
  • ConcreteState:每个子类实现与 Context 的状态相关的行为。

Collaborations

  • Context 将特定状态的请求委托给当前的 ConcreteState 对象。
  • Context 可以将自身作为参数传递给处理请求的 State 对象。
  • Context 是 Client 的主要接口。Client 可以通过 State 对象配置 context。一旦 Context 配置了,它的 client 不需要直接处理 State 对象。
  • 无论是 Context 还是 ConcreteState 子类都能决定哪个状态接替另一个和在什么情况下。

Implementations

Click to expand!
public class Context{
private State state;
public Context(){}
public Context(State state){
this.state = state;
}
public void setState(State state){
this.state = state;
}
public void request(){
this.state.handle()
}
}

public interface State{
void handle();
}
public class ConcreteStateA implements State{
public void handle(){
System.out.println("handle by ConcreteStateA");
}
}
public class ConcreteStateB implements State{
public void handle(){
System.out.println("handle by ConcreteStateB");
}
}

public class Client{
public static void main(String[] args){
State state = new ConcreteStateA();
Context context = new Context(state);
context.request();
state = new ConcreteStateB();
context.setState(state);
context.request();
}
}

Consequences

Benefits

  • 它本地化特定状态的行为,它为不同的状态划分行为。
  • 它使状态转换变得明确。
  • 状态对象可以共享。

Strategy

What

定义一组算法,封装每一个,以及让它们是可互换的。Strategy 使算法的改变独立于 Client。

Why

Motivation

一个行为可能切换不同的实现方式。

Applicability

  • 许多相关的 classes 仅在行为上有所不同。Strategy 提供了一种使用多种行为之一配置 class 的方法。
  • 你需要不同的算法。
  • 算法使用了 Client 不应该知道的数据。
  • 一个类定义了许多行为,这些行为在其操作中显示为多个条件语句。代替条件,把相关的条件分支移到它们自己的 Strategy class 中。

Solution

Structure

Participants

  • Strategy:声明所有支持的算法通用的接口。Context 使用这个接口去调用 ConcreteStrategy 定义的算法。
  • ConcreteStrategy:使用 Strategy 实现算法。
  • Context:1)配置了一个 ConcreteStrategy 对象。2)维护一个 Strategy 对象的参考。3)可能定义一个接口让 Strategy 访问它的数据。

Collaborations

  • Strategy 和 Context 交互以实现所选的算法。当算法调用时,Context 可能将算法需要的所有数据传递给 Strategy。或者,Context 把自己作为参数传递给 Strategy 操作。这样,Strategy 可以根据需要回调 Context。
  • Context 将来自它的 Client 的请求转发给它的 Strategy。Client 通常创建和传递 ConcreteStrategy 对象给 Context。之后,Client 仅与 Context 交互。通常会有一些列的 ConcreteStrategy 类供 Client 选择。

Implementations

Click to expand!
public interface Strategy{
public void algorithmInterface();
}
public ConcreteStrategyA implements Strategy{
public void algorithmInterface(){
System.out.println("algorithm implements by ConcreteStrategyA");
}
}
public ConcreteStrategyB implements Strategy{
public void algorithmInterface(){
System.out.println("algorithm implements by ConcreteStrategyB");
}
}

public class Context{
private Strategy strategy;

public contextInterface(Strategy strategy){
this.strategy = strategy;
}
public void runAlgorithm(){
this.strategy.algorithmInterface();
}
}
public class Client{
public static void main(String[] args){
Strategy strategyA = new ConcreteStrategyA();
Strategy strategyB = new ConcreteStrategyB();
Context context = new Context();
context.contextInterface(strategyA);
context.runAlgorithm();
context.contextInterface(strategyB);
context.runAlgorithm();
}
}

Consequences

Benefits

  • 相关的算法家族。Strategy classes 的层级结构定义了一组让 Context 重用的算法或行为。
  • 它是子类化的替代方法。你可以使用 inheritance 的方式去支持多种算法或行为。你可以 subclass Context class 直接执行不同的行为。但这将硬性地把 behavior 关联到 Context。
  • Strategy 可以消除条件语句。
  • 多种实现方式。

Drawbacks

  • Client 必须知道 Strategies 之间的不同。这个模式有个潜在的缺点就是 Client 在选择合适的 strategy 之前必须理解 strategies 有什么不同。
  • 在 Strategy 和 Context 之间有交流消耗。所有 ConreteStrategy 共享 Strategy 接口,无论它们实现的算法是简单还是复杂的。因此,某些 ConcreteStrategy 可能不适用接口传递的所有信息。这就意味着 Context 可能会创建和初始化未使用的参数。
  • 它增加了对象的数量。

Template Method

What

在操作中定义算法的骨架,将某些步骤推迟到子类。Template Method 让子类重新定义算法的某些步骤,而无需更改算法的结构。

Why

Motivation

通过使用抽象操作定义算法的某些步骤,模板方法可以固定其顺序,但可以让子类更改这些步骤以适合其需求。

Applicability

  • 算法不变的部分仅实现一次,并将可变化的行为留给子类来实现。
  • 子类间的共同行为应该分解并集中在一个共同类中,以避免代码重复。
  • 控制子类扩展。你可以定义一个 template method,它叫做在特定点调用 hook 操作,从而允许在哪些点进行扩展。

Solution

Structure

Participants

  • AbstractClass:1)定义抽象的基本操作。2)实现 template method 定义算法骨架。
  • ConcreteClass:实现基本操作以完成子类具体的算法步骤。

Collaborations

  • ConcreteClass 依赖 AbstractClass 实现算法不变的步骤。

Implementations

Click to expand!
public abstract class AbstractClass{
public void templateMethod(){
primitiveOperation1();
primitiveOperation2();
}
abstract void primitiveOperation1();
abstract void primitiveOperation2();
}
public class ConcreteClass extends AbstractClass{
public void primitiveOperation1(){
System.out.println("operation1...");
}
public void primitiveOperation2(){
System.out.println("operation2...");
}
}
public class Client{
public static void main(String[] args){
AbstrctClass target = new ConcreteClass();
target.templeateMethod();
}
}

Consequences

Benefits

  • 提高代码的复用性。

Visitor

What

Visitor 表示要在对象结构的元素上执行的操作。Visitor 可以让你定义新的的操作,而无需更改其所操作的元素的类。

Why

Motivation

抽象父类定义了一组操作,不同的子类不一定需要实现所有的操作。强行将父类的所有操作放在一个不需要这个方法的子类中,会让人感到疑惑,以及难以维护。

上面这种情况可以使用 Visitor 将对象结构和对对象的操作分离,并且它可以让你轻易的增加新的操作。

Applicability

  • 一个对象结构包含很多不同接口的类的对象,你想要根据它们具体的类来执行这些对象的操作。
  • 需要对一个对象结构中的对象执行许多不同且不相关的操作,并且你要避免使用这些操作“污染”它们的类。Vistor 让你将相关的操作放在一起,通过把它们定义在一个类中。
  • 定义对象结构的类很少改变,但是你经常想要在该结构上定义新的操作。更改对象结构类需要重新定义所有 Visitor 的接口,这可能导致很高的花费。如果你的对象结构类经常改变,那么它可能更适合把操作定义在类中。

Solution

Structure

Participants

  • Visitor:为对象结构中每一个 ConcreteElement 类声明一个 Visit 操作。
  • ConcreteVisitor:实现 Visitor 中声明的每个操作。
  • Element:定义一个 Accept 操作,它接收一个 visitor 作为参数。
  • ConcreteElement:实现 Accept 操作。
  • ObjectStructure:1)枚举它的元素。2)提供一个高层级的接口去允许 Visitor 访问它的元素。3)它可以是组合(Composite)或者集合(List or Set)。

Collaborations

  • Client 创建一个 ConcreteVisitor 对象,然后遍历对象结构,和 visitor 一起访问每个元素。
  • 当一个元素被访问,它调用对应的 Visitor 操作。如果需要,这个元素支持把自己作为参数传给这个操作,让 visitor 访问它的状态。

Implementations

Click to expand!
public interface Visitor{
int visit(Element concreteElementA);
int visit(Element concreteElementB);
}
public class ComputeSumConcreteVisitor1 implements Visitor{
private int result = 0;

public void visit(Element concreteElementA){
result += concreteElementA.getValue();
}
public void visit(Element concreteElementB){
result += concreteElementB.getValue();
}
public int getSum(){
return this.result;
}
}
public class ComputeProductConcreteVisitor2 implements Visitor{
private int result = 1;

public void visit(Element concreteElementA){
result *= concreteElementA.getValue();
}
public void visit(Element concreteElementB){
result *= concreteElementB.getValue();
}
public int getProduct(){
return this.totalValue;
}
}

public interface Element{
int accept(Visitor visitor);
}
public class ConcreteElementA implements Element{
int value;
public ConcreteElementA(){}
public ConcreteElementA(int value){
this.value = value;
}
int accept(Visitor visitor){
return visitor.visit(this);
}
}
public class ConcreteElementB implements Element{
int value;
public ConcreteElementB(){}
public ConcreteElementB(int value){
this.value = value;
}
int accept(Visitor visitor){
return visitor.visit(this);
}
}

public class Client{
pubilc static void main(String[] args){
Element[] elements = new Element[]{new ConcreteElementA(1), new ConcreteElementB(2)};

// operation 1 in elements object strucutre
Visitor sumVisitor = new ComputeSumConcreteVisitor1();
for (Element e : elements){
e.accept(sumVisitor);
}
int sum = sumVisitor.getSum();

// operation 2 in elements object strucutre
Visitor productVisitor = new ComputeProductConcreteVisitor2();
for (Element e : elements){
e.accept(productVisitor);
}
int product = productVisitor.getProduct();
}
}

Consequences

Benefits

  • Visitor 使得添加新的操作很容易。通过添加一个新的 visitor 来定义对 object 结构的新的操作。
  • Visitor 收集相关的操作并将不相关的操作分开。
  • 跨 class 层级结构进行访问。你可以定义没有公共父类的 visit objects。你可以在 Visitor 接口中添加任何类型的对象。
  • 积累状态。

Drawbacks

  • 增加新的 ConcreteElement class 是复杂的。每个ConcreteVisitor 都需要添加操作这个元素的新的方法。
  • 打破封装。该模式中的 element 必须提供访问元素内部状态的 public 方法。

References

[1] Design Patterns: Elements of Reusable Object-Oriented Software by Erich Gamma, Richard Helm, Ralph Johnson and John Vlissides

本篇将介绍设计模式中的7种结构型模式,其中设计模式的实现代码使用 Java 语言描述。

Structural Design Patterns

结构型设计模式关注 classes 和 objects 如何组成更大的结构。

Adapter

What

Adapter 将一个 interface class 转换为 Client 期望的另一个 interface。Adapter 让不兼容的 classes 一起工作。

Why

Motivation

一个不能兼容另一个接口的接口,希望他能够兼容。

例子:画图编辑器可以让用户画图和布置图形元素,如 lines,polygons,text 等。定义了一个抽象的图形化接口为 Shape。为每种图形对象定义子类,LineShape 对应 lines,PolygonShape 对应 polygons。基本的集合图形是比较容易实现的,但是对文本的编辑很难实现,文本类 TextView 存在额外的方法,它不能兼容 Shape 接口。我们可以使用 Adapter 模式让其兼容。我们可以定义 TextShape 它使得 TextView 接口适应 Shape 接口,其中 TextShape 为 Adapter,TextView 为 Adaptee。

Adapter 模式有两种实现方式,一种是继承,另一种是组合。(1)让 TextShape 实现 Shape 接口,以及继承 TextView 的实现。(2)让 TextShape 实现 Shape 接口,以及组合一个 TextView 实例。

Applicability

  • 你想要用一个已存在的 class,它的接口不匹配你的需求。
  • 你想要创建一个重用的 class ,它能够于不相关的或无法预见的 classes 合作,即不一定具有兼容的接口。
  • 你需要使用一些存在的子类,但是它是不能兼容每一个子类的接口。

Solution

Structure

Adapter 使用 multiple inheritance 实现的结构:

Adapter 使用 object composition 实现的结构:

Participants

  • Target:定义 Client 使用的特定领域的接口。
  • Client:与符合 Target 接口的对象协作。
  • Adaptee:定义一个需要适应的接口。
  • Adapter:使 Adaptee 接口适应 Target 接口的类。

Collaborations

  • Clients 请求 Adapter 实例的方法。Adapter 通过请求 Adaptee 的方法得到结果。
  • Adapter 让 Adaptee 适应 Target。

Implementations

方法一:Object Adapter(Adapter 组合 Adaptee 实例和实现 Target 接口)

Click to expand!
public interface Target{
public void requestA();
}

public class TargetImpl{
public void requestA(){
System.out.println("requestA");
}
}

public interface Adaptee{
public void requestB();
}

public class AdapteeImpl implements Adaptee{
public void requestB(){
System.out.println("requestB");
}
}

public class Adapter implements Target{
private Adaptee adaptee;
public Adapter(Adaptee adaptee){
this.adaptee = adaptee;
}
public void requestA(){
System.out.println("Adapter requestA");
}
public void requestB(){
adaptee.requestB();
}
}

public class Client{
public static void main(String[] args){
Adapter adapter = new Adapter(new AdapteeImpl());
adapter.requestA();
adapter.requestB();
}
}

方法二:Class Adapter(Adapter 继承 AdapteeImpl 和实现 Target 接口)

Click to expand!
public interface Target{
public void requestA();
}

public class TargetImpl{
public void requestA(){
System.out.println("requestA");
}
}

public interface Adaptee{
public void requestB();
}
public class AdapteeImpl implements Adaptee{
public void requestB(){
System.out.println("requestB");
}
}
public class Adapter extends AdapteeImpl implements Target{
public void requestA(){
System.out.println("Adapter requestA");
}
}

public class Client{
public static void main(String[] args){
Adapter adapter = new Adapter();
adapter.requestA();
adapter.requestB();
}
}

Consequences

Class Adapter (Inheritance)

  • 它只能让 Adaptee 的具体子类适应 Target。但它不能使一个 class 和它的子类都适应 Target。
  • 可以让 Adapter 重写 Adaptee 的一些行为。
  • 仅仅引入一个对象,并且不需要其他指针间接访问 adaptee。

Object Adapter (Composition)

  • Adapter 让 Adaptee 和它的所有子类都适应 Target。Adapter 可以同时为所有 Adaptee 增加功能。
  • 很难 override Adaptee 的行为。他需要参考 Adaptee 的子类,而不是自己作为它的子类。

Bridge

What

Bridge 模式解耦抽象与它的实现,因此它们可以独立地改变。抽象类和实现类没有通过 implement 关键字进行绑定,而是通过构造方法注入的方式,将实现类注入到抽象类中。

Why

Motivation

一个抽象有很多种可能的实现,通常使用 inheritance 来实现。定义一个 interface 去抽象,和具体的 subclasses 不同的方式去实现。但是这种方式不总是足够的灵活。inheritance 将 implementation 永久地绑定了 abstraction,这使得很难独立地去修改、扩展,以及重用 abstraction 和 implementation。

例子:Window 需要在两个平台(X Window System and IBM’s Presentation Manager)实现,可以定义一个抽象类 Window 和定义两个子类 XWindow 和 PMWindow。这个方法有两个缺点:1. 它不方便去扩展 Window 抽象在不同种类的 windows 或新的平台。支持新的 IconWindow 需要实现两个新的类 XIconWindow 和 PMIconWindow。支持新的平台需要每种 window 都需要新的 Window 子类。2. 它使得 client 代码是平台依赖的。

Client 应该能够创建 window 对象时不需要指定具体的实现。仅仅只有 window implementation 应该是依赖平台的,Windows 的 abstraction 不需要指定平台。

Applicability

  • 你想避免永久地绑定抽象和它的实现。
  • 抽象和它的实现都应该通过子类扩展。
  • 改变一个抽象的实现应该不会对 Client 有影响。
  • 你想要完全地隐藏一个抽象的实现。
  • 你的 classes 种类非常多。class 层级结构表明 classes 需要分离到两个部分。
  • 你想多个 objects 共享一个实现,并且这个事实应该对 client 隐藏。

Solution

Structure

Participants

  • Abstraction:定义 abstraction 的接口。维护一个对 Implementor 的对象的参考。
  • RefinedAbstraction:扩展 Abstraction 接口。
  • Implementor:定义 implementation 的接口。
  • ConcreteImplementor:实现 implementor 接口。

Collaborations

  • Abstraction 将 client 的请求转发到它的 implementor 对象。

Implementations

Click to expand!
abstract interface Abstraction{
protected Implementor implementor;
protected Abstraction(Implementor implementor){
this.implementor = implementor;
}
abstract public void operation();
}

public class AbstractionImpl{
public AbstractionImpl(Implementor implementor){
super(implementor);
}
public void operation(){
implementor.operationImpl();
}
}

public interface Implementor{
void operationImpl();
}

public class ImplementorImpl1{
public void operationImpl(){
System.out.println("operation implements by ImplementorImpl1...");
}
}
public class ImplementorImpl2{
public void operationImpl(){
System.out.println("operation implements by ImplementorImpl2...");
}
}

public class Client{
public static void main(String[] args){
Abstraction abstraction = new AbstractionImpl(new ImplementorImpl2());
abstraction.operation();
}
}

Consequences

Benefits

  • 解耦 interface 和 implementation。
  • 提升扩展性。你可以独立地扩展 Abstraction 和 Implementor 层级结构。
  • 可以对 client 隐藏 implementation 的实现。

Composite

What

将对象组成树形结构以表示部分-整体层次结构。Composite 让 client 统一地对待单一对象和组合对象。

Why

Motivation

可以将多个 components 组成更大的 components,每个节点可以是单个组件,也可以是多个组件的组合,每个节点用统一的接口对象表示。

例子:如画图编辑器,可以画线、多边形和文字,也可以是图文的方式。图文这种元素是多种组件的结合,我们想把组合元素和单一元素统一对待,减少代码的复杂性。我么可以使用 Composite 模式来实现这个功能。

Applicability

  • 你想表示对象的部分整体层次结构。
  • 你想让 Client 忽略组合对象和单一对象的区别。Client 统一地对待所有 Composite 接口的 Object。

Solution

Structure

Participants

  • Component:为 Composite 对象定义一个接口。为访问和管理子节点声明接口。
  • Composite:实现 Component 的类。它为有 Children 的组件定义行为。存储子组件。实现 child-related 操作。
  • Client:通过 Component 接口库操作 Composite 中的对象。

Collaborations

  • Client 使用 Component class interface 去与 Composite 结构中的 Objects 进行交互。

Implementations

Click to expand!
public interface Component{
void operation();
void add(Component component);
void remove(Component component);
Component getChildren(int index);
}

public class Composite implements Component{
private String name;
private List<Composite> compositeList;

public Composite(String name){
this.name = name;
}
public void operation(){
System.out.println("I am " + name);
if (compositeList != null){
for (Composite composite : compositeList){
composite.operation();
}
}
}
public void add(Composite composite){
if (compositeList == null){
compositeList = new ArrayList<Composite>();
}
compositeList.add(Composite)
}
public void remove(Composite composite){...}
public Composite getChild(int index){
if (index >= 0 && && composite != null && index < compositeList.size()){
return compositeList.get(index);
}
return null;
}
}

public class Client{
public static void main(String[] args){
Component parent = new Composite("parent");
Component child = new Composite("child");
parent.add(child);
parent.operation();
child.operation();
}
}

Consequences

Benefits

  • 定义由 primitive objects 和 composite objects 组成的 class 层级结构。Primitive objects 可以组合为更复杂的 objects。
  • 它使得 client 简单。 Client 可以统一地对待 composite structures 和 individual objects。
  • 更容易添加新类型的 components。

Drawbacks

  • 它使你的设计过于笼统。很难去限制 composite component。你需要自己在运行时检查对 composite component 的约束,如限制一个组合组件的子组件的个数。

Decorator

What

动态地给一个 object 附加额外的职责。Decorator 为子类提供了灵活的替代方案,以扩展功能。

Why

Motivation

有时我们想为 objects 添加职责,而不是整个 class。如,为文本阅读器添加 border 或者 scroll 等。

一种实现方式是通过 inheritance 来添加职责,这种方式是不灵活的,它是静态的,它为每一个实例都添加了这个职责,而且每次添加额外的职责都需要修改 class。另一种更灵活的方法是使用 Decorator 设计模式。它让你循环嵌套 decotrators,允许无限的添加职责。

Applicability

  • 动态地、透明地为单个 object 添加职责,没有影响其他的 objects。
  • 职责是可以被撤回的。
  • 当不能使用子类去扩展。

Solution

Structure

Participants

  • Component:定义组件接口。
  • ConcreteComponent:实现组件接口的 class。
  • Decorator:为 Component 对象维护一个引用。定义一个符合 Component 接口的接口。
  • ConcreteDecorator:为 component 对象添加职责的 class。

Collaborations

  • Decorator 将请求转发给它的 Component 对象。它可能选择性地在请求之前和之后执行额外的操作。

Implementations

Click to expand!
public interface Component{
pubilc String operation();
}

public class ConcreteComponet implements Component{
public String operation(){
return "concreteComponent operation..."
}
}

public interface Decorator extends Component{}

public class ConcreteDecorator implements Decorator{
private Component component;
public ConcreteDecorator(Component component){
this.component = component;
}
public String operation(){
return this.component.operation() + "ConcreteDecorator1 operation..."
}
}

public class Client{
public static void main(String[] args){
Component component = new ConcreteComponent();
component = new ConcreteDecorator(component);
System.out.println(component.operation());
}
}

Consequences

Benefits

  • Decorator 比静态 inheritance 更灵活。
  • 避免功能丰富的 classes 增加层级结构。

Drawbacks

  • decorator 和它的 component 不是同一个 object。当你使用 decorator 时,你不应该依赖一个对象。
  • 系统存在大量很小的 objects。

Facade

What

为一个 subsystem 中的一组接口提供一个统一的接口。Facade 定义了一个更高层级的接口,它使得 subsystem 更容易使用。

Why

Motivation

将系统构建为子系统有助于降低复杂性。一个普遍的设计目标时最小化系统之间的交流和依赖。一种实现这个目标的方式是使用 Facade 模式。

Applicability

  • 你想为复杂的子系统提供一个简单的接口。
  • 在 Client 和抽象类的实现之间有很多依赖。使用 Facade 去解耦子系统与 client 和其他子系统,从而提升子系统的独立性和可移植性。
  • 你想将你的子系统分层。使用 Facade 为每个子系统定义一个接入点。

Solution

Structure

Participants

  • Facade:知道 subsystem 中的哪个 class 能处理哪个 request。将 client 的请求委托给合适的 subsystem 的 objects。
  • subsystem classes:subsystem 的 classes。

Collaborations

Implementations

Click to expand!
public interface SubSystemInterface{
public String handleRequest();
}
public class SubSystemClass1 implements SubSystemInterface{
public String handleRequest(){
return "SubSystemClass1 return result...";
}
}
public class SubSystemClass2 implements SubSystemInterface{
public String handleRequest(){
return "SubSystemClass2 return result...";
}
}

public class Facade{
public String handleRequest1(){
return new SubSystemClass1().handleRequest();
}
public String handleRequest2(){
return new SubSystemClass2().handleRequest();
}
}

public class Client{
public static void main(String[] args){
Facade facade = new Facade();
String result1 = facade.handleRequest1();
String result2 = facade.handleRequest2();
}
}

Consequences

Benefits

  • 它对 client 隐蔽子系统的 components,减少 client 需要处理的 objects 的数量,以及使得 subsystem 更容易使用。
  • 它减少了 subsystem 和 clients 之间的耦合。
  • 它没有阻止应用程序在需要时使用子系统的 classes。因此你可以在易用性和通用性之间进行选择。

Flyweight

What

通过共享可以有效地支持大量细粒度的对象。

Why

Motivation

一个应用存在大量的相同的元素,如果每个元素都使用一个对象去表示,那么会出现大量内容相似的对象,这样是没有必要的,造成空间的浪费。我们可以通过共享对象来处理这个问题。

例子:文档编辑器由文本编辑和格式化的功能。一个文档中如果每一个 character 创建一个对象,那么会出现大量的对象。我们可以每一种字符创建一个对象,然后所有相同类型的字符都共享一个相同的对象。

Applicability

  • 应用使用了大量的对象。
  • 对象的存储花费很高。
  • 大部分对象是没有本质区别的,是可以共享相同对象的。
  • 应用不依赖对象的本身。

Solution

Structure

Participants

  • Flyweight:定义一个接口,Flyweight 可以通过该接口来接收外部状态,并对其存储的内部状态进行操作。
  • ConcreteFlyweight:实现 Flyweight 接口的类。它存储 intrinsic state。它的对象是可被分享的。
  • UnsharedConcreteFlyweight:不需要分享的 Flyweight 的 subclasses。不是所有的 Flyweight subclasses 需要被分享。
  • FlyweightFactory:创建和管理 Flyweight 对象。确保 Flyweight 是正确地分享的。
  • Client:维护对 Flyweight 的引用。计算和存储 Flyweight 的 extrinsic state。

Collaborations

  • Intrinsic state 在 ConcreteFlyweight 对象中存储,Extrinsic state 在 Client 对象中存储和计算。当调用对象的操作时,Client 传递这个 state 给 flyweight。
  • Client 不应该直接实例化 ConcreteFlyweight。Client 必须通过 FlyweightFactory 对象来获取 ConcreteFlyweight 对象,以确保它们是正确地分享。

Implementations

Click to expand!
public interface Flyweight{
void operation(String extrinsicState);
}

public class ConcreteFlyweight implements Flyweight{
public String intrinsicState;
public ConcreteFlyweight(String intrinsicState){
this.intrinsicState = intrinsicState;
}
public void operation(String extrinsicState){
return intrinsicState;
}
}

public class FlyweightFactory{
Map<key, Flyweight> flyweightList = new HashMap<>();

public Flyweight getFlyweight(String key){
if (flyweightList.keySet().contains(key)){
return flyweightList.get(key);
}else{
Flyweight newFlyweight = new ConcreteFlyweight(key);
flyweightList.add(newFlyweight);
return newFlyweight;
}
}
}

public class Client{
FlyweightFactory flyweightFactory = new FlyweightFactory();
Flyweight flyweight = flyweightFactory.get("a");
flyweight.operation("print")
}

Consequences

Benefits

  • Flyweight 是节省空间的,它减少了对象实例的数量,对象被共享的越多节省越多的空间。

Drawbacks

  • Flyweight 可能带来与 transferring, finding, computing extrinsic state 相关的 run-time costs。

Proxy

What

Proxy 为另一个对象提供了一个代孕(Surrogate)或占位符(Placeholder)去控制对这个对象的访问。

Why

Motivation

控制访问一个对象的原因是推迟它的创建和初始化的全部花费,直到我们真正需要使用它。如,文档编辑器可以在文档中嵌入图形对象。创建一些非常大的图像是十分昂贵的。一般来说,打开一个文档应该越快越好,所以我们应该避免在文档打开的时候立刻创建昂贵的对象。解决这个问题可以使用另一个 image proxy 对象,而不是 image 对象,proxy 对象作为真实 image 的替身。

Applicability

  • Remote proxy。为在不同地址空间的对象提供一个本地的代表。
  • Virtual proxy。创建昂贵的对象 on demand。
  • Protection proxy。通过控制访问来保护原始对象。
  • Smart reference。在访问对象时执行额外的操作。

Solution

Structure

运行时 proxy 结构

Participants

  • Proxy:1)维护一个引用,让 proxy 访问 real subject。2)提供一个与 Subject 相同的接口,使得 proxy 可以代替 real subject。3)控制对 real subject 的访问,以及可能有创建和删除它的职责。
  • Subject:为 RealSubject 定义一个公共接口。
  • RealSubject:定义 proxy 表示的 real object。

Collaborations

  • Proxy 在适当的时候将请求转发给 RealSubject,具体取决于 proxy 的类型。

Implementations

Click to expand!
public interface Subject{
void request();
}

public class RealSubject implements Subject{
public void request(){
System.out.println("request to RealSubject...");
}
}

public interface Proxy extends Subject{}

public class ConcreteProxy implements Proxy{
RealSubject realSubject = new RealSubject();
public void request(){
System.out.println("before ...");
realSubject.request();
System.out.println("after ...");
}
}

public class Client{
public static void main(String[] args){
Subject subject = new ConcreteProxy();
subject.request();
}
}

Consequences

Benefits

  • remote proxy 可以隐藏位于不同地址空间的对象。
  • virtual proxy 可以执行优化。如,按需创建对象。
  • protection proxy 和 smart reference 在对象被访问的时候允许额外的看管。

References

[1] Design Patterns: Elements of Reusable Object-Oriented Software by Erich Gamma, Richard Helm, Ralph Johnson and John Vlissides

[2] Difference between object adapter pattern and class adapter pattern

本篇将介绍设计模式中的5种创建型模式,其中设计模式的实现代码使用 Java 语言描述。

Creational Design Patterns

创建型设计模式是抽象实例化过程。它们帮助使系统独立于对象是如何创建、如何组合和如何表示的。

创建型模式封装了系统使用的具体的 classes 的细节,隐藏了 classes 的 objects 是如何创建的和如何一起协作的,系统最多知道对象的接口或抽象类是什么。创建型模式给你很大的灵活性在“创建什么”,“谁创建的”,“怎样创建的”和“什么时候创建的”等方面。

Abstract Factory

What

Abstract Factory 模式提供了一个接口来创建相关的对象家族,而不用指定它们具体的 classes。

Why

Motivation

我们想要整体的改变多个 objects,从一种类型的对象家族改变为另一种类型。

例子:一个应用可以切换不同的外观,主要是切换 widgets 的样式,widgets 包括:scroll bars,windows,和 buttons。不同的风格需要创建不同的 objects,我们希望风格切换时,可以轻易地创建不同风格的 widgets 对象。

Applicability

  • 一个系统它的产品是如何 created,composed,和 represented 应该是独立的。
  • 一个系统应该是配置多个产品家族中的一个。
  • 相关的家族产品对象是设计成一起使用。
  • 你想要提供一个产品的类库,只暴露它们的接口,不暴露它们的实现。

Solution

Structure

Participants

  • AbstactFactory:提供创建抽象产品对象的接口。
  • ConcreteFactory:实现创建具体产品对象的操作。
  • AbstractProduct:定义一类产品对象的接口。
  • ConcreteProduct:具体的产品对象。
  • Client:仅仅使用 AbstractFactory 和 AbstractProduct classes 去创建家族产品对象。

Collaborations

在运行时,正常只有单个 ConcreteFactory class 实例是创建的。这个 concrete factory 创建产品对象有特定的实现。创建不同的产品对象,client 应该使用不同的 concrete factory。

AbstractFactory 将产品对象的创建推迟到它的 subclass ConcreteFactory.

Implementations

Click to expand!
interface AbstractFactory{
abstract ProductA createProductA();
abstract ProductB createProductB();
}

class ConcreteFactory1 implements AbstractFactory{
public ProductA createProductA(){
return new ProductA1();
}
public ProductB createProductB(){
return new ProductB1();
}
}

class ConocreteFactory2 implements AbstractFactory{
public ProductA createProductA(){
return new ProductA2();
}
public ProductB createProductB(){
return new ProductB2();
}
}

interface ProductA{}
class ProductA1 implements AbstractProductA{}
class ProductA2 implements AbstractProductA{}

interface ProductB{}
class ProductB1 implements AbstractProductB{}
class ProductB2 implements AbstractProductB{}

public class FactoryProvider{
public static AbstractFactory getFactory(String choice){
return "1".equals(choice) ? new ConcreteFactory1() : new ConcreteFactory2();
}
}

public class Client{
public static void main(String[] args){
AbstrsctFactory factory = FactoryProvider.getFactory("1");
ProductA productA = factory.createProductA();
ProductB productB = factory.createProductB();
}
}

Consequences

Benefits

  • 它隔离具体的 classes。
  • 它使得改变产品家族对象变得容易。
  • 它提升了产品对象的一致性。

Drawbacks

  • 支持新种类的产品是困难的。他需要修改 AbstractFactory 接口和它的子类。

Builder

What

将复杂对象的构造与其表现分开,因此相同的构造过程可以创建不同的表现。

Why

Motivation

不改变过程,构造不同的对象。

例子:阅读器软件可以将 RTF(Rich Text Format)文档转换为很多其他的文本格式。每一种格式转换处理器对应一个 Converter 对象,它们都是 TextConverter 的子类。我们想要不改变阅读器转换的处理逻辑,轻易的增加新的格式转换处理器。

Applicability

  • 创建一个复杂的对象的算法应该是独立于组成对象的部分及其组装方式。
  • 对象构造过程必须允许不同的表现。

Solution

Structure

Participants

  • Bulider:指定一个创建产品对象的抽象接口。
  • ConcreteBuilder:实现 Builder 接口,构造和装配产品对象。
  • Director:使用 Builder 接口构造对象。
  • Product:要被创建的产品对象。

Collaborations

  • Client 创建 Director 对象和配置一个想要的 Builder 对象。
  • Director 通知 builder 产品对象什么时候应该构建。
  • Builder 处理来自 director 的请求。
  • Client 从 builder 中取出产品对象。

Implementations

Click to expand!
interface Builder{
void buildComponet1();
void buildComponet2();
}
public class ConcreteBuilder1 implements Builder{
Product product;
public ConcreteBuilder1(){
this.product = new Product();
}
void buildComponet1(){
this.product.setComponet1(xxx);
}
void buildComponet2(){
this.product.setComponet2(xxx);
}
public Product getProduct(){
return this.product.
}
}
public class ConcreteBuilder2 implements Builder{
Product product;
public ConcreteBuilder2(){
this.product = new Product();
}
void buildComponet1(){
this.product.setComponet1(xxx);
}
void buildComponet2(){
this.product.setComponet2(xxx);
}
public Product getProduct(){
return this.product.
}
}

public class Director{
private Builder builder;
public Director(Builder builder){
this.builder = builder;
}
public void construct(){
this.builder.buildComponet1();
this.builder.buildComponet2();
}
pubilc Product getProduct(){
return this.builder.getProduct();
}
}

public class Client{
public static void main(String[] args){
Builder builder = new ConcreteBuilder1();
Director director = new Director(builder);
director.construct();
Product product = director.getProduct();
}
}

Consequences

Benefits

  • 它让你轻易改变一个产品内部的表现。
  • 它隔离了构建和表现得代码。构建过程不变,可以有不同的表现。
  • 它让你更好的控制构建过程。

Drawbacks

  • 改变产品的内部表现,需要定义不同的 Builder 子类。

Factory Method

What

定义一个创建对象的接口,但是让子类决定那个类应该被实例化。Factory Method 让一个类的实例化延迟到子类。

Why

Motivation

使用抽象对象维持关系,一个抽象类型的具体子类决定另一个抽象类型的具体子类。把对象的实例化延迟到子类。

例子:文本编辑器可以创建多种格式的文档,不同的子应用决定了不同的文档类型,如 WPS 软件可以创建 word,excel,ppt 等文档。系统只知道创建一个应用就要创建一个对应的文档,但是不能预测文档 Document 的哪个子类被实例化。可以通过 Factory Method 解决这个问题,封装 Document 具体子类的创建过程,将它从客户端处理过程分离。

Applicability

  • 一个必须被实例化的 class 不能被实例化。
  • 一个 class 想要它的子类去指定哪个对象要被创建。

Solution

Structure

Participants

  • Product:定义要被创建的对象的接口。
  • ConcreteProduct:实现 Product 的具体的类。
  • Creator:声明 factory method,返回 Product 类型的对象。
  • ConcreteCretor:override factory method 返回一个 ConcreteProduct 的实例。

Collaborations

  • Creator 依靠它的子类去返回一个合适的 ConcreteProduct 实例。

Implementations

Click to expand!
public interface Product{}

public class ProductA implements Product{}

public class ProductB implements Product{}

public interface Creator{
Product createProduct();
}

public class CreatorA implements Creator{
Product createProduct(){
return new ProductA();
}
}

public class CreatorB implements Creator{
Product createProduct(){
return new ProductB();
}
}

public class Client{
Creator creator = new CreatorA();
Product productA = creator.createProudct();
}

Consequences

Benefits

  • 消除绑定具体的子类的代码。代码仅仅处理 Product 接口。
  • 更灵活的创建对象。
  • 连接平行的类的层次结构。

Drawbacks

  • Client 必须通过实例化 Creator 子类去创建一个特定的 ConcreteProduct 对象。当 ConcreteProduct 类增加时,ConcreteCreator 类也需要增加 。

Prototype

What

指定使用 prototype 实例创建的对象的类型,并且通过拷贝这个 prototype 去创建新的对象。

Why

Motivation

想要重复创建类似的对象,且这个对象的创建过程比较复杂。

例子:一个可以编辑音乐谱的编辑器。他需要重复的添加多个音符。

Applicability

  • 一个系统它的产品对象的创建,组合,和表现应该是独立的。
  • 一个 class 的实例化在运行时指定的。
  • 很方便的去克隆大量的对象,而不是手动实例化。

Solution

Structure

Participants

  • Prototype:定义一个克隆自己的接口。
  • ConcretePrototype:实现克隆自己的操作。
  • Client:通过请求 prototype 克隆方法来创建对象。

Collaborations

  • Client 请求 prototype 去克隆自己。

Implementations

Click to expand!
public interface Prototype extends Cloneable{
Prototype clone();
}
public class ConcretePrototype1 implements Prototype{
private String name;

public ConcretePrototype1(){
this.name= "ConcretePrototype1-aha";
// simulating complex construction process
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}

public ConcretePrototype1 clone() {
Object prototype = null;
try {
prototype = super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return (ConcretePrototype1) prototype;
}
}
public class ConcretePrototype2 implements Prototype{
//... same with ConcretePrototype1
}
public class Client{
public static void main(String[] args){
ConcretePrototype1 concretePrototype1 = new ConcretePrototype1;
ConcretePrototype1 copy1 = concretePrototype1.clone();
System.out.println(copy1.equals(concretePrototype1));
}
}

Consequences

Benefits

  • Prototype 有一些和 Abstract Factory 和 Builder 模式类似的优点,即对 client 隐藏具体的产品类,这些模式让 client 使用不同的具体的子类,却不用修改 Client 实现逻辑。
  • 在运行时动态地增加或去除产品对象。

Drawbacks

  • 每一个 Prototype 必须实现 clone 操作,这个 clone 操作的实现有可能是复杂的。如,不支持拷贝或存在循环参考。

Singleton

What

确保一个 class 仅有一个实例,并且提供一个全局的访问它的方法。

Why

Motivation

有些 class 必须只有一个实例。如,一个系统应该仅有一个文件系统和一个窗口管理器。

Applicability

  • 一个 class 必须有且仅有一个实例,并且有一个全局访问的接入点。

Solution

Structure

Participants

  • Singleton:它负责去创建一个唯一的实例。它提供一个获取实例的操作,让 client 得到唯一的实例。

Collaborations

  • Client 通过 Sigleton 的获取实例操作,获取到唯一的 Singleton 实例。

Implementations

  • Private constructor method.
  • Public getInstance() method.
Click to expand!
// Hungry
public class Singleton{
private final static Singleton instance = new Singleton();
private Singleton(){}
public static Singleton getInstance(){
return instance;
}
}

// Lazy 1 by lazily create instance.
public class Singleton
{
private static Singleton instance;
private Singleton() {}
public synchronized static Singleton getInstance()
{
if (instance == null)
{
synchronized (Singleton.class)
{
if (instance == null)
{
instance = new Singleton();
}
}
}
return instance;
}
}

// Lazy 2 by Inner Class
public class Singleton
{
private Singleton() {}

private static class Inner
{
private static final Singleton INSTANCE = new Singleton();
}

public static Singleton getInstance()
{
return Inner.INSTANCE;
}
}

Consequences

Benefits

  • 控制访问唯一的实例。

References

[1] Design Patterns: Elements of Reusable Object-Oriented Software by Erich Gamma, Richard Helm, Ralph Johnson and John Vlissides

欢迎来到设计模式系列文章,这是设计模式系列文章的第一篇,本篇将介绍设计模式的基本概念,主要介绍“什么是设计模式”,“为什么要使用设计模式”,以及“如何使用设计模式”等内容。

设计面向对象软件是困难的,设计可重用的面向对象软件则更加困难。你必须找到相关的 objects,以适当的 granularity 将他们分解为 classes,定义 class interfaces 和 inheritance hierarchies,以及在他们之间建立关系。你的设计应当解决当前的问题,也要足以解决未来的问题和需求。你还要避免重复设计,或者最小化重复设计。做好这些事情不是一件简单的事情,设计模式可以快速的帮助我们有效的解决复杂问题。

What is Design Patterns?

设计模式是描述特定环境下重复出现的问题和问题解决方案的核心内容,且这个方案可以重复的使用。具体来说,它描述了 objects 和 classes 之间的交流,它是定制化地去解决一个在特定场景下的设计问题。

一般来说,一个设计模式包含以下四个基本元素:

  • Pattern Name。模式名称可以用于描述问题和它的解决方案。它是对设计的高度的抽象。模式的专业词汇,能够方便我们去讨论交流和写文档等。
  • Problem。问题描述了我们什么时候要使用这个模式。它解释了问题和模式的应用场景。它可能描述一个不灵活的 class 和 object 结构设计的特征,通过这些特征可以知道应该通过哪些设计模式去解决这些问题。
  • Solution。抽象地描述问题和一般的解决方案。它描述了设计方案,以及 classes 或 objects 之间的 relationships,responsibilities,和 collaborations 等。
  • Consequences。应用一个模式的结果和 trade-offs。它描述了应用一个模式的 costs 和 benefits。

Why design patterns are needed?

设计模式是帮助我们快速解决特定场景的设计问题,帮助我们设计一个可重用、可扩展性的应用。它主要有以下几个优点:

  • 设计模式使得重用成功的设计和架构变得更加容易。
  • 设计模式通过提供 class 和 object 相互作用和它们的潜在意图的明确规范,来提升对存在系统的文档编写和维护。
  • 设计模式可以帮助设计者更快的得到一个正确的设计。

How Design Patterns Solve Design Problems

面向对象程序是由 objects 组成的。一个 object 包含 data 和 procedures。当 object 接收到来自 client 的请求时,执行相关的操作。请求只能让 object 执行一个操作。操作是唯一改变 object 内部数据的方式。object 内部状态是 encapsulated,对外部是不可见的。

面向对象最难的部分就是把一个系统 decomposing 为 objects。这个任务是很复杂的,因为它涉及到很多因素。如:encapsulated,granularity,dependency,flexibility,performance,evolution,reusability 等等。这些影响因素往往是相互冲突的,需要有所权衡,然而,兼顾这么多因素不是一件很容易的事。设计模式通过考虑这些因素,针对特定的应用场景的问题,提供了一个有效的解决方案。

How to Select a Design Pattern

常见的设计模式有20多种,我们应该怎么去选择呢?下面列出了一些找到合适的设计模式的方法:

  • 考虑设计模式是如何解决问题的。
  • 查看每个设计模式的意图和目的。
  • 思考设计模式是如何相互关联的。设计模式之间的关系。
  • 检查导致重复设计的原因。看你的设计是否有类似的问题,看哪些模式可以帮助避免重复设计。
  • 考虑你的设计中什么是变化的。

How to Use a Design Pattern

当你选择好了设计模式之后,你可以通过以下步骤将设计模式应用在你的程序中。

  1. 查看该模式的 Applicability 和 Consequence 部分的描述内容,确定你选择的模式能够正确地解决你的问题。
  2. 学习该模式的 Structure,Participants 和 Collaborations 部分内容,确保你理解了 classes 和 objects 在模式中是如何关联的。
  3. 查看具体的代码实现例子。学习如何实现该模式。
  4. 选择对于你的应用环境有意义的 Participants 的名称。如使用 Strategy 模式设计文本组合算法,你可以命名为 SimpleLayoutStrategy,TeXLayoutStrategy。
  5. 定义 Classes。具体为:声明 interfaces,建立继承关系,定义对象的变量。
  6. 定义该模式在具体应用中的classes 的操作名称。即定义有意义的方法名称。如在 factory method 模式中,可能使用 create- 为前缀的方法名称。
  7. 实现定义好的方法,实现模式中的 responsibilities 和 collaborations。

References

[1] Design Patterns: Elements of Reusable Object-Oriented Software by Erich Gamma, Richard Helm, Ralph Johnson and John Vlissides

前几个月我从0到1完成了 hot-crawler 这个网站项目,它也是我个人的第一个网站,至今(2019年12月)已稳定运行了四个多月。功能虽然很简单,但是整个过程却十分的艰难。当时完全没想到开发一个网站需要做这么多事情,很多事情都是第一次做,只能遇到问题解决问题,硬着头皮上。当时对完整的开发流程不是很清楚,做的时候基本上是想到什么做什么,流程可能不完善或者不正确,但从0到1实现一个网站要做的事情基本上都做了。根据之前的经验和整理,下面按照我自己的理解,介绍一下从0到1创建一个网站的大致过程。

I. 设计阶段

这个阶段主要是构思想法、收集需求、设计功能和时间计划。

主要的工作:

  • 需求分析。
  • 原型和UI设计。
  • 系统设计。
  • 时间规划和任务划分。

产出的结果:

  • 用户需求和设计
    • 软件需求规格说明书(User/Software Requirements Specification, SRS)
    • 确定网站域名,网站名称,LOGO,Slogan。
    • 系统交互原型图。
    • UI 设计图。
  • 软件设计
    • 软件架构文档(Software Architecture Document)
    • 数据库设计文档(Database Design Document)
    • API 文档(API Documentation)
    • 软件详细设计文档(Software detailed design)
    • 开发流程规范文档
  • 过程文档(Process Documentation)
    • 项目计划,估计和时间表。(Plans, estimates, and schedules.)

What is plans, estimates, and schedules in project management?

Click to expand!

Plans: A project plan outlines the objectives, scope, deliverables, resources, and timelines of a project. It serves as a roadmap that guides the project team throughout the project lifecycle. A project plan includes various sections such as project scope, milestones, work breakdown structure (WBS), resource allocation, risk management, and communication plan.

Estimates: Project estimates involve estimating the time, effort, and cost required to complete project activities. These estimates are crucial for budgeting, resource allocation, and determining project feasibility. Estimation techniques such as expert judgment, analogous estimating, parametric estimating, and three-point estimating are commonly used to predict project durations, costs, and resource requirements.

Schedules: A project schedule is a timeline that details the start and end dates of project activities. It helps in visualizing the project’s progress and ensuring that tasks are completed in a logical sequence. Gantt charts, network diagrams (e.g., PERT/CPM), and critical path analysis are tools commonly used to create and manage project schedules. Schedules also consider dependencies between tasks, resource availability, and project constraints to ensure timely completion.

II. 代码实现阶段

这个阶段主要是根据软件需求文档、软件设计文档和开发流程规范文档进行项目代码开发。

主要的工作:

  • 配置服务器。具体包括:购买服务器,搭建项目运行环境,部署项目。
  • 配置域名。具体包括:购买域名,DNS 解析,Nginx 反向代理,HTTPS。
  • 搭建持续集成(CI/CD)。具体包括:创建 Git 仓库,添加 gitignore,搭建或购买 CI 服务,配置 Docker,配置 Jenkins。
  • 后端代码实现。具体包括:创建项目,创建数据库,配置数据源,集成第三方类库,编写单元测试,编写模块代码。
  • 前端代码实现。具体包括:构建项目,实现UI布局,实现交互动作,兼容多个终端(PC,Mobile,Tablet 等)
  • 前后端对接。

产出的结果:

  • 功能完整和可运行的前后端的代码。
  • 项目说明文档(Source Code Document)。

III. 功能测试阶段

这个阶段主要是为预发布做准备。全面的系统功能测试、bug修复、功能优化等。

主要的工作:

  • 功能测试。
  • Bug 修复。
  • 前端功能优化。如,优化样式,兼容PC端和移动端。
  • 后端功能优化。如,可配置,可扩展,和用户体验优化等。

产出的结果:

  • 软件能够达到需求文档的所有要求。

IV. 性能测试和性能优化

这个阶段主要是为发布前做准备,保证服务的高性能、高可用和安全性等。

主要的工作:

  • 性能测试(Performance Testing)
    • 负载测试(Load Testing)。测试软件系统是否达到需求文档设计的目标,譬如软件在一定时期内,最大支持多少并发用户数,软件请求出错率等,测试的主要是软件系统的性能。
    • 压力测试(Stress Testing)。测试硬件系统是否达到需求文档设计的性能目标,譬如在一定时期内,系统的 CPU 利用率,内存使用率,磁盘I/O吞吐率,网络吞吐量等,压力测试和负载测试最大的差别在于测试目的不同。
    • 容量测试(Volume Testing)。确定系统最大承受量,譬如系统最大用户数,最大存储量,最多处理的数据流量等。
  • 高性能
    • 算法实现优化。
    • JVM 优化。
    • 数据库优化。如,数据库表结构、索引和SQL优化。读写分离,分库分表。
    • 添加缓存和搜索引擎等。
    • 静态资源 CDN。
  • 高可用
    • 应用服务高可用。如,1. 详细的日志记录。2. 软件程序作为 Linux 服务,设置崩溃后可自动重启。3. DNS 负载均衡。4. Nginx 负载均衡。
    • 数据库高可用。如,数据库集群,数据备份。
    • 热部署、热更新。
    • 云监控(阿里云监控)。
  • 安全性
    • 设置系统防火墙。
    • 设置软件防火墙。
    • SSH 公钥登录。
    • 防止 SQL 注入和 XSS 攻击。
    • 防止 DDoS 攻击。

产出的结果:

  • 应用服务具备高性能、高可用和安全性等特点。

IV. 预发布和内测

这个阶段主要是小范围的推广试用,收集反馈意见,持续打磨和优化。

主要的工作:

  • 推广。
  • 收集意见。
  • 持续改进和优化。

产出的结果:

  • 软件功能完善、体验良好。

VI. 正式发布和推广

网站正式发布,全面推广。

VII. 日常维护

这个阶段的主要是潜在问题修复,用户体验优化,和功能调整等。

主要的工作:

  • 潜在问题缺陷发现和修复。
  • 优化用户体验。

产出的结果:

  • 保证应用服务的正常运行。
  • 功能持续调整和优化。

VIII. 其他

其他一些优化网站的事情。如下:

  • 埋点,数据采集和分析。统计用户行为,分析数据优化功能。

  • SEO。

References

[1] Technical Documentation in Software Development: Types, Best Practices, and Tools

本篇将介绍数据库设计相关的概念。主要分为两个部分:数据库概念模型设计和关系型数据库设计。它们分别对应的设计方法是 E-R 模型(Entity-Relationship Model)和标准化(Normalization)。

数据库设计和 E-R 模型

数据库设计过程

设计过程

  • 需求分析阶段。与用户和领域专家交流,收集需求,充分地描述出预期的数据库用户需要的数据。这个阶段产出的是用户需求的规范
  • 概念设计阶段。选择一个数据模型,将用户需求转换为数据库概念模式。这个阶段主要是专注于描述数据和数据之间的关系。用专业的、概念的数据模型去描述数据结构,主要使用 entity-relationship model 和 normalization 方式去设计。这个阶段一般产出的是实体关系图(E-R图)。完整的概念设计 schema 还包含功能需求规范。描述用户对数据的操作类型,如修改,查询和取出具体的数据,删除数据。
  • 逻辑设计阶段。将抽象的数据模型转换为数据库的实现。将概念模型转换为数据库系统使用的模型,如将 entity-relationship model 转换为 relational model。
  • 物理设计阶段。设计物理存储的细节。包括文件组织的形式和选择索引结构。

物理的 schema 相对来说是比较容易改变的,而改变逻辑的 schema 是很难的,可能会影响到大量分散在应用程序中的查询和更新代码。因此,在构建应用程序之前,必须谨慎地和考虑周全地进行数据库设计。

设计中常见的问题

  • 冗余(Redundancy)。冗余的信息表示可能导致重复的信息出现不一致。如一些过时的或错误的信息依然存在数据库中。信息应该准确的出现在一个地方。
  • 不完全(Incompleteness)。一个坏的设计可能会使企业的某些方面难以建模或无法建模。不合理的分解关系的属性,导致难以插入一个新增的数据,或者必须其它属性设为 null。如,没有单独的课程表,只有课程设置表,当新增课程时,无法插入课程属性到课程设置表。

E-R 模型

E-R 模型可以将现实世界中企业的含义和相互作用映射到一个概念的模式。E-R 数据模型采用三个基本的概念:实体集,关系集,和属性。

Entity Sets

实体(Entity)是现实中的一个事情或一个对象,每个对象是与其它对象有区别的。如大学中的一个人就是一个实体。一个实体有一组属性,它的属性值可以唯一标识一个实体。如,一个人可能有一个身份证号码属性,这个属性值可以唯一的标识一个人。一个实体可以是具体的,如一个人或一本书等,也可以是抽象的,如一个课程或一个机票预订等。

实体集(Entity Set)是一组有相同类型的实体,它们共享相同的属性。

Relationship Sets

关系(Relationship)是几个实体之间的关联。如一个老师实体和一个学生实体之间有一个指导的关系。

关系集(Relationship Sets)是一组相同类型的关系。多个实体集之间的关系总和构成了关系集。实体集之间的关联称为参与,如 实体集 E1,E2… 参与关系集 R。实体在关系中扮演的功能称为该实体的角色

相同的实体集之间的可能超过一个关系集。两个实体集之间的称为二元关系集(Binary Relationship Set)。然而,关系集可能涉及超过两个实体集。参与一个关系集的实体集的数量称为Degree of the relationship set。二元关系集的 degree 是 2,三元关系集的 degree 是 3。

Attributes

每一个属性有一组允许的值,称为 domain 或者 value set。每一个实体可以用一组属性和属性值对来描述。

E-R 模型中的属性类型有:

  • 简单(Simple)和组合(Composite)属性。组合属性可以分为多个子部分,如 name 属性可以由 first_name, middle_name, 和 last_name 组合而成。
  • 单值(Single-valued)和 多值(Multivalued)属性。单值属性表示只能出现一个值,如 ID 属性只能一个,多值属性表示可以有多个值,如 phone_number 可以有多个电话号码。
  • 派生属性(Derived)。这类属性的值可以从其它相关属性的值得出。如 age 可以从 date_of_birth 属性值得出。

一个属性为 null 值表示不存在或者未知。

Constraints

E-R 模型中主要有两种约束:映射基数和参与约束。

映射基数(Mapping Cardinality)主要用于描述二元关系集。映射基数的类型有:

  • One-to-one。一个实体 A 最多关联一个实体 B。
  • One-to-many。一个实体 A 可以关联多个实体 B。
  • Many-to-one。多个实体 A 可以关联一个实体 B。
  • Many-to-many。一个实体 A 可以关联多个实体 B。一个实体 B 也可以关联多个实体 A。

参与约束(Participation Constraints)表示一个实体集参与一个关系的数量。主要有两种参与约束:

  • Total。它表示一个实体集所有实体都参与了一个关系集。
  • Partial。它表示一个实体集部分实体参与了一个关系集。

Keys

一个 key (键)是能够区分不同的实体的一组属性。Superkey 表示可以区分实体的一组属性。Candidate key 表示最小的 Superkey 即用来区分实体的最小的属性集。Primary key 与 candidate key 是相同的,只是在不同的地方的表述,Primary key 用在数据库的表述中,而另一个用在 E-R 模型中。

去除冗余的属性

设计一个数据库的 E-R 模型,通常是先确定有哪些实体集,为实体集选择合适的属性,为实体集建立合适的关系。一些属性同时出现在多个实体集中,为它们建立关系集,并取出重复的属性。

E-R 图

E-R 图可以图形化地表示数据库地总体的逻辑结构。E-R 图简单而清晰,被广泛使用在 E-R 模型中。

基本结构

E-R 图主要的组件有:

  • 分为两部分的长方形(Rectangle Divided):用来表示实体集。

  • 菱形(Diamond):表示关系集。

  • 没有划分的长方形(Undivided Rectangle):表示一个关系集的属性。

  • 线(Line):用来连接实体集和关系集。

  • 虚线(Dashed Line):用来连接一个关系集的属性到另一个关系集。

  • 双线(Double Line):用来指出一个实体集完全参与一个关系集。即表示参与约束。

  • 双线菱形(Double Diamond):表示关系集是被弱实体连接的。

  • 实体集的主键属性用下划线标识。

一个简单的 E-R 图,如下所示:

E-R 图映射基数的表示

E-R 图使用关系集到实体集之间的有向线(Directed Line)和无向线(Undirected Line)来表示 one-to-one,one-to-many 等映射基数。有向线表示 one,无向线表示 many。如下 E-R 图表示 student 与 instructor 的 many-to-one 关系。

更复杂的映射基数使用 l..h 表示,l 表示最小的基数,h 表示最大的基数。基数的值可以是 0 到无穷, * 表示无穷。常见的表示,如 1..1,0..* 。左边线上的基数表示右边实体集的参与数量,反之亦然。

弱实体集

一个实体集没有足够的属性去组成一个主键,即没有主键的实体集称为弱实体集(Week Entity Sets)。一个实体必须依赖另一个实体的属性才能完整,即实体需要添加其它实体的属性才能被唯一标识或者说能实现主键。

为了使弱实体集有意义,它必须关联另一个实体集,称为标识实体集(Identifying Entity Set)或所有者实体集(owner entity set)。每个弱实体必须与一个标识实体相关联; 就是说,弱实体集依赖于标识实体集而存在。 标识实体集拥有它标识的弱实体集。 将弱实体集与标识实体集相关联的关系称为标识关系(Identifying Relationship)。如 section(课时) 必须依赖 course (课程)的 cource_id。

更多的 E-R 图的表示,如:

  • 组合属性(Composite Attributes)

  • 角色(Role)

  • 非二元关系集(Nonbinary Relationship Sets)

这些 E-R 表示的内容,不详细解释了,如有兴趣可查阅文章最后给出的参考书籍。

一个大学的 E-R 图例子如下图所示:

E-R 模型转换为关系模型

E-R 模型和关系模型都是抽象的逻辑的表示真实世界。要实现数据库必须把 E-R 模型转换为数据库系统对应的数据模型。当前小节,我们描述如何将 E-R 模型转换为关系型数据库的关系模型。主要涉及两个问题:E-R schema 如何转换为 relation schema,E-R schema 中的约束如何转换为 relation schema 的约束。

强实体集的简单属性的表示

E-R 模型中的一个 Entity Set 的每个属性对应一个 Relation 的每个元组。

强实体集的复杂属性的表示

组合属性(Composite Attributes)。组合属性中的每一个属性作为一个单独的属性。

多值属性(Multivalued Attributes)。为多值属性单独创建一个表。如一个教师有多个手机号码,可以单独创建一个表 instructor_phone (ID, phone number)

派生属性(Derived Attributes)。派生是属性可以简单的作为表的属性。

弱实体集的表示

把依赖的实体集的属性与当前表属性组合成主键,同时为依赖的属性创建一个外键约束。

关系集的表示

  • many-to-many。组合所有参与实体集的主键作为关系集的主键。

  • one-to-one。任意选一个实体集的主键作为关系集的主键。

  • many-to-one or one-to-many。选择 many 那一边的实体集的主键作为关系集的主键。

  • n-ary without any arrows edges。组合所有参与实体集的主键作为关系集的主键。

  • n-ary with an arrow edge。组合所有不在箭头那边的实体集的主键作为关系集的主键。

Schema 的冗余

连接弱实体集和强实体集的关系集的 schema 是多余的。E-R 图中的这一类关系集不需要出现在数据库关系型模型中。

Schema 的结合

Schema 的结合表示:去除关系集,使它结合到实体集中。常见的结合如下:

  • one-to-one 中的关系集,可以结合在任一实体集的 schema 中。

  • many-to-one or one-to-many 的关系集,可以结合在 many 那边的实体集的 schema 中。

  • many-to-many 的关系集一般无法与实体集结合。

去除了关系集,一个实体集使用外键关联另一个实体集。

不完全参与关系结合后的实体集,可以使用 null 表示不存在的关联。

E-R 模型设计中的问题

有时实体集和关系集是不明确的,可能有大量不同的方式定义实体集和关系集。我们讨论一些基本的 E-R 模型设计的问题。

Entity Sets versus Attributes

一个实体有多值属性,这个多值属性是设计为一个属性还是一个实体集?

这需要看具体情况,如 instructor 实体集有一个多值属性 phone_number。如果这个多值属性的每个属性值需要维护额外的信息,如一个手机号码需要指定一个地点属性。这种情况下,多值属性需要设计成一个实体集 inst_phone(phone_number, location), inst_phone 实体集与 instructor 实体集建立一个关系集。

Entity Sets versus Relationship Sets

一个对象应该表示为实体集还是关系集,有时候它是不明确的。

如学生(student)实体集和课时(section)实体集之间的对象应该如何表示?可以表示为一个关系 take。也可以表示为 一个实体登记(registration) 和两个关系 section_reg 和 student_reg。

一个对象是使用关系集还是实体集一个重要的参考准则:关系集是实体集之间的动作。

Binary versus n-ary Relationship Sets

当存在多元关系集时,多元关系集应该使用二元关系集代替。

Placement of Relationship Attributes

关系集的属性在结合的实体集中。如 instructor 和 student 之间是 one-to-many 的关系,其中关系集 advisor 有一个属性 date 表示成为一个学生的指导者的日期。在结合的时候这个属性可以放在 student 实体集中。

扩展的 E-R 特性

Specialization

实体集包含实体子集,这些子集在某种程度上不同于集合中的其它实体。

设计子集实体集的过程称为 Specialization。一个例子如下图所示:

Generalization

Generalization 是 specialization 的逆过程。不同的实体集通过共同的属性提取为父集实体集。

Attribute Inheritance

Specialization 和 Generalization 中的高层的实体集被底层实体集继承。

Aggregation

E-R 模型中无法表示关系之间的关系。Aggregation 是一个把关系集作为更高层级的实体集的抽象。一个例子如下图所示:

Extended E-R Features Reduction to Relation Schemas

E-R 模型扩展特性 Generalization 如何在关系型 Schema 中表示

第一种方式是,为更高层的实体集创建一个 schema,为每一个低层的实体集创建一个 schema,低层的 schema 属性由它特有的属性和高层的主键组成,添加一个外键约束,底层的 schema 主键参考高层 schema 的主键。如一个高层实体集下的两个低层实体集的关系型 schema 的例子:

person(ID, name, street, city)
employee(ID, salary)
student(ID, tot_cred)

第二种方式是,当实体集只有两层,而且分化是完全的。可以不创建高层的 schema,只是创建第二层的 schema,底层的 schema 的属性由高层的属性和自己特有的属性组成。如下:

employee(ID, name, street, city, salary)
student(ID, name, street, city, tot_cred)

上面两种方式都是用高层的实体集的主键作为它们自己的主键。第二种方式的缺点是:1)没有创建高层的实体集的 schema,当一个关系集与高层实体集关联时,无法参考这个实体集。避免这个问题可以创建一个 person 表,只包含它的主键。2)如果是 overlapping generalization 即一个实体可以同时作为多个子对象,那么一些值会重复的存储在多个表中。3)如果 disjoint 是不完整的,如有的对象不属于任何一个子对象,那么第二种方式无法表示这个对象。

E-R 模型扩展特性 Aggregation 如何在关系型 Schema 中表示

把 aggregation 当作一个实体集。aggregation 的主键就是关系集的主键。

其它的数据建模方式

E-R 图没有统一的标准,不同的地方可能使用不同的标识或图形。E-R 图可以帮助我们对系统组件的数据表现进行建模,但是数据表现只有系统设计的一部分,系统设计还需要设计,如用户和系统的交互,系统的功能模块的规范等等。我们可以使用 UML (Unified Modeling Language)来表示系统的更多的设计。

数据库设计的其它方面

Schema 的设计只是数据库设计中的一部分。其它方面的设计也不可忽略。

  • 数据约束和关系型数据库设计。除了属性和关系,还是大量的数据约束需要设计,如主键约束,外键约束,检查约束,断言,和触发器等等。
  • 使用需求。性能要求,如吞吐量(Throughput),响应时间(Response Time)。
  • 数据库授权。
  • 数据流工作流。
  • 考虑未来可能的变化。

关系型数据库设计和标准化

关系型数据库设计的目标是生成一组关系 schema 使得存储信息没有不必要的冗余,以及高效地取出信息。实现这个目标可以通过设计一个满足范式(Normal Form)的 schema。

我们将介绍一种基于函数依赖(Functional Dependencies)的正式的关系数据库设计方法。 然后,我们根据函数依赖和其他类型的数据依赖定义范式。

好的关系型设计的特点

一个好的关系型数据库设计。

  • 不能出现太多的数据冗余,需要将冗余的信息属性进行适当的分解。
  • 不能过度的分解,使得丢失信息完整性。

关系型数据库设计的核心在于:在保证数据完整性的情况下,如何减少数据的冗余。以及如何在性能和冗余之间权衡。

Functional Dependencies

在介绍范式之前,我们需要先了解什么是函数依赖(functional dependencies)。

Notations

  • 使用 Greek Letters 表示 Functional Dependency 中的一组属性。如 α, β。
  • 使用小写 Roman Letter 加上在小括号中的大写 Roman Ltter 表示一个 relation schema,如 r(R),其中大写字母 R 表示一组属性。Greek Letter 表示的一组属性可能是部分属性或者全部属性,Roman Letter 一般表示全部属性。
  • 一组属性组成的 superkey 使用 K 表示。我们可以说 K 是 r(R) 的一个 superkey。
  • 我们使用小写表示关系。如 instructor。在定义或者算法中,使用单个字符表示关系,如 r 。

Functional Dependencies

表示一个 functional dependency 存在一个 schema 中的定义如下:

设 schema 为 r(R), α ⊆ R 且 β ⊆ R.

  • 给定的一个 r(R) 的实例中,如果实例中的所有的元组对 t1 和 t2 满足:若 t1[α] = t2[α], 那么 t1[β] = t2[β],则可以说这个实例满足 functional dependency α ⟶ β。
  • 如果在schema r(R) 的每一个合法的实例中,都满足一个 functional dependency α ⟶ β,则可以说这个 functional dependency α ⟶ β holds on schema r(R)。

使用 functional-dependency notation 表示一个 schema 的 superkey:如果 functional dependency K ⟶ R holds on r(R),则 K 是 r(R) 的一个 superkey。

具体的例子:使用 functional dependency 表示 inst_dept(ID, name, salary, dept_name, building, budge) 的 superkey:

ID, dept_name ⟶ name, salary, building, budget

Functional Dependencies 有两种用途:

  1. 测试给定的 relation 的一个实例是否满足一组给定的 functional dependencies F。
  2. 指定对合法的 relation 的约束。

Trivial

一些 functional dependencies 是 trivial,因为它们满足所有的 relations。例如 A ⟶ A,AB ⟶ A。

Trivial functional dependencies 的定义:如果 β ⊆ α,则 functional dependencies α ⟶ β 是 trivial。

Closure

所有可以从给定的 functional dependencies F 推导出来的 functional dependencies 集合称为 closure of F,表示为 F+。F+ 包含了所有 F 中的 functional dependencies。

Logically Implied

我们可以证明有其他的 functional dependencies 也 hold on the schema,我们可以说这些 functional dependencies 是 logically Implied by F。更正式地说,给定一个 schema r(R),如果满足 F 每一个 r(R) 的实例也满足 ⨍,则一个 functional dependency ⨍ 是通过一组 functional dependencies F 逻辑暗示的(logically implied)。

如给定一个 relation schema r (A, B, C, G, H, I) 和一组 functional dependencies:A ⟶ B, A ⟶ C, CG ⟶ H, CG ⟶ I, B ⟶ H。那么 A ⟶ H 是 logically Implied。

closure of F 即 F+ 是一组所有被 F logically implied 的 functional dependencies。

Axioms

通过一些公理(Axioms)可以找到一个relation schema 的 logically implied functional dependencies。Armstrong‘s Axioms 表示如下:

  • Relexivity rule。If α is a set of attributes and β ⊆ α, then α → β holds.
  • Augmentation rule。 If α → β holds and γ is a set of attributes, then γα → γβ holds.
  • Transitivity rule。 If α → β holds and β → γ holds, then α → γ holds.

Armstrong’s Axioms ,它是 sound,因为它们不生成任何不正确的 functional dependencies。它是 complete,因为对于给定的一组 functional dependencies F 它可以生成所有的 F+。

其它的公理:

  • Union rule。 If α → β holds and α → γ holds, then α → βγ holds.
  • Decomposition rule。If α → βγ holds, then α → β holds and α → γ holds.
  • Pseudotransitivity rule。 If α → β holds and γβ → δ holds, then αγ → δ holds.

Formal Forms

常见的范式(Formal Forms)有:第一范式,第二范式,第三范式,BC 范式,和第四范式。

Atomic Domains and First Normal Form

为了减少单个属性的数据冗余,我们常用的方法是:对于组合属性,如 address 由 street,city,state 和 zip 等组成,我们创建一个单独的表来表示这些属性。对于多值属性,我们让每一个多值属性中的每一项作为一个单独的属性。

在关系模型中,我们通过形式化(Formalize)来实现一个属性没有任何子结构。如果一个 domain 是不可再分的单元称这个 domain is atomic。我们定义:如果一个 relation schema R 中的所有属性的 domain 是 atomic,则称这个 schema R 是在 First Normal Form (1NF,第一范式) 中的。

Boyce-Codd Normal Form

Boyce-Codd Normal Form (BCNF) 它消除了可以基于 functional dependencies 发现的所有数据冗余。但是可能存在一些其它类型的的冗余,需要用其它的方法来解决,如 multivalue dependencies。

如果对于来自 α → β, a ⊆ R, β ⊆ R 的 F+ 中的所有 functional dependencies 满足以下至少一项条件,则 relation schema R 的 functional dependencies F 在BCNF中:

  • α → β 是一个 trivial functional dependency。
  • α 是 schema R 的一个 superkey。

以上条件可以理解为:schema 中的任何 nontrivial functional dependency 的左侧必须是一个 superkey。

一个 schema 不在 BCNF 中的例子:

inst_dept (ID, name, salary, dept_name, building, budget)

其中存在 dept_name → budget hold on inst_dept,由于它不是一个 trivial functional dependency,且 dept_name 不是一个 superkey。BCNF 的两个可选项一个也不满足,所以 inst_dept 不在 BCNF 中。

一个不在 BCNF 中的 schema 可以进行分解。分解为两个 schema 如下:

  • (α ∩ β)
  • (R - (β - α))

注意其中 “-” 表示属性集之间的差集。

如 inst_dept (ID, name, salary, dept_name, building, budget) 中 α = dept_name, β = {building, budget}, α → β 不满足 BCNF,inst_dept 分解为:

  • (α ∩ β) = (dept_name, building, budget)
  • (R - (β - α)) = (ID, name, dept_name, salary)

Third Normal Form

Third Normal Form(3NF,第三范式)通过允许某些 nontrivial functional dependencies (其左侧不是 superkey)来稍微放松约束。

如果对于来自 α → β, 其中 a ⊆ R, β ⊆ R 的 F+ 中的所有 functional dependencies 满足以下至少一项条件,则 relation schema R 的 functional dependencies F 在 3NF 中:

  • α → β 是一个 trivial functional dependency。
  • α 是 schema R 的一个 superkey。
  • β - α 中的每一个属性 A 是包含于 R 的 candidate key 中的。

3NF 的定义前两个可选项是和 BCNF 一样的,它多给出了一个可选项。任何满足 BCNF 的 schema 也满足 3NF,BCNF 是更严格的 3NF。

如,schema dept_advisor(s_ID, i_ID, dept_name),存在下面的 functional dependencies:

i_ID → dept_name
s_ID, dept_name → i_ID

可以看出 i_ID → dept_name 不在 BCNF 中。α = i_ID, β = dept_name, β - α = dept_name,因为 s_ID, dept_name → i_ID hold on dept_advisor,dpet_name 包含于 candidate key中,所以 dept_advisor 是在 3NF 中的。

Multivalue Dependencies and Fourth Normal Form

从某种意义上说,一些 relation schema 即使它们在 BCNF 中,似乎仍未得到足够的规范化(normalized),因为它们仍然遭受信息重复的问题。Multivalue dependencies 就是这一类的数据冗余问题,而 Fourth Normal Form (4NF,第四范式)就是为了消除 multivalue dependencies 问题。

Multivalue Dependencies(多值依赖)使用双箭头 “↠” 符号表示,如 A ↠ B。如果一个关系同时满足下面三个条件,则这个relation schema 存在 multivalue dependencies:

  • 一个属性 A 的值,对应多个属性 B 的值。
  • 关系表至少由 3 个属性。
  • 设关系表有三个属性分别为 A,B,C,属性 B 和 C 是独立的。

如下列 enrolment 表存在多值依赖 s_id ↠ hobby。其中,s_id 表示学生ID,course 表示课程,hobby 表示爱好。

s_id course hobby
1 science Cricket
1 math Hockey
1 science Hockey
1 math Cricket

消除多值依赖,将它分解为两个表。

stu_course

s_id cource
1 science
1 math

stu_hobby

s_id hobby
1 Hockey
1 Cricket

Second Normal Form

一个 schema 在 Second Normal Form (2NF,第二范式)它需要满足以下条件:

  • 它是在 1NF 中的。
  • 所有非键属性是完全 functional dependent on the primary key。

如下 purchase_detail schema:

purchase_detail(customerID, storeID, purchase_location)

它的主键是 customerID 和 storeID。但存在 functional dependency storeID → purchase_location,storeID 不是完整的 primary key 所以 purchase_detail 不在 2NF 中。我们将 purchase_detail 进行分解使他满足 2NF:

purchase(customerID, storeID)
store(storeID, purchase_location)

范式总结

名称 定义
1NF 单个属性原子性,不可再分
2NF 1. 满足 1NF 。2. 没有部分依赖。非键属性完全依赖于主键属性。
3NF 1. 满足 2NF 。2. 没有传递依赖。每一个非 nontrivial functional dependency X → Y, X 是一个 superkey,或者 Y 是 prime attributes(part of candidate key)。
BCNF 1. 满足 3NF。2. 每一个非 nontrivial functional dependency X → Y, X 是一个 superkey。
4NF 1. 满足 BCNF。2. 没有 multivalue dependencies。

小结

E-R 模型专注于实体和它们之间的关系,而标准化专注于去除冗余数据,防止数据修改不完整,标准化有利于保证数据的一致性。完整性约束防止数据错误修改,与标准化类似,它也是为了保证数据的一致性。

References

[1] Database System Concept (6th) by Avi Silberschatz, Henry F. Korth, and S. Sudarshan

[2] Second Normal Form

[3] Normal Forms in DBMS - geeksforgeeks

[4] Unicode Math Symbols

0%