sponsored links

到底什么是框架?

作者:Intopass
链接:https://www.zhihu.com/question/25654738/answer/31302541
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。


1 DRY 原则

首先从DRY原则开始说起Don’t Repeat Yourself,不要重复你的代码。DRY原则的重要性怎么提都不过分,很多人说编程是种机械性的工作,而有很多程序员也自嘲为码农,意为编程成了一种没有技术含量的体力性工作。如果不想沦为这个境界,首先需要的就是将DRY原则融入你的血液,在今后的编码工作中加以运用。


2 初级 DRY 应用



1)最初级的DRY:语法级别

System.out.println(1);
System.out.println(2);
……
System.out.println(10);

我想只要学过基础语法,都会采用下面的形式。

for (int i = 1; i <= 10; i++) {
    System.out.println(i);
}

如果发现有任何人采用上面一种形式的编码形式,那么不用怀疑,他对于编程绝对还没有入门。我们当然会选择省力的做法,这种做法不但省力,还会有利于我们后续修改或扩展这组代码,如:

for (int i = 1; i <= 10; i++) {
    System.out.println(i * 2 + 1);
}

我们进行这样的修改,只需要修改一处,而上面的形式却需要修改10处,当然会更麻烦且更容易出错,所以请记住能不重复就不重复。

2)进阶的DRY原则:方法级别当我们经常写一些重复性代码时,我们就要注意看能否将其抽取出来成为一个方法,如:

try {
    Thread.sleep(1000);
} catch (InterruptedException e) {
    e.printStackTrace();
}

让我们将其抽取到一个方法 threadSleep() 中,这样我们只需要调用 threadSleep() 就可以实现原来的功能,不但所需敲击的代码更少,而且代码看起来更加清楚明白。而为了增加这个方法的复用性,我们还可以将其中固定的数字抽取成为参数,如:

private static void threadSleep(int millis) {
    try {
        Thread.sleep(millis);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}

这样我们就可以利用这个方法实现不同时间的sleep了。要注意提高代码的复用性也是实践DRY原则的一个重要方法,在后面我们也可以看到框架为了提高所谓的灵活性进行的一些设计,如在适当的位置增加扩展点。

3)继续进阶的DRY原则:类型级别现在我们看一个类

public class Person {
    private String name;
    private int age;
    // Setter & Getter ...
}

我们新建一些Person类实例,并进行一些操作:

Person person = new Person();
person.setName("jack");
person.setAge(18);
Person person2 = new Person();
person2.setName("rose");
person2.setAge(17);
.....
System.out.printf("Name: %s, Age:%d\n", person.getName(), person.getAge());
System.out.printf("Name: %s, Age:%d\n", person2.getName(), person2.getAge());
.....

观察这些代码,其实有很大的DRY改造空间,首先可以添加一个构造方法

public Person(String name, int age) {
    this.name = name;
    this.age = age;
}

其次,可以添加一个toString()方法

public String toString() {
    return String.format("Name: %s, Age: %d", name, age);
}

这样的话,上面的代码就可以改成下面的形式。

Person person = new Person("jack", 18);
Person person2 = new Person("rose", 17);
......
System.out.println(person.toString());
System.out.println(person2.toString());
......

4)继续继续进阶的DRY原则:多个类组合级别上面的代码我们其实还是有改善空间,就是利用容器类

List<Person> list = new ArrayList<>();
list.add(new Person("jack", 18));
list.add(new Person("rose", 17));
......
list.forEach(p -> System.out.println(p));

这里利用JDK8的Stream API以及Lambda表达式输出,其实可以进一步简化为

list.forEach(System.out::println);

这里我们可以看到,基本上我们写代码只写有变化的代码,而尽量不写机械性重复性的代码,其实后面我们就会知道,这就叫专注于业务逻辑,所谓业务逻辑就是你这个项目中,与别的项目都不一样的地方,必须由你亲自去编写实现的部分。

其实容器类很大程度上也是为了帮助我们编写代码而被设计出来的,首先让我们不必为每一个对象起名字(省去了person,person2,…等变量),然后又为批量操作提供了可能性。像是这样一系列有用的类组合起来可以称之为类库。常用的类库有Commons-Lang包等,为我们提供了一大批实用方法,我之所以提到类库,也是因为框架其实也是一种特殊的类库,但是却与一般的类库有着本质的不同。


3 高级的 DRY 应用



②设计模式,更高层级的DRY应用

上面我讲到了DRY原则的几个层次,一般情况下大家也早就这样使用了,属于入门之后很容易自己就想到得一些层次。但是设计模式不一样,设计模式是经过长时间编码之后,经过系统性的总结所提出的针对某一类问题的最佳解决方案,又称之为最佳实践。

而在小规模的编码工作中,其实并不需要什么设计模式,只有大型程序才有设计模式发挥的空间,所以我们需要借助一些特定领域有足够规模的问题来了解一下设计模式存在的必要性。

1)连接数据库,进行一些操作,并安全释放数据库连接。

