15.就不能不换DB吗? - 抽象工厂模式(Abstract Factory Pattern)

15.1 就不能不换 DB 吗?

  • 时间: 4月17日23点 地点: 小菜大鸟住所的客厅 人物: 小菜, 大鸟

     “这么晚才回来, 都11点了.” 大鸟看着刚推门而入小菜问道.

     “嗨, 没办法呀, 工作忙.” 小菜叹气说道.
     “怎么会这么忙, 加班有点过头了啊.”
     “都是换数据库惹的祸呗.”
     “怎么了?”
     “我本来写好了一个项目, 是给一家企业做的电子商务网站, 是用 Sql server 作为数据库的, 应该说上线后除了开始有些小问题, 基本都还可以. 而后, 公司接到另外一家公司类似需求的项目, 但这家公司想省钱, 租用了一个空间, 只能用 Access, 不能用 SQL Server, 于是要求我今天改做原来那个项目的代码.”
     “哈哈, 你的麻烦来了.”
     “是啊, 那是相当的麻烦. 但是开始我觉得很简单呀, 因为地球人都知道, SQL Server 和 Access 在 ADO.NET 上的使用是不同的, 在SQL Server 上用的是 System.Data.SqlClient 命名空间下的 SqlConnection, SQLCommand, SQLParameter, SqlDataReader, SQLDataAdapter, 而 Access 则要用 System.Data.OleDb 命名空间下的相应对象, 我以为只要做一个全体替换就可以了, 哪知道, 替换后, 错误百出.”
     “那是一定的, 两者有不少不同地方. 你都找到了什么问题?”
     “实在是多呀. 在出入数据时 Access 必须要 insert into 而 Sql Server 可以不用 into 的; Sql Server 中的 GetData() Access 中没有, 需要改成 Now(); Sql server 中有字符创函数 SubString, 而 Access 中根本不能用, 我找了很久才知道, 可以用 Mid, 这好像是VB 中的函数.”
    ”小菜还真犯了不少错呀, insert into 是标准语法, 你干嘛不加 into 这是自找麻烦.”
     “这些小问题也就罢了, 最气人的是程序的登陆代码, 老是报错, 我怎么也找不到出了什么问题, 搞了几个小时. 最后才知道, 原来是 Access 对一些关键字, 例如 password 是不能作为数据库字段的, 如果密码的字段名是 password, Sql server 中什么问题都没有, 运行正常, 在 Access 中就是报错, 而且报的让人莫名其妙.”
     “关键字应该用 ‘[‘ 和 ‘]‘ 括起来, 不然当然是容易出错的.”
     “就这样, 今天加班到这时候才回来.”
     “以后你还有的是班要加了.”
     “为什么?”
     “只要网站要维护, 比如修改或增加一些功能, 你就得改两个项目吧, 至少在数据库中做改动, 相应的程序代码都要改, 甚至和数据库不相干的代码也要改, 你既然有两个不同的版本, 两本的工作量也是必然的.”
     “是呀, 如果那一天要用, Oracel 数据库, 估计我要改动的地方更多了.”
     “那是当然, Oracle 的 Sql 语法与 Sql Server 的差别更大. 你的改动将是空前的.”
     “大鸟只会夸张, 哪有这么严重, 大不了我再加两天班就什么都搞定了.”
     “哼哼”, 大鸟笑着摇了摇头, 很不屑一顾, “菜鸟程序员碰到问题, 只会用时间来摆平, 所以即使整天加班, 老板也不想给菜鸟加工资, 原因就在于此.”
     “你什么意思嘛!” 小菜气到, “我是菜鸟我怕谁.” 接着又拉了拉大鸟, “那你说怎么搞定才是好呢”
     大鸟端起架子, “教你可以, 这一周的碗你洗.”

15.2 最基本的数据问程序

     “你先写一段你原来的数据访问的做法给我看看吧.”
     “那就用 ‘新增用户’ 和 ‘得到用户’ 为例吧”

用户类, 假设只有 ID 和 Name 两个字段, 其余省略

