实践任务1:代码规范与标注
目标
1. 阅读和理解样例代码
- fork样例工程,并clone到本地仓库;
- 在本地开发环境上运行样例工程,理解样例工程的代码逻辑;
- 精读样例工程软件代码,描述代码结构及部件组成;
- 以UML图描述样例工程的组成及结构图(类及类之间的关系);
2. 标注样例工程中的代码
- 基于javadoc规范标注代码,对包、类、方法、代码片段、参数和语句等代码层次进行注释(可参考Game类的标注样例);
- 注释后的代码提交到本地代码库后,同步推送到远程代码仓库;
- 可参考ESLint、github/super-linter等开发插件了解关于代码规范的相关知识;
3. 扩充和维护样例工程
- 对样例代码中的功能设计进行分析,找出若干设计缺陷和改进点,并进行修正或扩充,并集成到工程代码中;
- 可借助代码质量分析工具或代码规范检查工具(如SonarQube、ESLint等)对代码质量进行分析,发现潜在问题;
4. 任务输出
- 以UML图表示的样例工程软件结构;
- 在所有源代码文件中完成源代码标注和扩充,并通过git提交到代码库;
- 在项目根目录下创建一个名称为REPORT.md的文件(与README.md文件同级),以markdown语法格式编写本实训任务的报告,主要包含样例工程的代码结构分析(可以用UML类图及文字进行说明),以及自己改进的功能实现说明;
1. 阅读和理解样例代码
通过Git 工具将项目 fork 到本地,打开IDEA加载此项目,在加载过程中需要配置运行项目的JDK等环境。
此项目采用JDK1.8 版本编译执行。
1.1 项目结构
项目整体可以看成是一种C/S架构,区分为客户端和服务器,由用户启动游戏后在终端输入一些命令。这些命令被服务器所接收识别 ,逐步经过命令接收、命令识别、命令解析、执行命令四个步骤,然后将结果反馈给用户。
1.2 系统用例图
当前项目主要的实现目标就是玩家能够输入 Help、Go、Quit指令来操纵游戏,因此从用户角度来考虑系统用例就是玩家进入游戏输入命令。在系统内部包含对用户输入指令的识别、解析、然后执行,最后根据指令的类型确定是否有结果反馈。
1.3 系统类图
1.3.1 类功能分析
整个项目主要由以下五个类构成:
- Command :用来接收用户输入的指令,内部包含基础指令(commandWord)、具体指令(secondWord);
- CommandWords :系统内部可用指令,主要是三个指令 Quit、Go、Help;
- Room :系统中所有房间的基类,主要包含两个属性 description(房间名称)、exits(房间出口与其它房间的关系);后续可以实现继承该类来构建不同的房间对象。
- Parser :解析终端用户输入命令的工具类。和
CommandWords
类是组合关系。内部主要通过Scanner对象获取用户输入,然后对输入的命令进行解析,解析成两个单词,前一个单词必须是系统内部可用指令之一,后一个单词表示具体的命令。比如说房间移动命令go east
,就表示启用go
命令,然后向东方向移动。 - Game :游戏的主类,用户进入游戏后项目就执行此类的
play()
方法开始执行游戏。在初始化Game
类对象时执行createRooms()
方法来初始化房间,同时初始化加载Parser
工具类对象来执行命令解析的操作。
1.3.2 类间关系分析
Main
public class Main {
public static void main(String[] args) {
Game game = new Game();
game.play();
}
}
主启动类,通过 new 生成了一个 Game
类的对象,然后调用它的 play()
方法开始游戏。
主启动类的功能实现需要 Game
类的协助,Game
类表现为 Main
中的局部变量,因此二者是依赖关系。
Game
public class Game
{
private Parser parser;
private Room currentRoom;
private void createRooms(){Room outside, theater, pub, lab, office;}
public void play()
{
printWelcome();
// Enter the main command loop. Here we repeatedly read commands and
// execute them until the game is over.
boolean finished = false;
while (! finished) {
// 获取解析后的命令
Command command = parser.getCommand();
// 边执行命令边获取结果(可退出)
finished = processCommand(command);
}
System.out.println("Thank you for playing. Good bye.");
}
private boolean quit(Command command)
{
if(command.hasSecondWord()) {
System.out.println("Quit what?");
return false;
}
else {
return true; // signal that we want to quit
}
}
}
Game
类中有两个成员变量,分别是 Parser
类和 Room
类,因此Game
和Parser
、Room
之间是一种组合关系。
Game
类的方法quit(Command command)
中 Command
类对象作为方法参数出现,因此二者之间是依赖关系。
Parser
public class Parser
{
private CommandWords commands; // 所有有效命令集合
private Scanner reader; // Scanner输入对象
public Command getCommand()
{
String inputLine; // will hold the full input line
String word1 = null;
String word2 = null;
if(commands.isCommand(word1)) {
return new Command(word1, word2);
}
}
}
Parser
类中有CommandWords
类对象作为成员变量,因此二者之间是组合关系;
Parser
类的方法getCommand()
中有Command
类作为方法的局部变量,因此二者之间是依赖关系。
2. 标注样例工程中的代码
具体的工程注解已经通过Git同步到GitHub上,可前往查看。
3. 扩充和维护样例工程
3.1 优化点一、Room类与Game类耦合度高
问题发现
通过类图我们可以发现 Room
类和Game
类之间是组合关系,二者的耦合度非常高。但是游戏中肯定不止一个房间,假设存在RoomA
、RoomB
、RoomC
三个房间类,这样就会造成这些房间类和Game
类之间的强耦合,那么当我们需要修改某一个房间类时就会牵扯到Game
类的相关代码改动,这样每次都需要进行很繁杂的维护,这里就存在问题。
解决策略1:(依赖倒置原则+工厂方法设计模式)
思想:
按照设计模式六大原则中的依赖倒置
原则。
-
上层不应该依赖于下层的实体类,两者都应该依赖于抽象或者接口,实现面向接口编程。
-
抽象不应该依赖于细节(具体实现类)。
-
细节(具体实现类)应该依赖于抽象。
那么我们就可以将Room
类构造成一个抽象类,然后Game
类依赖于抽象类Room
,通过构造函数或者setter()
方法来传递依赖对象。
同时在创建不同类型的房间时我们不需要知道房间内部的创建细节,只需要得到这个房间对象就可以了,因此还可以结合工厂方法模式来搭建房间工厂生成房间对象,这样做的好处就是当我们每次需要生成一个新房间时只需要新增该房间对应的生产工厂,在工厂内部定义如何生产房间,其它代码都不需要修改,这样极大程度上降低了程序代码的耦合度,代码维护起来更方便。
代码:
新加入了一个room包,该包中主要放置生产房间的工厂和房间基类。
更详细代码可查看 Github
AbsRoom
/**
* 抽象房间类
*/
public abstract class AbsRoom {
private String description;
private HashMap<String, AbsRoom> exits;
public void setDescription(String name){
this.description = name;
}
public void setExit(String direction, AbsRoom neighbor)
{
exits.put(direction, neighbor);
}
public String getShortDescription()
{
return description;
}
public String getLongDescription()
{
return "You are " + description + ".\n" + getExitString();
}
private String getExitString()
{
String returnString = "Exits:";
Set<String> keys = exits.keySet();
for(String exit : keys) {
returnString += " " + exit;
}
return returnString;
}
public AbsRoom getExit(String direction)
{
return exits.get(direction);
}
}
SchoolBuilder
public class SchoolBuilder implements RoomBuilderFactory{
@Override
public AbsRoom createRoom() {
int number = 401;
int floor = 3;
HashMap<String,Object> objects = new HashMap<>();
objects.put("desk",40);
objects.put("computer",1);
objects.put("fan",6);
objects.put("airConditioning",2);
return new SchoolRoom(number,floor,objects);
}
}
结果对比:
解决策略2:引入Spring框架
对于耦合性高的问题,更加简单高效的方法是采用Spring
框架,对象的管理权不由使用对象的双方来控制管理,而由与对象无关的Spring 容器
来管理,Spring
的IOC
思想(控制反转),就是降低耦合度的非常棒的选择。
而且引入Spring
框架后还可以使用AOP
的功能对项目中的某些代码进行增强,它使用了动态代理的原理实现了对某些方法的进一步强化,而不用去修改其它模块的代码。比如说玩家探险时可以在某个固定的时刻进行消息提示、对于一些禁止操作也可以进行记录。
这里由于时间有限就不附上代码了,想要学习了解可以参考我的Github
3.2 优化点二、策略模式使用
问题发现
针对Game
类的如下代码可进行相关的优化操作:
private boolean processCommand(Command command)
{
boolean wantToQuit = false;
// 无效命令
if(command.isUnknown()) {
System.out.println("I don't know what you mean...");
return false;
}
// 获取命令字段1
String commandWord = command.getCommandWord();
if (commandWord.equals("help")) {
printHelp();
}
else if (commandWord.equals("go")) {
goRoom(command);
}
else if (commandWord.equals("quit")) {
wantToQuit = quit(command);
}
// else command not recognised.
return wantToQuit;
}
可以发现代码中存在多个 if-else
语句,那么当项目中再增加多个有效指令时就会产生许多if-else
,不仅代码不美观而且性能、安全性都比较差。
解决策略:采用策略模式调整代码结构
思想:
可以采取策略模式来减少代码中的if-else
语句,实现算法和具体上下文的分离。策略模式需要定义一个上下文对象Context
、抽象的策略角色Strategy
、具体的策略角色ConcreteStrategy
。
代码:
上下文Context
/**
* 策略模式-------上下文类
*/
public class Context {
private Strategy strategy;
public Context(Strategy strategy){
this.strategy = strategy;
}
public Object getResult(){
return strategy.copeWithCommand();
}
}
抽象策略角色 Strategy
public abstract class Strategy {
private Command command = null;
private Game game = null;
public Command getCommand(){
return this.command;
}
public Game getGame(){
return this.game;
}
public Strategy(Command command,Game game) {
this.command = command;
this.game = game;
}
public abstract Object copeWithCommand();
}
具体策略对象 StrategyGo
public class StrategyGo extends Strategy{
private Command command = getCommand();
private Game game = getGame();
public StrategyGo(Command command, Game game) {
super(command,game);
}
@Override
public Object copeWithCommand() {
if(!command.hasSecondWord()) {
// if there is no second word, we don't know where to go...
System.out.println("Go where?");
return "Unknow where to Go....";
}
String direction = command.getSecondWord();
// 尝试离开当前房间,前往新房间
AbsRoom nextRoom = game.getCurrentRoom().getExit(direction);
if (nextRoom == null) {
System.out.println("There is no door!");
}
// 切换房间
else {
game.setCurrentRoom(nextRoom);
System.out.println(game.getCurrentRoom().getLongDescription());
}
return "Success moving !";
}
具体策略对象 StrategyHelp
public class StrategyHelp extends Strategy{
private Game game = null;
public StrategyHelp(Command command, Game game) {
super(command,game);
}
@Override
public Object copeWithCommand() {
System.out.println("You are lost. You are alone. You wander");
System.out.println("around at the university.");
System.out.println();
System.out.println("Your command words are:");
game.getParser().showCommands();
return null;
}
}
具体策略对象 StrategyQuit
public class StrategyQuit extends Strategy{
public StrategyQuit(Command command, Game game) {
super(command,game);
}
@Override
public Object copeWithCommand() {
Command command = getCommand();
if(command.hasSecondWord()) {
System.out.println("Quit what?");
return false;
}
else {
return true; // signal that we want to quit
}
}
}
结果对比:
4. 利用Javadoc检查代码规范
通过在IDEA中进行相关的配置,导出代码注释的规范性检查文件
Javadoc工具的使用:
注释样例
/**
* 该类是项目中所有房间的基类
* 包含两个属性: description(房间名称)、exits(房间出口与其它房间的关系)
* @author Michael Kölling and David J. Barnes
* @version 1.0
*/
public class Room
{
private String description; //房间名
private HashMap<String, Room> exits; //与其它房间的关系
public Room(String description)
{
this.description = description;
exits = new HashMap<>();
}
/**
* 设置当前房间与其它相邻房间的关系
* @param direction 目标Room所指向的方向描述
* @param neighbor 相邻方向的房间对象
*/
public void setExit(String direction, Room neighbor)
{
exits.put(direction, neighbor);
}
/**
* 简要描述当前位置
* @return 返回房间的简要描述
*/
public String getShortDescription()
{
return description;
}
/**
* 详细描述当前位置
* @return String 返回房间的详细描述
*/
public String getLongDescription()
{
return "You are " + description + ".\n" + getExitString();
}
/**
* 展示所有和当前房间相邻的房间信息
* @return String 相邻房间的信息
*/
private String getExitString()
{
String returnString = "Exits:";
Set<String> keys = exits.keySet();
for(String exit : keys) {
returnString += " " + exit;
}
return returnString;
}
/**
* 获取指定方向的房间信息
* @param direction 所描述的方向
* @return Room 对应方向的房间对象
*/
public Room getExit(String direction)
{
return exits.get(direction);
}
}
命令行检查结果
输入结果文件