作为一个想好好学习设计模式的同学,我打算先放一放具体的设计模式,对设计模式的几大原则进行一次深入的思考和理解。在页尾有几篇Uncle Bob非常不错的文章可以和大家共享。

我写下这篇文章,讲讲我对这几大原则的理解,顺便也为自己复习复习。

SOLID

首先搞搞清楚SOLID代表了啥吧.

  • S代表SRP(单一责任原则),The Single Responsibility Principle。A class should have one, and only one, reason to change。
  • O代表OCP(开放封闭原则),The Open Closed Principle。You should be able to extend a classes behavior, without modifying it。
  • L代表LSP(里氏替换原则),The Liskov Substitution Principle。Derived classes must be substitutable for their base classes。
  • I代表ISP(接口分离原则),The Interface Segregation Principle。Make fine grained interfaces that are client specific.
  • D代表DIP(依赖倒置原则),The Dependency Inversion Principle。Depend on abstractions, not on concretions.

S–单一责任原则

简单来说,引起类变化的因素只有一个。一个类只有一个职责,如果一个类有多余一个职责,那么就应该拆分这个类。如果类包含多个职责,代码会变得耦合;SRP看起来是把事物分离成分子部分,以便于能被复用和集中管理,这点也同样适用于方法。

O–开放封闭原则

简单来说,允许扩展,不许修改。“允许扩展”指的是设计类时要考虑到新需求提出时类可以增加新的功能。“不许修改”指的是一旦一个类开发完成,除了改正bug就不再修改它。通常来说可以通过依赖关系的抽象实现开闭原则,比如接口或抽象类而不是具体类。通过创建新的类实现接口来增加功能。在项目中应用OCP原则可以限制代码的更改,一旦代码完成,测试和调试之后就很少再去更改。这减少了给现有代码引入新bug的风险,增强软件的灵活性。

L–里氏替换原则

里氏替换原则适用于继承层次结构,指出设计类时客户端依赖的父类可以被子类替代,而客户端无须了解这个变化。因此,所有的子类必须按照和他们父类相同方式操作。子类的特定功能可能不同,但是必须符合父类的预期行为。要成为真正的行为子类型,子类必须不仅要实现父类的方法和属性,也要符合其隐含行为。一般来说,如果父类型的一个子类型做了一些父类型的客户没有预期的事情,那这就违反LSP。比如一个派生类抛出了父类没有抛出的异常,或者派生类有些不能预期的副作用。基本上派生类永远不应该比父类做更少的事情。

一个违反LSP的典型例子是Square类派生于Rectangle类。Square类总是假定宽度与高度相等。如果一个正方形对象用于期望一个长方形的上下文中,可能会出现意外行为,因为一个正方形的宽高不能(或者说不应该)被独立修改。

I–接口分离原则

接口隔离原则指出客户不应该被强迫依赖于他们不使用的接口。当我们使用非内聚的接口时,ISP指导我们创建多个较小的内聚度高的接口。

当你应用ISP时,类和他们的依赖使用紧密集中的接口通信,最大限度地减少了对未使用成员的依赖,并相应地降低耦合度。小接口更容易实现,提升了灵活性和重用的可能性。由于很少的类共享这些接口,为响应接口的变化而需要变化的类数量降低,增加了鲁棒性。

D–依赖倒置原则

依赖反转原则(Dependency Inversion Principle,DIP)指出高层次模块不应该依赖于低层次模块;他们应该依赖于抽象。第二,抽象不应该依赖于细节;细节依赖于抽象。方法是将类孤立在依赖于抽象形成的边界后面。如果在那些抽象后面所有的细节发生变化,那我们的类仍然安全。这有助于保持低耦合,使设计更容易改变。DIP也允许我们做单独测试,比如作为系统插件的数据库等细节。

一个程序依赖于Reader和Writer接口,Keyboard和Printer作为依赖于这些抽象的细节实现了这些接口。CharCopier是依赖于Reader和Writer实现类的低层细节,可以传入任何实现了Reader和Writer接口的设备正确地工作