1
2
3
4
5
6
7
8
9
10
11
12
public class User {
private int _id;
private String name;
public int get_id() { return _id; }
public void set_id(int _id) { this._id = _id; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
}

SqlserverUser 类 — 用于操作 User 表, 假设只有 “新增用户” 和 “得到用户” 方法, 其与方法以及具体的 sql 语句省略.

1
2
3
4
5
6
7
8
9
10
public class SqlserverUser {
public void Insert(User user){
System.out.println("在 Sql server 中给 User 表增加一条记录");
}
public User getUser(int id){
System.out.println("在 sql server 中根据" + id + "得到User 表的一条记录");
return null;
}
}

客户端代码

1
2
3
4
5
6
public static void main(String[] args) {
User user = new User();
SqlserverUser su = new SqlserverUser();
su.Insert(user);
su.getUser(1);
}

     “我最开始就是这样写的, 非常点单.”
     “这里之所以不能换数据库, 原因就在于SqlserverUser su = new SqlserverUser() 使得 su 这个对象被框死在 sql server 上了. 如果这里是灵活的, 专业点的说法, 是多态的, 那么在执行 su.insert(user);su.getUser(1); 时, 就不用考虑是在用 sql server 还是 Access.”
     “我明白你的意思了, 你是希望我用 ‘工厂方法模式’ 来封装new SqlserverUser() 所造成的变化?”
     “小菜到了半夜, 还是很清醒嘛, 不错不错.” 大鸟表扬到, “工厂方法模式是定义一个用于创建对象的接口, 让子类决定实例化哪一个类.“ 接着说, “来试试看吧.”

15.3 用了工厂方法模式的数据访问程序

     小菜很快的给出了工厂方法实现的代码.



IUser 接口, 用于访问客户端, 解除与具体数据库访问的耦合

1
2
3
4
public interface IUser {
void Insert(User user);
User getUser(int id);
}

SqlserverUser 类, 用于访问 Sql server 的 User

1
2
3
4
5
6
7
8
9
10
11
12
public class SqlserverUser implements IUser {
@Override
public void Insert(User user) {
System.out.println("在 sql server 中给 User 表增加一条记录");
}
@Override
public User getUser(int id) {
System.out.println("在 sql server 中根据 id 得到 user 表一条记录");
return null;
}
}

AccessUser 类, 用于访问 Access 的 User

1
2
3
4
5
6
7
8
9
10
11
12
public class AccessUser implements IUser {
@Override
public void Insert(User user) {
System.out.println("在 access 中给 User 表增加一条记录");
}
@Override
public User getUser(int id) {
System.out.println("在 access 中根据 id 得到 user 表一条记录");
return null;
}
}

IFactory 接口 , 定义一个创建访问 User 表对象的抽象的工厂接口.

1
2
3
public interface IFactory {
public IUser createUser();
}

SqlserverFactory 类, 实现Ifactory 接口, 实例化 SqlserverUser

1
2
3
4
5
6
public class SqlserverFactory implements IFactory {
@Override
public IUser createUser() {
return new SqlserverUser();
}
}

AccessFactory 类, 实现 IFactory 接口, 实例化 AccessUser.

1
2
3
4
5
6
public class AccessFactory implements IFactory {
@Override
public IUser createUser() {
return new AccessUser();
}
}

客户端代码

1
2
3
4
5
6
7
8
9
10
11
public static void main(String[] args) {
User user = new User();
// 若改成Access 数据库, 只需将本剧改成 IFactory Factory = new AccessFactory()
IFactory factory = new SqlServerFactory();
IUser iu = factory.createUser();
iu.Insert(user);
iu.getUser(1);
}

     “大鸟, 来看看这样写对不对?”
     “非常好. 现在如果要更换数据库, 只需要把new SqlServerFactory() 改成 new AccessFactory(),此时由于多态的关系, 使得声明 IUser 的接口的对象 iu 实现根本不知道是在访问那个数据库, 却可以在运行时很好地完成工作, 这就是所谓的业务逻辑与数据访问的解耦.”
     “但是, 大鸟, 这样写, 代码里还是有声明new SqlserverFactory()呀, 我要改的地方, 依然很多.”
     “这个先不急, 待会再说, 问题没有完全解决, 你的数据库里不能只有一个 User 表吧, 很可能有其他表, 比如增加部门表(Department 表), 此是如何办呢?”

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class Department {
private int _id;
private String Name;
public int get_id() {
return _id;
}
public void set_id(int _id) {
this._id = _id;
}
public String getName() {
return Name;
}
public void setName(String name) {
Name = name;
}
}

