这里从java介入吧,在java中,接口是一种特殊的类,接口里面的量都是常量,接口的方法只有定义而没有实现,换句话说,接口就像一个菜单,它只会告知你我有什么菜,而并不会有实际的菜品,因此一般用接口来定义实现类的外观,根据外部应用所须要的功能,约定实现类的能力(类的功能不只限于接口约束)。经过接口,能够实现不相关类的相同功能,而不考虑这些类之间的层次关系,接口就是实现类对外的外观。java
上面那样说,可能显得很装13,那千言万语化成一句人话就是:一、定义功能,对外暴露 二、对内约束实现类的行为程序员
所谓面向接口去编程的核心含义就是为了——“封装隔离”算法
一般的封装,是指对数据结构的封装,将几种数据类型整到一块,组成一个新的数据类型;而java中的封装,包含对数据和行为进行抽象,而后定义出一个类,这个类即封装了数据和行为,但接口这里的封装,更多的是指对于行为(能力、方法)的封装,是一种“对被隔离体能力的封装”,而隔离对应的就是,外部的调用以及内部的实现,外部只根据接口来调用方法(根据菜单来点菜,具体填饱肚子的菜是内部去作),外部调用是不知道内部你是用什么方式实现的,举个例子,就像我有一个计算器,计算器的加减乘除按键就是我提供给用户的接口,用户只知道我有加减乘除的能力,但当他用乘法按键去运算的时候,后台具体是用二进制运算,仍是逐个数累加或者其余什么方式来完成这个乘法功能,用户是不知道的。也就是外部调用和内部实现是被隔离开的。数据库
既然外部调用和内部实现被隔离开了,那么只要接口不变,内部实现怎么变化都不会影响外部应用对这个接口的调用,从而让系统更加的灵活,更便于扩展和维护,也就是传说中的“接口是系统可插拔的保证”。编程
说到这里插一段题外话,emmm……我的感受编程是一我的的事,不少时候1+1<2,由于人这个不可控因素,每一个程序员的思想深度,技术水平,都是不相同的,因此每每会出现 “ 一个程序员A费劲心力,设计了面向对象的模块化代码结构,并完成了一部分功能,然后面有别的需求介入,另外一个的程序员B加入了研发过程,基于这个代码进行改动的时候,并读不懂A的结构和A事先预留的扩展方式,直接用他的方式去硬编码,强行破坏了整个结构”。以上这种状况每每很使人崩溃,因此对于水平良莠不齐的团队来讲,集体劳做的质量(单指代码)并不那么友好设计模式
总之,在开发中,优先选择使用接口,在即要定义子类的行为,又要为子类提供公共方法的时候选择抽象类。数据结构
这里我们从我我的比较熟悉的java入手,在java的设计中,常常出现的层的概念和模块的概念,我的常常作java Web的程序,咱们以此为例,最经典的MVC结构,抽象一点理解,也就是控制、逻辑、数据三层,它们之间所有经过接口来通讯。ide
在每一层里,又包含不少模块,每一个模块对外则是一个总体,因此一个模块应该对外提供接口,其余地方须要某个功能时,可根据接口直接调用模块,也就是上面的 “ 接口是被其隔离部分的外观”。模块化
设计中常常会提到组件,模块,其实不管是组件仍是模块,均可以理解为 封装了必定功能的集合体, 一个类,一个功能块,一个插件,一个系统,均可以理解成组件、模块,由于,一个类多是一个功能块的一部分,一个功能块多是一个插件的一部分,一个插件多是一个系统的一部分,小系统放到大系统中,也就是个组件罢了,就是组合的关系,从设计的角度,系统,子系统,插件、模块、组件等,其实说的就是一个东西,就是完成必定功能的封装体。编码
前面咧咧了那么多,看官们确定看烦了,差评差评!这里我上面说了那么多接口的东西,总得用来看看吧,咱们用一个例子来切入主题,这里我打算写一个功能,就是比对两个字符串类似的程度,确定会有人说了,你这真废话,直接 equals() 它不香么!香是香,但是它不能展现(让我装13啊)呀,咱们以java的方式来搞个类似度计算。
上面说了接口,那咱们先定义接口:
public interface MatcherAlg { /** * 计算两个串的类似度 * @param srcStr * @param dstStr * @return Float 类似值 */ public Float CalculateSimilarityRatioValue(String srcStr,String dstStr); }
接口已经约束了咱们这个功能只有一个方法,那么咱们来内部实现一下:
public class JaccardMatcher implements MatcherAlg { @Override public Float CalculateSimilarityRatioValue(String srcStr, String dstStr) { if(srcStr == null && dstStr ==null){ return 1f; } if(srcStr == null || dstStr == null){ return 0f; } Set<Integer> aChar = srcStr.chars().boxed().collect(Collectors.toSet()); Set<Integer> bChar = dstStr.chars().boxed().collect(Collectors.toSet()); int intersection = SetUtils.intersection(aChar,bChar).size(); if(intersection == 0){ return 0f; } int union = SetUtils.union(aChar,bChar).size(); return ((float)intersection/(float)union); } }
这时候咱们要用它了,来比较两个字符串类似程度:
public class Test{ public static void main(String args[]){ String str= "sdfsf"; String dst= "1234d"; MatcherAlg matcher = new JaccardMatcher(); Float result = matcher.CalculateSimilarityRatioValue(str,dst); } }
运行一下,也十分正常,完美落地,但是仔细看下来,这样我定义那个 MatcherAlg 接口,后面又
MatcherAlg matcher = new JaccardMatcher();
好像是在 “脱了裤子放p”,没事找事。干吗不直接定义JaccardMatcher类,而后:
JaccardMatcher matcher = new JaccardMatcher();
可是上面说过了,咱们应该面向接口编程,接口的核心就为了 “封装隔离”,实现类JaccardMatcher应该是被接口 MatcherAlg封装并同客户端隔离开来。
客户端根本不该该知道 JaccardMatcher的存在,更不用说 new JaccardMatcher()这种“脱裤放p”操做了。可是问题又来了,若是客户端没有new JaccardMatcher(),只有MatcherAlg接口的定义,那么后面的代码是没法使用的。
因而纠结的地方出现了,上面花了那么大篇幅说怎么怎么面向接口,纯面向接口了你又不能运行了,能运行又违反了“隔离封装”了, 问题进入死环了。
因此“脱裤放p”的操做是对应这个死环一种蹩脚的写法(它能够运行,但专业的咱们不认)。
这个死环如何解决,咱们先看一下设计模式中的一段话,它是这样说的 :提供一个建立对象实例的功能,而无需关系其具体的实现。被建立实例的类型能够是接口、抽象类、也能够是具体的类。
受到那句话的启发,咱们尝试得出一个解开上面那个死环的方案:咱们在模块内部建一个类,这个类的功能就是建立可以使用的接口,而且把建立的接口提供给客户端,这一客户只须要根据这个类来获取相应的接口对象,于此同时,接口具体使用哪一个实现,咱们就能够抽离到这个类里面,给咱们提供了一个控制 使用哪一个类的 隔离扩展区,客户端也不须要关心他用的这个类是对应哪一种实现,如何实现的。
上面这套思想,设计模式中称之为 “工厂”
样例代码:
//客户端类 public class Client { public static void main(String[] args) { Product p = SimpleFactory.makeProduct(Const.PRODUCT_A); p.show(); } }
//抽象产品 public interface Product { void show(); } //具体产品:ProductA public class ConcreteProduct1 implements Product { public void show() { System.out.println("具体产品1显示..."); } } //具体产品:ProductB public class ConcreteProduct2 implements Product { public void show() { System.out.println("具体产品2显示..."); } }
//枚举 public final class Const { static final int PRODUCT_A = 0; static final int PRODUCT_B = 1; static final int PRODUCT_C = 2; } //工厂 public class SimpleFactory { public static Product makeProduct(int kind) { switch (kind) { case Const.PRODUCT_A: return new ConcreteProduct1(); case Const.PRODUCT_B: return new ConcreteProduct2(); } return null; } }
首先看上面简单工厂的样例代码,有人会困惑,不就是把new操做从客户端移动到了额外的类里去了么,本质仍是new 了一个实现类,这里咱们再次回到原点,咱们前面提到的接口,接口是用来封装隔离的,目的就是让客户端不要知道封装体内的具体实现,简单工厂的位置是处于封装体内的,简单工厂跟接口的具体实如今一块儿,算是封装体内部的一个类,因此简单工厂知道具体的实现类是没有关系的,咱们再来看一下简单工厂的类图:
图中浅蓝色的虚线框即为一个封装的边界,表示接口、工厂、实现类组合成了一个组件,在这个组件中,只有接口和工厂是对外的,也只有这俩,外界可使用和访问到,可是具体的实现类,彻底是内部的,对外透明的,不可见的,因此它被全包裹进蓝框,对于客户端而言,它只知道这个Alg接口和生产含有Alg功能实例的工厂,经过Factory就能获取Alg的能力了,因此,new操做划在工厂内,在设计和隔离的意义上,有了质的变化。
静态工厂
所谓静态工厂,就是咱们使用工厂的时候,不须要实例化工厂了,直接将生产的方法设为静态方法,经过类名便可调用,或者作成单例的模式,也就是说简单工厂的方法一般都是静态的,因此称之为静态工厂。
万能工厂
一个简单工厂能够包含不少用来构建东西的方法,这些方法能够建立不一样的接口、实力类,一个简单的工厂理论上能够构造任何东西,因此又称之为“万能工厂”
简单工厂的本质是:选择实现
选择实现,重点在于选择,实现是已经作好了的,就算实现再简单(哪怕是new实例)也要由具体的实现类来实现,而不是在简单工厂里面来实现,简单工厂的目的在为客户端提供一个选择,选择哪一种实现,从而使客户端和具体的实现之间解耦。这样具体实现不管如何变更,都不须要客户端随之变更,这个变更会在工厂这一层里被吸取和隔断。
实现简单工厂的难点在于“选择”的实现,能够经过传参,也能够经过动态的参数,好比在运行期间去读取配置文件或数据库、内存中的某个值,根据这个值来进行具体的实现。
基本的实现套路,已经有较为明确的模板了,如今有一个问题,就是若是MatcherAlg的实现类不止一个,咱们能够经过在工厂的方法中传入参数来处理
public Class Factory{ public static MatcherAlg createAlg(String type){ if( type.equals("a") ){ return new aAlg(); }else if ( type.equals("b") ){ return new bAlg(); }else{ …… } } }
但是,当咱们又又扩展了新的实现类的时候,if else 又须要扩展一句,同时对客户端也要告知,这样对于Factory这个类来讲,严重违反了开闭原则。
为了解决这个问题,咱们能够经过配置文件的形式来解决,当有了新的实现类或者须要默认指定用哪个实现的时候,只须要经过配置文件的配置项便可,经过配置文件的方式,多须要使用java的反射来支持动态创建对象。这里摘取本身的一个代码来做为一个样例:
/** * 基础工厂,其余组件工厂的实现可用基于该类进行扩展 * 功能:根据配置文件动态生成对象 * @author GCC */ public abstract class AbstractFactory { private static Logger logger = Logger.getLogger(AbstractFactory.class); //默认自带的类控制配置文件 private final static String DEFAULTCONFIG_FILE_URL = "factoryconfig.ini"; //默认的配置文件 static URL defaultConfigFileUrl = AbstractFactory.class.getClassLoader().getResource(DEFAULTCONFIG_FILE_URL); /** * 根据配置文件以及key值,获取对象的类路径 * @param url 配置文件路径 * @param key 关键字 * @return String 类路径 */ static String getClassUrl(String url,String key){ ConfigUtil config = new ConfigUtil(url); return config.getValueByConfigkey(key); } /** * 根据指定配置文件及指定关键字生成对象 * @param url 配置文件路径 * @param key 关键字 * @return Object 具体对象 */ static Object getObject(String url,String key){ String classurl = getClassUrl(url,key); try{ Class oneclass = Class.forName(classurl); return oneclass.newInstance(); }catch (Exception e){ logger.error(e.getMessage() +" plase check"+ DEFAULTCONFIG_FILE_URL ); } return null; } }
配置文件(.ini文件)内容:
#matcher.algclassurl:算法类地址
matcher.algclassurl=org.gds.matcher.impl.LevenshteinMacther
简单工厂实现简单,很是友好的提供了一套实现组件封装的功能,同时也解决了客户端何内部实现类的强耦合,实现了解耦。这是简单工厂的优势,但世事都是两面的,它也有不可避免地缺点:
首先,它增长了客户端的复杂程度,若是经过客户端的参数来选择具体的实现类,那客户端必须额外须要一份枚举表或者字典,而且知道每一个枚举的意义,这样会增长客户端的复杂程度,同时必定程度上暴露了内部的实现(虽然可配置方案必定程度上能够对冲这一问题)。
其次,简单工厂使用静态方法(又叫静态工厂)来建立接口,当面临一些复杂的组件建立,静态方法会很是庞大,没法经过继承来扩展建立接口的方法的行为了。