总结

这些原则就是设计模式的基础原则,虽然现在可能还不能深刻的理解,但是作为学习设计模式的基础,先了解,再慢慢学习各种设计模式的例子,最后可以菜能彻底明白这几个原则的意义所在。

Design Principles and Design Patterns
The Principles of OOD

概念

装饰模式属于包装模式,顾名思义是对一个类进行必要的装饰。为什么要装饰呢?

思考一个问题,我们常常会把一些的核心功能抽出来放在一个单独的类中,把这些核心功能封装起来。然而有一些不常用,也不重要的功能,该怎么让这个类去实现呢?

  1. 假如说我们把这些并非核心的功能封装在核心的类中,那这些核心功能与非核心功能混杂在一起就失去了“核心”的效果。
  2. 如果使用继承的办法,需要哪些功能就新建一个类,继承核心类。假如说功能很多,并且功能之间也需要进行组合,那么我们会被无数的继承出来的类折磨的焦头烂额。就算这样也做不到非核心功能之间的自由组合,代码也没有办法很好的复用。

上帝说,要有光。于是,装饰模式出现了。

举个例子:大雄和静香打算出去约会,对于第一次约会大雄很是紧张,穿什么衣服赴约,令他很苦恼,他干脆不想去了。哆啦A梦拿出了临时克隆人机,打算帮助大雄克隆几个自己。大雄这下就放下心来了,但是一个克隆大雄只能穿一套衣服,穿短袖,牛仔裤要克隆一个大雄出来,穿休闲西装,牛仔裤,又要克隆一个大雄出来。大雄小小的屋子几分钟遍塞满了,这还没有给临时克隆人穿鞋呢。

大雄鼓足了勇气,还是决定亲自去赴约。他先穿了漂亮的T恤,又穿了好看的短裤,最后穿上漂亮的皮鞋。看起来帅气极了~于是大雄高高兴兴的去赴约了。

上面的例子中,克隆的大雄“们”就是对核心类不断继承产生的窘境,大雄(Concrete Component)是一个具体的人类(Component),而短裤,T恤,鞋子(Concrete Decorator)都属于服饰(Decorator)。

这就是装饰模式,就像各种各样的服饰穿在身上。

模式意图

动态的给一个对象添加一些额外的职责,添加的功能不会影响到原来的对象。就增加功能来说,Decorator模式相比生成子类更为方便,灵活。

模式参与者

  • 抽象构件(Component):定义一个对象接口,可以给这些对象动态地添加职责。
  • 具体构件(Concrete Component):定义了一个具体的对象,也可以给这个对象添加一些职责。
  • 装饰(Decorator):抽象装饰类。
  • 具体装饰(Concrete Decorator):具体的装饰对象,起到给Component添加职责的功能。

UML图

center

模式实现

首先实现Component接口,作为抽象出的具体构建,以及用于Decorator实现

public interface Component{
	public void show();
}

具体的构建实现Component接口

public class Nobi implement Component{
	private Age age;
	private Height height;
	public setAge(Age age){
		this.age =age;
	}
	public setHeight(Height height){
		this.height =hright;
	}
	public void show(){
		do something;
	}
}

再实现Decorator抽象类

public abstract class Decorator implement Component{
	private Component component;
	public Decorator(Component component){
		this.component = component;
	}
	@Override
	public void show(){
		component.show();
	}
}

实现几个具体装饰类

public class Tshirt extends Decorator{
	private Component component;
	public Tshirt(Component component){
		this.component = component;
	}
	public void showTshirt(){
		show Tshirt;
	}
}
public class Shorts extends Decorator{
	private Component component;
	public Shorts(Component component){
		this.component = component;
	}
	public void showShorts(){
		show Shorts;
	}
}
public class Shoes extends Decorator{
	private Component component;
	public Shoes(Component component){
		this.component = component;
	}
	public void showShoes(){
		show Shoes;
	}
}