     “啊, 我觉得那要增加好多类了, 我来试试看.”
     “多写点类有什么关系, 只要能增加灵活性, 以后就不用加班了. 小菜好好加油.”

15.4 用了抽象工厂模式(Abstract Factory Pattern)的数据访问程序

     “小菜再次修改代码, 增加了关于部门表的处理.”



IDepartment 接口, 用于客户端访问, 解除与具体数据库访问的耦合.

1
2
3
4
public interface IDepartment {
void insert(Department department);
Department getDepartment(int id);
}

SqlserverDepartment 类, 用于访问 sql server 的 Department

1
2
3
4
5
6
7
8
9
10
11
12
public class SqlserverDepartment implements IDepartment {
@Override
public void insert(Department department) {
System.out.println("在 sql server 中给 department 表增加一条记录");
}
@Override
public Department getDepartment(int id) {
System.out.println("在 Sqlserver 中根据id 得到 department表一条记录");
return null;
}
}

AccessDepartment 类, 用于访问 Access 的 Department

1
2
3
4
5
6
7
8
9
10
11
12
public class AccessDepartment implements IDepartment {
@Override
public void insert(Department department) {
System.out.println("在 access 中给 department 表增加一条记录");
}
@Override
public Department getDepartment(int id) {
System.out.println("在 access 中根据id 得到 department表一条记录");
return null;
}
}

IFactory 接口, 定义一个创建访问 Department 表对象的抽象的工厂接口.

1
2
3
4
public interface IFactory {
public IUser createUser();
public IDepartment createDepartment(); // 增加的接口方法.
}

SqlserverFactory 类, 实现 IFactory 接口, 实例化 SqlserverUser 和 SqlserverDepartment.

1
2
3
4
5
6
7
8
9
10
11
12
public class SqlserverFactory implements IFactory {
@Override
public IUser createUser() {
return new SqlserverUser();
}
// 增加了 SqlserverDepartment 工厂
@Override
public IDepartment createDepartment() {
return new SqlserverDepartment();
}
}

AccessFactory 类, 实现 IFactory 接口, 实例化 AccessUser 和 AccessDepartment.

1
2
3
4
5
6
7
8
9
10
11
12
public class AccessFactory implements IFactory {
@Override
public IUser createUser() {
return new AccessUser();
}
// 增加了 AccessDepartment 工厂
@Override
public IDepartment createDepartment() {
return new AccessDepartment();
}
}

客户端代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public static void main(String[] args) {
User user = new User();
Department dept = new Department();
// 需要确定实例化哪一个数据库访问对象给 factory
//IFactory factory = new SqlserverFactory();
IFactory factory = new AccessFactory();
IUser iu = factory.createUser(); // 此是已解除与具体工厂的依赖
iu.Insert(user);
iu.getUser(1);
IDepartment idept = factory.createDepartment(); // 同理, 已解除依赖
idept.insert(dept);
idept.getDepartment(1);
}

显示结果如下:

1
2
3
4
在 access 中给 User 表增加一条记录
在 access 中根据 id 得到 user 表一条记录
在 access 中给 department 表增加一条记录
在 access 中根据id 得到 department表一条记录

     “大鸟, 这样就可以做到, 只需要改IFactory factory = newAccessFactory()IFactory factory = new SqlserverFactory(), 就实现了数据库访问的切换了.”
     “很好, 实际上, 在不知不觉间, 你已经通过需求的不断演化, 重构出一个非常重要的设计模式.”
     “刚才不就是工厂方法模式吗?”
     “只有一个User 类和 User 操作系统的时候, 是只需要工厂方法模式的, 但现在显然你的数据库中有很多的表, 而 Sql server 与 Access 又是两大不同的分类, 所以解决这种设计到多个产品系列的问题, 有一个专门的工厂模式叫抽象工厂模式.”

15.5 抽象工厂模式(Abstract Factory Pattern)

抽象工厂模式(Abstract Factory), 提供一个创建一系列相关或相互依赖对象的接口, 而无需指定他们具体的类.



15.6 抽象工厂模式的优点与缺点

