Java与模式之重逢 - xiaoR's Blog

Java与模式之重逢

xiaoR posted @ 2016年7月14日 19:57 in Java with tags lambda java , 5584 阅读

在很久以前,人们对世界的抽象还停留在比较原始的阶段。还流行过一句话,程序=数据结构+算法。随着时间的推移,人们认识到数据和行为的统一性,用类来封装数据,隐藏数据,并且对行为建模。这种更高级的ADT应该是OO的最基本形态.用类继承的方式来渐进式地描述现实生活中的概念.对概念之间的差别用子类覆盖父类某一个方法的途径来表达.这就是所谓的面相对象三大特征"封装,继承,多态".当这个概念被提出时,人们就像回到初恋(自行脑补食神中的沙滩场景),世界一下子就明亮了起来。然而现实还是很骨感的,多重继承深深的打击了人们(初恋往往都是苦涩的)。于是乎站在神坛的4人帮就站出来说:你们这般low X,你们必选要按照一定的“模式”来和异性相处才能更好的向往未来。作为工业界的新生Java君当然会响应这个号召,而去更近一步明确确认了subType(Interface)和subClass之间的职责。这也是为啥模式起于C++却在Java这发扬广发(自己脑补的,带主观排定).那时候你可能会有


或者

或者这个?

到了近代,“大数据”的概念被炒热,某哥发表了“三大论文”,lambad被重新提起。Java君也顺应时代加上了lambda的新衣,今天就带着各位看官走马观花下披上新衣的java与模式的重复。

单列模式

是否还记得面试的时候面试官叫你用多种方式写出单列模式?单列的写法有很多比如像什么懒汉式啊,饿汉式啊,双重锁的形式甚至有些文艺青年用Guice等IOC容器直接撸。当然这里更推荐使用<>的枚举写法,至于为什么就留给各位看官自己去思考了:
 
public enum Singleton {
  
  INSTANCE;
	
}

在Lambda这边,比如scala,单列是其内置的一种语法元素用object来表示实现一切皆对象的大一统。

object Singeton{}

策略模式

先简单回顾下策略模式的基本要术:

Strategy: 算法的公共接口,Context交互

Context: 上下文用于Strategy算法的选择

ConcreteStrategy: 算法的具体实现

简单的使用一个根据运行环境来完成支付金额的例子,生产环境为全额,测试环境为0.01

public interface PayStrategy {
    BigDecimal calculateMoney(BigDecimal money);
}
/**具体算法实现 **/
public class DebugPayStrategy implements PayStrategy{
   public BigDecimal calculateMoney(BigDecimal money){
        return new BigDecimal("0.01");
   }
}
	