模式优缺点

优点1

  1. 装饰模式与继承关系的目的都是要扩展对象的功能,但是装饰模式可以提供更多的灵活性
  2. 通过使用不同的具体装饰类以及这些装饰类的排列组合,可以创造出很多不同行为的组合

缺点

  1. 装饰模式相比继承产生较少的类,但同时生成更多的对象 并且这些对象看上去都比较相似
  2. 装饰模式比继承更加容易出错

装饰模式与其他模式的对比

装饰模式和适配器模式

  • 装饰模式——保持端口,增强所考虑对象的功能
  • 适配器模式——改变所考虑对象的接口,而不一定改变对象的功能

装饰模式和策略模式

  • 装饰模式——表皮任意更换,保持核心,要求Component尽量轻
  • 策略模式——保持接口不变,使具体算法可以互换,要求抽象策略类尽量重

装饰模式和合成模式

  • 装饰模式常常用于合成模式的行为扩展上,是集成的替代方案。
设计模式--策略模式

概念

策略模式属于对象的行为模式。其用意是针对一组算法,将每一个算法封装到具有共同接口的独立的类中,从而使得它们可以相互替换。策略模式使得算法可以在不影响到客户端的情况下发生变化。

通俗的来说,同一件事情有不同的做法,客户端并不需要具体的做法是什么,只要指明了要哪种做法,便会按照客户端的要求实施做法。实施办法的改变不会影响到客户端的代码,能够实现具体实现算法与客户端之间的解耦。

举个例子:也比大雄一直深深爱着静香,哆啦A梦给了大雄一些钻石,让大雄用于向静香求婚使用。但是静香是一个傲娇的女孩子,对钻石有颇多要求,形状,重量都要完完全全满足要求,才会答应大雄的求婚。于是大雄就问静香,你想要什么样的钻石。静香说:我要一个圆形的,重量为1克拉的钻石。于是大雄就从哆啦A梦给的钻石中挑出了一颗满足要求的钻石给了静香,满足了她的要求。

在上面这个例子里面,大雄就是Context,持有一个钻石。钻石则作为(Strategy),这个Abstract Class实现两个接口,大小接口和形状接口。哆啦A梦给的每一颗钻石都是一个具体策略(Concrete Strategy),继承了Strategy,实现了接口,有具体的实现(有具体的形状和大小)。

静香作为客户端,对大雄(Context)提了一个具体要求,Context就把这个抽象的钻石实例化为一颗满足要求的具体钻石,而静香不需要关心钻石的具体生产过程。就算哆啦A梦又多给了几个钻石,或者是切割了一个钻石,改变了它的形状都和静香没有关系,静香不用关心这些。

模式意图

将算法封装起来,使得他们可以互相替换,算法的改变不会影响到客户端代码。

模式参与者

  • 上下文(Context): 根据客户端的要求,持有一个由客户端上下文指定的strategy引用。
  • 抽象策略(Strategy):一个抽象的对象,通常是一个接口或者是一个抽象类,这一个或多个接口(抽象类)要给出所有具体策略所需要的接口。
  • 具体策略(Concrete Strategy):抽象策略的具体实现,封装了具体的算法。

UML图

center

模式实现

Strategy要实现的两个interface

//两个interface:
public interface WeightInterface{
	public Weight getWeight();
}
public interface ShapeInterface{
	public Shape getShape();
}

策略(Strategy)

//Strategy:
public abstract class StrategyDiamond implement WeightInterface,ShapeInterface{
	public Weight getWeight(){}
	public Shape getShape(){}
}

具体策略(Concrete Strategy)