public static boolean updatePassword(String username, String password, String newpassword) {
    Connection conn = null;
    PreparedStatement stmt = null;
    ResultSet rs = null;
    boolean success = false;
    try {
        conn = beginTransaction();
        stmt = conn.prepareStatement("select id, password from user where username = ?");
        stmt.setString(1, username);
        rs = stmt.executeQuery();
        if (rs.next()) {
            if (rs.getString("password").equals(password)) {
                PreparedStatement stmt2 = null;
                try {
                    stmt2 = conn.prepareStatement("update user set password = ? where id = ?");
                    stmt2.setString(1, newpassword);
                    stmt2.setLong(2, rs.getLong("id"));
                    success = stmt2.executeUpdate() > 0;
                } finally {
                    safeClose(stmt2);
                }
            }
        }
        commitTransaction(conn);
        return success;
    } catch (SQLException e) {
        rollbackTransaction(conn);
        throw new RuntimeException(e);
    } finally {
        safeClose(rs);
        safeClose(stmt);
        safeClose(conn);
    }
}

上面是一个简单的数据库事务,虽然只有一个查询和一个更新,但是想要将其继续简化却并不容易,虽然其中有关于业务逻辑的部分只是少量几行代码,但是初始化,异常,提交,回滚操作让我们很难抽取出一个合适的方法来。虽然我们已经抽取出了 begin,commit,rollback,safeClose等方法,但是仍嫌繁琐。

我们发现之所以我们难以抽取方法,主要是因为流程,因为里面牵扯到流程控制,而流程控制一般是由我们程序员来控制的,所以也就必然需要我们手动编码来完成。难道真的就不能继续简化了吗?这就是需要设计模式的时候了。

2)应用设计模式「模板方法模式」

public static boolean updatePassword(String username, String password, String newpassword) {
    return connection(conn -> statement(conn, "select id, password from user where username = ?", stmt -> {
        stmt.setString(1, username);
        return resultSet(stmt, rs -> {
            if (rs.next()) {
                if (rs.getString("password").equals(password)) {
                    long id = rs.getLong("id");
                    return statement(conn, "update user set password = ? where id = ?", stmt2 -> {
                        stmt2.setString(1, newpassword);
                        stmt2.setLong(2, id);
                        return stmt2.executeUpdate() == 1;
                    });
                }
            }
            return false;
        });
    }));
}

可以看到,所有的conn,stmt,rs的开启和关闭,事务的提交和回滚都不用自己手动编写代码进行操作了,之所以可以达到这个效果,就是因为使用了模板方法设计模式,核心就是通过回调方法传递想对资源进行的操作,然后将控制权交给另一个方法,让这个方法掌握流程控制,然后适当的时候回调我们的代码(也就是我们自己写的业务逻辑相关的代码)。这是需要额外写的几个方法

public interface ConnectionCallback<T> {
    T doConnection(Connection conn) throws SQLException;
}
public interface StatementCallback<T> {
    T doStatement(PreparedStatement stmt) throws SQLException;
}
public interface ResultSetCallback<T> {
    T doResultSet(ResultSet rs) throws SQLException;
}
public static <T> T connection(ConnectionCallback<T> callback) {
    Connection conn = null;
    T result = null;
    try {
        conn = beginTransaction();
        result = callback.doConnection(conn);
        commitTransaction(conn);
    } catch (SQLException e) {
        rollbackTransaction(conn);
        throw new RuntimeException(e);
    } finally {
        safeClose(conn);
    }
    return result;
}
public static <T> T statement(Connection conn, String sql, StatementCallback<T> callback) throws SQLException {
    PreparedStatement stmt = null;
    T result = null;
    try {
        stmt = conn.prepareStatement(sql);
        result = callback.doStatement(stmt);
    } finally {
        safeClose(stmt);
    }
    return result;
}
public static <T> T resultSet(PreparedStatement stmt, ResultSetCallback<T> callback) throws SQLException {
    ResultSet rs = null;
    T result = null;
    try {
        rs = stmt.executeQuery();
        result = callback.doResultSet(rs);
    } finally {
        safeClose(rs);
    }
    return result;
}

你们可能会疑惑,这些代码加上我们写的业务逻辑的代码,比原来的代码还要长,有什么必要使用这个设计模式。这正是我前面已经指出的一个问题,那就是要你的程序规模足够大才有必要应用设计模式,试想如果你有上百个乃至上千个数据库操作方法需要写,那么是不是写这几个额外的方法,就不算什么了呢。其实这正是DRY原则在更高层次上的应用,即结合设计模式来达到更高层次的代码复用效果,进而应用DRY原则。而想要在这个层次继续向上攀升,那就必须是结合众多设计模式以及一些高层架构设计,能够帮助我们实现这一目的的就是框架。

3)框架,是设计模式的集大成者,是DRY原则的最高应用

先让我们来看一下,使用框架会是什么样的一种体验?这里以Hibernate + Spring声明式事务为例

@Transactional
public boolean updatePassword(String username, String password, String newpassword) {
    User user = (User) session().createQuery("from User where username = :username")
            .setString("username", username)
            .uniqueResult();
    if (user != null && user.getPassword().equals(password)) {
        user.setPassword(newpassword);
        return true;
    }
    return false;
}

可以发现令人惊讶的简洁,而且代码逻辑异常清晰,完全不需要考虑conn,stmt,rs等资源的释放,以及事务的提交和回滚,但是这些事情其实框架已经默默的帮我们做到了。这才叫真正的专注于业务逻辑,尽最大可能的只写与业务逻辑有关的代码。当然这些框架的效果虽然神奇,其实只要细细探究其内部原理,是完全可以理解并掌握的。

Tags: