Java设计原则
设计原则和设计模式是两个不同的概念。
设计模式提供了一套软件开发过程中面临的一些问题的最佳解决方案,在实践中使用这些方案会使得你的代码然具有很好的可读性,可靠性,以便于优秀的代码可以直接被他人使用。而设计原则是设计模式最直接的体现,逻辑上更符合Java编程中追求的高内聚和低耦合的基本思想,好的代码理应遵守!
这里通过一个小游戏来理解代码开发中最基本的五大设计原则——S.O.L.I.D.(大佬们可以自动忽略:)
package castle;
import java.util.Scanner;
public class Game {
private Room currentRoom;
public Game()
{
createRooms();
}
private void createRooms()
{
Room outside, lobby, pub, study, bedroom;
// 制造房间
outside = new Room("城堡外");
lobby = new Room("大堂");
pub = new Room("小酒吧");
study = new Room("书房");
bedroom = new Room("卧室");
// 初始化房间的出口
outside.setExits(null, lobby, study, pub);
lobby.setExits(null, null, null, outside);
pub.setExits(null, outside, null, null);
study.setExits(outside, bedroom, null, null);
bedroom.setExits(null, null, null, study);
currentRoom = outside; // 从城堡门外开始
}
private void printWelcome() {
System.out.println();
System.out.println("欢迎来到城堡!");
System.out.println("这是一个超级无聊的游戏。");
System.out.println("如果需要帮助,请输入 'help' 。");
System.out.println();
System.out.println("现在你在" + currentRoom);
System.out.print("出口有:");
if(currentRoom.northExit != null)
System.out.print("north ");
if(currentRoom.eastExit != null)
System.out.print("east ");
if(currentRoom.southExit != null)
System.out.print("south ");
if(currentRoom.westExit != null)
System.out.print("west ");
System.out.println();
}
// 以下为用户命令
private void printHelp()
{
System.out.print("迷路了吗?你可以做的命令有:go bye help");
System.out.println("如:\tgo east");
}
private void goRoom(String direction)
{
Room nextRoom = null;
if(direction.equals("north")) {
nextRoom = currentRoom.northExit;
}
if(direction.equals("east")) {
nextRoom = currentRoom.eastExit;
}
if(direction.equals("south")) {
nextRoom = currentRoom.southExit;
}
if(direction.equals("west")) {
nextRoom = currentRoom.westExit;
}
if (nextRoom == null) {
System.out.println("那里没有门!");
}
else {
currentRoom = nextRoom;
System.out.println("你在" + currentRoom);
System.out.print("出口有: ");
if(currentRoom.northExit != null)
System.out.print("north ");
if(currentRoom.eastExit != null)
System.out.print("east ");
if(currentRoom.southExit != null)
System.out.print("south ");
if(currentRoom.westExit != null)
System.out.print("west ");
System.out.println();
}
}
public static void main(String[] args) {
Scanner in = new Scanner(System.in);
Game game = new Game();
game.printWelcome();
while ( true ) {
String line = in.nextLine();
String[] words = line.split(" ");
if ( words[0].equals("help") ) {
game.printHelp();
} else if (words[0].equals("go") ) {
game.goRoom(words[1]);
} else if ( words[0].equals("bye") ) {
break;
}
}
System.out.println("感谢您的光临。再见!");
in.close();
}
}
package castle;
public class Room {
public String description;
public Room northExit;
public Room southExit;
public Room eastExit;
public Room westExit;
public Room(String description)
{
this.description = description;
}
public void setExits(Room north, Room east, Room south, Room west)
{
if(north != null)
northExit = north;
if(east != null)
eastExit = east;
if(south != null)
southExit = south;
if(west != null)
westExit = west;
}
@Override
public String toString()
{
return description;
}
}
这是一个简单的城堡游戏,下面是它的UML图。
一、单一责任原则(Single Responsibility Principle)
上面的代码中,一个很突出的问题就是——代码复制。在Game类中,printWelcome()和GoRoom()函数分别重写了一大段关于输出提示语句的代码,代码复制存在的问题是如果需要修改一个副本,那么就必须同时修改所有其他的副本,否则就会存在不一致的问题,这增加了程序员的工作量。但是如果将这些程序中多次出现的代码集中起来封装成函数,每次只是去调用这些函数的话——那么如果需要改变需求当然仅仅需要修改函数就行了。
大牛们早就看到这一点——换句话说,上述代码违背了S原则(Wikipedia):
1 Every module or class should have responsibility over a single part of the functionality provided by the software,
2 and that responsibility should be entirely encapsulated by the class. All its services should be narrowly aligned with that responsibility.
3 Robert C.Martin express the principle, "A class should have only One reason to change".
消除代码复制的基本手段,就是函数和父类。
所以我们修改上述的代码,将描述提示语句的代码提取出来写成函数:
1 public void showPrompt() {
2 System.out.println("你在" + currentRoom);
3 System.out.print("出口有: ");
4 if(currentRoom.northExit != null)
5 System.out.print("north ");
6 if(currentRoom.eastExit != null)
7 System.out.print("east ");
8 if(currentRoom.southExit != null)
9 System.out.print("south ");
10 if(currentRoom.westExit != null)
11 System.out.print("west ");
12
13 }
这个时候,如果要对游戏提示语句进行修改,只需要该showPrompt()函数就行。
二、开放封闭原则(Open Closed Principle)
the open/closed principle states "software entities(classes, modules, functions, etc)should be open for extension,
but closed for modification";that is, such an entity can allow its behaviour to be extended without modifying its
source code.The name open/closed principle has been used in two ways. Both ways use generations(for instance, inheritance
or delegate functions)to resolve the apparent dilemma, but the goals, techniques and results are different.7
评价代码好坏的标准是多元的,其中很重要的一条指标就是代码需要面对将来的需要——维护。如果需求有了变更,当下的代码是否可以继续使用或者很好的拿来进行重构。
假设现在这个游戏需要进行扩展——增加两个方向UP和DOWN。就涉及到可扩展性的问题,可扩展性指的是:代码的某些部分不需要经过修改就能适应将来可能的变化。
1.封装降低耦合度
通过分析原来的代码,我们发现Room类和Game类之间的耦合度很高(耦合度指的是类与类之间的关系)。这个时候如果我想在Room类中增加新的方向或者新的命令将会对Game类中的所有Room相关代码进行修改,这样的结果就是每一个类中很小的一点修改动辄就会有一大片其他类中的代码跟着遭殃,所以低耦合一直是程序良好的体现。我们首先尝试对当前的代码进行低耦合的处理。
方案1:看到上述代码Room类中的所有成员变量都是public的,意味着Room类的属性是直接曝光给Game类的,这是不允许的。所以第一个肯定是将其中所有的方向属性改为private。
方案2:顺着经验走下去,一般我就开始写get函数了。。。But!!!老师上课一直强调的private的用途就是保护私有属性,那你给了别人一把锁,又给了一把钥匙,这可不是OOP的风格。那怎么办呢?
方案3:不妨换一种思路:我们写Game代码的时候,总是习惯于站在Game类的角度去检查Room类中有什么方向,那为什么不直接让Room类告诉我们它自己有什么方向呢?(这样的思路在电梯状态和出租车状态查询处理上给我很大的启发。)
先上代码:
1
2 //这是Room中新增加的两个向外界提供内部属性的接口函数
3
4 public String getExit() {
5 //这里使用stringbuffer只要是因为String类是immutable类,所以每一次进行申请都会有很大的开销,而stringbuffer不是
6 StringBuffer ret = new StringBuffer();
7 if(northExit!=null){
8 ret.append("north");
9 }else if(southExit != null) {
10 ret.append("south");
11 }else if(eastExit != null) {
12 ret.append("east");
13 }else if(westExit != null) {
14 ret.append("west");
15 }
16 return ret.toString();
17 }
18
19
20 public Room getDire(String direction) {
21 Room dir = null;
22 if(direction.equals("north")) {
23 dir = northExit;
24 }
25 if(direction.equals("east")) {
26 dir =eastExit;
27 }
28 if(direction.equals("south")) {
29 dir =southExit;
30 }
31 if(direction.equals("west")) {
32 dir =westExit;
33 }
34 return dir;
35 }
36
37
38
39 //这里需要对Game类中的一些函数进行稍微的修改,修改部分用橘色标出
40 public void showPrompt() {
41 System.out.println("你在" + currentRoom);
42 System.out.print("出口有: ");
43 System.out.print(currentRoom.getExit());
44 System.out.println();
45 }
46
47
48 private void goRoom(String direction)
49 {
50 Room nextRoom = currentRoom.getDire(direction);
51 if (nextRoom == null){
52 System.out.println("那里没有门!");
53 }
54 else {
55 currentRoom = nextRoom;
56 showPrompt();
57 }
58 }
这样修改之后,类与类之间的耦合度降低了不少,而且代码看起来一点儿也不冗余。不管是Game类中的showPrompt()函数或者是Game类中的goRoom()函数,在最早的版本中对于Room类的耦合性是很强的,到处都是Room的相关代码,而修改之后发现,这个时候的Room把自己很好的封装起来,不仅对外很友好——实现了所有的需求,而且也让自己不那么分散,变的统一起立。以前Room的所有属性都是直接曝光给Game类的,但是现在将属性全部私有化并且提供了两个接口来与外界进行交互,这样Room类自己就可以很方便的对自己进行修改也不影响与外界的交互。松耦合的好处就是如果一个类需要进行修改,只需要修改自身的代码而不会殃及其他无辜的类。
2.提高聚合度
聚合度指的是一个方法或者单独的实现单元应该有一个具体的任务,负责一个很小的块以便于可以重用代码。
我们可以看到这个游戏的方向属性有四种,但是如前所述需要对增加方向,这个时候又需要对Room代码进行修改以便于增加新的方向属性,而且还需要在命令中添加相应的代码,在输出说明中添加相应的代码——所以这种“硬编码”的风格实际上对程序的可扩展性是没有好处的,解决方式也很简单,就是将方向变量统一起来,用HashMap进行动态调整,这时候如果需要增加新的属性,完全不需要大量的修改。
修改后的代码,可以在不改动原来的代码的基础上增加新的功能:UP方向。这个时候,Room类就对方向属性体现出了可扩展性。
1 package castle;
2
3 import java.util.HashMap;
4
5 public class Room {
6 private String description;
7 private HashMap<String, Room> exits = new HashMap<String, Room>();
8
9 public Room(String description)
10 {
11 this.description = description;
12 }
13
14 public void setExis(String dir, Room direction) {
15 exits.put(dir, direction);
16 }
17
18 @Override
19 public String toString()
20 {
21 return description;
22 }
23
24
25 public String getExit() {
26 StringBuffer ret = new StringBuffer();
27 for(String dir : exits.keySet()) {
28 ret.append(dir);
29 ret.append(' ');
30 }
31 return ret.toString();
32 }
33
34
35 public Room getDire(String direction) {
36 return exits.get(direction);
37 }
38
39
40 }
1 package castle;
2
3 import java.util.Scanner;
4
5 public class Game {
6 private Room currentRoom;
7
8 public Game()
9 {
10 createRooms();
11 }
12
13 private void createRooms()
14 {
15 Room outside, lobby, pub, study, bedroom;
16
17 // 制造房间
18 outside = new Room("城堡外");
19 lobby = new Room("大堂");
20 pub = new Room("小酒吧");
21 study = new Room("书房");
22 bedroom = new Room("卧室");
23
24 // 初始化房间的出口
25 outside.setExis("east", lobby);
26 outside.setExis("south", study);
27 outside.setExis("west", pub);
28 lobby.setExis("west", outside);
29 pub.setExis("east", outside);
30 study.setExis("north", outside);
31 study.setExis("east", bedroom);
32 bedroom.setExis("west", study);
33
34
35 //这个时候如果需要增加方向UP或者DOWN只需要直接加上
36 lobby.setExis("up", pub);
37 pub.setExis("down", lobby);
38
39
40 currentRoom = outside; // 从城堡门外开始
41 }
42
43 private void printWelcome() {
44 System.out.println();
45 System.out.println("欢迎来到城堡!");
46 System.out.println("这是一个超级无聊的游戏。");
47 System.out.println("如果需要帮助,请输入 'help' 。");
48 System.out.println();
49 System.out.println("现在你在" + currentRoom);
50 showPrompt();
51 }
52
53 // 以下为用户命令
54
55 private void printHelp()
56 {
57 System.out.print("迷路了吗?你可以做的命令有:go bye help");
58 System.out.println("如:\tgo east");
59 }
60
61
62
63 private void goRoom(String direction)
64 {
65 Room nextRoom = currentRoom.getDire(direction);
66 if (nextRoom == null){
67 System.out.println("那里没有门!");
68 }
69 else {
70 currentRoom = nextRoom;
71 showPrompt();
72 }
73 }
74
75 public void showPrompt() {
76 System.out.println("你在" + currentRoom);
77 System.out.print("出口有: ");
78 System.out.print(currentRoom.getExit());
79 System.out.println();
80 }
81 public static void main(String[] args) {
82 Scanner in = new Scanner(System.in);
83 Game game = new Game();
84 game.printWelcome();
85
86 while ( true ) {
87 String line = in.nextLine();
88 String[] words = line.split(" ");
89 if ( words[0].equals("help") ) {
90 game.printHelp();
91 } else if (words[0].equals("go") ) {
92 game.goRoom(words[1]);
93 } else if ( words[0].equals("bye") ) {
94 break;
95 }
96 }
97
98 System.out.println("感谢您的光临。再见!");
99 in.close();
100 }
101
102 }
3.总结
这个时候回过头来看一下这条号称是最抽象的"open for extension, closed for modification"的设计——开放封闭原则;
实际上就是六个字:低耦合,高聚合。另一种体会是:优秀的程序员都是很懒的。。。就是用尽一切方式让修改代码变得方便(其实也不尽然)。所以才希望能一次改成功就一次改成功,不要将一个类中的修改变成全局修改;所以才希望在用户提出新的需求的时候,只需要修改很少的代码量就可以实现,这些都是最后要达到的目的。
三、里氏替换原则(liskov substitution principle)---------- 四、接口分离原则(interface segregation principle)
将这两条放在一起来比较因为有很多相似的地方:
LSP:
Substitutability is a principle in object-oriented programming stating that, in a computer program, if S is a subtype of T,
then objects of type T may be replaced with objects of type S (i.e. an object of type T may be substituted with any object of a subtype S) without
altering any of the desirable properties of the program (correctness, task performed, etc.).
ISP:
interface-segregation principle (ISP) states that no client should be forced to depend on methods it does not use.
ISP splits interfaces that are very large into smaller and more specific ones so that clients will only have to know about the methods that are of interest to them.
Such shrunken interfaces are also called role interfaces.[2] ISP is intended to keep a system decoupled and thus easier to refactor, change, and redeploy.
觉得两者很有相似处,因为都是将一些具有高度统一的功能统一成为类或者接口,然后再去实现具体细节,只不过LSP针对的是父类和子类的兼容性——就是可以向上兼容,而ISP更加强调了局部细节——就是提高聚合度。这个后面再谈。
上一个例子实际上对我们代码的设计有了很好的启发,就是好的代码一定是具备可扩展性的,通过将成员变量从硬编码改为使用容器就很好的增加了方向的可扩展性,对于Room类来说,内部函数构成了一个很大的框架,而像方向,命令这些可以看成是框架下的数据。所以这个模型基本即出来了:框架+数据;
这也可以看成是一种思维方式:就是对于程序的实现,通过框架+数据的方式来实现程序的可扩展性,而对应于本程序中的数据,剩下的具有可扩展性的数据就是命令了(可以增加其他命令比如上楼、坐电梯等)。所以接下来就是如何将游戏中的命令作为数据也实现可扩展性呢?如果是字符串对应,那HashMap当然是最好的选择,但是这时候一个字符串对应一个函数(见Main函数中的while循环)?(HashMap的map和value都针对的是对象)。所以这里很明显需要自己实现一个很符合“行情”的数据结构来实现这一功能。通过分析命令的格式,其实就是每一个命令对应一个操作,如果还是想使用之前的容器作为可扩展性的基础,那么就需要给每一个不同的函数操作披上对象的外衣,所以我们很容易就想到了使用继承。
一共有四个命令,所以我们可以首先创建一个Handler类,然后在对Handler类进行扩展,实现HandlerHelp,HandlerBye, HandlerGo三个子类,这样就之后如果在有其他的一些命令,就可以增加一个子类并重写一些方法。
//这是父类Handler
1 public class Handler {
2
3 protected Game game;
4
5 public Handler(Game game) {
6 this.game = game;
7 }
8
9 public void doCmd(String word) {
10
11 }
12
13 public boolean isBye() {
14 return false;
15 }
16
17 }
19
20 //这是子类HandlerHelp
21 public class HandlerHelp extends Handler{
22 public HandlerHelp(Game game) {
23 super(game);
24 }
25
26 @override
27 public void doCmd(String word) {
28
29 System.out.println("迷路了吗?你可以做的命令有:go bye help");
30 System.out.println("比如: go east");
31 }
32
33 }
34
35
36 //这是子类HandlerBye
37 public class HandlerBye extends Handler{
38 public HandlerBye(Game game) {
39 super(game);
40 }
41
42 public boolean isBye() {return false;}
43
44 public void doCmd(String word) {
45 System.out.println("感谢您的光临,再见!");
46 }
47 }
48
49 //这是子类HandlerGo
50public class HandlerGo extends Handler {
51
52 public HandlerGo(Game game) {super(game);}
53
54 public void doCmd(String word) {
55 game.goRoom(word);
56 }
57 }
这个时候在去看原来的代码,只需要修改的部分实际上很少了——只需要将构造函数对命令的初始化部分进行修改,增加一个关于命令的HashMap,然后将Main函数中冗余的关于如何操作命令的一些代码修改成为函数,直接调用就OK;
private HashMap<String, Handler> handlers = new HashMap<String, Handler> ();
public void play() {
Scanner in = new Scanner(System.in);
while(true) {
String line = in.nextLine();
String[] words = line.split(" ");
Handler handler = handlers.get(words[0]);
String value = "";
if(words.length > 1) {
value = words[1];
}
if(handler != null) {
handler.doCmd(value);
if(handler.isBye()) {break;}
}
}
in.close;
}
这时候发现Main函数:
1 public static void main(String[] args) {
2 Game game = new Game();
3 game.printWelcome();
4 game.play();
5
6 }
最后贴上修改完毕的最终版本:
1 package castle;
2
3 import java.util.HashMap;
4 import java.util.Scanner;
5
6 public class Game {
7 private Room currentRoom;
8 private HashMap<String, Handler> handers = new HashMap<String, Handler>();
9
10 public Game() {
11
12 handers.put("bye", new HandlerBye(this));
13 handers.put("help", new HandlerHelp(this));
14 handers.put("go", new HandlerGO(this));
15 createRooms();
16 }
17
18 private void createRooms() {
19 Room outside, lobby, pub, study, bedroom;
20
21 // 制造房间
22 outside = new Room("城堡外");
23 lobby = new Room("大堂");
24 pub = new Room("小酒吧");
25 study = new Room("书房");
26 bedroom = new Room("卧室");
27
28 // 初始化房间的出口
29 outside.setExis("east", lobby);
30 outside.setExis("south", study);
31 outside.setExis("west", pub);
32 lobby.setExis("west", outside);
33 pub.setExis("east", outside);
34 study.setExis("north", outside);
35 study.setExis("east", bedroom);
36 bedroom.setExis("west", study);
37
38
39 //这个时候如果需要增加方向UP或者DOWN只需要直接加上
40 lobby.setExis("up", pub);
41 pub.setExis("down", lobby);
42
43
44 currentRoom = outside; // 从城堡门外开始
45 }
46
47 private void printWelcome() {
48 System.out.println();
49 System.out.println("欢迎来到城堡!");
50 System.out.println("这是一个超级无聊的游戏。");
51 System.out.println("如果需要帮助,请输入 'help' 。");
52 System.out.println();
53 System.out.println("现在你在" + currentRoom);
54 showPrompt();
55 }
56
57 public void goRoom(String direction) {
58 Room nextRoom = currentRoom.getDire(direction);
59 if (nextRoom == null){
60 System.out.println("那里没有门!");
61 }else{
62 currentRoom = nextRoom;
63 showPrompt();
64 }
65 }
66
67 public void play() {
68 Scanner in = new Scanner(System.in);
69 while ( true ) {
70 String line = in.nextLine();
71 String[] words = line.split(" ");
72 Handler handler = handers.get(words[0]);
73 String value = "";
74 if(words.length > 1){
75 value = words[1];
76 }
77 if(handler != null) {
78 handler.doCmd(value);
79 if(handler.isBye()) {
80 break;
81 }
82 }
83
84
85 }
86 in.close();
87 }
88
89 public void showPrompt() {
90 System.out.println("你在" + currentRoom);
91 System.out.print("出口有: ");
92 System.out.print(currentRoom.getExit());
93 System.out.println();
94 }
95
96
97 public static void main(String[] args) {
98 Game game = new Game();
99 game.printWelcome();
100 game.play();
101
102 }
103
104 }
1 package castle;
2
3 import java.util.HashMap;
4
5 public class Room {
6 private String description;
7 private HashMap<String, Room> exits = new HashMap<String, Room>();
8
9 public Room(String description) {
10 this.description = description;
11 }
12
13 public void setExis(String dir, Room direction) {
14 exits.put(dir, direction);
15 }
16
17 @Override
18 public String toString() {
19 return description;
20 }
21
22 public String getExit() {
23 StringBuffer ret = new StringBuffer();
24 for(String dir : exits.keySet()) {
25 ret.append(dir);
26 ret.append(' ');
27 }
28 return ret.toString();
29 }
30
31 public Room getDire(String direction) {
32 return exits.get(direction);
33 }
34
35
36 }
总结:有些时候一个很完备的接口或者父类其实不一定是有效的,如果针对的是一个需求很小的用户,反而显得很是无用,所以最好能将一个大的接口实现为一些小的接口,并保证这些接口向上兼容,这样会有很多好处(原谅经验不多实在想不到其他的例子。。)
前四条原则有很多相同的地方,或者从一个角度看实际上说的都是一个事情——就是提高代码的可扩展性。当然也有可能是因为现在还很菜,理解力只能到这。再接再厉吧。
四、依赖注入或倒置原则(dependency inversion principle)
这个实在是没有明白,希望以后用到的时候在说吧。这里有一个很好的设计原则详解。
五、总结
- 一个对象或者一个函数只承担一种功能,对外提供服务接口。需要这种功能的地方实例化该对象或者调用该函数。
- 程序应该符合可扩展性原则,即“框架+数据”的模型——不变的是框架(整体的架构),变的是数据(需要添加的属性或者功能)。重点是这种可扩展性必须是很容易实现的即代码的低耦合。
- 子类应该可以用来替代它所继承的类。
- 一个类对另一个类的依赖应该限制在最小化的接口上。
- 依赖抽象层(接口),而不是具体类。
注:大一的时候在网上听过浙江大学翁凯老师的Java进阶,这些例子都是来自于他的课程,但翁老师当时并没有具体到每一种设计原则只是将其作为一种规范讲给初学者,当时的我还在纠结数组怎么没用所以对这些很虚的东西不是很在意,前段时间打开笔记才发现——真的是珍宝。今天加以总结,为以后学习设计模式做铺垫吧。
持续更新中~