//Concrete Strategy:
public class DiamondA extends StrategyDiamond{
	Weight weight;
	Shape shape;
	public Weight getWeight(){
		return this.weight;
	}
	public Shape getShape(){
		return this.shape;
	}
}
public class DiamondB extends StrategyDiamond{
	Weight weight;
	Shape shape;
	public Weight getWeight(){
		return this.weight;
	}
	public Shape getShape(){
		return this.shape;
	}
}
public class DiamondC extends StrategyDiamond{
	Weight weight;
	Shape shape;
	public Weight getWeight(){
		return this.weight;
	}
	public Shape getShape(){
		return this.shape;
	}
}

上下文,持有Strategy的对象(Context)

public class Context{
	private StrategyDiamond diamond;

	public Context(StrategyDiamond req){
		this.diamond = req;
	}

	public Weight getWeight(){
		return this.diamond.getWeight();
	} 
	public Shape getShape(){
		return this.diamond.getShape();
	} 
}

模式优缺点

策略模式有很多优点和缺点。1

它的优点有:

  1. 策略模式提供了管理相关的算法族的办法。策略类的等级结构定义了一个算法或行为族。恰当使用继承可以把公共的代码移到父类里面,从而避免重复的代码。

  2. 策略模式提供了可以替换继承关系的办法。继承可以处理多种算法或行为。如果不是用策略模式,那么使用算法或行为的环境类就可能会有一些子类,每一个子类提供一个不同的算法或行为。但是,这样一来算法或行为的使用者就和算法或行为本身混在一起。决定使用哪一种算法或采取哪一种行为的逻辑就和算法或行为的逻辑混合在一起,从而不可能再独立演化。继承使得动态改变算法或行为变得不可能。

  3. 使用策略模式可以避免使用多重条件转移语句。多重转移语句不易维护,它把采取哪一种算法或采取哪一种行为的逻辑与算法或行为的逻辑混合在一起,统统列在一个多重转移语句里面,比使用继承的办法还要原始和落后。

策略模式的缺点有:

  1. 客户端必须知道所有的策略类,并自行决定使用哪一个策略类。这就意味着客户端必须理解这些算法的区别,以便适时选择恰当的算法类。换言之,策略模式只适用于客户端知道所有的算法或行为的情况。

  2. 策略模式造成很多的策略类。有时候可以通过把依赖于环境的状态保存到客户端里面,而将策略类设计成可共享的,这样策略类实例可以被不同客户端使用。换言之,可以使用享元模式来减少对象的数量。

与简单工厂模式的异同

相同之处:2

  1. 它们都有一个抽象类或公共接口,并且在抽象类或者接口中,定义一个方法(或虚拟抽象方法);

  2. 通过子类进行继承父类或者实现接口方法。

  3. 使用多态特性,进行实例方法调用,调用的是子类的方法;

区别之处:

  1. 简单工厂模式 强调的创建类对象,根据 字符串类型参数传入参数,进行实例化;

  2. 简单工厂模式,必须定义一个制造实例的工厂类Factory,且其工厂类,返回父类类型,不是子类类型;

  3. 策略模式强调的算法封装,而算法的不同,增加相应的子类进行实现。

  4. 必须有一个Content类,提供两个方法,一个接受各个算法的实例对象,一个是对子类方法的封装,提供访问客户端统一的接口;

  5. 策略模式中接受算法实例的方法,可以结合简单工厂模式,传入字符串参数,内部进行实例化,降低和客户端的耦合;

概念

简单工厂模式是创建者模式的一种,也是一种特殊的工厂模式。简单工厂模式是由一个工厂对象决定创建出哪一种产品类的实例。简单工厂模式是工厂模式家族中最简单实用的模式是工厂模式的一个特殊实现。

大雄被胖虎欺负了,回家找多拉A梦哭诉:我被打了,我好委屈,我要一个“少爷保镖”。这时候能哆啦A梦就从他的口袋里,“噹噹噹噹”掏出一个“少爷保镖”,给了大雄。过了两天,大雄又被胖虎欺负了,回家找哆啦A梦哭诉:我被打了,我要一个“报仇机器人”,这时候能哆啦A梦又从他的口袋里,“噹噹噹噹”掏出一个“报仇机器人”,给了大雄。

