Java 代码重用:操作与上下文重用
我几乎不需要讨论为什么重用代码是有利的。代码重用通常使得程序开发更加快速,并使得 BUG 减少。一旦一段代码被封装和重用,那么只需要检查很少的一段代码即可确保程序的正确性。如果在整个应用程序中只需要在一个地方打开和关闭数据库连接,那么确保连接是否正常则容易的多。但我确信这些你已经都知道了。
有两种类型的重用代码,我称它们为重用类型:
- 功能重用(Action Reuse)
- 上下文重用(Context Reuse)
第一种类型是功能重用,这是最常见的一种重用类型。这也是大多数开发人员掌握的一种。即重用一组后续指令来执行某种操作。
第二种类型是上下文重用,即不同功能或操作代码在相同上下文之间,将相同上下文封装为重用代码(这里的上下文指的是一系列相同的操作指令)。虽然它在控制反转中越来越受欢迎但它并不常见。而且,上下文重用并没有被明确的描述,因此它并没有像功能重用一样被系统的使用。我希望你看完这篇文章之后会有所改变。
功能重用
功能重用是最常见的重用类型。它是一组执行某种操作指令的重用。下面两个方法都是从数据库中读取数据:
public List readAllUsers(){
Connection connection = null;
String sql = "select * from users";
List users = new ArrayList();
try{
connection = openConnection();
PreparedStatement statement = connection.prepareStatement(sql);
ResultSet result = statement.executeQuery();
while(result.next()){
// 重用代码
User user = new User();
user.setName (result.getString("name"));
user.setEmail(result.getString("email"));
users.add(user);
// END 重用代码
}
result.close();
statement.close();
return users;
}
catch(SQLException e){
//ignore for now
}
finally {
//ignore for now
}
}
public List readUsersOfStatus(String status){
Connection connection = null;
String sql = "select * from users where status = ?";
List users = new ArrayList();
try{
connection = openConnection();
PreparedStatement statement = connection.prepareStatement(sql);
statement.setString(1, status);
ResultSet result = statement.executeQuery();
while(result.next()){
// 重用代码
User user = new User();
user.setName (result.getString("name"));
user.setEmail(result.getString("email"));
users.add(user);
// END 重用代码
}
result.close();
statement.close();
return users;
}
catch(SQLException e){
//ignore for now
}
finally {
//ignore for now
}
}
对于有经验的开发人员来说,可能很快就能发现可以重用的代码。上面代码中注释“重用代码”的地方是相同的,因此可以封装重用。这些是将用户记录读入用户实例的操作。可以将这些行代码封装到他们自己的方法中,例如:
// 将相同操作封装到 readUser 方法中
private User readUser(ResultSet result) throws SQLException {
User user = new User();
user.setName (result.getString("name"));
user.setEmail(result.getString("email"));
users.add(user);
return user;
}
现在,在上述两种方法中调用readUser()
方法(下面示例只显示第一个方法):
public List readAllUsers(){
Connection connection = null;
String sql = "select * from users";
List users = new ArrayList();
try{
connection = openConnection();
PreparedStatement statement = connection.prepareStatement(sql);
ResultSet result = statement.executeQuery();
while(result.next()){
users.add(readUser(result))
}
result.close();
statement.close();
return users;
}
catch(SQLException e){
//ignore for now
}
finally {
//ignore for now
}
}
readUser()
方法也可以在它自己的类中使用修饰符private
隐藏。
以上就是关于功能重用的内容。功能重用是将一组执行特定操作的指令通过方法或类封装它们来达到重用的目的。
参数化操作
有时,你希望重用一组操作,但是这些操作在使用的任何地方都不完全相同。例如readAllUsers()
和readUsersOfStatus()
方法都是打开一个连接,准备一条语句,执行它,并循环访问结果集。唯一的区别是readUsersOfStatus()
需要在PreparedStatement
上设置一个参数。我们可以将所有操作封装到一个readUserList()
方法。如下所示:
private List readUserList(String sql, String[] parameters){
Connection connection = null;
List users = new ArrayList();
try{
connection = openConnection();
PreparedStatement statement = connection.prepareStatement(sql);
for (int i=0; i < parameters.length; i++){
statement.setString(i, parameters[i]);
}
ResultSet result = statement.executeQuery();
while(result.next()){
users.add(readUser(result))
}
result.close();
statement.close();
return users;
}
catch(SQLException e){
//ignore for now
}
finally {
//ignore for now
}
}
现在我们从readAllUsers()
和readUsersOfStatus()
调用readUserList(...)
方法,并给定不同的操作参数:
public List readAllUsers(){
return readUserList("select * from users", new String[]{});
}
public List readUsersWithStatus(String status){
return readUserList("select * from users", new String[]{status});
}
我相信你可以找出其他更好的办法来实现重用功能,并将他们参数化使得更加好用。
上下文重用
上下文重用与功能重用略有不同。上下文重用是一系列指令的重用,各种不同的操作总是在这些指令之间进行。换句话说,重复使用各种不同行为之前和之后的语句。因此上下文重用通常会导致控制风格类的反转。上下文重用是重用异常处理,连接和事务生命周期管理,流迭代和关闭以及许多其他常见操作上下文的非常有效的方法。
这里有两个方法都是用 InputStream 做的:
public void printStream(InputStream inputStream) throws IOException {
if(inputStream == null) return;
IOException exception = null;
try{
int character = inputStream.read();
while(character != -1){
System.out.print((char) character); // 不同
character = inputStream.read();
}
}
finally {
try{
inputStream.close();
}
catch (IOException e){
if(exception == null) throw e;
}
}
}
public String readStream(InputStream inputStream) throws IOException {
StringBuffer buffer = new StringBuffer(); // 不同
if(inputStream == null) return;
IOException exception = null;
try{
int character = inputStream.read();
while(character != -1){
buffer.append((char) character); // 不同
character = inputStream.read();
}
return buffer.toString(); // 不同
}
finally {
try{
inputStream.close();
}
catch (IOException e){
if(exception == null) throw e;
}
}
}
两种方法与流的操作是不同的。但围绕这些操作的上下文是相同的。上下文代码迭代并关闭 InputStream。上述代码中除了使用注释标记的不同之处外都是其上下文代码。
如上所示,上下文涉及到异常处理,并保证在迭代后正确关闭流。一次又一次的编写这样的错误处理和资源释放代码是很繁琐且容易出错的。错误处理和正确的连接处理在 JDBC 事务中更加复杂。编写一次代码并在任何地方重复使用显然会比较容易。
幸运的是,封装上下文的方法很简单。 创建一个上下文类,并将公共上下文放入其中。 在上下文的使用中,将不同的操作指令抽象到操作接口之中,然后将每个操作封装在实现该操作接口的类中(这里称之为操作类),只需要将该操作类的实例插入到上下文中即可。可以通过将操作类的实例作为参数传递给上下文对象的构造函数,或者通过将操作类的实例作为参数传递给上下文的具体执行方法来完成。
下面展示了如何将上述示例分隔为上下文和操作接口。StreamProcessor
(操作接口)作为参数传递给StreamProcessorContext
的processStream()
方法。
// 流处理插件接口
public interface StreamProcessor {
public void process(int input);
}
// 流处理上下文类
public class StreamProcessorContext{
// 将 StreamProcessor 操作接口实例化并作为参数
public void processStream(InputStream inputStream, StreamProcessor processor) throws IOException {
if(inputStream == null) return;
IOException exception = null;
try{
int character = inputStream.read();
while(character != -1){
processor.process(character);
character = inputStream.read();
}
}
finally {
try{
inputStream.close();
}
catch (IOException e){
if(exception == null) throw e;
throw exception;
}
}
}
}
现在可以像下面示例一样使用StreamProcessorContext
类打印出流内容:
FileInputStream inputStream = new FileInputStream("myFile");
// 通过实现 StreamProcessor 接口的匿名子类传递操作实例
new StreamProcessorContext()
.processStream(inputStream, new StreamProcessor(){
public void process(int input){
System.out.print((char) input);
}
});
或者像下面这样读取输入流内容并添加到一个字符序列中:
public class StreamToStringReader implements StreamProcessor{
private StringBuffer buffer = new StringBuffer();
public StringBuffer getBuffer(){
return this.buffer;
}
public void process(int input){
this.buffer.append((char) input);
}
}
FileInputStream inputStream = new FileInputStream("myFile");
StreamToStringReader reader = new StreamToStringReader();
new StreamProcessorContext().processStream(inputStream, reader);
// do something with input from stream.
reader.getBuffer();
正如你所看到的,通过插入不同的StreamProcessor
接口实现来对流做任何操作。一旦StreamProcessorContext
被完全实现,你将永远不会有关于未关闭流的困扰。
上下文重用非常强大,可以在流处理之外的许多其他环境中使用。一个明显的用例是正确处理数据库连接和事务(open - process - commit()/rollback() - close())。其他用例是 NIO 通道处理和临界区中的线程同步(lock() - access shared resource - unlock())。它也能将API的已检查异常转换为未检查异常。
当你在自己的项目中查找适合上下文重用的代码时,请查找以下操作模式:
- 常规操作之前(general action before)
- 特殊操作(special action)
- 常规操作之后(general action after)
当你找到这样的模式时,前后的常规操作就可能实现上下文重用。
上下文作为模板方法
有时候你会希望在上下文中有多个插件点。如果上下文由许多较小的步骤组成,并且你希望上下文的每个步骤都可以自定义,则可以将上下文实现为模板方法。模板方法是一种 GOF 设计模式。基本上,模板方法将算法或协议分成一系列步骤。一个模板方法通常作为一个单一的基类实现,并为算法或协议中的每一步提供一个方法。要自定义任何步骤,只需创建一个扩展模板方法基类的类,并重写要自定义的步骤的方法。
下面的示例是作为模板方法实现的 JdbcContext。子类可以重写连接的打开和关闭, 以提供自定义行为。必须始终重写processRecord(ResultSet result)
方法, 因为它是抽象的。此方法提供不属于上下文的操作,在使用JdbcContext
的不同情况下的操作都不相同。这个例子不是一个完美的JdbcContext
。它仅用于演示在实现上下文时如何使用模板方法。
public abstract class JdbcContext {
DataSource dataSource = null;
// 无参数的构造函数可以用于子类不需要 DataSource 来获取连接
public JdbcContext() {
}
public JdbcContext(DataSource dataSource){
this.dataSource = dataSource;
}
protected Connection openConnection() throws SQLException{
return dataSource.getConnection();
}
protected void closeConnection(Connection connection) throws SQLException{
connection.close();
}
// 必须始终重写 processRecord(ResultSet result) 方法
protected abstract processRecord(ResultSet result) throws SQLException ;
public void execute(String sql, Object[] parameters) throws SQLException {
Connection connection = null;
PreparedStatement statement = null;
ResultSet result = null;
try{
connection = openConnection();
statement = connection.prepareStatement(sql);
for (int i=0; i < parameters.length; i++){
statement.setObject(i, parameters[i]);
}
result = statement.executeQuery();
while(result.next()){
processRecord(result);
}
}
finally {
if(result != null){
try{
result.close();
}
catch(SQLException e) {
/* ignore */
}
}
if(statement != null){
try{
statement.close();
}
catch(SQLException e) {
/* ignore */
}
}
if(connection != null){
closeConnection(connection);
}
}
}
}
这是扩展 JdbcContext 以读取用户列表的子类:
public class ReadUsers extends JdbcContext{
List users = new ArrayList();
public ReadUsers(DataSource dataSource){
super(dataSource);
}
public List getUsers() {
return this.users;
}
protected void processRecord(ResultSet result){
User user = new User();
user.setName (result.getString("name"));
user.setEmail(result.getString("email"));
users.add(user);
}
}
下面是如何使用 ReadUsers 类:
ReadUsers readUsers = new ReadUsers(dataSource);
readUsers.execute("select * from users", new Object[0]);
List users = readUsers.getUsers();
如果ReadUsers
类需要从连接池获取连接并在使用后将其释放回该连接池,则可以通过重写openConnection()
和closeConnection(Connection connection)
方法来插入该连接。
注意如何通过方法重写插入操作代码。JdbcContext
的子类重写processRecord
方法以提供特殊的记录处理。 在StreamContext示例中,操作代码封装在单独的对象中,并作为方法参数提供。实现操作接口StreamProcessor
的对象作为参数传递给StreamContext
类的processStream(...)
方法。
实施上下文时,你可以使用这两种技术。JdbcContext
类可以将实现操作接口的ConnectionOpener
和ConnectionCloser
对象作为参数传递给execute
方法,或作为构造函数的参数。就我个人而言,我更喜欢使用单独的操作对象和操作接口,原因有两个。首先,它使得操作代码可以更容易单独进行单元测试;其次,它使得操作代码在多个上下文中可重用。当然,操作代码也可以在代码中的多个位置使用,但这只是一个优势。毕竟,在这里我们只是试图重用上下文,而不是重用操作。
结束语
现在你已经看到了两种不同的重用代码的方法。经典的功能重用和不太常见的上下文重用。希望上下文的重用会像功能重用一样普遍。上下文重用是一种非常有用的方法,可以从 API 的底层细节(例如JDBC,IO 或 NIO API等)中抽象出代码。特别是如果 API 包含需要管理的资源(打开和关闭,获得并返回等)。
persistence/ORM API、Mr.Persister 利用上下文重用来实现自动连接和事务生命周期管理。 这样用户将永远不必担心正确打开或关闭连接,或提交或回滚事务。Mr.Persister 提供了用户可以将他们的操作插入的上下文。 这些上下文负责打开,关闭,提交和回滚。
流行的 Spring 框架包含大量的上下文重用。 例如 Springs JDBC 抽象。 Spring 开发人员将其使用上下文重用作为“控制反转”。 这不是 Spring 框架使用的唯一一种控制反转类型。 Spring 的核心特性是依赖注入 bean 工厂或“应用程序上下文”。 依赖注入是另一种控制反转。
参考文章:http://tutorials.jenkov.com/ood/code-reuse-action-and-context-reuse.html#closing-notes
Java 代码重用:操作与上下文重用的更多相关文章
- 在Eclipse中运行JAVA代码远程操作HBase的示例
在Eclipse中运行JAVA代码远程操作HBase的示例 分类: 大数据 2014-03-04 13:47 3762人阅读 评论(2) 收藏 举报 下面是一个在Windows的Eclipse中通过J ...
- 大数据之路week07--day01(HDFS学习,Java代码操作HDFS,将HDFS文件内容存入到Mysql)
一.HDFS概述 数据量越来越多,在一个操作系统管辖的范围存不下了,那么就分配到更多的操作系统管理的磁盘中,但是不方便管理和维护,因此迫切需要一种系统来管理多台机器上的文件,这就是分布式文件管理系统 ...
- python调用Java代码,完毕JBPM工作流application
1.缘由 有一庞大Python django webproject,要引入工作流引擎,像OA一样.方便的流程控制与管理.Python或django关于工作流的开源插件,稀少,并且弱爆了,终于选用jav ...
- java:nginx(java代码操作ftp服务器)
1.检查是否安装了vsftpd [root@linux01 ~]# rpm -qa|grep vsftpd 2.安装vsftpd [root@linux01 ~]# yum -y install vs ...
- 不使用spring的情况下原生java代码两种方式操作mongodb数据库
由于更改了mongodb3.0数据库的密码,导致这几天storm组对数据进行处理的时候,一直在报mongodb数据库连接不上的异常. 主要原因实际上是和mongodb本身无关的,因为他们改的是配置 ...
- Java代码操作HDFS测试类
1.Java代码操作HDFS需要用到Jar包和Java类 Jar包: hadoop-common-2.6.0.jar和hadoop-hdfs-2.6.0.jar Java类: java.net.URL ...
- jsp页面:js方法里嵌套java代码(是操作数据库的),如果这个js 方法没被调用,当jsp页面被解析的时候,不管这个js方法有没有被调用这段java代码都会被执行?
jsp页面:js方法里嵌套java代码(是操作数据库的),如果这个js 方法没被调用,当jsp页面被解析的时候,不管这个js方法有没有被调用这段java代码都会被执行? 因为在解析时最新解析的就是JA ...
- 分享知识-快乐自己:java代码 操作 solr
POM 文件: <!-- solr客户端 --> <dependency> <groupId>org.apache.solr</groupId> < ...
- 在命令提示符窗口下(cmd)使用指令操作并编译java代码,运行java编译代码
使用cmd操作java代码,编译.java文件,运行.class文件. 操作步骤: 1:创建一个文件夹: 例如:在e盘根目录(\)下面创建一个名为Hello的文件夹: 使用md指令:如图 在e盘中会生 ...
随机推荐
- Linux的资源管理器
说是资源管理器,其实就是使用命令来对Linux运行系统的参数的查看.下面就一起看一看怎么像在windows下查看资源管理器吧. 1.查看进程(额,自然是电脑上正在运行的进程咯) ps aux 其中a ...
- 【翻译】如何在Ext JS 6中使用Fashion美化应用程序
原文:How to Style Apps with Fashion in Ext JS 6 在Ext JS 6,一个最大的改变就是框架合并,使用一个单一的代码库,就可以为每一种设备开发各具有良好体验的 ...
- Linux多线程实践(10) --使用 C++11 编写 Linux 多线程程序
在这个多核时代,如何充分利用每个 CPU 内核是一个绕不开的话题,从需要为成千上万的用户同时提供服务的服务端应用程序,到需要同时打开十几个页面,每个页面都有几十上百个链接的 web 浏览器应用程序,从 ...
- SpriteBuilder中应用智能精灵集之后提示找不到文件的解决
SpriteBuilder中有一个将方便的功能,可以用文件夹中的若干图片生成1张图片;这称之为智能精灵集合(smart sprite sheet).好处是可以一次性的加载图片到显存中去,提升了性能. ...
- 09_Android中ContentProvider和Sqllite混合操作,一个项目调用另外一个项目的ContentProvider
1. 编写ContentPrivider提供者的Android应用 清单文件 <?xml version="1.0" encoding="utf-8"? ...
- VS2010安装Boost库
source URL: http://stackoverflow.com/questions/2629421/how-to-use-boost-in-visual-studio-2010 While ...
- Make3D Convert your image into 3d model
Compiling and Running Make3D on your own computer source: http://make3d.cs.cornell.edu/code_linux.ht ...
- TCP的核心系列 — ACK的处理(一)
TCP发送数据包后,会收到对端的ACK.通过处理ACK,TCP可以进行拥塞控制和流控制,所以 ACK的处理是TCP的一个重要内容.tcp_ack()用于处理接收到的ACK. 本文主要内容:TCP接收A ...
- 手动将jar添加到maven仓库中
1.将jar放到E:\workspace\lib中.如下图: 2.编写pom.xml文件,定义jfinal的坐标. <project xmlns="http://maven.ap ...
- OpenCV——素描
具体的算法原理可以参考: PS滤镜,素描算法 // define head function #ifndef PS_ALGORITHM_H_INCLUDED #define PS_ALGORITHM_ ...