public class ProPayStrategy implements PayStrategy{
    public BigDecimal calculateMoney(BigDecimal money{
          return money;
    }
}
	/**context 直接使用Main方法了**/
public static void main(String...args){
      List<PayStrategy> payStrategyList = Arrays.asList(new DebugPayStrategy(),new ProPayStrategy());
      for(PayStrategy strategy : payStrategyList){
	   System.out.println(strategy.calculateMoney(new BigDecimal("1000")));
	}
}

使用标准的策略模式需要的代码还是有点小多(定义每个特定的算法实现),现在使用lambda来对其进行改造,看代码:

public static void main(String...args){
	List<PayStrategy> payStrategyList = Arrays.asList(
			(money) -> new BigDecimal("0.01"),
			(money) -> money
	);
	payStrategyList.foreach(strategy -> {
		System.out.println(strategy.calculateMoney(new BigDecimal("1000")));
	}); 
}
/**如果需要对策略进行选择可以通过Predicate来判定**/
Predicate<PayStrategy> choose = (strategy) -> Objects.equals(strategy.evn,"debug")
/**当然也可以用模式匹配去实现(日志策略)**/
public static String addPrefix(String log) {
     return Matching
        .when().isMatch(allOf(not(containsString("Exception")),not(containsString("weird"))))
                    .thenApply(message -> "INFO " + message)
        .when().isMatch(containsString("weird"))
                    .thenApply(message -> "WARN " + message)
        .when().isMatch(containsString("Exception"))
                    .thenApply(message -> "ERROR " + message)
        .match(log).get();
}

装饰模式

同样简单回顾下装饰模式的基本要术:

Component:给出一个抽象接口,以规范准备接收附加责任的对象.
ConcreteComponent:定义一个将要接收附加责任的类
Decorator:持有一个构件(Component)对象的实例,并实现一个与抽象构件接口一致的接口
ConcreteDecorator:负责给构件对象添加上附加的责任
装饰模式有一个显著的特点就是类特别多,类特别多,类特别多!!!!!尤其在面向对象中,比如java的IO库,各种XXXStream,大大增加了实用的复杂度,也就催生了Java8新的IO库。下边用一个计算还款金额的例子来演示.
/**
 * 为了简单,这里直接使用Java8的Function接口的andThen方法来模拟
 * 还款金额 = 本金+逾期费+红包减免+服务费
 **/
Function<Double, Double> gift        = (current) -> current * 0.2;

Function<Double, Double> overDue     = (current) -> current * 1.2;

Function<Double, Double> serviceFree = (current) -> current + 10;

List<Function<Double, Double>> decorators = //添加装饰构件对象
            new ArrayList<Function<Double, Double>>(){{
              add(gift);
              add(overDue);
              add(serviceFree);
            }};
double amount = decorators.stream()
            .reduce(Function::andThen) /**模拟装饰的过程**/
            .orElse(Function.identity()/**base的过程,没有修饰组件的情况**/ 
            .apply(10.0));
/**Function中的两个关键方法andThen和compose可以将多个Function进行串联,最主要的区别在与调用的顺序不同,这里是加法就无所谓了看代码**/
default <V> Function<T, V> andThen(Function<? super R, ? extends V> after) {
    Objects.requireNonNull(after);
    return (T t) -> after.apply(apply(t));
}

default <V> Function<V, R> compose(Function<? super V, ? extends T> before) {
    Objects.requireNonNull(before);
    return (V v) -> apply(before.apply(v));    
}

/**
 * 如果用面向对象的方法去写,可能需要4个装饰类,一个接口....一个...个...
 * 而去还要这么用 new Gift(new OverDue(new ServiceFree(new .....)))好吧。。。如果你喜欢
 *人生苦短,我用lambda  
 **/ 

 

借贷模式

借贷模式主要用于一些资源的管理,比如文件,Lock之类的

对于文件操作以前这么写(使用Java7新增加的Automatic Resource Management)
Properties prop = new Properties();
try (FileInputStream fis = new FileInputStream("config.properties")) {
    prop.load(fis);
}
//auto closed
ARM确实简化了代码,但是必须按要按照一定的"模式"才能够享用其带来的自动管理。
用lambda这么写
public static void use(String fileName,Consumer<InputStream> block) throws Exception {
  try (FileInputStream fis = new FileInputStream(fileName)) {
        block.accept(fis);
  }
}
/**使用**/
Properties prop = new Properties();
use("config.properties",input->{
  try{
    prop.load(input);
  }catch(IOException e){
    //handler exception here
  }
});

改写之后就不需要去关心资源的关闭,但是异常依然带来了好多噪音。接下来我们来处理异常的部分.定一个可以抛出异常的函数式接口

@FunctionalInterface
public interface ThrowableConsumer<T,E extends Throwable> {
  void accept(T t) throws E;
}

public static void use(String fileName,ThrowableConsumer<InputStream,IOException> block) throws IOException {
  try (FileInputStream fis = new FileInputStream(fileName)) {
        block.accept(fis);
  }
}
/**这么用**/
Properties prop = new Properties();
use("config.properties",prop::load);//世界是不是一下子就干净了许多

对于Lock就留给各位看官自己去实现了

递归

讲Lambda不得不提的就是递归了,某些函数式语言本身是没有循环结构,而是通过递归的方式来完成对应的操作。比较常见的两种实用方式是尾递归和备忘录(缓存子问题结果).

曾经有位大神说过:迭代的是人,递归的是神。——L. Peter Deutsch
那么尾递归和传统递归有什么区别呢?来个经典例子。
int factorialTailRecursion(int n, int acc) {
    if (n == 0) return acc;
    return factorialTailRecursion(n - 1, acc * n);
}
在例子中可以看到尾递归和递归最大的不同在于尾递归的逻辑发生在方法调用的最后,而传统的递归往往发生在中部或者拿到递归的结果后还需要对结果进行进一步的处理,当递归的深度很深的时候就会引发StackOverflowError(需要栈帧去纪录结果)。一般而言尾递归优化都是通过编译器优化来完成,但很不幸的是时进今日Java依然没有这个功能,不过可以用Lambda来模拟实现
首先来看完成尾递归的几个关键要术:
1. 判断递归是否已经完成
2. 获取结果
3. 触发递归继续进行
由此可以归纳得到如下接口:
@FunctionalInterface
interface StreamRecursively<T> extends Supplier<T> {
  