在这个例子里面,哆啦A梦就是一个简单工厂,生产一系列“报仇”产品给大雄使用。大雄并不知道“少爷保镖”和“报仇机器人”的生产过程,只是按照需求直接拿到了这个产品。

模式意图

简单工厂模式根据提供给工厂的要求(数据),返回几个可能类中的一个类的实例。

模式参与者

  • 抽象产品(Abstract Product): 是工厂模式所创建对象的父类或是共同拥有的接口。可是抽象类或接口。(上例中的:复仇系列产品)
  • 具体产品(Concrete Product): 工厂模式所创建的对象都是这个角色的实例。(上例中的:“少爷保镖”,“复仇机器人”)
  • 工厂(Factory): 接受客户请求,按照请求返回给客户所要求的对象(上例中的:哆啦A梦)

UML图

center

模式实现

首先是抽象产品

//Abstract Product:
public abstract class RevengeProduct{
	String name;
	String discribe;
	public abstract void revenge();
}

然后是具体产品

//Concrete Product A:
public class BodyGuard extends RevengeProduct{
	String name;
	String discribe;
	public void revenge(){
	    dorevenge();
	}
}
//Concrete Product B:
public class RevengeRobot extends RevengeProduct{
	String name;
	String discribe;
	public void revenge(){
	    dorevenge();
	}
}

然后是工厂

//Factory
public Class Factory{
	pulic static RevengeProduct create(String command){
		if(command.equals(bodyGuard))
			return new BodyGuard();
		if(command.equals(revengeRobot))
			return new RevengeRobot();
	}	
}

模式优缺点

优点:工厂类含有必要的判断逻辑,可以决定在什么时候创建哪一个产品类的实例,客户端可以免除直接创建产品对象的责任,而仅仅”消费”产品。简单工厂模式通过这种做法实现了对责任的分割。

缺点:

  1. 当产品有复杂的多层等级结构时,工厂类只有自己,以不变应万变,就是模式的缺点。因为工厂类集中了所有产品创建逻辑,一旦不能正常工作,整个系统都要受到影响。

  2. 系统扩展困难,一旦添加新产品就不得不修改工厂逻辑,有可能造成工厂逻辑过于复杂,违背了”开放–封闭”原则(OCP)。另外,简单工厂模式通常使用静态工厂方法,这使得无法由子类继承,造成工厂角色无法形成基于继承的等级结构。

大文件去重这种问题,记得以前校招面试的时候曾经遇到过,基本思路无外乎分而治之,hash一下等等,布隆过滤器了解的也不多,只能说是听说过这等神奇。今天来看看给大文件去重上,他能发挥多大力量吧。

分而治之的方案大概来说就是把文件先分割为多个小文件,然后小文件之间进行merge,相同的跳过。然后依次递归进行。如果合并后的文件大小超过合并文件的大小限制,就再分割。Hash的做法是如果每个item比较大,那么就hash下,对hash值进行去重操作。

Hash还有另外一种比较高效的做法,就是对每一个要去重的item计算一个md5值出来,再构建一个Trie树,再插入进来的md5如果存在于这个树里就抛弃这个item,继续下一个。这个效率也是比较高的,但是可能做法就比较复杂,耗时也会相对较长,对于item重复度比较高的情况,有不错的表现。但是如果重复度比较低,则耗时耗内存。

对于重复度不高,要求速度快,空间省,可以容忍一些小差误的情况下,布隆过滤器是一个很好的选择。最简单的例子:在制作一个网络爬虫程序时,URL错综复杂,可能会构成环。为了避免形成环,我们需要知道是不是访问过某个URL,如果用hash表来存储的话,巨大的hash表内存很有可能存不下,使用布隆过滤器就能在保证空间和时间性能的情况下,完成这一任务。

原理

