欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

论面向组合子程序设计方法 之 重构2 OOCC++C#IOC 

程序员文章站 2022-06-03 15:16:02
...
已经有点感觉用ioc container来说明co不见得是个好主意了。
这个container的例子举出来,明显提出意见的人比那个简单的logging例子少了很多。
毕竟连pico是怎么回事,怎么用,很多人都还不见得了了。更不提多少人对pico的用法就是一个很in的fancy factory。买椟还珠。



不过,既然开始了,让我还是有始有终吧。


这章还是让我们看看co的refactor。

其实,很多人问:怎样把握co里面的基本组合子的度;什么样的组合子算是基本;怎样做到正交;多少的基本组合子才算够用;怎么知道这个组合子会被用到等等。


其实,答案都来自重构。

没有谁一下子就作对的。co比起oo,我感觉在设计上反而更容易避免过度设计。

为什么?

设计oo的时候,你要分析需求,设计各个模块的通信接口,这个过程,同样需要经验,同样需要摸索,同样没有一踀而就的捷径。

但是,oo设计的时候又要避免过度,一些时候,在是否通过接口预留灵活性,提取容易变化的部分,或者是尽量简单之间,还是有冲突的。你需要做一个艰难的猜测和抉择。
而一旦抉择作出,以后如果发现事情进展不如所愿,那么改动接口的代价相当的大。


而如果使用co,在设计简单的各个组合子的时候,你会以一种非常渐进式的方式来发现:哦,原来的组合子设计不够正交,有这个地方可以抽出来,好,抽出来,把波及到的几个组合子的设计修改一下。

因为组合子都非常简单,这个变化的波及范围一般来说相当小。


好,空话少说,我们还是看具体例子。


现在,我们发现,除了withArgument, withProperty,我们还希望更灵活地设置参数,比如,我们希望说:
[list]对组件X的各个参数,类型为A的,选取以"a1"标识的组件作为参数值,其它的按照缺省方式。
对组件Y的各个参数,类型为A的,选取以"a2"标识的组件为参数值,其它的按照缺省方式。[/list:u]


这个需求有几个点:
[list]1。需要能够通过key来直接指定某个某个组件,相当于一个"ref"。
2。需要对参数配置有除了按照参数位置之外的更灵活的配置(比如,按照参数类型)。[/list:u]


对第一点,我们制作以下的组合子来对应。(看,我们是可以随着需求虽然丰富我们的基本组合子的集合的)
我们期望做一个UseKey组合子,它可以从容器里面取得另外一个用某个key标识的组件,然后把一切动作都delegate过去。

class UseKey extends Component{
  private final Object key;
  public Object create(Dependency dep);{
    //?????????
  }
  ....
}



可是,一开始写代码,就发现,这个代码写不下去!我们需要得到这个容器,才能从这个容器里面取得那个要delegate的组件。可是这个可爱容器对象在哪里呀?

仔细分析下来,发现,没有办法。唯一的办法是修改Dependency接口,让它除了帮助解析参数和property之外,再提供给我们当前容器的信息。

Dependency接口变为:


interface Dependency{
  Object getArgument(int i, Class type);;
  Object getProperty(Object key, Class type);;
  Container getContainer();;
}



Wow!要改接口了!其实,这一点也不可怕。为什么?

co还有另外一个优点我们一直没有提及:细节封装。这个封装不是一般OO意义上的封装,而是说:把要实现的接口细节封装起来,让客户通过预定义好的组合方式来扩展,而不是象oo那样让用户实现实现这个接口来扩展。

其实,如果用户使用的都是Component对象,而创建Component对象都是通过:
Container.getInstance(Object key);;

这种方式,那么,Dependency这个接口已经实际上沦为我们的内部实现细节了。用户根本不需要知道存在这么一个接口。
实际上,当我们的组合子足够丰富之后,完全可以把Dependency接口隐藏在包内部,彻底地对用户屏蔽这个接口。
如此,客户的扩展完全通过组合Component对象,而不是实现Component接口并且调用Dependency接口。
不管这个Dependency接口是如何设计的,如何变化,我们都可以把变化隔离在我们包内部,而不会影响用户。

好吧。现在假设我们修改了Dependency接口,那么UseKey可以被写为:

class UseKey extends Component{
  private final Object key;
  public Object create(Dependency dep);{
     final Component c = dep.getContainer();.getComponent(key);;
     if(c==null);throw new ComponentNotFoundException(...);;
     return c.create(dep);;
  }
  ....
}



然后,更灵活的参数配置。对这个,我们可以借鉴bind操作,做一个对参数的bind。


interface ParameterBinder{
  Component bind(int i, Class type);;
}