     “这样做的好处是什么呢”
     “最大的好处便是已与交换产品系列, 由于具体工厂类, 例如IFactory factory = new AccessFactory() 在一个引用中只需要在初始化的时候出现一次, 这就使得改变一个应用的具体工厂变得非常容易, 他只需要改变具体工厂即可使用不同的产品配置. 我们的设计不能去防止需求的更改, 那么我们的理想便是让改动变得最小, 现在如果你要更改数据库的访问, 我们只需要更改具体工厂就可以做到. 第二大好处是, 他让具体的创建实例过程与客户端分离, 客户端是通过它们的抽象接口操纵实例, 产品的具体类名也被具体工厂实现分离, 不会出现在客户代码中. 事实上, 你刚才写的例子, 客户端所认识的还有 IUser 和 IDepartment, 至于它使用 Sql Server 来实现还是 Access 来实现就不知道了.”
     “啊, 我感觉抽象工厂模式把开放-封闭原则, 依赖倒转原则发挥到极致了.” 小蔡说.
     “没这么夸张, 应该说就是这些设计原则的良好运用. 抽象工厂模式也有缺点. 你想得出来吗?”
     “想不出来, 我觉得他已经很好用了, 那有什么缺点.”
     “是个模式都是会有缺点的, 都有不适用的时候, 要辩证的看待问题哦. 抽象共产模式可以很方便的切换两个数据库访问的代码, 但是如果你的需求来自增加功能, 比如我们现在要增加项目表 Project, 你需要改动哪些地方?”
     “啊, 那就至少要增加三个类, IProduct, SqlserverProject, AccessProject, 还需要修改Ifactory, SqlserverFactory 和 AccessFactory 才可以完全实现. 啊, 要改这三个类, 者太糟糕了.”
     “是的, 这非常糟糕.”
     “还有就是刚才你问的, 我的客户端程序类显然会不会只有一个, 有很多地方都在使用 IUser 或 IDepartment, 而这样的设计, 其实在每一个累的开始都需要声声明 IFactory factory = new SqlserverFactory();如果我有100个钓友数据库访问的类, 是不是就要改100次 IFactory factory = new AccessFactory() 这样的代码才行了? 这部门管解决我要更改数据库访问时, 改动一处就完全改的要求啊!”
     “改就改了, 公司花这么多钱养你干嘛? 100个改动, 不算难得, 加个班什么都搞定了.” 大鸟一脸坏笑的说.

15.7 用简单工厂来改进抽象工厂

     十分钟后, 小菜给出了一个改进方案. 去除 IFactory , SqlserverFactory 和 AccessFactory 三个工厂类, 取而代之的是 DataAccess 类, 用一个简单工厂模式来实现.



1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public class DataAccess {
// private static final String db = "Sqlserver"; // 数据库名称, 可替换
private static fianl String db = "Access";
// 由于db 的事先设置, 所以此处可以根据选择实例化出相应的对象.
public static IUser createUser(){
IUser result = null;
switch(db){
case "Sqlserver":
result = new SqlserverUser();
break;
case "Access":
result = new AccessUser();
break;
}
return result;
}
public static IUser createUser(){
IDepartment result = null;
switch(db){
case "Sqlserver":
result = new SqlserverDepartment();
break;
case "Access":
result = new AccessDepartment();
break;
}
return result;
}
}

客户端代码

1
2
3
4
5
6
7
8
9
10
11
12
public static void main(String[] args) {
User user = new User();
Department dept = new Department();
IUser iu = DataAccess.createUser();
iu.Insert(user);
iu.getUser(1);
IDepartment idept = DataAccess.createDepartment();
idept.insert(dept);
idept.getDepartment(1);
}

     “大鸟, 来看看我的设计, 我觉得在设立与其用那么多工厂类, 不如直接用一个简单工厂来实现, 我抛弃了 IFactory , SqlserverFactory 和 AccessFactory 三个工厂类, 由于事先设置了 db 的值(Sqlserver 或 Access), 所以简单工厂的方法都不需要输入参数, 这样在客户端就只需要 DataAccess.createUser()DataAccess.createDepartment() 来生成具体的数据库访问类实例, 客户端没有出现任何一个Sql server 和 Access 字样, 达到了解耦的目的.
     “哈, 小才厉害了, 你的改进确实比之前的代码要更进一步了, 客户端已经不再受改动数据库访问的影响了. 可以打95分了.” 大鸟拍了拍小菜, 鼓励的说, “为什么不能得满分, 原因是如果我需要增加 Oracle 数据库访问, 本来抽象工厂只怎加一个 OracleFactory 工厂类就可以了, 现在就比较麻烦了.”
     “是的, 没办法, 这样就需要在 DataAccess 类中每个方法的switch 中加 case 了.”

15.8 用反射+抽象工厂的数据访问程序