  default boolean complete(){return true;}//本次计算是否已经完成
  @Override T get();
  default T result(){return get();}
  /**下一次递归的调用**/
  default  StreamRecursively<T> next(){
    return this;
  }
  /**提供简单方法直接完成一个结果**/
  static <T> StreamRecursively<T> done(T result){ return () -> result;}
  /**通过stream吧递归调用串列(Stream本事是一个无穷的列表,即不会产生栈帧), 再使用终止方法 **/
  /**filter和findFirst得到结果,当且仅当最后一个调用返回true的时候调用连结束 **/
  static <T> StreamRecursively<T> stream(StreamRecursively<StreamRecursively<T>> tasks){
    return new StreamRecursively<T>() {
      @Override public boolean complete(){return false;}
      @Override public StreamRecursively<T> next(){return tasks.get();}
      @Override public T get(){return continual(this);}
      T continual(StreamRecursively<T> call){
        /**Java8 Stream模拟递归过程**/
        return Stream.iterate(call,StreamRecursively::next)
                  .filter(StreamRecursively::complete)
                  .findFirst()
                  .get()
                  .result();
      }
    };
  }
}

/**使用:为了体现价值,这里使用BigInteger **/
StreamRecursively<BigInteger> factorial(BigInteger acc,BigInteger init){
  return BigInteger.ZERO.equals(acc) ? StreamRecursively.done(acc) 
                    : StreamRecursively.steam(() -> 
              factorial(acc.subtract(BigInteger.ONE),init.multiply(acc)));
}
factorial(new BigInteger("20000"),BigInteger.ONE).result();

这里还是有点小噪音(init参数不是必须的),java本事没有curry这个功能只好再封装一个函数把init隐藏掉(这里就不写了啊)。回过头来看为啥经典例子中会StackOverflowError吗?为什么?

DSL

当语言的类型系统和语法(函数式语言)足够强大的时候,使用lambda(高阶函数,函数字面量)就很容易的构建出DSL,比如发邮件,在Java方可能这么去用

@Getter
@ToString
@Builder
@AllArgsConstructor
public class Email {
  private final String from;
  private final String to;
  private final String subject;
  private final String body;
  
  public void send(){
    //send email
  }
}
/**使用**/
Email.builder()
    .from("xxxx@gmail.com")
    .to("xxx@qq.com")
    .subject("hello world")
    .body("content!!!!")
    .send();

使用Lambda(Java不支持函数字面量),这里使用kotlin来演示:

class EmailSpec {
  fun from(from: String) = println("From: $from")
  fun to(to: String) = println("To: $to")
  fun subject(subject: String) = println("Subject: $subject")
  fun body(init: BodySpec.() -> Unit): BodySpec {
      val body = BodySpec()
      body.init()
      return body
    }
}

class BodySpec {
    fun p(p: String) = println("P: $p")
}

fun email(init: EmailSpec.() -> Unit): EmailSpec {
    val email = EmailSpec()
    email.init()
    return email
}
/**使用**/
fun main(args:Array[String]){
  email {
      from ("xxx@gmail.com")
      to ("xxx@qq.com")
      subject ("hello world")
      body {
          p ("content!!!")
          p ("content!!!")
      }
  }
}

下边是一个爬虫的例子(猜猜这事啥语言)

def crawl = {
    navigateTo("http://www.google.com") {
      in(form having id("tsf")) {
        in(textField having id("lst-ib")) {
          typeIn("bplawler")
        }
        in(submit having name("btnK")) {
          click ==>...
        }
      }
    }
    onCurrentPage {
      result = from(div having id("resultStats")) getTextContent
      val url = from(
        anchor having xPath("//div[@id='field_timetable_file-wrapper']/a")
      ).getAttributes.getNamedItem("href").getTextContent
      download(url).writeTo(output)
    }
  }

不是彩蛋的彩蛋

在日常开发中我们时常需要把一些第三方的参数转化为javaBean,在参数没有值的时候还需要给出默认值,如果在保证类型安全的前提下来完成这个功能呢?不知道各位看官是否想过Java中的Annotation+动态代理,来看看怎么用吧.

public final class AnnotationPropertyConverter<T>{
    private final Class<T> target;
    private AnnotationPropertyConverter(Class<T> target){this.target = target;}
    
