`
ajoo
  • 浏览: 453117 次
社区版块
存档分类
最新评论

论面向组合子程序设计方法 之 重构2

阅读更多
已经有点感觉用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的重构过程。
分享到:
评论

相关推荐

    戏说面向对象程序设计(C#版).pdf

    综上所述,面向对象程序设计不仅仅是关于语言特性的应用,更是一种设计理念和方法论。通过合理运用面向对象的设计原则和设计模式,可以极大地提高软件的质量和效率。对于开发者来说,深入理解和掌握这些核心概念是...

    面向对象程序设计之C#版Grady Booch.pdf

    ### 面向对象程序设计之C#版Grady Booch.pdf #### 重要知识点概览 本资料《面向对象程序设计之C#版》由知名计算机科学家Grady Booch编写,旨在通过轻松幽默的方式介绍面向对象编程的核心概念及其在C#中的应用。...

    重构改善既有代码的设计(PDF)

    2. 提炼方法(Extract Method):当发现一个方法太过庞大、复杂时,可以通过提炼出子方法来简化原有的方法。这有助于代码的复用和清晰性。 3. 移除中间人(Remove Middle Man):当一个类的接口内有太多方法只是...

    面向服务的分析与设计原理之二

    面向服务的分析与设计(Service-Oriented Analysis and Design,简称SOAD)是软件开发领域中的一种重要方法,尤其在构建灵活、可扩展的企业级应用时。本文将深入探讨SOAD的基本原理,以及如何满足其需求,以促进更好...

    java面向对象设计的六大原则

    这六个面向对象设计原则为开发人员提供了一套指导思想,帮助他们在设计和重构系统时做出更明智的选择。遵循这些原则不仅可以提高代码的质量,还能增强系统的可扩展性和可维护性。虽然这些原则看起来简单,但在实际...

    JAVA61条面向对象设计的经验原则

    ### JAVA61条面向对象设计的...这些原则涵盖了面向对象设计的各个方面,从封装、依赖管理、方法设计到系统架构等多个层面提供了宝贵的指导建议。遵循这些原则可以帮助开发者编写出高质量、可维护性强的面向对象代码。

    道法自然 面向对象实践指南6-6

    面向对象编程(Object-Oriented Programming,简称OOP)是一种编程范式,它基于“对象”的概念,将数据和操作数据的方法封装在一起,以实现代码的模块化和可重用性。在“道法自然 面向对象实践指南6-6”中,我们可能...

    C#面向对象设计模式纵横谈(1)面向对象设计模式

    面向对象设计模式是软件开发中的重要概念,尤其是在C#这样的面向对象编程语言中。设计模式是一种在特定场景下解决问题的标准化解决方案,它来源于实践中并被广泛验证为有效的。本篇文章将深入探讨C#中面向对象设计...

    61条面向对象设计的经验原则

    面向对象设计(Object-Oriented Design,简称OOD)是一种广泛应用于软件工程领域的设计方法论,它基于类和对象的概念,强调模块化、封装、继承和多态性等核心原则。以下是对"61条面向对象设计的经验原则"的详细解释...

    二十三种设计模式【PDF版】

    在真正可复用的面向对象编程中,GoF 的《设计模式》为我们提供了一套可复用的面向对象技术,再配合 Refactoring(重构方法), 所以很少存在简单重复的工作,加上Java 代码的精炼性和面向对象纯洁性(设计模式是 java 的...

    .NET应用程序架构设计 原则 模式与实践

    4. **面向服务架构(SOA)**:将应用程序设计为一组可重用的服务,以提高可扩展性和互操作性。 5. **微服务架构**:将大型应用程序分解为一组小型、独立的服务,每个服务都围绕特定业务能力构建。 6. **领域驱动...

    Object-Oriented Design Knowledge:Principles, Heuristics And Best Practices.pdf

    ### 面向对象设计知识:原则、启发式方法与最佳实践 #### 一、概述 面向对象设计(Object-Oriented Design, OOD)是软件工程领域中的一个重要分支,它强调通过对象来构建系统,并利用类、继承、封装、多态等机制来...

Global site tag (gtag.js) - Google Analytics