创建一个m位BitSet,先将所有位初始化为0,然后选择k个不同的哈希函数。第i个哈希函数对字符串str哈希的结果记为h(i,item),且h(i,item)的范围是0到m-1,然后将对应位的BitSet置为1。

The false positive probability p as a function of number of elements n in the filter and the filter size m. An optimal number of hash functions k= (m/n) \ln 2 has been assumed.
  • Add:要添加一个元素,使用K个hash函数将item Hash后Bloom Filter中的K个bit位置为1
  • Query:要查询一个元素,使用K个hash函数将item Hash后的K个bit位与Bloom Filter中的相应位置比较,如果全部为1,则该item出现过,如果有任何一位不为1则该item没有出现过。
  • Remove:不允许删除元素。因为要remove的item的K个hash位可能也被其他item的位所共用,所以不允许删除元素。

当然如果Bloom Filter中的元素n过多,导致n/m过大时错误率就会相应提高。

误判

误判可能是我们使用布隆过滤器时最担心的问题,那我们就来算算误判概率大概是多少吧。枯燥的数学计算,不喜欢就跳过吧,没啥意义。

假设我们使用的hash函数能够等概率的把item hash到m个bit位里面去。

那么某一位没有被置为1的概率是

k个hash函数都没有给某一位置为1的概率是

插入了n个元素以后这个位依然没有被置为1的概率是

那么插入了n个元素以后这个位被置为1的概率就是

在查询一个元素是否存在时,假设这个元素从没有出现过,但是hash得到的k个位全部为1的概率就是

下面高数终于能派上点用场了…化简这个式子

时,有 则:

要算在k为多少的情况下误判率最少,还要求导,还要求最值,估计我写了也没人爱看,直接从书上copy答案了。

有了这个式子就可以根据自己期望的误判率和预估的需要判重的item的量来确定给定多少位bit分配多大的Bloom Filter。

假设我们有一百万条数据需要判重,如果需要错误率低于1%,那么我们可以算出来。需要不到一千万bit和7个hash函数。也就是不到10MB的空间就可以完成。如果要求0.1%的错误率也只需要14M空间和10个hash函数。这些hash函数是互相不相关的可以并行的运算,更是提高了速度。

实现

粘一段很简单的实现:参考这里

import java.util.BitSet;
public class  SimpleBloomFilter {
     private static final  int  DEFAULT_SIZE  =2 << 24 ;
     private static final  int [] seeds =new  int []{5,7, 11 , 13 , 31 , 37 , 61};
     private  BitSet bits= new  BitSet(DEFAULT_SIZE);
     private  SimpleHash[]  func=new  SimpleHash[seeds.length];   
     public  SimpleBloomFilter() {
         for( int  i= 0 ; i< seeds.length; i ++ ) {
            func[i]=new  SimpleHash(DEFAULT_SIZE, seeds[i]);
        }
    }
     public void  add(String value) {
         for(SimpleHash f : func) {
            bits.set(f.hash(value),  true );
        }
    }
     public boolean  contains(String value) {
         if(value ==null ) {
             return false ;
        }
         boolean  ret  = true ;
         for(SimpleHash f : func) {
            ret=ret&& bits.get(f.hash(value));
        }
         return  ret;
    }
     
     //内部类,simpleHash
     public static class SimpleHash {
         private int  cap;
         private int  seed;
         public  SimpleHash( int cap, int seed) {
             this.cap= cap;
             this.seed =seed;
        }
         public int hash(String value) {
             int  result=0 ;
             int  len= value.length();
             for  (int i= 0 ; i< len; i ++ ) {
                result =seed* result + value.charAt(i);
            }
             return (cap - 1 ) & result;
        }
    }
     
     public static void  main(String[] args) {
         String value  = "stone2083@yahoo.cn" ;
         SimpleBloomFilter filter=new  SimpleBloomFilter();
         System.out.println(filter.contains(value));
         filter.add(value);
         System.out.println(filter.contains(value));
     }
}
Wiki --Bloom Filter
Github --Bloom Filter