     “我们要考虑的是可不可以不再程序里写明 ‘如果是 Sqlserver 就去实例化 Sql server 数据库相关类, 如果是 Access 就去实例化 Access 相关类’ 这样的语句, 而是根据字符串 db 的值去某个地方找应该要实实例化的类是哪一个. 这样, 我们的switch 就可以对他说再见了.”
     “听不太懂哦, 设么叫 ‘去某个地方找应该要实例化的类是哪一个’ ?” 小菜糊涂地问.
     “我要说的是就是一种编程方式: 依赖注入(Dependency Injection), 从字面上不太好理解, 我们也不去管它. 关键在于如何去用这种方法来解决我们的 switch 问题. 本来依赖注是需要专门的 loC 容器提供, 比如 Spring.NET, 显然当前这个程序不需要这么麻烦, 你只需要在了解一个简单的 .NET 技术 ‘反射’ 就可以了.”
     “大鸟, 你一下在说有事 ‘依赖注入’ 又是 ‘反射’ 这些莫名的名词, 很晕.” 小菜有些犯困, “我就想知道, 如何向switch 说 bye-bye! 至于那些什么概念我不想了解.”
     “心急讨不了好媳妇! 你急什么?” 大鸟嘲笑道, “反射技术看起来很玄乎 其实实际用起来不算难. 他的格式是: “

Class cl = (Class) Class.forName(“包名+类名”);
result = cl.newInstance();

     “引用反射就可以帮助我们来客服抽象工厂模式的先天不足了. 我们获得实例可以用下面两种写法”

1
2
// 常规写法
IUser result = new SqlserverUser();

1
2
3
// 反射的写法
Class<IUser> cl = (Class<IUser>) Class.forName("包名.SqlserverUser");
result = cl.newInstance();

     “实例化的效果是一样的, 但这两种方法的区别在哪里?” 大鸟问道.
     “常规方法是写明了要实例化 SqlserverUser 对象. 反射的写法, 其实也是致命了要实例化的 SqlserverUser 对象啊.”
     “常规的方法你可以灵活的换为AccessUser 吗?”
     “不可以, 这都是实现编译好的代码.”
     “那你看看, 在反射中 ‘createInstance(“包名.SqlserverUser”)’ 可以灵活的换 ‘SqlserverUser’ 为 ‘AccessUser’ 吗?”
     “还不是一样, 写死在代码中…….等等, 哦!!! 我明白了.” 小菜一下子顿悟过来, 兴奋起来. “因为这里是字符串, 可以用变量来处理, 也就是可以根据需要更换. 哦, My God! 太妙了!”
     “哈哈, 之前跟你说过, ‘体会到面向对象带来的好处, 怎么一个爽子了得啊’, 你有这种感觉了?”
     “恩, 我一下子知道这里的差别主要在原来的实例化是写死在程序里, 而现在用了反射就可以利用字符串来实例化对象, 而变量是可以更换的.” 小菜说道.
     “写死在程序里, 太难听了. 准确的说, 是将程序邮编意识转为运行时. 由于 ‘createInstance(“包名.SqlserverUser”)’ 中的字符串是可以写成变量的, 耳边两的事到底是 sql server, 还是 Access, 完全可以有实现的那个 db 变量来决定. 所以就除去了 switch 分支的判断.”

DataAccess类, 用反射技术, 取代 IFactory, SqlserverFactory 和 AccessFactory.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public class DataAccess {
private static final String package = "second";
private static String db = "Sqlserver";
public static IUser createUser(){
IUser result = null;
try {
Class<IUser> cl = (Class<IUser>) Class.forName(
package + "."+ db + "User");
result = cl.newInstance();
} catch (Exception e) {
e.printStackTrace();
} finally{
return result;
}
}
public static IDepartment createDepartment() {
IDepartment result = null;
try {
Class<IDepartment> cl = (Class<IDepartment>) Class.forName(
package + "." + getDataBase() + "Department");
result = cl.newInstance();
} catch (Exception e) {
e.printStackTrace();
} finally{
return result;
}
}
}

