前言
由于性格原因,笔者很难沉下心来进行严肃的系统学习,总是喜欢折腾一些奇淫技巧,非常喜欢代码设计,扣代码的细节,所以本次分享一下我所知道的如何写最少的代码的小技巧。
Java:我想返回多个返回值
秀一下Go的多返回值:
- package main
- import "fmt"
- // 返回 X + Y 和 X * Y
- func Computer(X, Y int) (int, int) {
- return X + Y, X * Y
- }
众所周知,Java仅支持单一返回值,一般情况下如果需要返回多个对象,我们会根据代码语义选择容器或者新建一个新的类,把我们需要的数据包起来。
这样做有没有问题?当然没有问题,但是瑕疵就在于:可能会产生没啥语义但又不得不存在的中间类,我个人非常讨论该类代码,那么该如何解决这种问题呢?
首先需要认识到,解决方案必须满足几个要求:
- 代码可复用
- 语义要清晰
- 安全
既然如此,我们可以采用泛型来满足复用、语义清晰的要求,用中间类来满足代码安全性的要求,代码如下:
- public class MultipleTwoReturn<A, B> {
- /** 第一个返回值 **/
- private final A first;
- /** 第二个返回值 **/
- private final B second;
- public MultipleTwoReturn(A first, B second) {
- this.first = first;
- this.second = second;
- }
- // 省略Get方法
- }
同时,我们可以依赖于继承,让该工具类拓展更多的参数:
- public class MultipleThreeReturn<A, B, C> extends MultipleTwoReturn<A, B> {
- /** 第三个返回值 **/
- private final C third;
- public MultipleThreeReturn(A first, B second, C third) {
- super(first, second);
- this.third = third;
- }
- }
测试类:
- public class MultipleApp {
- public static void main(String[] args) {
- MultipleTwoReturn<Integer, String> returnTest = MultipleApp.getReturnTest();
- System.out.println(returnTest.getFirst());
- System.out.println(returnTest.getSecond());
- }
- private static MultipleTwoReturn<Integer, String> getReturnTest() {
- MultipleTwoReturn<Integer, String> demo = new MultipleTwoReturn<>(0, "Kerwin Demo.");
- return demo;
- }
- }
本质还是普通对象,但是加上泛型后威力剧增!由于在方法定义时就强制了泛型约束,语义非常清晰,同时可以完全杜绝上述的无语义中间类,当然一些必要的,有业务含义的组装类,不建议使用这种方式。
泛型:我想new一个对象
大家在学Java泛型之初有没有这种想法?我想利用作为泛型约束,却需要new一个T,但是Java它new不出来啊 😂
很久之前我在写一个通用的Java爬虫接口,里面有一个功能就是传入目标网页的即可获取到针对不同网页设计的Bean,大概如下所示:
- public interface SpiderBeansHandle<T> {
- /** 获取Url **/
- String getUrl();
- /** 获取Cookie **/
- String getCookie();
- /** 获取CSS selector **/
- String getSelector();
- // ....
- }
中间关键的一点即如何获取到这个Bean,那个时候我只有一个想法:new 一个 T
事实证明,我过于天真了 🙄
但是换种思路,既然new不出来,那我就返回一下吧,于是代码出炉了~
- public interface SpiderBeansHandle<T> {
- /**
- * 获取Url
- */
- String getUrl();
- /**
- * 获取Cookie
- */
- String getCookie();
- /***
- * 获取CSS selector
- */
- String getSelector();
- /***
- * 解析Element
- * @param element element
- */
- T parseElement(Element element);
- /***
- * Get Beans
- * @param handle Bean对象 | handle对象
- * @param <T> Bean类型
- * @return List<Beans>
- */
- static <T> List<T> getBeans(SpiderBeansHandle<T> handle) {
- List<T> list = new ArrayList<>();
- List<Element> elements = SpiderUtils.getElementWithCookie(handle.getUrl(), handle.getSelector(), handle.getCookie());
- for (Element element : elements) {
- T bean = handle.parseElement(element);
- if (bean != null) {
- list.add(bean);
- }
- }
- return list;
- }
- }
关键一步就在于:
- /***
- * 解析Element
- * @param element element
- */
- T parseElement(Element element);
那么这个小技巧有什么用呢?仔细看会不会觉得它像一种设计模式的变形体?没错!真相只有一个:模板方法模式
我刚提到了我需要一个处理爬虫的通用接口,因为简单爬虫无非就是拿到url然后请求,解析细节封装到自身的Bean里,然后获取一个列表,那么在开发业务代码的时候类似,肯定有某些场景和需求具有高度的一致性,那么使用这种设计方案即可大大的减少重复代码~
方法:你到底想干嘛?
咱们在写代码的时候有没有遇到过这种问题?写了一个工具类方法,但是功能又过于单一,虽说单一原则好吧,但是一个小逻辑写一堆方法,总感觉不得劲,如何解决咧?
Java8提供的函数式编程即可帮我们一定程度上解决这种问题,如:
- // 写一个获取文件列表,且判断是否为txt结尾的工具类方法,新手会这么写
- public static File getFileWithTxt(String path) throws IOException {
- File file = new File(path);
- if (!file.exists()) {
- throw new IOException("File is not exist.");
- }
- if (file.getName().endsWith(".txt")) {
- return file;
- }
- return null;
- }
老手一般会把 .txt 作为参数传入,但是某一天我需要判断文件大小,文件长度,甚至是文件内容的时候,我该咋办?再写N个?
最好的方案即传入 Predicate 谓词,让调用者自定义处理逻辑,然后再把最常用的逻辑基于该方法复写一下,拓展性Max!代码如下:
- /***
- * 文件夹谓词匹配
- * @param file 文件
- * @param predicate 谓词匹配
- * @return List<File>
- * @throws IOException IOException
- */
- public static List<File> listFilesInDirWithFilter(File file, Predicate<String> predicate) throws IOException {
- if (!file.exists()) {
- throw new IOException("File is not exist.");
- }
- List<File> fileList = new ArrayList<>();
- if (file.isDirectory()) {
- File[] files = file.listFiles();
- for (File f : Objects.requireNonNull(files)) {
- fileList.addAll(listFilesInDirWithFilter(f, predicate));
- }
- } else {
- if (predicate.test(file.getName())) {
- fileList.add(file);
- }
- }
- return fileList;
- }
类似的还比如说处理 IO,直接上代码:
- public static void readLine(BufferedReader br, Consumer<String> handle, boolean close) {
- String s;
- try {
- while (((s = br.readLine()) != null)) {
- handle.accept(s);
- }
- } catch (IOException e) {
- e.printStackTrace();
- } finally {
- if (close && br != null) {
- try {
- br.close();
- } catch (IOException e) {
- e.printStackTrace();
- }
- }
- }
方法说你到底想干嘛?!算了,你想干嘛就干嘛吧,请随意😎~
重载:写的更多也是为了写的更少
写的更多也是为了写的更少,这句话乍一听感觉非常矛盾,但是编程经验比较丰富的小伙伴应该能体会到方法重载的威力,尤其是在写工具类或者底层接口的时候,建议大家先写一个大而全的内部方法,然后一点点去根据需要重载它,会有意想不到的好处。
最简单的例子,如下:
- // Root 方法
- private static void readLine(BufferedReader br, Consumer<String> handle, boolean close) {
- String s;
- try {
- while (((s = br.readLine()) != null)) {
- handle.accept(s);
- }
- } catch (IOException e) {
- e.printStackTrace();
- } finally {
- if (close && br != null) {
- try {
- br.close();
- } catch (IOException e) {
- e.printStackTrace();
- }
- }
- }
- }
- // 重载方法一
- public static void readLine(String path, Consumer<String> handle, boolean close) {
- try {
- BufferedReader br = new BufferedReader(new FileReader(path));
- readLine(br, handle, close);
- } catch (FileNotFoundException e) {
- e.printStackTrace();
- }
- }
- // 重载方法二
- public static void readLine(String path, Consumer<String> handle) {
- readLine(path, handle, true);
- }
重载可以让我们的方法调用方式变得丰富多彩,在语义明确的情况下,写代码有如神助,配合函数式编程,可以让工具类或者底层接口的能力大大增强。
同时,当我们需要调整某一个方法逻辑时,也可以使用继续重载的方式,将影响面降到最小,尽量不动其他模块的代码。
终极:从设计模式到抽象
与其说是如何写最少的代码,不如说是:如何只写真正有价值的代码。
面对这种问题的时候,我们第一反应肯定就是设计模式了,例如上文的泛型章节提到的模板方法模式,小小的推荐一下我之前的文章:
- 【一起学系列】之模板方法:写SSO我只要5分钟
- 设计模式总篇:从为什么需要原则到实际落地
通过良好的设计模式或者其变形体,我们可以得到高内聚低耦合的代码,这是一个非常好的思路。
另一个思路,所有人都认同一点:程序 = 算法 + 数据结构,选择好正确的数据结构可以事倍功半,比如说我们做类似文件夹需求的时候,会想到使用链表或者树结构,在做如:如何高效的给用户发送生日短信时会想到用堆结构(用当前时间对比堆中的最大值,满足则继续迭代,减少遍历)等等。
这其实都是抽象,或深或浅而已,我最开始学习Java的时候,老师会说一句话:万物皆为对象,我们来看看上面的技巧各自对应着什么?
- 多返回值:封装对象 + 泛型约束
- 泛型:封装对象的公共接口,高度抽象
- 函数式方法:把方法当作一个对象
- 重载:对象方法(行为)的不断演化
所以如何只写真正有价值的代码?官方一点的话就是:把变化的抽象出来,那么到底该怎么抽?
这就需要我们一点点的去探索了,毕竟奇淫技巧只是小道尔,不过我会一直探索下去。