Trick and Magic(OO博客第二弹)

  代码是设计,不是简单的陈述。而设计不只要求功能的正确性,更注重设计风格和模式。java

  真正能够投入应用的程序设计,不是那种无脑的“黑箱”,超巨大的数组,多重循环暴力搜索,成吨全局变量……事实上,在实际应用中更重要的是权衡兼顾功能,性能,可读性,鲁棒性等等方面,而最终完成一个综合的工程。咱们真正作的事是程序设计,而不是无脑地写代码。git

  在本次OO三次做业的周期里,我逐渐开始接触了多线程并发程序和真正工程化的模板设计,历经疯狂Google--爆肝写码--艰难读码的过程后,我从中发现了许多许多很是tricky的东西,但愿能把它们总结出来并与你们分享。github

(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

相关文章
相关标签/搜索