     “如果我们增加了 Oracle 数据访问, 相关的类的增加是不可避免的, 这点我们用任何办法都解决不了, 不过这叫扩展, 开放-封闭原则告诉我们, 对于扩展, 我们开放. 但对于修改, 我们应该要尽量关闭, 就目前而言, 我们只需要更改 private static final String db = "Sqlserver";private static final String db = "Oracle"; 也就意味着, Class<IUser> cl = (Class<IUser>) Class.forName("包名.SqlserverUser"); 这一句发生了变化.”

1
Class<IUser> cl = (Class<IUser>) Class.forName("包名.SqlserverUser");

更改为:

1
Class<IUser> cl = (Class<IUser>) Class.forName("包名.OracleUser");

     “这样的结果就是 DataAccess.createUser() 本来得到的是 SqlserverUser 的实例, 而现在变成了 OracleUser 的实例了.”
     “那么如果我们需要增加 Project 产品时, 如何做呢?”
     “只需要增加三个与 Project 相关的类, 在修改 DataAccess, 在其中增加一个 public static IProject createProject() 方法就可以了.”
     “怎么样, 编程的艺术感是不是出来了?”
     “哈, 比以前, 这代码是漂亮多了. 但是, 总感觉还是有点缺陷, 因为在更换数据库访问时, 我还是需要去该程序(db 这个字符串的值) 重新编译, 如果不修改程序, 那才是真正的符合开放封闭原则.”

15.9 用反射+配置文件实现数据访问程序

     “小菜追求很完美嘛! 我们还可以利用配置文件来解决更改 DataAccess 的问题.”
     “哦, 对的, 对的, 我也可以读取文件来个 DB 字符串复制, 在配置文件中写明是 Sqlserver 还是 Access, 这样就连 DataAccess 类也不用改了.”

添加配置文件 database.properties

1
sql=Access

     “再添加反射, 更改 DataAccess 类字段的 DB 赋值代码”

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
package second;
public class DataAccess {
// 包名
private static final String PACKAGE_NAME = "second";
// 待访问的数据库
private static String db;
// 数据库表表示
private static final String user = "User";
private static final String dept = "Department";
// 从配置文件中获取访问字段
private static String getDataBase() {
if( db != null){
return db;
}
Properties prop = new Properties();
InputStream in = DataAccess.class.getResourceAsStream("database.properties");
try {
prop.load(in);
db = prop.getProperty("sql");
} catch (IOException e) {
e.printStackTrace();
}
return db;
}
// 返回 User 表的访问类
public static IUser createUser(){
IUser result = null;
try {
Class<IUser> cl = (Class<IUser>) Class.forName(
PACKAGE_NAME + "."+ getDataBase() + user);
result = cl.newInstance();
} catch (Exception e) {
e.printStackTrace();
}
return result;
}
// 返回 Department 表的访问类
public static IDepartment createDepartment() {
IDepartment result = null;
try {
Class<IDepartment> cl = (Class<IDepartment>) Class.forName(
PACKAGE_NAME + "." + getDataBase() + dept);
result = cl.newInstance();
} catch (Exception e) {
e.printStackTrace();
}
return result;
}
}

15.10 无痴迷, 不成功

     “设计模式真的好神奇啊, 如果早先是这样设计的话, 我今天就不要用加班加点了”
     “好了, 都快1点了, 你还要不要睡觉呢?”
     “啊, 今天都加了一晚上的班, 但是学起来设计模式, 我把时间都给忘记了, 什么劳累都没了.”
     “这说明你是做程序员的料, 一个程序员如果从来没有熬夜写程序的经历, 不能锁是一个好程序员, 因为他没有痴迷过, 所以他不会有大成就.(其实吧, 我觉得早晨起来写也是可以的, 不要太死板).”
     “是的, 无痴迷不成功,. 我一定会成为优秀的程序员. 我坚信.” 小菜非常自信的说到.

~感谢捧场,您的支持将鼓励我继续创作~