Trick and Magic(OO博客第二弹)
代码是设计,不是简单的陈述。而设计不仅要求功能的正确性,更注重设计风格和模式。
真正可以投入应用的程序设计,不是那种无脑的“黑箱”,超巨大的数组,多重循环暴力搜索,成吨全局变量……事实上,在实际应用中更重要的是权衡兼顾功能,性能,可读性,鲁棒性等等方面,而最终完成一个综合的工程。我们真正做的事是程序设计,而不是无脑地写代码。
在本次OO三次作业的周期里,我逐渐开始接触了多线程并发程序和真正工程化的模板设计,历经疯狂Google--爆肝写码--艰难读码的过程后,我从中发现了许多许多非常tricky的东西,希望能把它们总结出来并与大家分享。
(1)从增加逻辑到增加数据
当你遇到一系列功能性的名词时,你会怎么办,例如IFTTT作业中的四种触发器:modified,renamed,size-changed,path-changed。我想很多人的第一反应是下面这样:
/*
* str代表从输入中获取到的字符串,数字对应各种触发器类型
*/
if(str.equals("RENAMED")
return 0;
else if(str.equals("MODIFIED")
return 1;
else if(str.equals("SIZE-CHANGED")
return 2;
else if(str.equals("PATH-CHANGED")
return 3;
else
return -1;
if-else加数字变量,简单粗暴,迅速解决,但是当整个程序变的很大时,或者多个人合作完成工程任务时,这样的方法会导致可读性和协作性非常差。
于是乎,稍微动动脑,又出现了另外一种改进版:
/*
* str代表从输入中获取到的字符串,四个变量均初始化为false;
*/
if(str.equals("RENAMED")
renamed=true;
else if(str.equals("MODIFIED")
modified=true;
else if(str.equals("SIZE-CHANGED")
size-changed=true;
else if(str.equals("PATH-CHANGED")
path-changed=true;
else
throw new XXXException(str);
这种做法初始化四个boolean类型的变量,然后在if-else中对应改变他们的值,四个变量使用实际意义命名,达到了不错的效果。
但是更往深层次想一想,假如我每个if分支里不单单是一条语句,或者有新的触发器增加,即增加需求,那这种方法需要增加一个变量定义,增加一路分支,再去完善新分支中的内容。这样每次都增加逻辑分支,有没有更好的方式呢?
当然是有的。不过首先,我们要明确一下程序中添加逻辑和添加数据的区别。添加逻辑和数据的方式是不一样的,成本更是不一样的。用一句话总结,就是添加数据是非常简单,低成本和低风险的,而添加数据是复杂,高成本和高风险的。下面是一个添加数据方法,即表格驱动法的例子,我使用出租车作业中出租车的四种状态为例:
package enums;
import java.util.HashMap;
import java.util.Map;
//该作业中关于出租车状态的输入输出都是以数字形式,所以这里的键值使用数字字符串。
public enum TaxiStatus {
SERVICE,
ORDER,
WAIT,
STOP;
private static Map<String,TaxiStatus> taxi_map=new HashMap<>();
public static void initialize(){
taxi_map.put("0",TaxiStatus.STOP);
taxi_map.put("1",TaxiStatus.SERVICE);
taxi_map.put("2",TaxiStatus.WAIT);
taxi_map.put("3",TaxiStatus.ORDER);
}
public static TaxiStatus getValueOf(String str){
return taxi_map.get(str);
}
public static boolean inMap(String str){
return taxi_map.containsKey(str);
}
}
通过使用枚举enum和Map将输入形式和枚举类型映射起来,建立了一一对应关系。每次在判断输入信息时调用inMap和getValueOf方法即可,如为false则说明输入不合法,抛出相应异常,如果输入正确则返回对应的枚举名。每次新增需求,仅仅需要增加枚举类型中的数据和映射关系即可。
if(inMap(str))
return TaxiStatus.getValueOf(str);
else
throw new XXXException;
注:由于本人java萌新,所以对于enum和map的使用还比较初级,所以表达的可能比较复杂。对于表格驱动法,最简单的例子就是字典。
从添加逻辑到添加数据的优点如下:
1.将代码中的数据部分和逻辑部分分割开来,使整个程序设计一目了然。
2.对于需求更新甚至是全新的需求有着非常强的适应能力,每次仅需修改数据部分。
3.测试时,只要数据正确就不用测试程序本身的正确性,而添加逻辑必须得再进行测试。
4.添加数据法,或者说类似这样的代码可以重用于各种各样的场景下,而逻辑只能用于其所处的具体语境下,换句话说就是写死在程序中。
5.如果是在大型系统中,添加数据仅仅需要任意人员填写一个表单请求将新的数据加入数据库即可,而逻辑修改必须需要专门的开发人员来处理。
6.添加数据的方式强制限定了代码的风格,任何人添加数据必须遵守已经定义在数据存储容器中的模式,而添加逻辑有很多可以自定义的空间,在多人合作时容易产生问题。
(2)输入处理时的小魔法(正则技巧和自定义异常类)
谈起输入处理,正则表达式就是必不可少的一环了。主流的处理方法有两类:
1. group法
String regex = "(IF) (.+) (renamed|modified|path-changed|size-changed)"
+ " (THEN) (recover|record-summary|record-detail)";
Matcher matcher = Pattern.compile(regex).matcher(s);
//中间省略错误处理
path = matcher.group(2);
trigger = Trigger.parse(matcher.group(3));
task = Task.parse(matcher.group(5));
2.spilt法
if(input_line.matches(regex)){
String[] part=input_line.split("[|]");
String filename=part[1];
String trigger=part[2];
String mission=part[4];
}
这两种方法有一个尴尬的地方就是只有开发者在写代码的当天知道数组下标1,2,4或者group参数2,3,5的代表含义,一旦时间过去很久或是输入格式变化,再次进行修改更新就很麻烦。而事实上正则表达式中有一种给每个匹配部分打上“标签”的方法,通过这种方法将实际含义作为标签,瞬间解决了相关问题。
String INPUT_FORMAT= "\\[(?<id>.*),(?<src>\\(\\d+,\\d+\\)),(?<dst>\\(\\d+,\\d+\\))\\]";
Pattern INPUT_PATTERN=Pattern.compile(INPUT_FORMAT);
Matcher mc=INPUT_PATTERN.matcher(input);
if(mc.find()){
String identifier=mc.group("id");
String src_str=mc.group("src");
String dst_str=mc.group("dst");
}
这段代码的关键点即在正则表达式中使用"(?<标签名>匹配内容)"这样的格式来进行匹配,而对应group的下标就是各个标签名,这样的对应一目了然,也易于添加和修改。
输入处理部分,是一个难度不大但是情况复杂的部分。由于需要判断的情况很多,对于每一种不合法的输入情况又得有专门的处理,所以稍不注意就会形成好几层循环嵌套分支的局面,看起来十分复杂,在作业初期一个inputHandler方法写到七八十行是常有的事,很多人都会陷入如下模式:
if(condition1)
do something
if(condition2)
do something
//省略各种情况
if(condition2333)
do something
现在,就要隆重推出我们的异常类大法了!!!首先是我出租车作业的异常类继承层次图:
首先,在每个异常类中,定义每种情况对应的处理方式和信息反馈,例如:
package exceptions; public class SameSrcDstException extends InputFailedException{
public SameSrcDstException(String src,String dst,double time){
super(String.format("#Same Src and Dst:%s %s %f",src,dst,time));
}
}
其实,在输入内容的具体解析方法中,根据不同的情况,抛出对应的异常:
public static TaxiRequest inputParse(String input,double time)throws InputFailedException {
input=InputHelper.removeSpace(input);
Matcher mc=INPUT_PATTERN.matcher(input);
if(mc.find()){
String identifier=mc.group("id");
String src_str=mc.group("src");
String dst_str=mc.group("dst");
String[] src_spilt=src_str.split(SPILT_FORMAT);
String[] dst_spilt=dst_str.split(SPILT_FORMAT);
int src_x=Integer.parseInt(src_spilt[1]);
int src_y=Integer.parseInt(src_spilt[2]);
int dst_x=Integer.parseInt(dst_spilt[1]);
int dst_y=Integer.parseInt(dst_spilt[2]);
if(rangeJudge(src_x,src_y)){
throw new OutLocationException(src_str,time);
}
if(rangeJudge(dst_x,dst_y)){
throw new OutLocationException(dst_str,time);
}
if(sameSrcDst(src_x,src_y,dst_x,dst_y)){
throw new SameSrcDstException(src_str,dst_str,time);
}
return new TaxiRequest(identifier,new Point(src_x,src_y),
new Point(dst_x,dst_y),time);
}
else{
throw new InvalidInputContent(input,time);
}
}
最后,在输入处理的全局范围中,使用try-catch进行捕捉,调用相关异常类的方法进行处理,一套清晰完整高效的输入处理流程就构建起来了。
private void inputRequest(){
Scanner input=new Scanner(System.in);
while(input.hasNext()){
String input_line=input.nextLine();
if(InputHelper.isEND(input_line))
break;
try{
TaxiRequest request=InputHelper.inputParse(input_line,time);
quene.add(request,start_time);
}catch (InputFailedException e){
System.out.println(String.format("#Failed:%s",e.getMessage()));
}
}
input.close();
}
(3)线程安全的迷惑点
关于线程安全,每个人都有这样一个问题,哪些类是线程安全的?而我可以不负责任的告诉你,所有类都不是线程安全的!!!
相信很多人都发现了作为队列用容器ArrayList和Vector的问题,其中Vector一般被称作线程安全类,这是为什么呢,通过对比两者的源码,原因很明显,就是一个synchronized的问题,Vector中可能会产生线程安全问题的方法都加了synchronized进行修饰,而ArrayList则不然:
public synchronized int size() {
return elementCount;
}
public int size() {
return size;
}
//前者是Vector中实现而后者是ArrayList中实现
但是,切不可简单的认为只要使用Vector就万事无忧了。这里的线程安全只是指Vector类实例化的对象的单个方法本身是线程安全的,但如果一个类中的方法A调用了所谓线程安全中的类的多个方法,并且A没有synchronized修饰,那么该方法A如果被多个线程重入,是仍然会产生线程安全问题的。实例如下:
Object value = map.get(key);
if(value == null)
{
value = new Object();
map.put(key,value);
}
return value;
map是一个线程安全类的对象,但是在该方法片段中get和put方法之间的部分,是有可能发生当前线程时间片结束,而另一个线程获得时间片进入该段代码执行的,从而造成一个key可能对应两个value这样的问题,所以是线程不安全的。而解决的方法就是对这段代码整体加synchronized。由此可见,在这种意义上,没有真正线程安全的类,我们必须依靠手中的synchronized,在合适的位置对具体的代码片段进行保护,线程安全类只是给我们提供一个小单元的线程安全。
(4)三次作业的心路历程
这三次多线程大冒险是非常曲折的,具体的失败经历就一笔带过了。其中多线程的控制问题一直刁难着我,在初始接触的时候,我一直纠结的一个问题就是:为什么提供的方法中不能确定地让某个线程休息,让某个线程工作,同时notify方法每次通知的又是哪个线程,究竟线程的执行顺序是怎样的?为什么不采用将所有线程固定好顺序分享时间片来执行。经过这三次作业之后,我目前的答案是:
1.线程执行的顺序不一定可以遵循一种固定的模式,例如网页请求的处理,请求的发送时间是随机的,为了做到及时响应,我们必须在输入到达的时候就做到及时响应,完成线程切换。
2.有一些情况根本不用考虑线程执行的先后顺序,无论什么样的顺序程序也能正常执行,所以不需要多费力去调整顺序。
3.可以通过外部定义相关条件判断来构造“有序”。如经典生产者消费者问题中的变量empty和full,记录产品的队列的事实情况从而有序的控制生产者和消费者之间的顺序。
多线程程序,最重要的就是共享对象,正是由于共享对象的存在才导致了多线程相关的问题。若没有共享对象,多线程程序只是几个同时执行的单线程罢了,没有什么两样。在设计的阶段,定义怎样的共享对象类,能够有效的在线程之间传递信息,是首要问题。接着,对于具体的方法,判断是否有对共享对象产生影响的行为,如果是,就要做相应的保护和同步,这是根据类的设计来对应处理的。解决好了这两个问题多线程编程中多线程的部分也就基本OK了。
在公测和互测中的各类BUG,要么是对于某些特殊情况缺乏考虑,要么是功能变多后代码之间的互相影响。而在构建完整个工程后再debug是非常痛苦的,不光找bug痛苦,改bug更痛苦,牵一发而动全身,你永远不知道改了当前的错误会不会产生新的错误。由此看来,代码的整体框架设计,可读性,可扩展性真的是基石一般的存在。好的代码风格和习惯,会一点一点给那些坚持好习惯的人带来惊喜。
最后,非常感谢这几次作业结束后分享给我代码的同学和互测遇到的同学!!!读代码真的是一件收益良多的事情。
当然特别特别感谢HansBug,他的工程模板堪称业界良心,最后附上模板GitHub链接 https://github.com/HansBug/java-project-template
Trick and Magic(OO博客第二弹)的更多相关文章
- OO博客总结——OO落下帷幕
OO博客总结--OO落下帷幕 凡此过往,皆为序章. 不知不觉OO课程即将落下帷幕,一路坎坎坷坷磕磕绊绊,可算是要结束了,心里终于松了一口气,也有小小的不甘和遗憾.凡此过往,皆为序章.特殊的线上OO课程 ...
- OO博客作业-《JML之卷》
OO第三单元小结 一.JML语言理论基础以及应用工具链情况梳理 一句话来说,JML就是用于对JAVA程序设计逻辑的预先约定的一种语言,以便正确严格高效地完成程序以及展开测试,这在不能容忍细微错误的工程 ...
- 一鼓作气 博客--第二篇 note2
1.循环正常结束是指没有中间截断,即没有执行break; for i in range(10) print(i) else: print("循环正常结束") 2.嵌套循环 for ...
- OO博客作业1:第1-3周作业总结
(1)基于度量来分析自己的程序结构 注:UML图中每个划分了的圆角矩形代表一个类或接口,箭头可代表创建.访问数据等行为.类的图形内部分为3个部分,从上到下依次是类的名称.类包含的实例变量(属性).类实 ...
- 第二次oo博客作业--多线程电梯
这次的系列作业是写一个电梯调度,主要目的是让我们熟悉多线程. 第一次作业是一个傻瓜电梯的调度问题,要求也很简单,即每次接一个人就行了.我只用了两个线程,一个是输入线程,一个是电梯线程,输入线程负责从标 ...
- 接着继续(OO博客第四弹)
.测试与JSF正确性论证 测试和JSF正确性论证是对一个程序进行检验的两种方式.测试是来的最直接的,输入合法的输入给出正确的提示,输入非法的输入给出错误信息反馈,直接就能很容易的了解程序的运行情况.但 ...
- 设计与实现分离——面向接口编程(OO博客第三弹)
如果说继承是面向对象程序设计中承前启后的特质,那么接口就是海纳百川的体现了.它们都是对数据和行为的抽象,都是对性质和关系的概括.只不过前者是纵向角度,而后者是横向角度罢了.今天呢,我想从设计+语法角度 ...
- 小菜鸡儿的第三次OO博客
规格化设计历史 规格化设计的历史目前网上的资料并不多,百度谷歌必应也表示无能为力...... 在这里结合现实情况讲一讲自己对程序规格化的理解,首先代码规格化对代码的影响是间接的,或许它不能让你代码里面 ...
- OO博客作业3:第9-11周作业总结
一.总结介绍规格化设计的大致发展历史和为什么得到了人们的重视 1.规格化设计的大致发展历史 规格化设计,又称契约式设计,最早由Bertrand Meyer于1986年提出,出自于<面向对象软件构 ...
随机推荐
- 在Linux环境下设置ArcGIS Server 服务开机自启
在 VMware 11.0 中安装了CentOS 6.5的Linux系统中部署ArcGIS Server,安装完后默认开机不自动启动此服务,每次开机都要手动启动(如下图所示),这样太麻烦.本文记录了设 ...
- SharePoint自动初始化网站列表
1,由于目前的SharePoint网站需要部署到多个服务器上,每个网站的内容都不一样,所以使用备份还原是不可以的.常用的方式便是将列表导出为列表模版,然后将列表模版复制到服务器上,根据列表模版创建列表 ...
- C++练习 | 计算两日期之间天数差
#include<iostream> #include<string> #include<cstring> using namespace std; class D ...
- mysql/mariadb学习记录——查询3(AVG、SUM、COUNT)函数
AVG() 求平均数函数: //求emp表中的sal属性的平均值 mysql> select avg(sal) as salAverage from emp; +-------------+ | ...
- Flume定时启动任务 防止挂掉
一,查看Flume条数:ps -ef|grep java|grep flume|wc -l ==>15 检查进程:给sh脚本添加权限,chmod 777 xx.sh #!/bin/s ...
- 20155226 2016-2017-2 《Java程序设计》第9周学习总结
20155226 2016-2017-2 <Java程序设计>第9周学习总结 教材学习内容总结 JDBC简介 JDBC是用于执行SQL的解决方案,开发人员使用JDBC的标准接口,数据库厂商 ...
- 12-[数据库]--图形工具Navicat
1.Navicat介绍 在生产环境中操作MySQL数据库还是推荐使用命令行工具mysql,但在我们自己开发测试时,可以使用可视化工具Navicat,以图形界面的形式操作MySQL数据库 官网下载:ht ...
- 4-[HTML]-body常用标签1
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8&quo ...
- Mac下 Windows 7 虚拟机搭建SVN服务器的详细步骤(此方法同样适用于单纯的Windows系统搭建SVN)
内容中包含 base64string 图片造成字符过多,拒绝显示
- Scikit-Learn机器学习入门
现在最常用的数据分析的编程语言为R和Python.每种语言都有自己的特点,Python因为Scikit-Learn库赢得了优势.Scikit-Learn有完整的文档,并实现很多机器学习算法,而每种算法 ...