    public static <T> AnnotationPropertyConverter<T> to(Class<T> target){
        return new AnnotationPropertyConverter<>(target);
    }
    
    @SuppressWarnings("unchecked")
    public T from(Map<String,String> source) {
      return (T) Proxy.newProxyInstance(target.getClassLoader(),new Class[]{target},
              (proxy, method, args)->{
                String value = source.get(method.getName());
                if(value == null){
                  Object defaultValue = method.getDefaultValue();
                  if(defaultValue == null){
                    throws new IllegalArgumentException("no default value define");
                  }
                  return defaultValue;
                }
                //apache BeanUtils
                return converter.convert(value,method.getReturnType());
              });
    }
}
//用法
Map<String,String> source = new HashMap<String,String>(){{
  put("orderId","123778006102384AF");
  put("requestId","787619");
}};

@interface YOrder{
  String orderId default "";
  int requestId default 0;
  /**枚举也是可以的,但要注册一个枚举的转换器**/
}
YOrder  order = AnnotationPropertyConverter.to(YOrder.class).from(source);
order.orderId();
order.requestId();

Java8之后interface具有了默认方法,用默认方法也可以做到同样的效果,看代码

public final class Java8DefaultMethodPropertyConverter<T>{
  private final Class<T> target;
    
  private Java8DefaultMethodPropertyConverter(Class<T> target){
      this.target = target;
  }
    
  public static <T> Java8DefaultMethodPropertyConverter<T> to(Class<T> target){
      return new Java8DefaultMethodPropertyConverter<>(target);
  }
  @SuppressWarnings("unchecked")
  public T from(Map<String,String> source) {
      return (T) Proxy.newProxyInstance(target.getClassLoader(),new Class[]{target},
              (proxy, method, args)->{
                String value = source.get(method.getName());
                if(value == null){
                  if(method.isDefault()){
                    return MethodHandles.lookup()
                    .in(method.getDeclaringClass())
                    .unreflectSpecial(method, method.getDeclaringClass())
                    .bindTo(proxy)
                    .invokeWithArguments(args);
                  }else{
                    throws new IllegalArgumentException("no default value define");
                  }
                }
                //apache BeanUtils
                return converter.convert(value,method.getReturnType());
              });
    }
}

//用法是一致的

写在最后

通过上面的例子,不难发现函数式和面向对象最大的区别(抛开抽象的角度和其他一些因素,单从代码表达上)就是函数式更多的是使用表达式(Expression),面向对象更多的是使用语句(Statement).Expression

能够产生值而且可以组合Function Chain就像积木一样相互瓶装和组合。然而lambda就是这些? 当然不是。函数式语言讲究的是没有不可变,无副作用,也就更适合多核编程(现在的服务器动不动就是10+核数)。在多核领域中"actor"更是明星中的战斗机.你是否想象过:
大千世界就是一个FSM。每个 Actor 对接收到的事件或变化做出相应反应,改变状态,然后传递出新的事件或变化,而并行行为则是大量 Actors 的个体行为的整体表现。如果我们遵循这样一种 Actor 模式,那么每台设备都可以映射成一个 Actor,每个用户也正好是一个 Actor,诸如此类,不一而足,由此组成一个由事件流驱动、并行运行的虚拟世界,它正好是现实世界的映射,像现实世界一样精彩。
--邓草原
若想知道具体细节,请听下回分晓.科科
 

 

 


登录 *


loading captcha image...
(输入验证码)
or Ctrl+Enter
Host by is-Programmer.com | Power by Chito 1.3.3 beta | © 2007 LinuxGem | Design by Matthew "Agent Spork" McGee