不知道你从Binder接口和ParameterBinder接口看出点什么没有?
1。Binder, ParameterBinder接口都是给用户去实现的。
2。这两个接口都不暴露Component的细节,它们的参数和返回值都不涉及Component的接口签名,客户在实现这两个接口的时候,完全不必关心象Dependency接口这种细节。
3。返回值都是Component,这样,所有的Component组合子都可以被*使用。

实际上,monad组合子就是通过这种方式来在高阶逻辑的层次上隐藏底层细节。



class ParameterBoundDependency implements Dependency{
  private final Dependency dep;
  private final ParameterBinder binder;
  public Object getArgument(int i, Class type);{
    return binder.bind(i, type);.create(dep);;
  }
  ...
}

ParameterBoundComponent extends Component{
  private final Component c;
  private final ParameterBinder binder;
  public Object create(Dependency dep);{
    return c.create(new ParameterBoundDependency(dep, binder););;
  }
  ...
}

用ParameterBinder来做一个Dependency的decorator,问题得到了解决。


然后我们来使用ParameterBoundComponent,为了书写简便,我们假设Component类有一个函数叫做bind(ParameterBinder binder)。另外Components类有一个useKey(Object key)函数来生成一个Component对象,用来指向容器内的另外一个组件。

于是,上面的需求被实现为:

Component x = ...;
Component x2 = x.bind(new ParameterBinder();{
  public Component bind(int i, Class type);{
    if(type.equals(A.class););{
      return Components.useKey("a1");;
    }
    else{
      //???? 行1
    }
  }
});;

这个x2组件,就是为了实现“当参数类型为A,使用a1,否则使用缺省方式”。
可是,在行1处,再次遇到了障碍。这个所谓的“缺省方式”,怎么表示?


经过思考,我们决定实现一个useArgument(int i, Class type)这样一个组合子,这个组合子可以主动在当前的Dependency对象中选择某个参数作为自己的值。这样,上面的行1就可以写作:
Component x = ...;
Component x2 = x.bind(new ParameterBinder();{
  public Component bind(int i, Class type);{
    if(type.equals(A.class););{
      return Components.useKey("a1");;
    }
    else{
      return Components.useArgument(i, type);;// 行1
    }
  }
});;


下面来实现一个UseArgument类:

class UseArgument extends Component{
  private final int i;
  private final Class type;
  public Object create(Dependency dep);{
    return dep.getArgument(i, type);;
  }
  .....
}


哈。完美。一切仍然尽在掌握。
我们可以以几乎任何方式来customizer组件的参数和property。

实际上,如果我们回头看看,甚至可以发现,withArgument(int i, Class type)完全可以用bind(ParameterBinder)来重写:
Component withArgument(Component c, final int i, final Component arg);{
  return c.bind(new ParameterBinder();{
    public Component bind(int k, Class type);{
      if(k==i); return arg;
      else return Components.useArgument(k, type);;
    }
  });;
}


我们很开心地看到,原来的WithArgument类,WithProperty类都可以扫进垃圾箱了。我们只需要实现更加简单的ParameterBinder接口就可以搞定一切。哈。


同时,希望你也看到了隐藏这些具体的WithArgument,ValueComponent类,而用静态工厂函数withArgument(), value()来代替的好处:
我们可以*地重构。当发现某个组合子本身并非最简单,而是可以从一些更简单的组合子推演出来,我们只需要改动这些静态工厂函数,而不必告诉用户:对不起,我的设计改了,不想要WithArgument类了,你能不能改改你的那段new WithArgument(...)的代码?


co让用户只关注接口,而不要管某个功能是直接实现的,还是组合出来的。静态工厂函数提供了对这个细节的封装。


另外一个也许会比较常见的需求,是用一个数组来一次性指定某个组件的所有参数,比如:

c.withArguments(new Component[]{c1, c2, c3});;


这个功能用bind非常非常好实现:

Component withArguments(final Component[] args);{
  return bind(new ParameterBinder();{
    public Component bind(int i, Class type);{
      return args[i];
    }
  });;
}


当然,你还可以举一反三地提出很多其它的定制参数和property的方法。


好了。今天就到这里。在结束前,我来先提出两个新的需求:
1。希望对一些用到Logger对象的类注射Logger实例,而这个Logger实例需要用这个使用Logger对象的类对象来创建,这样,这个Logger对象可以静态地知道谁在使用它,而不必每次都构造一个异常来取得StackTrace。
比如,
new ClassX(..., Loggers.instance(ClassX.class);, ...);;

怎样在容器级别全局地规定这个规则呢?我们不知道哪些组件需要注射Logger,也不知道这些组件在哪个参数注射Logger对象。

2。怎样提供缺省参数?这样,如果某个参数的需要可以在容器中解析,则拥这个解析出来的实例,否则,使用一个缺省组件。

在下一节,我们会通过这两个例子来继续解释co的重构过程。