代理模式

代理模式定义为其他对象提供一种代理,以控制对这个对象的访问。(代理对象在客户端和目标对象之间起到中介的作用)优点代码模式能将代理对象与真实被调用的目标对象分离。一定程度上降低了系统的耦合度,扩展性好。保护目标对象。(客户端直接交互的是代理类而不是目标对象,这样就保护了目标对象)增强目标对象缺点在客户端和目标对象增加一个代理对象,会造成请求处理速度变慢。增加了系统的复杂度。分类静态代理在代码中显式的代理目标。动态代理jdk中的动态代理只能对实现了接口的类进行动态代理。并不能针对一个具体的类进行动态代理。无法代理可以代理接口,在代理类的时候只有在调用到代理类是才由jvm动态的创建,jvm根据传进来的对象以及代理方法名动态的创建了代理类的class文件,这个class文件被字节码引擎执行,然后通过该代理类的对象进行方法的调用。(比CGLib快大约百分之10)CGLib代理主要针对jdk无法代理具体的类的问题。实现原理是生成一个被代理类的子类,通过覆盖父类的方法来实现动态代理。也就是用过继承然后重写,所以被代理的不能是一个final的类或者是代理方法不能是一个final方法。spring中代理选择当bean有实现接口时,spring 就会使用jdk的动态代理。当bean没有实现接口时,spring使用CGLib可以强制使用CGLib (具体配置自行谷歌吧)下面我们实现一下静态。我们假设一个应用场景就是,要保存一个订单,订单就包括订单实体类,service。dao等,我们要用代理的方式来实现数据库分库,不同的订单要放在不同的数据库中。下面开始看代码。订单实体类。public class Order { private Object orderInfo; private Integer userId; public Object getOrderInfo() { return orderInfo; } public void setOrderInfo(Object orderInfo) { this.orderInfo = orderInfo; } public Integer getUserId() { return userId; } public void setUserId(Integer userId) { this.userId = userId; }}订单操作的dao接口public interface IOrderDao { int insert(Order order);}dao的实现类,我们这里只是模拟一下数据库操作。public class OrderDaoImpl implements IOrderDao { @Override public int insert(Order order) { System.out.println(“Dao层添加Order成功”); return 1; }}service接口public interface IOrderService { int saveOrder(Order order);}service实现类,我们没有集成spring 模拟操作public class OrderServiceImpl implements IOrderService { private IOrderDao iOrderDao; @Override public int saveOrder(Order order) { //Spring会自己注入,我们课程中就直接new了 iOrderDao = new OrderDaoImpl(); System.out.println(“Service层调用Dao层添加Order”); return iOrderDao.insert(order); }}要分库我们来用ThreadLocal实现一个上下文对象,模拟暂放在哪个数据的操作。public class DataSourceContextHolder { private static final ThreadLocal<String> CONTEXT_HOLDER = new ThreadLocal<String>(); public static void setDBType(String dbType){ CONTEXT_HOLDER.set(dbType); } public static String getDBType(){ return (String)CONTEXT_HOLDER.get(); } public static void clearDBType(){ CONTEXT_HOLDER.remove(); }}动态的使用数据库的实现。(模拟操作)public class DynamicDataSource extends AbstractRoutingDataSource { @Override protected Object determineCurrentLookupKey() { return DataSourceContextHolder.getDBType(); }}进入重点,代理类的实现。这里的核心就是在执行代理方式之前加入我们的增强操作。public class OrderServiceStaticProxy { private IOrderService iOrderService; public int saveOrder(Order order){ beforeMethod(order); iOrderService = new OrderServiceImpl(); int result = iOrderService.saveOrder(order); afterMethod(); return result; } private void beforeMethod(Order order){ int userId = order.getUserId(); int dbRouter = userId % 2; System.out.println(“静态代理分配到【db”+dbRouter+"】处理数据"); //todo 设置dataSource; DataSourceContextHolder.setDBType(“db”+dbRouter); System.out.println(“静态代理 before code”); } private void afterMethod(){ System.out.println(“静态代理 after code”); }}最后就是测试类public class StaticProxyTest { public static void main(String[] args) { Order order = new Order(); order.setUserId(2); OrderServiceStaticProxy orderServiceStaticProxy = new OrderServiceStaticProxy(); orderServiceStaticProxy.saveOrder(order); }}输出结果静态代理分配到【db0】处理数据静态代理 before codeService层调用Dao层添加OrderDao层添加Order成功静态代理 after code静态代理每次代理都要写一个代理类,代理方法,容易产生类爆炸的情况。下面我们实现一下动态代理。实现动态的时我们需要实现InvocationHandler接口,实现invoke方法这个方法需要Object proxy, Method method, Object[] objs args三个参数,一个参数是动态生成的类的对象,一般我们在invoke方法中很少用到。method顾名思义就是需要代理的方法,Object数据就是参数集合。在使用的时候先要 Proxy.newProxyInstance创建一个被代理的对象,这个方法有ClassLoader loader, Class<?>[] interfaces, InvocationHandler h三个参数,ClassLoader对象我们通过getClass方法的getClassLoad方法很容易拿到,Class<?>[] interfaces就是接口集合了,通过getInterfaces也可以很容易拿到,最后一个就是动态代理类的对象了。下面我们看一下代码。public class OrderServiceDynamicProxy implements InvocationHandler { private Object target; public OrderServiceDynamicProxy(Object target) { this.target = target; } public Object bind(){ Class cls = target.getClass(); return Proxy.newProxyInstance(cls.getClassLoader(),cls.getInterfaces(),this); } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { Object argObject = args[0]; beforeMethod(argObject); Object object = method.invoke(target,args); afterMethod(); return object; } private void beforeMethod(Object obj){ int userId = 0; System.out.println(“动态代理 before code”); if(obj instanceof Order){ Order order = (Order)obj; userId = order.getUserId(); } int dbRouter = userId % 2; System.out.println(“动态代理分配到【db”+dbRouter+"】处理数据"); //todo 设置dataSource; DataSourceContextHolder.setDBType(“db”+String.valueOf(dbRouter)); } private void afterMethod(){ System.out.println(“动态代理 after code”); }}代理模式就这些内容啦。 ...

January 14, 2019 · 2 min · jiezi

JS设计模式之Module(模块)模式、Revealing Module(揭示模块)模式

Module(模块)模式概念Module模式最初被定义为一种在传统软件工程中为类提供私有和共有封装的方法。通过这种方式,能够使一个单独的对象拥有共有/私有方法和变量,从而屏蔽来自全局作用局的特殊部分。产生的结果是: 函数名与在页面上其他脚本定义的函数冲突的可能性降级应用实现一个简单的计数器orderModule外的代码无法直接读取getNum(),addNum()和subNum(),orderNum变量实际上是与全局作用域隔离的,因此它表现的就像是一个四有变量,他的存在被局限于模块的闭包内,因此唯一能访问其作用域的代码就是这三个函数。这样实现同时也进行了有效的命名空间设置。var orderModule = (function() { var orderNum = 1; return { getNum: function() { return orderNum; }, addNum: function() { orderNum = orderNum + 1; }, subNum: function() { orderNum = orderNum > 1 ? orderNum - 1 : orderNum; } }})();orderModule.getNum();Revealing Module(揭示模块)模式概念揭示模块模式是在模块模式的基础上进行改进,在私有范围内简单定义所有的函数和变量,并返回一个匿名对象,它拥有指向私有函数的指针,该函数是展示为共有的方法。应用var orderModule = (function() { var orderNum = 1; function getNum() { return orderNum; } function addNum() { orderNum = orderNum + 1; } function subNum() { orderNum = orderNum > 1 ? orderNum - 1 : orderNum; } //将暴露的公有指针指向到私有函数和属性上 return { get: getNum, add: addNum, sub: subNum }})();orderModule.get();参考《JavaScript设计模式》JS设计模式系列文章JS设计模式之Module(模块)模式、Revealing Module(揭示模块)模式JS设计模式之Singleton(单例)模式JS设计模式之Facade(外观)模式 ...

January 11, 2019 · 1 min · jiezi

javascript设计模式--策略模式

策略模式的目的就是将算法的使用与算法的实现分离开来;将不变的部分和变化的部分隔开是每个设计模式的主题;搬一个《设计模式与开发实践》中的例子。关于绩效工资的计算:// 普通的写法var calculateBonus = function( performanceLevel, salary ){ if ( performanceLevel === ‘S’ ){ return salary * 4; } if ( performanceLevel === ‘A’ ){ return salary * 3; } if ( performanceLevel === ‘B’ ){ return salary * 2; } };calculateBonus(‘S’,20000);这段代码最大的问题就是:弹性差,绩效规则一变就要去改函数,新加一套规则那就更难受了;问题出在:算法的设计和算法的使用,混杂在了一起;所以我们要把算法的设计分离出来;// 使用了策略模式var strategies = { “S”: function( salary ){ return salary * 4; }, “A”: function( salary ){ return salary * 3; }, “B”: function( salary ){ return salary * 2; }};var calculateBonus = function( level, salary ){ return strategies level ;};calculateBonus( ‘S’, 20000 );strategies是一个策略类,calculateBonus函数接受参数后会把请求委托给一个策略类;strategies是算法的实现,一般不变;calculateBonus是对算法的使用,可变;将不变的部分和变化的部分隔开是每个设计模式的主题; ...

January 11, 2019 · 1 min · jiezi

前端设计模式

作为一个前端新人,学习了设计模式以后,希望能从源头上,用浅显易懂的语言来解释它。当然不一定是正确的,只是我个人对设计模式的一点浅显理解。创建型设计模式创建型设计模式:故名思意,这些模式都是用来创建实例对象的。单例模式首先我们需要理解什么是单例。单:指的是一个。例:指的是创建的实例。单例:指的是创建的总是同一个实例。也就是使用类创建的实例始终是相同的。我们先看下面的一段代码:class Person{ constructor(){}}let p1 = new Person();let p2 = new Person();console.log(p1===p2) //false上面这段代码,定义了一个Person类,通过这个类创建了两个实例,我们可以看到最终这两个实例是不相等的。也就是说,通过同一个类得到的实例不是同一个(这本就是理所应当),但是如果我们想始终得到的是同一个实例,那么这就是单例模式。那么应该如何实现单例模式了:想要实现单例模式,我们需要注意两点:需要使用return。使用new的时候如果没有手动设置return,那么会默认返回this。但是,我们这里要使得每次返回的实例相同,也就是需要手动控制创建的对象,因此这里需要使用return。我们需要每次return的是同一个对象。也就是说实际上在第一次实例的时候,需要把这个实例保存起来。再下一个实例的时候,直接return这个保存的实例。因此,这里需要用到闭包了。代码实现如下:(function(){ let instance = null; return class{ constructor(){ if(!instance){ //第一次创建实例,那么需要把实例保存 instance = this; }else{ return instance; } } }})()let p3= new Person();let p4 = new Person();console.log(p3===p4) //true从上面的代码中,我们可以看到在闭包中,使用instance变量来保存创建的实例,每次返回的都是第一次创建的实例。这样的话就实现了无论创建多少次,创建的都是同一个实例,这就是单例模式。

January 11, 2019 · 1 min · jiezi

javascript设计模式1--前言

设计模式是程序员进阶的必经之路,真正地理解、掌握设计模式对编写高质量代码是极有帮助的。所以想从之前的笔记、工作中碰到的场景及反思、一些书籍的参考、和网上查到的用例,梳理一下自己对设计模式的理解,整理成一系列文章。设计模式设计模式的本质是利用多态、封装、等技术,提炼出一些可以重复使用的设计技巧,或者说套路。当然,不同语言中,运用的技术是不一样的,在js中,用的比较多的是多态、闭包、高阶函数等技术。设计模式注重可复用性和可维护性,而不注重代码量和逻辑复杂度。深刻理解了模式的意图之后,再结合项目的实际场景进行使用,误用会适得其反。动态语言的好处js是动态语言,动态语言对变量类型的宽容给实际编码带来了很大的灵活性;静态语言中,实现“面向接口编程”不容易,因为在类型检查的强制要求下,必须通过抽象类或者接口进行向上转型,才能体现出多态的价值;而在js中,向上转型不存在的,任何对象都可以替换使用,实现多态简直轻而易举,具体见下文;多态多态的概念:同一个操作作用于不同的对象上,可以产生不同的解释和不同的执行结果。在JavaScript这种类型模糊的语言中,对象多态性是天生的,一个变量既可以指向一个类, 又可以随时指向另外一个类。多态的核心思想:’做什么‘和’怎么做、谁去做‘分离开来,也是’不变的事情‘和’有可能变的事情‘分离开来;var makeSound = function( animal ){ animal.sound();};var Duck = function(){}Duck.prototype.sound = function(){ console.log( ‘嘎嘎嘎’ );};var Chicken = function(){}Chicken.prototype.sound = function(){ console.log( ‘咯咯咯’ );};makeSound( new Duck() ); // 嘎嘎嘎makeSound( new Chicken() ); // 咯咯咯代码参考自《设计模式与开发实践》

January 10, 2019 · 1 min · jiezi

桥接模式

桥接模式定义将抽象部分与具体部分分离,使他们都可以独立的变化。通过组合的方式建立两个雷之间的联系,而不是继承。类型结构型使用场景抽象和具体实现之间的增加更多的灵活性。一个类存在两个(或多个)独立变化的维度,且者两个(或多个)维度都需要独立进行扩展。不希望使用多层继承导致系统中类的个数剧增。优点分离抽象部分机器具体实现部分(使抽象和继承不再在同一个继承层次中,让抽象和实现可以在各自的维度中发展)提高了系统的可扩展性符合开闭原则符合合成复用原则难点在使用桥接模式时难点就是需要正确的识别系统中两个独立变化的维度。在写代码之前我们先假设一个场景,我们现在要实现一个银行的存款操作,存款的话就包括有活期和定期之分,然后存款也有不同的银行。我们就要用代码来模拟这个操作。我们说桥接模式的难点就在于抽象和实现进行分离。我们现在先用一个接口当做抽象层。把账户的相关操作放进这个接口里面。public interface Account { Account openAccount(); void showAccountType();}这里定义了两个方法,然后我们写一下银行的抽象类,一会我们要委托这个抽象类来执行具体的操作。public abstract class Bank { protected Account account; public Bank(Account account){ this.account = account; } abstract Account openAccount();}我们这里定义了一个与接口相同名称的方法,为什么要这么做呢?我们一会再解答。这里有一个账户的属性,然后将账户对象注入进来,使用构造器的方式注入。我们再实现一下具体的银行类。我们来写一个农业银行ABCBank一个工商银行ICBCBank(爱存不存银行)。这个两个银行继承bank抽象类。实现抽象方法。//ABCBankpublic class ABCBank extends Bank { public ABCBank(Account account) { super(account); } @Override Account openAccount() { System.out.println(“打开中国农业银行账号”); account.openAccount(); //这一行很关键,这是委托父类执行操作的代码 return account; }}//ICBCBankpublic class ICBCBank extends Bank { public ICBCBank(Account account) { super(account); } @Override Account openAccount() { System.out.println(“打开中国工商银行账号”); account.openAccount(); return account; }}这个实现类继承抽象类,在执行openAccount是直接调用父类的方法。这时候我们说一下为什么要让抽象类和接口一样的名称呢,其实这个不一样也是可以的,我们写成一样的只是想让他们表一样的操作,我们的openAccount方法重点是要调用account的openAccount。// DepositAccountpublic class DepositAccount implements Account { @Override public Account openAccount() { System.out.println(“打开定期账号”); return new DepositAccount(); } @Override public void showAccountType() { System.out.println(“这是一个定期账号”); }}//SavingAccountpublic class SavingAccount implements Account { @Override public Account openAccount() { System.out.println(“打开活期账号”); //… return new SavingAccount(); } @Override public void showAccountType() { System.out.println(“这是一个活期账号”); }}这两个实现类实现了接口方法。我们最后看看测试类。public class BridgeTest { public static void main(String[] args) { Bank icbcBank = new ICBCBank(new DepositAccount()); Account icbcAccount = icbcBank.openAccount(); icbcAccount.showAccountType(); Bank icbcBank2 = new ICBCBank(new SavingAccount()); Account icbcAccount2 = icbcBank2.openAccount(); icbcAccount2.showAccountType(); Bank abcBank = new ABCBank(new SavingAccount()); Account abcAccount = abcBank.openAccount(); abcAccount.showAccountType(); }}我们创建一个银行对象将农业银行赋值给他,同时将一个定期账户传入构造器,注入银行类中,然后银行类在执行openAccount方法调用的是account类的openAccount方法,这样的话我们就讲account和bank类组合到了一起。后面我们在扩展比如添加bank类时就和account类解耦了,再通过他们的排列组合就可以得到很多结果了。最后我们看一下运行结果。打开中国工商银行账号打开定期账号这是一个定期账号打开中国工商银行账号打开活期账号这是一个活期账号打开中国农业银行账号打开活期账号这是一个活期账号 ...

January 10, 2019 · 1 min · jiezi

开闭原则

个人博客原文:开闭原则设计模式六大原则之六:开闭原则。简介姓名 :开闭原则英文名 :Open Closed Principle价值观 :老顽童就是我,休想改变我个人介绍 :Software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification.(软件中的对象(类,模块,函数等等)应该对于扩展是开放的,但是对于修改是封闭的)(来自维基百科)停更了三四天了,这几天比较忙,不仅仅是工作上,更多是精神上。周日突然老胃病又复发了,一直疼到凌晨 4,5 点。因为这次疼得蛮厉害的,所以准备去医院看一下医生,这时候才体验到大城市就医之苦。周日晚下载了微医 App (不是做广告哈),也不知道哪家医院好,在深圳两年半还没去过医院,随便选个三甲医院:北京大学深圳医院,看了消化内科门诊的医生列表,整整这一周主任医生都预约满了,顿时很崩溃,打电话给医院预约,最快只能预约 17 号,are you kidding?App 上有个 『立即问诊』功能,在线把状况告诉医生,医生一天之内接诊,需要花 60 块,我就尝试一下,没想到第二天医生回复后,说可以下午去医院看,他可以临时加号。就这样跳过了预约,直接看病,不知道你是否也苦于看病烦,可以尝试这个方法,当然,如果你有更好的方法,可以留言让更多的人了解到。跑题了跑题了,今天是想和大家分享设计模式最后一个原则:开闭原则。这个原则要求就是允许扩展,拒绝修改。既然上面讲到看医生,那就用一个跟看病有关的例子。故事从这里开始小明去医院看病,医生开了阿司匹林药,小明去了收费台,付了钱,总共 20 块钱。例子的代码如下:public class OcpTest { public static void main(String[] args) { Hospital hospital = new Hospital(); IPatient xiaoMing = new Patient(“小明”); hospital.sellMedicine(xiaoMing); }}class Medicine { private String name; private BigDecimal price; public Medicine(String name, BigDecimal price) { this.name = name; this.price = price; } public String getName() { return name; } public void setName(String name) { this.name = name; } public BigDecimal getPrice() { return price; } public void setPrice(BigDecimal price) { this.price = price; }}class Hospital { private Medicine medicine = new Medicine(“阿司匹林”, new BigDecimal(20)); public void sellMedicine(IPatient patient) { BigDecimal money = patient.pay(medicine); System.out.println(patient.getName() + " 花了 " + money.setScale(2, BigDecimal.ROUND_UP) + " 块钱买了药:" + medicine.getName()); }}interface IPatient { String getName(); BigDecimal pay(Medicine medicine);}class Patient implements IPatient{ private String name; public Patient(String name) { this.name = name; } @Override public BigDecimal pay(Medicine medicines) { return medicines.getPrice(); } @Override public String getName() { return name; }}第二天和朋友聚会聊起这事,小红说道:不对呀,前几天我在医院也拿了阿司匹林药,才 14 块钱呢。小花说:奇怪了,我买的是 16 块钱。小杰回应:怎么我买的是 18 块。怎么这药这么多个价格。小明 Google 搜了一下,发现价格跟社保有关,几个人便发现,原来他们都是“不同人”:小明没有社保,小红社保是一档,小花社保是二挡,小杰社保是三挡。(假设社保一档打 7 折,社保二挡打 8 折,社保三挡打 9 折,虚拟的哈)发现了这秘密后,作为和 IT 工作相关的人,便讨论起医院系统具体实现是怎么实现的。小红说:这很简单呢,药品给不同人提供不同的价格。代码如下:class Medicine { private String name; private BigDecimal price; public Medicine(String name, BigDecimal price) { this.name = name; this.price = price; } public String getName() { return name; } public void setName(String name) { this.name = name; } public BigDecimal getPrice() { return price; } public BigDecimal getPrice1() { return price.multiply(new BigDecimal(0.7)); } public BigDecimal getPrice2() { return price.multiply(new BigDecimal(0.8)); } public BigDecimal getPrice3() { return price.multiply(new BigDecimal(0.9)); } public void setPrice(BigDecimal price) { this.price = price; }}小花说:药片本身的价格是不会变的,只是给不同人不同价格,所以可以在病人获取价钱的时候去区分。代码如下:class Patient implements IPatient{ private String name; private int level; public Patient(String name) { this.name = name; } @Override public BigDecimal pay(Medicine medicines) { if (level == 1) { return medicines.getPrice().multiply(new BigDecimal(0.7)); } else if (level == 2) { return medicines.getPrice().multiply(new BigDecimal(0.8)); } else if (level == 3) { return medicines.getPrice().multiply(new BigDecimal(0.9)); } return medicines.getPrice(); } @Override public String getName() { return name; }}小杰陷入了沉思。。。小明发话:你们说的方法都可以实现,但是总感觉不对劲,如果以后有社保四挡,还是要修改原来的代码,前 2 天设计模式老师讲的开闭原则忘记了么?里面说要对扩展开放,对修改封闭。我觉得这个药片价格是因为我们人而变的,那是不是我们可以把没社保的归为一类人,一档社保的也为一类,以此类推。我觉得这样实现更好,增加多 3 类病人,分别是一档社保、二挡社保、三挡社保。代码如下:class OneLevelSocialSecurityPatient implements IPatient { private String name; public OneLevelSocialSecurityPatient(String name) { this.name = name; } @Override public BigDecimal pay(Medicine medicine) { return medicine.getPrice().multiply(new BigDecimal(0.7)); } @Override public String getName() { return this.name; }}class TwoLevelSocialSecurityPatient implements IPatient { private String name; public TwoLevelSocialSecurityPatient(String name) { this.name = name; } @Override public BigDecimal pay(Medicine medicine) { return medicine.getPrice().multiply(new BigDecimal(“0.8”)); } @Override public String getName() { return this.name; }}class ThreeLevelSocialSecurityPatient implements IPatient { private String name; public ThreeLevelSocialSecurityPatient(String name) { this.name = name; } @Override public BigDecimal pay(Medicine medicine) { return medicine.getPrice().multiply(new BigDecimal(“0.9”)); } @Override public String getName() { return this.name; }}// 测试代码public static void main(String[] args) { Hospital hospital = new Hospital(); IPatient xiaoMing = new Patient(“小明”); hospital.sellMedicine(xiaoMing); IPatient xiaoHong = new OneLevelSocialSecurityPatient(“小红”); hospital.sellMedicine(xiaoHong); IPatient xiaoHua = new TwoLevelSocialSecurityPatient(“小花”); hospital.sellMedicine(xiaoHua); IPatient xiaoJie = new ThreeLevelSocialSecurityPatient(“小杰”); hospital.sellMedicine(xiaoJie);}代码:OcpTest.java看了他们的对话和代码,是不是能知道哪种方式更好了?对于小红来说,她没理清价格变化的原因,价格变化不在于药片;小花理清了,但是实现方式差了点,以后如果新增了四挡社保,她的实现要修改原有的代码,不符合开闭原则;小明的方法就符合开闭原则,如果新增四挡社保人员,他的方法只需要再额外扩展一个四挡社保人员就可以,不用动用其他代码。用了这个大家可能不太喜欢的看病的场景来描述这个开闭原则,不要忌讳哈,希望大家都健健康康,远离医院。总结重申一下:对扩展开放,对修改封闭。如果有同学经常看一些开源框架源码就会发现,有很多很多抽象类和接口,debug 进去很绕,其实这些抽象类和接口很多都是为了扩展用,因为作为开源框架,不得不实现各种可想象到的方案,而这些都基于开闭原则来实现的。以后有机会也可以写一下源码的文章分享给大家。参考资料:《大话设计模式》、《Java设计模式》、《设计模式之禅》、《研磨设计模式》、《Head First 设计模式》这周事情比较多,更新会不及时,周五还要出差去一趟上海,周六回深圳,周日回一趟老家,各种奔波。。。希望文章对您有所帮助,设计模式系列会持续更新,感兴趣的同学可以关注公众号,第一时间获取文章推送阅读,也可以一起交流,交个朋友。公众号之设计模式系列文章 ...

January 9, 2019 · 3 min · jiezi

组合模式

组合模式定义将对象组合成树形结构以表示“部分-整体”的层次结构。组合模式是客户端对单个对象和组合对象保持一致的方式处理。类型结构型适用场景希望客户端可以忽略组合对象与单个对象的差异是处理一个树形结构时优点清楚地定义分层次的复杂对象,表示对象的全部或部分层次。让客户端忽略了层次的差异,方便对整个层次结构进行控制。简化客户端代码下面开始写代码,我们首先假设一个业务场景,假设我们有一个课程,这个课程里面有很多课程包括java课程python等等,我们定义一个抽象类,让这些课程类继承这个抽象类,那么我们就可以认为这些课程是一个对象。public abstract class CatalogComponent { public void add(CatalogComponent catalogComponent){ throw new UnsupportedOperationException(“不支持添加操作”); } public void remove(CatalogComponent catalogComponent){ throw new UnsupportedOperationException(“不支持删除操作”); } public String getName(CatalogComponent catalogComponent){ throw new UnsupportedOperationException(“不支持获取名称操作”); } public double getPrice(CatalogComponent catalogComponent){ throw new UnsupportedOperationException(“不支持获取价格操作”); } public void print(){ throw new UnsupportedOperationException(“不支持打印操作”); }}这个就是抽象方法,至于我们为什么要在这个抽象类中抛出异常呢?这个问题我们等写完了其他的类再解答。public class Course extends CatalogComponent { private String name; private double price; public Course(String name, double price) { this.name = name; this.price = price; } @Override public String getName(CatalogComponent catalogComponent) { return this.name; } @Override public double getPrice(CatalogComponent catalogComponent) { return this.price; } @Override public void print() { System.out.println(“Course Name:"+name+” Price:"+price); }}这是一个课程类,我们继承了抽象类,有价格和名称两个属性。public class CourseCatalog extends CatalogComponent { private List<CatalogComponent> items = new ArrayList<CatalogComponent>(); private String name; private Integer level; public CourseCatalog(String name,Integer level) { this.name = name; this.level = level; } @Override public void add(CatalogComponent catalogComponent) { items.add(catalogComponent); } @Override public String getName(CatalogComponent catalogComponent) { return this.name; } @Override public void remove(CatalogComponent catalogComponent) { items.remove(catalogComponent); } @Override public void print() { System.out.println(this.name); for(CatalogComponent catalogComponent : items){ if(this.level != null){ for(int i = 0; i < this.level; i++){ System.out.print(" “); } } catalogComponent.print(); } }}这个是目录类,有两个属性,层级和名称。这个是我们看到代码,就解释一下为什么在抽象类中要抛出异常,因为我们的子类有的功能我们都重写了抽象类的方法,但是如果没有重写的方法也是被继承了的。如果这时候被调用了,子类是没有这个功能我希望不被调用所以就直接抛异常。最后我们看看测试类的代码。public class CompositeTest { public static void main(String[] args) { CatalogComponent linuxCourse = new Course(“Linux课程”,11); CatalogComponent windowsCourse = new Course(“Windows课程”,11); CatalogComponent javaCourseCatalog = new CourseCatalog(“Java课程目录”,2); CatalogComponent mmallCourse1 = new Course(“Java设计模式一”,55); CatalogComponent mmallCourse2 = new Course(“Java设计模式二”,66); CatalogComponent designPattern = new Course(“Java设计模式三”,77); javaCourseCatalog.add(mmallCourse1); javaCourseCatalog.add(mmallCourse2); javaCourseCatalog.add(designPattern); CatalogComponent imoocMainCourseCatalog = new CourseCatalog(“课程主目录”,1); imoocMainCourseCatalog.add(linuxCourse); imoocMainCourseCatalog.add(windowsCourse); imoocMainCourseCatalog.add(javaCourseCatalog); imoocMainCourseCatalog.print(); }}组合模式最大的问题在于要花代码去判断到底是那一个类,因为我们在使用的时候都是使用了父类对象。 ...

January 8, 2019 · 2 min · jiezi

设计模式之软件开发原则(开闭原则和依赖倒置原则)

开闭原则定义所谓开闭原则就是一个软件实体如类、模块和函数应该对扩展开放、对修改关闭。强调用抽象构建框架,实现实现拓展细节。有优点是提高软件的复用性和易维护展性。是面向对象的最基本原则。依赖倒置原则定义高层模块不应该依赖底层模块,二者都应该依赖其抽象。抽象不应该依赖细节:细节应该依赖抽象。针对接口编程,不要针对实现编程。优点降低耦合提高稳定性,提高代码的可读性和易维护性。减少代码在修改时可能造成的风险。下面我们用代码来说明啥是依赖倒置原则假设我们想实现一个人在学习的需求,我们可以这样写/** * @author 杨红星 * @version 1.0.0 * @date 2018/11/30 /public class Redstar { public void studyJava() { System.out.println(“redstar 在学习java”); } public void studyFE() { System.out.println(“redstar 在学习前端”); }}然后有个测试类/* * @author 杨红星 * @version 1.0.0 * @date 2018/11/30 /public class RedstarTest { public static void main(String[] args) { Redstar redstar = new Redstar(); redstar.studyFE(); redstar.studyJava(); }}这时假设我们还想要学习Python那么我们则需要去修改Redstar这个类。这样写法是面向实现编程,因为整个Redstar类就是一个实现类。这个redstar类是要经常被修改的。也就拓展性比较差。我们这个RedstarTest类就是应用层(高层模块)的类是依赖于这个Redstar实现类(底层模块)的。因为我们没有抽象。根据依赖倒置原则高层次的模块不应该去依赖低层次的模块。每次Redstar拓展都要来RedstarTest进行补充。我们下面开始码具有依赖倒置的代码,首先创建一个接口。这个接口有个学习方法。/* * @author 杨红星 * @version 1.0.0 * @date 2018/11/30 / public interface ICourse { void studyCourse();}下面写几个实现类,实现这个接口public class JavaCourse implements ICourse { @Override public void studyCourse() { System.out.println(“redstar在学习Java课程”); }}public class FECourse implements ICourse { @Override public void studyCourse() { System.out.println(“redstar在学习FE课程”); }}此时我们将Redstar类修改为/* * @author 杨红星 * @version 1.0.0 * @date 2018/11/30 /public class Redstar { public void studyJava(ICourse iCourse) { iCourse.studyCourse(); }}Test类修改为/* * @author 杨红星 * @version 1.0.0 * @date 2018/11/30 /public class RedstarTest { public static void main(String[] args) { Redstar redstar = new Redstar(); redstar.studyJava(new JavaCourse()); redstar.studyJava(new FECourse()); }}输出结果为redstar在学习Java课程redstar在学习FE课程这时如果我们还有其他大的课程想要学习我们可以通过添加ICourse的实现类的方式来添加。Redstar这个类是不会去动他的。对于想要学习什么课程我们有RedstarTest类这高层类来自己选择。顺便提一下我们在RedstarTest类中使用接口方法的方式对ICourse接口的依赖进行注入。我们也可以使用构造器的方式对依赖进行注入。/* * @author 杨红星 * @version 1.0.0 * @date 2018/11/30 /public class Redstar { private ICourse iCourse; public Redstar(ICourse iCourse) { this.iCourse = iCourse; } public void studyJava() { iCourse.studyCourse(); }}此时我们Redstar中就应该这么写/* * @author 杨红星 * @version 1.0.0 * @date 2018/11/30 /public class RedstarTest { public static void main(String[] args) { JavaCourse javaCourse = new JavaCourse(); FECourse feCourse = new FECourse(); Redstar redstar = new Redstar(javaCourse); redstar.studyJava(); redstar = new Redstar(feCourse); redstar.studyJava(); }}有构造器的方式同样我们也可以用set 的方式,此时我们的Redstar就要这么写了/* * @author 杨红星 * @version 1.0.0 * @date 2018/11/30 /public class Redstar { private ICourse iCourse; public void setiCourse(ICourse iCourse) { this.iCourse = iCourse; } public void studyJava() { iCourse.studyCourse(); }}对应的RedstarTest就要这么写了/* * @author 杨红星 * @version 1.0.0 * @date 2018/11/30 */public class RedstarTest { public static void main(String[] args) { JavaCourse javaCourse = new JavaCourse(); FECourse feCourse = new FECourse(); Redstar redstar = new Redstar(); redstar.setiCourse(feCourse); redstar.studyJava(); redstar.setiCourse(javaCourse); redstar.studyJava(); }}好到这里我们的依赖倒置原则就讲完了,总结一下总体原则就是面向接口编程,或者说面向抽象编程。上面例子中的接口我们也可以使用抽象类来代替。同学们可以使用抽象类模拟一下上面的过程。 ...

January 8, 2019 · 2 min · jiezi

享元模式

享元模式定义提供了减少对象数量二改善应用所需的对象结构的方法运用共享技术有效的支持大量粗粒度的对象。用通俗的大白话来说就是减少对象的数量,提高对象的利用率,减少内存的使用,提高系统性能。类型创建型适用场景常常应用于系统底层的开发,一遍解决系统的性能问题。系统中有大量的相似对象、需要使用缓冲池的场景。优点减少对象的创建,降低内存中对象的数量,降低系统的内存,提高效率。减少内存之外的其他资源占用。(对象的创建是消耗其他的资源的)缺点关注内部/外部状态、关注线程安全问题。系统逻辑的复杂化。(比如我们经常做一道面试题Integer a=128;Integer b=128;a==b是true还是false,这里就有缓存的问题,后面我们再分析这个)内部状态在享对象类的内部,不会随着环境改变而改变的状态。外部状态记录在享元对象的外部,随着环境改变而改变。不可以共享的状态。内部状态是享元对象的属性,不会随环境变化而变化的属性。外部状态我们打个比方就是在外部调用享元对象的方法时传过来一个int 这个int 为1时一种状态,为2时一种状态这就是外部状态。我们来看看怎么实现。国际惯例假设一个应用场景,假设我们要做一个部门会议。public interface Employee { void report();}员工接口有个做报告方法。public class Manager implements Employee { @Override public void report() { System.out.println(reportContent); } private String title = “部门经理”; private String department; private String reportContent; public void setReportContent(String reportContent) { this.reportContent = reportContent; } public Manager(String department) { this.department = department; }}经理实现了员工接口。有三个属性,标题,部门,报告内容。public class EmployeeFactory { private static final Map<String,Employee> EMPLOYEE_MAP = new HashMap<String,Employee>(); public static Employee getManager(String department){ Manager manager = (Manager) EMPLOYEE_MAP.get(department); if(manager == null){ manager = new Manager(department); System.out.print(“创建部门经理:"+department); String reportContent = department+“部门汇报:此次报告的主要内容是……”; manager.setReportContent(reportContent); System.out.println(” 创建报告:"+reportContent); EMPLOYEE_MAP.put(department,manager); } return manager; }}员工工厂,这里使用了容器单例模式。public class FlyweightTest { private static final String departments[] = {“RD”,“QA”,“PM”,“BD”}; public static void main(String[] args) { for(int i=0; i<10; i++){ String department = departments[(int)(Math.random() * departments.length)]; Manager manager = (Manager) EmployeeFactory.getManager(department); manager.report(); } }}这里我们定义了一个部门列表,然后随机的使用其中一个部门去去执行报告操作。创建部门经理:RD 创建报告:RD部门汇报:此次报告的主要内容是……RD部门汇报:此次报告的主要内容是……创建部门经理:PM 创建报告:PM部门汇报:此次报告的主要内容是……PM部门汇报:此次报告的主要内容是……RD部门汇报:此次报告的主要内容是……创建部门经理:QA 创建报告:QA部门汇报:此次报告的主要内容是……QA部门汇报:此次报告的主要内容是……创建部门经理:BD 创建报告:BD部门汇报:此次报告的主要内容是……BD部门汇报:此次报告的主要内容是……RD部门汇报:此次报告的主要内容是……RD部门汇报:此次报告的主要内容是……PM部门汇报:此次报告的主要内容是……QA部门汇报:此次报告的主要内容是……BD部门汇报:此次报告的主要内容是…… ...

January 7, 2019 · 1 min · jiezi

每天一个设计模式之责任链模式

作者按:《每天一个设计模式》旨在初步领会设计模式的精髓,目前采用javascript和python两种语言实现。诚然,每种设计模式都有多种实现方式,但此小册只记录最直截了当的实现方式 :)0. 项目地址责任链模式·代码《每天一个设计模式》地址1. 什么是“责任链模式”?责任链模式:多个对象均有机会处理请求,从而解除发送者和接受者之间的耦合关系。这些对象连接成为链式结构,每个节点转发请求,直到有对象处理请求为止。其核心就是:请求者不必知道是谁哪个节点对象处理的请求。如果当前不符合终止条件,那么把请求转发给下一个节点处理。而当需求具有“传递”的性质时(代码中其中一种体现就是:多个if、else if、else if、else嵌套),就可以考虑将每个分支拆分成一个节点对象,拼接成为责任链。2. 优点与代价优点可以根据需求变动,任意向责任链中添加 / 删除节点对象没有固定的“开始节点”,可以从任意节点开始代价:责任链最大的代价就是每个节点带来的多余消耗。当责任链过长,很多节点只有传递的作用,而不是真正地处理逻辑。3. 代码实现为了方便演示,模拟常见的“日志打印”场景。模拟了 3 种级别的日志输出:LogHandler: 普通日志WarnHandler:警告日志ErrorHandler:错误日志首先我们会构造“责任链”:LogHandler -> WarnHandler -> ErrorHandler。LogHandler作为链的开始节点。如果是普通日志,那么就由 LogHandler 处理,停止传播;如果是 Warn 级别的日志,那么 LogHandler 就会自动向下传递,WarnHandler 接收到并且处理,停止传播;Error 级别日志同理。3.1 ES6 实现class Handler { constructor() { this.next = null; } setNext(handler) { this.next = handler; }}class LogHandler extends Handler { constructor(…props) { super(…props); this.name = “log”; } handle(level, msg) { if (level === this.name) { console.log(LOG: ${msg}); return; } this.next && this.next.handle(…arguments); }}class WarnHandler extends Handler { constructor(…props) { super(…props); this.name = “warn”; } handle(level, msg) { if (level === this.name) { console.log(WARN: ${msg}); return; } this.next && this.next.handle(…arguments); }}class ErrorHandler extends Handler { constructor(…props) { super(…props); this.name = “error”; } handle(level, msg) { if (level === this.name) { console.log(ERROR: ${msg}); return; } this.next && this.next.handle(…arguments); }}/以下是测试代码/let logHandler = new LogHandler();let warnHandler = new WarnHandler();let errorHandler = new ErrorHandler();// 设置下一个处理的节点logHandler.setNext(warnHandler);warnHandler.setNext(errorHandler);logHandler.handle(“error”, “Some error occur”);3.2 Python3 实现class Handler(): def init(self): self.next = None def set_next(self, handler): self.next = handlerclass LogHandler(Handler): def init(self): super().init() self.__name = “log” def handle(self, level, msg): if level == self.__name: print(‘LOG: ‘, msg) return if self.next != None: self.next.handle(level, msg)class WarnHandler(Handler): def init(self): super().init() self.__name = “warn” def handle(self, level, msg): if level == self.__name: print(‘WARN: ‘, msg) return if self.next != None: self.next.handle(level, msg)class ErrorHandler(Handler): def init(self): super().init() self.__name = “error” def handle(self, level, msg): if level == self.__name: print(‘ERROR: ‘, msg) return if self.next != None: self.next.handle(level, msg)# 以下是测试代码log_handler = LogHandler()warn_handler = WarnHandler()error_handler = ErrorHandler()# 设置下一个处理的节点log_handler.set_next(warn_handler)warn_handler.set_next(error_handler)log_handler.handle(“error”, “Some error occur”)4. 参考《JavaScript 设计模式和开发实践》javascript 之 责任链模式职责链模式5. ???? 博客软广 ????个人技术博客-godbmw.com 欢迎来玩! 每周至少 1 篇原创技术分享,还有开源教程(webpack、设计模式)、面试刷题(偏前端)、知识整理(每周零碎),欢迎长期关注!本篇博客地址是:《每天一个设计模式之责任链模式》。如果您也想尝试知识整理 + 搭建功能完善/设计简约/快速启动的个人博客,请直接戳theme-bmw ...

January 7, 2019 · 2 min · jiezi

JavaScript中常用的设计模式

本文已同步到Github JavaScript中常见的设计模式,如果感觉写的还可以,就给个小星星吧,欢迎star。最近拜读了曾探大神的《JavaScript设计模式与开发实践》,真是醍醐灌顶,犹如打通任督二脉的感觉,让我对JavaScript的理解加深了很多。本文中关于各种设计模式定义都是引用书中的,部分引用自百度百科已标出。另外,本文中所举例子大多是书中的,自已做了一些修改和补充,用ES6(书中都是ES5的方式)的方式实现,以加深自己对“类”的理解,并不是自己来讲解设计模式,主要是做一些笔记以方便自己过后复习与加深理解,同时也希望把书中典型的例子整理出来和大家分享,共同探讨和进步。一提起设计模式,相信大家都会脱口而出,23种设计模式,五大设计原则。这里就不说了,奈何我功力远远不够啊。下面把我整理出的常用JavaScript设计模式按类型做个表格整理。本文较长,如果阅读起来不方便,可链接到我的github中,单独查看每一种设计模式。先整理这些,后续会继续补充,感兴趣的同学可以关注。模式分类 名称 创建型 工厂模式 单例模式 原型模式 结构型 适配器模式 代理模式 行为型 策略模式 迭代器模式 观察者模式(发布-订阅模式) 命令模式 状态模式 创建型模式工厂模式工厂模式中,我们在创建对象时不会对客户端暴露创建逻辑,并且是通过使用一个共同的接口来指向新创建的对象,用工厂方法代替new操作的一种模式。class Creator { create(name) { return new Animal(name) }}class Animal { constructor(name) { this.name = name }}var creator = new Creator()var duck = creator.create(‘Duck’)console.log(duck.name) // Duckvar chicken = creator.create(‘Chicken’) console.log(chicken.name) // Chicken小结:构造函数和创建者分离,对new操作进行封装符合开放封闭原则单例模式举一个书中登录框的例子,代码如下:<!DOCTYPE html><html lang=“en”><body> <button id=“btn”>登录</button></body><script> class Login { createLayout() { var oDiv = document.createElement(‘div’) oDiv.innerHTML = ‘我是登录框’ document.body.appendChild(oDiv) oDiv.style.display = ’none’ return oDiv } } class Single { getSingle(fn) { var result; return function() { return result || (result = fn.apply(this, arguments)) } } } var oBtn = document.getElementById(‘btn’) var single = new Single() var login = new Login() // 由于闭包,createLoginLayer对result的引用,所以当single.getSingle函数执行完之后,内存中并不会销毁result。 // 当第二次以后点击按钮,根据createLoginLayer函数的作用域链中已经包含了result,所以直接返回result // 讲获取单例和创建登录框的方法解耦,符合开放封闭原则 var createLoginLayer = single.getSingle(login.createLayout) oBtn.onclick = function() { var layout = createLoginLayer() layout.style.display = ‘block’ }</script></html>小结:1.单例模式的主要思想就是,实例如果已经创建,则直接返回function creatSingleton() { var obj = null // 实例如已经创建过,直接返回 if (!obj) { obj = xxx } return obj}2.符合开放封闭原则原型模式用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象。–百度百科在JavaScript中,实现原型模式是在ECMAScript5中,提出的Object.create方法,使用现有的对象来提供新创建的对象的__proto__。var prototype = { name: ‘Jack’, getName: function() { return this.name }}var obj = Object.create(prototype, { job: { value: ‘IT’ }})console.log(obj.getName()) // Jackconsole.log(obj.job) // ITconsole.log(obj.proto === prototype) //true更多关于prototype的知识可以看我之前的JavaScript中的面向对象、原型、原型链、继承,下面列一下关于prototype的一些使用方法1. 方法继承var Parent = function() {}Parent.prototype.show = function() {}var Child = function() {}// Child继承Parent的所有原型方法Child.prototype = new Parent()2. 所有函数默认继承Objectvar Foo = function() {}Foo.prototype.proto = Object.prototype3. Object.creatvar obj = Object.creat(proto, [propertiesObject])obj.proto = proto4. isPrototypeOfprototypeObj是否在obj的原型链上prototypeObj.isPrototypeOf(obj)5. instanceofcontructor.prototype是否出现在obj的原型链上obj instanceof contructor6. getPrototypeOfObject.getPrototypeOf(obj) 方法返回指定对象obj的原型(内部[[Prototype]]属性的值)Object.getPrototypeOf(obj)7. setPrototypeOf设置一个指定的对象的原型 ( 即, 内部[[Prototype]]属性)到另一个对象或 nullObject.setPrototypeOf(obj, prototypeObj)obj.prototype = prototypeObj结构型模式适配器模式举一个书中渲染地图的例子class GooleMap { show() { console.log(‘渲染谷歌地图’) }}class BaiduMap { show() { console.log(‘渲染百度地图’) }}function render(map) { if (map.show instanceof Function) { map.show() }}render(new GooleMap()) // 渲染谷歌地图render(new BaiduMap()) // 渲染百度地图但是假如BaiduMap类的原型方法不叫show,而是叫display,这时候就可以使用适配器模式了,因为我们不能轻易的改变第三方的内容。在BaiduMap的基础上封装一层,对外暴露show方法。class GooleMap { show() { console.log(‘渲染谷歌地图’) }}class BaiduMap { display() { console.log(‘渲染百度地图’) }}// 定义适配器类, 对BaiduMap类进行封装class BaiduMapAdapter { show() { var baiduMap = new BaiduMap() return baiduMap.display() }}function render(map) { if (map.show instanceof Function) { map.show() }}render(new GooleMap()) // 渲染谷歌地图render(new BaiduMapAdapter()) // 渲染百度地图小结:适配器模式主要解决两个接口之间不匹配的问题,不会改变原有的接口,而是由一个对象对另一个对象的包装。适配器模式符合开放封闭原则代理模式本文举一个使用代理对象加载图片的例子来理解代理模式,当网络不好的时候,图片的加载需要一段时间,这就会产生空白,影响用户体验,这时候我们可在图片真正加载完之前,使用一张loading占位图片,等图片真正加载完再给图片设置src属性。class MyImage { constructor() { this.img = new Image() document.body.appendChild(this.img) } setSrc(src) { this.img.src = src }}class ProxyImage { constructor() { this.proxyImage = new Image() } setSrc(src) { let myImageObj = new MyImage() myImageObj.img.src = ‘file://xxx.png’ //为本地图片url this.proxyImage.src = src this.proxyImage.onload = function() { myImageObj.img.src = src } }}var proxyImage = new ProxyImage()proxyImage.setSrc(‘http://xxx.png’) //服务器资源url本例中,本体类中有自己的setSrc方法,如果有一天网络速度已经不需要预加载了,我们可以直接使用本体对象的setSrc方法,,并且不需要改动本体类的代码,而且可以删除代理类。// 依旧可以满足需求var myImage = new MyImage()myImage.setSrc(‘http://qiniu.sunzhaoye.com/CORS.png')小结:代理模式符合开放封闭原则本体对象和代理对象拥有相同的方法,在用户看来并不知道请求的本体对象还是代理对象。行为型模式策略模式定义一系列的算法,把它们一个个封装起来,并使它们可以替换var fnA = function(val) { return val * 1}var fnB = function(val) { return val * 2}var fnC = function (val) { return val * 3}var calculate = function(fn, val) { return fn(val)}console.log(calculate(fnA, 100))// 100console.log(calculate(fnB, 100))// 200console.log(calculate(fnC, 100))// 300迭代器模式直接上代码, 实现一个简单的迭代器class Creater { constructor(list) { this.list = list } // 创建一个迭代器,也叫遍历器 createIterator() { return new Iterator(this) }}class Iterator { constructor(creater) { this.list = creater.list this.index = 0 } // 判断是否遍历完数据 isDone() { if (this.index >= this.list.length) { return true } return false } next() { return this.list[this.index++] }}var arr = [1, 2, 3, 4]var creater = new Creater(arr)var iterator = creater.createIterator()console.log(iterator.list) // [1, 2, 3, 4]while (!iterator.isDone()) { console.log(iterator.next()) // 1 // 2 // 3 // 4}ES6中的迭代器:JavaScript中的有序数据集合包括:ArrayMapSetStringtypeArrayargumentsNodeList注意: Object不是有序数据集合以上有序数据集合都部署了Symbol.iterator属性,属性值为一个函数,执行这个函数,返回一个迭代器,迭代器部署了next方法,调用迭代器的next方法可以按顺序访问子元素以数组为例测试一下,在浏览器控制台中打印测试如下:var arr = [1, 2, 3, 4]var iterator = arrSymbol.iteratorconsole.log(iterator.next()) // {value: 1, done: false}console.log(iterator.next()) // {value: 2, done: false}console.log(iterator.next()) // {value: 3, done: false}console.log(iterator.next()) // {value: 4, done: false}console.log(iterator.next()) // {value: undefined, done: true}小结:JavaScript中的有序数据集合有Array,Map,Set,String,typeArray,arguments,NodeList,不包括Object任何部署了[Symbol.iterator]接口的数据都可以使用for…of循环遍历迭代器模式使目标对象和迭代器对象分离,符合开放封闭原则观察者模式(订阅-发布模式)先实现一个简单的发布-订阅模式,代码如下:class Event { constructor() { this.eventTypeObj = {} } on(eventType, fn) { if (!this.eventTypeObj[eventType]) { // 按照不同的订阅事件类型,存储不同的订阅回调 this.eventTypeObj[eventType] = [] } this.eventTypeObj[eventType].push(fn) } emit() { // 可以理解为arguments借用shift方法 var eventType = Array.prototype.shift.call(arguments) var eventList = this.eventTypeObj[eventType] for (var i = 0; i < eventList.length; i++) { eventList[i].apply(eventList[i], arguments) } } remove(eventType, fn) { // 如果使用remove方法,fn为函数名称,不能是匿名函数 var eventTypeList = this.eventTypeObj[eventType] if (!eventTypeList) { // 如果没有被人订阅改事件,直接返回 return false } if (!fn) { // 如果没有传入取消订阅的回调函数,则改订阅类型的事件全部取消 eventTypeList && (eventTypeList.length = 0) } else { for (var i = 0; i < eventTypeList.length; i++) { if (eventTypeList[i] === fn) { eventTypeList.splice(i, 1) // 删除之后,i–保证下轮循环不会漏掉没有被遍历到的函数名 i–; } } } }}var handleFn = function(data) { console.log(data)}var event = new Event()event.on(‘click’, handleFn)event.emit(‘click’, ‘1’) // 1event.remove(‘click’, handleFn)event.emit(‘click’, ‘2’) // 不打印以上代码可以满足先订阅后发布,但是如果先发布消息,后订阅就不满足了。这时候我们可以稍微修改一下即可满足先发布后订阅,在发布消息时,把事件缓存起来,等有订阅者时再执行。代码如下:class Event { constructor() { this.eventTypeObj = {} this.cacheObj = {} } on(eventType, fn) { if (!this.eventTypeObj[eventType]) { // 按照不同的订阅事件类型,存储不同的订阅回调 this.eventTypeObj[eventType] = [] } this.eventTypeObj[eventType].push(fn) // 如果是先发布,则在订阅者订阅后,则根据发布后缓存的事件类型和参数,执行订阅者的回调 if (this.cacheObj[eventType]) { var cacheList = this.cacheObj[eventType] for (var i = 0; i < cacheList.length; i++) { cacheListi } } } emit() { // 可以理解为arguments借用shift方法 var eventType = Array.prototype.shift.call(arguments) var args = arguments var that = this function cache() { if (that.eventTypeObj[eventType]) { var eventList = that.eventTypeObj[eventType] for (var i = 0; i < eventList.length; i++) { eventList[i].apply(eventList[i], args) } } } if (!this.cacheObj[eventType]) { this.cacheObj[eventType] = [] } // 如果先订阅,则直接订阅后发布 cache(args) // 如果先发布后订阅,则把发布的事件类型与参数保存起来,等到有订阅后执行订阅 this.cacheObj[eventType].push(cache) }}小结:发布订阅模式可以使代码解耦,满足开放封闭原则当过多的使用发布订阅模式,如果订阅消息始终都没有触发,则订阅者一直保存在内存中。命令模式–百度百科在命令的发布者和接收者之间,定义一个命令对象,命令对象暴露出一个统一的接口给命令的发布者,而命令的发布者不用去管接收者是如何执行命令的,做到命令发布者和接收者的解耦。举一个如果页面中有3个按钮,给不同按钮添加不同功能的例子,代码如下:<!DOCTYPE html><html lang=“en”><head> <meta charset=“UTF-8”> <title>cmd-demo</title></head><body> <div> <button id=“btn1”>按钮1</button> <button id=“btn2”>按钮2</button> <button id=“btn3”>按钮3</button> </div> <script> var btn1 = document.getElementById(‘btn1’) var btn2 = document.getElementById(‘btn2’) var btn3 = document.getElementById(‘btn3’) // 定义一个命令发布者(执行者)的类 class Executor { setCommand(btn, command) { btn.onclick = function() { command.execute() } } } // 定义一个命令接收者 class Menu { refresh() { console.log(‘刷新菜单’) } addSubMenu() { console.log(‘增加子菜单’) } } // 定义一个刷新菜单的命令对象的类 class RefreshMenu { constructor(receiver) { // 命令对象与接收者关联 this.receiver = receiver } // 暴露出统一的接口给命令发布者Executor execute() { this.receiver.refresh() } } // 定义一个增加子菜单的命令对象的类 class AddSubMenu { constructor(receiver) { // 命令对象与接收者关联 this.receiver = receiver } // 暴露出统一的接口给命令发布者Executor execute() { this.receiver.addSubMenu() } } var menu = new Menu() var executor = new Executor() var refreshMenu = new RefreshMenu(menu) // 给按钮1添加刷新功能 executor.setCommand(btn1, refreshMenu) var addSubMenu = new AddSubMenu(menu) // 给按钮2添加增加子菜单功能 executor.setCommand(btn2, addSubMenu) // 如果想给按钮3增加删除菜单的功能,就继续增加删除菜单的命令对象和接收者的具体删除方法,而不必修改命令对象 </script></body></html>状态模式举一个关于开关控制电灯的例子,电灯只有一个开关,第一次按下打开弱光,第二次按下打开强光,第三次按下关闭。<!DOCTYPE html><html lang=“en”><head> <meta charset=“UTF-8”> <title>state-demo</title></head><body> <button id=“btn”>开关</button> <script> // 定义一个关闭状态的类 class OffLightState { constructor(light) { this.light = light } // 每个类都需要这个方法,在不同状态下按都需要触发这个方法 pressBtn() { this.light.setState(this.light.weekLightState) console.log(‘开启弱光’) } } // 定义一个弱光状态的类 class WeekLightState { constructor(light) { this.light = light } pressBtn() { this.light.setState(this.light.strongLightState) console.log(‘开启强光’) } } // 定义一个强光状态的类 class StrongLightState { constructor(light) { this.light = light } pressBtn() { this.light.setState(this.light.offLightState) console.log(‘关闭电灯’) } } class Light { constructor() { this.offLightState = new OffLightState(this) this.weekLightState = new WeekLightState(this) this.strongLightState = new StrongLightState(this) this.currentState = null } setState(newState) { this.currentState = newState } init() { this.currentState = this.offLightState } } let light = new Light() light.init() var btn = document.getElementById(‘btn’) btn.onclick = function() { light.currentState.pressBtn() } </script></body></html>如果这时候需要增加一个超强光,则只需增加一个超强光的类,并添加pressBtn方法,改变强光状态下,点击开关需要把状态更改为超强光,超强光状态下,点击开关把状态改为关闭即可,其他代码都不需要改动。class StrongLightState { constructor(light) { this.light = light } pressBtn() { this.light.setState(this.light.superLightState) console.log(‘开启超强光’) }}class SuperLightState { constructor(light) { this.light = light } pressBtn() { this.light.setState(this.light.offLightState) console.log(‘关闭电灯’) }}class Light { constructor() { this.offLightState = new OffLightState(this) this.weekLightState = new WeekLightState(this) this.strongLightState = new StrongLightState(this) this.superLightState = new SuperLightState(this) this.currentState = null } setState(newState) { this.currentState = newState } init() { this.currentState = this.offLightState }}小结:通过定义不同的状态类,根据状态的改变而改变对象的行为,二不必把大量的逻辑都写在被操作对象的类中,而且容易增加新的状态符合开放封闭原则终于到最后可,历时多日地阅读与理解,并记录与整理笔记,目前整理出10中JavaScript中常见的设计模式,后续会对笔记继续整理,然后加以补充。由于笔者功力比较浅,如有问题,还望大家多多指正,谢谢。参考链接:JavaScript设计模式与开发实践深入理解JavaScript系列/设计模式–汤姆大叔的博客设计模式–菜鸟教程JavaScript 中常见设计模式整理ES6入门–阮一峰 ...

January 7, 2019 · 5 min · jiezi

js设计模式--模板方法模式

前言本系列文章主要根据《JavaScript设计模式与开发实践》整理而来,其中会加入了一些自己的思考。希望对大家有所帮助。文章系列js设计模式–单例模式js设计模式–策略模式js设计模式–代理模式js设计模式–迭代器模式js设计模式–发布订阅模式js设计模式–命令模式js设计模式–组合模式概念模板方法模式是一种只需使用继承就可以实现的非常简单的模式。模板方法模式由两部分结构组成,第一部分是抽象父类,第二部分是具体的实现子类。通常 在抽象父类中封装了子类的算法框架,包括实现一些公共方法以及封装子类中所有方法的执行顺序。子类通过继承这个抽象类,也继承了整个算法结构,并且可以选择重写父类的方法。场景一般用于可以抽取公共方法,例如泡咖啡和泡茶,我们可以抽取烧水 清洗杯具 冲泡等过程优缺点优点可以复用公共方法,子类也不需要实现算法部分例子模板引擎我们实现一个简单的模板引擎:<!DOCTYPE html><html lang=“en”><head> <meta charset=“UTF-8”> <meta name=“viewport” content=“width=device-width, initial-scale=1.0”> <meta http-equiv=“X-UA-Compatible” content=“ie=edge”> <title>Document</title></head><body> <div id=“nav”></div> <script> var formateStr = function (param, data) { return param.replace(/{#(\w+)#}/g, function (match, key) { return typeof data[key] === undefined ? "" : data[key]; }); }; var Nav = function (data) { var _this = this; _this.item = ‘<li><a href="{#hrefUrl#}" title="{#title#}" {#sign#}>{#content#}</a></li>’; _this.html = ‘<ul>’; for (var i = 0, l = data.length; i < l; i++) { _this.html += formateStr(_this.item, data[i]); } _this.html += ‘</ul>’; return _this.html; } var objNav = document.getElementById(’nav’); objNav.innerHTML = Nav([{ hrefUrl: ‘http://www.baidu.com’, content: ‘百度一下’ }, { hrefUrl: ‘http://www.zhihu.com’, content: ‘知乎一下’ } ]); </script></body></html>现在产品加了一个需求,想在content后面加个span标签展示访问次数新需求普通程序员就会动手去改Nav方法,但这违背了开放封闭原则,我们也不能确保不影响原来的功能,其实我们加多一个模板方法就可以规避这样的问题<!DOCTYPE html><html lang=“en”><head> <meta charset=“UTF-8”> <meta name=“viewport” content=“width=device-width, initial-scale=1.0”> <meta http-equiv=“X-UA-Compatible” content=“ie=edge”> <title>Document</title></head><body> <div id=“nav”></div> <script> var formateStr = function (param, data) { return param.replace(/{#(\w+)#}/g, function (match, key) { return typeof data[key] === undefined ? "" : data[key]; }); }; var Nav = function (data) { var _this = this; _this.item = ‘<li><a href="{#hrefUrl#}" title="{#title#}" {#sign#}>{#content#}</a></li>’; _this.html = ‘<ul>’; for (var i = 0, l = data.length; i < l; i++) { _this.html += formateStr(_this.item, data[i]); } _this.html += ‘</ul>’; return _this.html; } var infoNav = function (data) { var _this = this; _this.info = ‘<span>{#clickNum#}</span>’; for (var i = data.length - 1; i >= 0; i–) { data[i].content += formateStr(_this.info, data[i]); }; return Nav.call(this, data); }; var objNav = document.getElementById(’nav’); objNav.innerHTML = infoNav([{ hrefUrl: ‘http://www.baidu.com’, content: ‘百度一下’, title: ‘百度’, clickNum: ‘10’, sign: ‘sign=“1”’ }, { hrefUrl: ‘http://www.zhihu.com’, content: ‘知乎一下’, title: ‘知乎’, clickNum: ‘100’, sign: ‘sign=“2”’ } ]); </script></body></html> ...

January 6, 2019 · 2 min · jiezi

迪米特法则

个人博客原文:迪米特法则设计模式六大原则之五:迪米特法则。简介姓名:迪米特法则英文名:Law of Demeter小名:最少知识原则小名英文名:Least Knowledge Principle价值观:妈妈说不和陌生人说话个人介绍:Each unit should have only limited knowledge about other units: only units “closely” related to the current unit. (每个单元对于其他的单元只能拥有有限的知识:只是与当前单元紧密联系的单元)Each unit should only talk to its friends; don’t talk to strangers. (每个单元只能和它的朋友交谈:不能和陌生单元交谈)Only talk to your immediate friends. (只和自己直接的朋友交谈)(来自维基百科)还是脑洞大开来个小故事。这故事还是比较现实一些,其实也不算是故事,就是咱们经常经历的事情,现在知识付费已经广受欢迎,18 年底更是快速猛涨,各种各样的培训、读书、音频学习软件如雨后春笋一般涌现出来。我们就拿一个读书的例子。有一天,设计模式老师讲解了迪米特法则,同学们听得云里雾里的,老师怕同学们没掌握这个知识点,就给同学们布置了一个作业,需要同学们按迪米特法则实现。作业是这样子的:平常在零碎的时间里,喜欢看一些书籍,一般都是电子书,现在我们看书的操作是这样的:唤醒手机,打开阅读软件,选择书籍,然后阅读。总共 3 个步骤,涉及了 3 样东西:手机、软件、书籍。同学们用代码实现这个过程。第二天上课,同学们纷纷交了作业,老师随手一番,就看到了 2 个鲜明的例子,很明显,就是一好一坏。老师便给同学们讲解了这 2 个例子,让学生感受一番迪米特法则。错误例子public class LODErrorTest { public static void main(String[] args) { Phone phone = new Phone(); phone.readBook(); }}/** * 错误的示范 /class Phone { App app = new App(); Book book = new Book(“设计模式”); public void readBook() { app.read(book); }}class App { public void read(Book book) { System.out.println(book.getTitle()); }}class Book { private String title; public Book(String title) { this.title = title; } public String getTitle() { return title; } public void setTitle(String title) { this.title = title; }}代码:LODErrorTest代码是完成了读书这个过程,看样子是功能实现了,细看会发现代码的逻辑不对。哪里不对呢?书籍和应用对象都在手机上,现实是我们唤醒手机,这时手机是没有书籍的,只有当我们打开阅读软件,才有书籍可以看,没有阅读软件,书籍是看不了的。因此,手机和书籍没有一毛钱关系,书籍不应该在手机里面。正常的设计是:手机里面有阅读软件,阅读软件里面有书籍,这才符合迪米特法则,按定义来说:手机和阅读软件是朋友,阅读软件和书籍是朋友,可是朋友的朋友不是朋友,也就是手机和书籍不是朋友,所以它们不应该有交集,应该离得远远的。思考一下现实:工作中如果缺少代码复核这个步骤,就会出现这样子,后果是怎么样呢?会给后人挖坑,而且是大坑,因为和现实中的逻辑是对不上的,况且后人不知道当时的业务背景,只能看代码去熟悉,就会一步错、步步错,所以要好好把控代码质量这一关,因为代码千人千面,没法要求代码风格全部一致,但至少需要实现逻辑是清晰易懂的。正确例子public class LODRightTest { public static void main(String[] args) { Phone2 phone2 = new Phone2(); phone2.readBook(); }}/* * 正确的示范 */class Phone2 { private App2 app2 = new App2(); public void readBook() { app2.read(); }}class App2 { private Book2 book2 = new Book2(“设计模式”); public void read() { System.out.println(book2.getTitle()); }}class Book2 { private String title; public Book2(String title) { this.title = title; } public String getTitle() { return title; } public void setTitle(String title) { this.title = title; }}代码:LODRightTest这段代码就符合迪米特法则,手机中有阅读软件,阅读软件中有书籍,手机没有书籍任何影子。正确代码不用细讲,用心去感受就可以体会到。总结迪米特法则主要讲述的观点是高内聚、低耦合。我理解为:是你的,就别给别人;不是你的,就别拿。上面定义的朋友也是这个意思。参考资料:《大话设计模式》、《Java设计模式》、《设计模式之禅》、《研磨设计模式》、《Head First 设计模式》写到这,第五个原则了,六大原则就剩下最后一个开闭原则,凑齐 6 把大刀就可以准备去大干 23 个大汉啦。。。想想都激动。希望文章对您有所帮助,设计模式系列会持续更新,感兴趣的同学可以关注公众号,第一时间获取文章推送阅读,也可以一起交流,交个朋友。公众号之设计模式系列文章 ...

January 6, 2019 · 1 min · jiezi

JS设计模式之Singleton(单例)模式

定义限制类的实例化次数只能是一次。如果该实例不存在的情况下,可以通过一个方法创建一个类来实现创建类的新实例,如果实例已经存在,它会简单返回该对象的引用。适用场景需要频繁实例化然后销毁的对象。频繁访问数据库或文件的对象。创建对象时耗时过多或者耗资源过多,但又经常用到的对象。Talk is cheap通过打印privateRandomNumber来验证是否为同一个实例let mySingleton = (()=> { let instance; let init = ()=> { let privateMethod = ()=> {//私有方法 console.log(‘I am privateMethod’); } let privateVariable = ‘I am also private’; let privateRandomNumber = Math.random(); return {//共有方法和变量 publicMethod: ()=> { console.log(‘I am public’); }, publicProperty: ‘I am also public’, getRandomNumber: ()=> { return privateRandomNumber; } } } return {//获取Singleton的实例,如果存在就返回,不存在就创建新实例 getInstance: ()=> { if(!instance) { instance = init(); } return instance; } }})();let singleA = mySingleton.getInstance();let singleB = mySingleton.getInstance();console.log(singleA.getRandomNumber() === singleB.getRandomNumber());//ture简单封装node连接mongodb数据库const MongoDB = require(‘mongodb’)const MongoClient = require(‘mongodb’).MongoClientconst ObjectID = MongoDB.ObjectIDconst Config = require(’./config’)class Db { static getInstance() { if(!Db.instance) { Db.instance = new Db() } return Db.instance } constructor() { this.dbClient = ’’ this.connect() } connect() {//连接数据库 let that = this return new Promise((resolve, reject)=> { if(!that.dbClient) { MongoClient.connect(Config.dbUrl, {useNewUrlParser:true}, (err, client)=> { if(err) { reject(err) }else { that.dbClient = client.db(Config.dbName) resolve(that.dbClient) } }) }else { resolve(that.dbClient) } }) } find(collectionName, json) { return new Promise((resolve, reject)=> { this.connect().then((db)=> { let result = db.collection(collectionName).find(json) result.toArray((err, docs)=> { if(err) { reject(err) return } resolve(docs) }) }) }) } update(collectionName, json1, json2) { return new Promise((resolve, reject)=> { this.connect().then((db)=> { db.collection(collectionName).updateOne(json1, { $set: json2 },(err, result)=> { if(err) { reject(err) }else { resolve(result) } }) }) }) } insert(collectionName, json) { return new Promise((resolve, reject)=> { this.connect().then((db)=> { db.collection(collectionName).insertOne(json, (err, result)=> { if(err) { reject(err) }else { resolve(result) } }) }) }) } remove(collectionName, json) { return new Promise((resolve, reject)=> { this.connect().then((db)=> { db.collection(collectionName).removeOne(json, (err, result)=> { if(err) { reject(err) }else { resolve(result) } }) }) }) } getObjectId(id) { return new ObjectID(id) }}module.exports = Db.getInstance()参考《Javascript设计模式》 ...

January 5, 2019 · 2 min · jiezi

设计模式-单例模式及案例

单例模式单例模式:用来创建独一无二的,只能有一个实例对象,确保一个类只有一个实例,并提供一个全局访问点。通俗说单例模式只将对象 new 一次,与工厂模式区别,工厂模式像是与单例模式相同,其实差距很大,工厂模式并不是实现将对象只 new 一次,它是主要避免对象的紧耦合,实现最终的解耦,或者是说单例模式是类的设计者要考虑的问题,工厂模式是类的使用者需要考虑的问题,不要混淆两个设计模式者单例模式实现public class Singleton{ // 记录唯一实例对象 private static Singleton uniqueInstance; private Singleton(){} // 线程非安全 返回 uniqueInstance 对象 public static Singleton getInstance(){ if(uniqueInstance == null){ uniqueInstance = new Singleton(); } return uniqueInstance; }}编写测试类class Test{ public static void main(String[] args){ Singleton singleton = new Singleton(); singleton.getInstance(); System.out.println(singleton.getInstance()); System.out.println(singleton.getInstance()); System.out.println(singleton.getInstance()); }}显示打印结果Thread.Singleton@610455d6Thread.Singleton@610455d6Thread.Singleton@610455d6可以看出只创建一次对象每次调用返回同一个对象此方法在单线程环境下可行,没有任何问题。但是如果两个线程,访问这段代码就会出现问题,当开启线程 A 和线程 B 。线程 A 第一次执行判断 uniqueInstace 为空,进入实例化一个对象,这时候线程 B 在判断 uniqueInstance 同样为空,同样实例化一个对象,这时候就不符合单例模式的定义,会出现两个或多个实例化对象,在 Java 中解决此类问题方式是添加关键字 synchroized 加锁多线程单例模式我们先测试不加锁情况下会发生什么,单例模式类不改变,改动测试类的代码加入线程class Test extends Thread{ public static void main(String[] args){ Thread thread_1 = new Test(); Thread thread_2 = new Test(); thread_1.start(); thread_2.start(); } @Override public void run() { System.out.println(Thread.currentThread().getName() + “=” + Singleton.getInstance()); }}显示打印结果Thread-1=Thread.Singleton@470bc213Thread-0=Thread.Singleton@5eca2b42明显发现,两个线程会去实例化俩个不同的对象(有时会返回相同对象,这是电脑本身计算问题,不妨多运行几次,会发现不同),如果这样的话我们的单例模式就没有存在的意义,不符合单例模式的定义:用来创建独一无二的,只能有一个实例对象,确保一个类只有一个实例,并提供一个全局访问点。需要做出更改,给 getInstance() 方法加锁看会不会有所改变,能否解决此类问题更改单例模式类public class Singleton { private static Singleton uniqueInstance; Singleton() { } // 加入关键字 synchronized 上锁 public synchronized static Singleton getInstance() { if (uniqueInstance == null) { uniqueInstance = new Singleton(); } return uniqueInstance; }}显示打印结果Thread-0=Thread.Singleton@79d62897Thread-1=Thread.Singleton@79d62897经过多组测试,发现每次都只是返回同一个对象,感兴趣不妨可以自己测试,多添加几个线程。可以解决在多线程情况下的单例模式问题。但是仔细想这样是会很好解决问题吗?其实并不是,学习线程我们知道,加锁代价高,如果是单纯的读操作是不需要加锁的,涉及写操作才需要加锁,使用 synchronized 需要仔细考虑,优化锁。仔细看一下代码,这段代码我们将 synchronized 关键字放在了方法上。这样的话不论哪个一个线程访问都会进行上锁解锁,其实这一步只有第一次访问时需要,其他时候访问已经有了实例化对象,就不需要再加锁,这时候的锁就造成浪费,优势变成了累赘。更改多线程解决方式,为其添加双重锁public class Singleton { private volatile static Singleton uniqueInstance; Singleton() { } public static Singleton getInstance() { if (uniqueInstance == null) { synchronized (Singleton.class) { if (uniqueInstance == null) { uniqueInstance = new Singleton(); } } } return uniqueInstance; }}分析双重锁之前,会发现增加了新的关键字 volatile ,这方面知识需要我们恶补 JVM 的知识,我有关于该关键字介绍,附链接方式更改 getInstance() 方法,判断 uniqueInstance 是否为空,改为锁前判断一次,锁后判断一次。不要以为锁前锁后都进行判断空是多余的操作,每一步都有存在的必要性。首先锁前检查,如果需要的对象为空,进行加锁,去实例化一个对象,相反如果不为空,检查到已经存在一个需要的对象,就不要再进行加锁了,直接返回需要的对象。省掉了代价过高的加锁操作。锁前检查为了判断读写操作。锁后检查,很多小伙伴会这想之前已经为空了,再加锁判断为空是否多余。并不是你们想的那样,这两次的判断存在的性质不同,第一次判断读写,第二次是检查线程问题。可以编写一个测试类查看运行结果是不是自己想要的。不使用第二个判断的运行结果Thread-1=Thread.Singleton@352ebfc0Thread-2=Thread.Singleton@352ebfc0Thread-0=Thread.Singleton@3c03e5f5Thread-3=Thread.Singleton@352ebfc0开启四个线程,发现其中一个线程实例化了一个新对象,这时不符合单例模式的定义。当我们调整代码,加上第二次判断执行多次后,每次执行结果,不论哪个线程都是使用的同一个对象,这时才是真正的实现多线程的单例模式总结单例模式的实现相比之前学习的工厂模式较为简单,在学习之前我们先要对线程知识有一定了解,从而解决多线程中的单例问题。在单线程环境下我们不用考虑是否会创建多个对象,只有一个住线程,第一次执行方法为空,实例化一个对象,在整个代码运行期间,多次执行实例化方法时,第一次执行已经实例化一个对象,所以在之后调用该方法直接返回一个对象,不需要实例化一个新对象,从而实现单例模式。在多线程环境下,我们需要加锁来实现多线程环境下的单例模式,使用双重检查加锁来实现,避免直接在方法上加锁,造成资源浪费。第一次判断读写操作,第二次判断解决线程问题。需要注意的是在 Java 语言中双重检查加锁不适用于 1.4 及更早版本,原因是关键字 volatile 发挥的作用,我有学习关键字 volatile 相关的记录,会附链接方式 ...

January 5, 2019 · 1 min · jiezi

接口隔离原则

个人博客原文:接口隔离原则设计模式六大原则之四:接口隔离原则。简介姓名 :接口隔离原则英文名 :Interface Segregation Principle价值观 :宁缺毋滥个人介绍 :Clients should not be forced to depend upon interfaces that they don’t use.(客户端不应该依赖它不需要的接口。)The dependency of one class to another one should depend on the smallest possible interface.(类间的依赖关系应该建立在最小的接口上。)也用一个故事来讲这 2 句干巴巴的定义。一小伙子跑到大城市的工厂打工,工作了一年半载,越来越觉得没劲,每天干那么多活,又领那么一点工资,和他老爸抱怨这段时间的困扰,老爸想着,家里有个小作坊,自己也一年不如一年了,要不就让儿子回老家管理这小作坊。小伙子熬不过这个年,就跑回老家跟着老爸打理小作坊。(来自Google Image)小作坊主要是做布娃娃的,如上图,工作在于打扮包装布娃娃,工序有给布娃娃扎辫子、穿衣服、包装入箱、打标签。整个完整的流程都是一个人做的。有很多个工人每天都在做这个事情。老爸向小伙子诉苦,感觉招工挺多人的,生产力还是提不上去。小伙子记着老爸的话,在工厂里面观察了几天,他发现每个工人都要做这 4 个打扮包装布娃娃的工序,有些工人扎辫子很快但穿衣服很慢,有些工人扎辫子很慢但穿衣服快,他用了笔记本记下来:李大姨扎辫子快,王大妈穿衣服快,就这样把每个人有效率的工作都记录下来。一天晚上吃饭,小伙子跟老爸说了自己观察到的现象,也把本子拿给老爸看,跟老爸商量:可不可以做个尝试,不要每个人负责打扮包装布娃娃全步骤,而是按工序分开,每个人只负责一个工序,每个工人只干一件事,更容易熟能生巧。老爸听着觉得有道理。第二天早上,就到小作坊里,召集了所有工人,按小伙子的笔记上面的名单分工,大家都做好各自负责的内容,像流水线一样,做好了就放到下个工序的地方,让下个工序的人去做。到了下班,小伙子清点了今天工作的成果,包装完成的娃娃比前一天多了 50% 。晚上小伙子跟老爸喝着百威吃起大肉庆祝一番。这个故事你看了可能想骂爹骂娘,跟上面的定义有啥毛关系?故事只是把大家带入这个场景,我们在工作中,着手开发之前不都得先理清好需求背景,这就是要讲接口隔离原则的背景,通过代码来给大家讲解一下如何用好接口隔离原则。父亲的运营模式先看代码interface Work { void hairBraiding(); void getDressed(); void packingIntoTheBox(); void makeTag();}class WangMather implements Work{ @Override public void hairBraiding() { System.out.println(“王大妈给布娃娃扎辫子”); } @Override public void getDressed() { System.out.println(“王大妈给布娃娃穿衣服”); } @Override public void packingIntoTheBox() { System.out.println(“王大妈把布娃娃装入箱子”); } @Override public void makeTag() { System.out.println(“王大妈给箱子打标签”); }}class LiAunt implements Work { @Override public void hairBraiding() { System.out.println(“李大姨给布娃娃扎辫子”); } @Override public void getDressed() { System.out.println(“李大姨给布娃娃穿衣服”); } @Override public void packingIntoTheBox() { System.out.println(“李大姨把布娃娃装入箱子”); } @Override public void makeTag() { System.out.println(“李大姨给箱子打标签”); }}// 测试代码WangMather wangMather = new WangMather();wangMather.hairBraiding();wangMather.getDressed();wangMather.packingIntoTheBox();wangMather.makeTag();LiAunt liAunt = new LiAunt();liAunt.hairBraiding();liAunt.getDressed();liAunt.packingIntoTheBox();liAunt.makeTag();在父亲管理下的小作坊,是大家各自完成好一个布娃娃,工作互不交接,在这种运营模式下,我们把所有工作都合并在一个接口 Work 是没有问题的。有人可能要问,不是说接口隔离么?这里面 Work 接口的 4 个方法都可以分离开,它们都是各自的工作内容。稍等一下,我们现在是基于老父亲运营的模式下实现,如果小作坊一直都是这种模式运营,这段代码有问题么?其实没问题的,我们根据当时的业务考虑,在这种情况下,把 Work 抽成 4 个接口不是不可以,只是不现实,每个工人都去实现一模一样的 4 个接口在老父亲运营模式下是不切实际。儿子的运营模式接下来介绍儿子的运营模式。儿子提倡的是每个工人职责分明,只负责一个事情,在这种情况下,如果还是用老父亲的 Work 接口会有什么问题呢?上面我们说了,李大姨扎辫子快,王大妈穿衣服快,所以李大姨被分配去给布娃娃扎辫子,王大妈被分配去给布娃娃穿衣服。我们沿用老父亲的 Work 接口实现,代码如下class WangMather2 implements Work{ @Override public void hairBraiding() { } @Override public void getDressed() { System.out.println(“王大妈给布娃娃穿衣服”); } @Override public void packingIntoTheBox() { } @Override public void makeTag() { }}class LiAunt2 implements Work { @Override public void hairBraiding() { System.out.println(“李大姨给布娃娃扎辫子”); } @Override public void getDressed() { } @Override public void packingIntoTheBox() { } @Override public void makeTag() { }}看出问题来了么?李大姨仅仅参与扎辫子工作,王大妈参与了穿衣服工作,但是却都要依旧实现其他 3 个多余的接口。所以在儿子的运营模式下,老父亲的 Work 接口需要重新分配,以工序的角度分配,而不是以完成一个布娃娃的角度分配。总共有 4 个工序:扎辫子、穿衣服、包装入箱、打标签,我们需要定义 4 个接口,让员工去实现各自负责的工序接口。代码如下interface Hair { void hairBraiding();}interface Dress { void getDressed();}interface Box { void packingIntoTheBox();}interface Tag { void makeTag();}/** * 李大姨给布娃娃扎辫子快 /class LiAunt3 implements Hair { @Override public void hairBraiding() { System.out.println(“李大姨给布娃娃扎辫子”); }}/* * 王大妈给布娃娃穿衣服快 /class WangMather3 implements Dress{ @Override public void getDressed() { System.out.println(“王大妈给布娃娃穿衣服”); }}/* * 陈大叔包装快 /class ChenUncle implements Box { @Override public void packingIntoTheBox() { System.out.println(“陈大叔给布娃娃装箱”); }}/* * 黄大姐贴标签快 */class HuangSister implements Tag { @Override public void makeTag() { System.out.println(“黄大姐给箱子打标签”); }}// 测试代码LiAunt3 liAunt3 = new LiAunt3();WangMather3 wangMather3 = new WangMather3();ChenUncle chenUncle = new ChenUncle();HuangSister huangSister = new HuangSister();liAunt3.hairBraiding();wangMather3.getDressed();chenUncle.packingIntoTheBox();huangSister.makeTag();这段代码看起来就很清晰了,在儿子的运营模式下,大家都是只做一道工序,这样子实现就非常合理。看了这个过程,你理解了接口隔离原则了么?再看一看上面的定义:客户端不应该依赖它不需要的接口。闭上眼睛,静默 3 秒,感受一下。我们也可以回忆一下在工作中编写的代码,是不是有遵守接口隔离原则?在特定的场景下,如果很多类实现了同一个接口,并且都只实现了接口的极少部分方法,这时候很有可能就是接口隔离性不好,就要去分析能不能把方法拆分到不同的接口。总结接口隔离原则最最最重要一点就是要根据实际情况,具体业务具体分析,不能犯了上面说到的错误:在老父亲的运营模式下,按儿子的工序划分接口去实现,那样子会得不偿失。参考资料:《大话设计模式》、《Java设计模式》、《设计模式之禅》、《研磨设计模式》、《Head First 设计模式》希望文章对您有所帮助,设计模式系列会持续更新,感兴趣的同学可以关注公众号,第一时间获取文章推送阅读,也可以一起交流,交个朋友。公众号之设计模式系列文章 ...

January 4, 2019 · 2 min · jiezi

一分钟学会《模板方法模式》

前言只有光头才能变强。文本已收录至我的GitHub仓库,欢迎Star:https://github.com/ZhongFuCheng3y/3y在上一篇有读者说,一分钟就看完门面模式了,所以今天的标题就取《一分钟学会模板方法模式》回顾前面所写过的设计模式:给女朋友讲解什么是代理模式包装模式就是这么简单啦单例模式你会几种写法?工厂模式理解了没有?策略模式原来就这么简单!三分钟学会门面模式!无论是面试还是个人的提升,设计模式是必学的。今天来讲解模板方法模式~一、模板方法模式1.1模板方法模式现实例子大家都知道,我每次写原创技术文章,开头总会有“只有光头才能变强”。我当然不可能每次写文章的时候都去复制这句话(因为这样太麻烦了)。我有自己的写作模板,给大家看一下:前言和最后都是固定下来的,至于第一点和第二点就得看是写什么文章,写不同的文章对应的内容也是不一样的。每次我写文章的时候,只要在这个模板上添加我自己想写的东西就好了,就不用每次都复制一遍相同的内容,这样就大大减少我的工作量啦。1.2回到代码世界代码来源于生活,同样地我可以将我写文章的过程用代码来描述,大家来看一下。3y每篇文章都会有“前言”和“最后”的内容,3y把这两个模块写出来了。// 3y的文章模板public class Java3yWriteArticle { // 前言 public void introduction() { System.out.println(“只有光头才能变强”); } // 最后 public void theLast() { System.out.println(“关注我的公众号:Java3y”); }}3y写文章的时候,3y可能就会这样使用: // 3y写文章 public static void main(String[] args) { Java3yWriteArticle writeArticle = new Java3yWriteArticle(); // 前言 writeArticle.introduction(); // 实际内容 System.out.println(“大家好,我是3y,今天来给大家分享我写的模板方法模式”); // 最后 writeArticle.theLast(); }这样是可以完成3y写文章的功能,但是这样做好吗?这时候3y女朋友也想写文章,她的文章同样也想有“前言”和“最后”两个模块,所以3y女朋友的文章模板是这样的:// 3y女朋友的文章模板public class Java3yGFWriteArticle { // 前言 public void introduction() { System.out.println(“balabalabalalabalablablalab”); } // 最后 public void theLast() { System.out.println(“balabalabalalabalablablalab”); }}那3y女朋友写文章的时候,可能也会这样使用: // 3y女朋友写文章 public static void main(String[] args) { Java3yGFWriteArticle java3yGFWriteArticle = new Java3yGFWriteArticle(); // 前言 java3yGFWriteArticle.introduction(); // 实际内容 System.out.println(“3y是傻子,不用管他”); // 最后 java3yGFWriteArticle.theLast(); }可以发现3y和3y女朋友要写文章的时是要重复调用introduction();和theLast();。并且,3y的文章模板和3y女朋友的文章模板中的“前言”和“最后”只是实现内容的不同,却定义了两次,明显就是重复的代码。面对重复的代码我们会做什么?很简单,抽取出来!于是我们就可以抽取出一个通用的WriteArticle(为了方便调用,我们还将写文章的步骤封装成一个方法):// 通用模板public abstract class WriteArticle { // 每个人的“前言”都不一样,所以抽象(abstract) protected abstract void introduction(); // 每个人的“最后”都不一样,所以抽象(abstract) protected abstract void theLast(); // 实际要写的内容,每个人的“实际内容”都不一样,所以抽象(abstract) protected abstract void actualContent(); // 写一篇完整的文章(为了方便调用,我们将这几个步骤分装成一个方法) public final void writeAnCompleteArticle() { // 前言 introduction(); // 实际内容 actualContent(); // 最后 theLast(); }}所以,3y的模板就可以继承通用模板,在通用模板上实现自己想要的就好了:// 3y的文章模板public class Java3yWriteArticle extends WriteArticle { // 前言 @Override public void introduction() { System.out.println(“只有光头才能变强”); } // 最后 @Override public void theLast() { System.out.println(“关注我的公众号:Java3y”); } @Override protected void actualContent() { System.out.println(“大家好,我是3y,今天来给大家分享我写的模板方法模式”); }}同样地,3y女朋友的文章模板也是类似的:// 3y女朋友的文章模板public class Java3yGFWriteArticle extends WriteArticle { // 前言 @Override public void introduction() { System.out.println(“balabalabalalabalablablalab”); } // 最后 @Override public void theLast() { System.out.println(“balabalabalalabalablablalab”); } @Override protected void actualContent() { System.out.println(“3y是傻子,不用管他”); }}想要真正写文章的时候就十分方便了: // 3y写文章 public static void main(String[] args) { WriteArticle java3ywriteArticle = new Java3yWriteArticle(); java3ywriteArticle.writeAnCompleteArticle(); } // 3y女朋友写文章 public static void main(String[] args) { WriteArticle java3yGFWriteArticle = new Java3yGFWriteArticle(); java3yGFWriteArticle.writeAnCompleteArticle(); }要点:把公共的代码抽取出来,如果该功能是不确定的,那我们将其修饰成抽象方法。将几个固定步骤的功能封装到一个方法中,对外暴露这个方法,就可以非常方便调用了。嗯,上面的就是模板方法模式,就这么简单!1.3模板方法模式介绍《设计模式之禅》:定义一个操作中的算法框架,而将一些步骤延迟到子类中。使子类可以不改变一个算法的结构即可重定义该算法的某些步骤。根据我们上面的例子,来讲讲这段话的含义:定义一个操作中的算法框架,而将一些步骤延迟到子类中。WriteArticle中有一个writeAnCompleteArticle()方法,该方法定义了发文章的所有步骤,但是这些步骤大多是抽象的,得由子类来实现。使子类可以不改变一个算法的结构即可重定义该算法的某些步骤外界是通过调用writeAnCompleteArticle()方法来写文章的,子类如果改变具体的实现就会间接改变了算法的细节。比如3y在文章模板中的introduction()改了,“只有充钱才能变强” @Override public void introduction() { System.out.println(“只有充钱才能变强”); }我们没有碰过writeAnCompleteArticle()的代码,但再次调用这个方法的时候,具体的实现就会发生改变(因为writeAnCompleteArticle受子类的具体实现影响)下面我们看一下模板方法模式的通用类图:在模板方法模式中,也有几个术语,根据我们的例子中的注释,我给大家介绍一下:// 抽象模板类public abstract class WriteArticle { // 基本方法 protected abstract void introduction(); // 基本方法 protected abstract void theLast(); // 基本方法 protected abstract void actualContent(); // 模板方法 public final void writeAnCompleteArticle() { introduction(); actualContent(); theLast(); }}// 具体模板类public class Java3yWriteArticle extends WriteArticle { // 实现基本方法 @Override public void introduction() { System.out.println(“只有充钱才能变强”); } // 实现基本方法 @Override public void theLast() { System.out.println(“关注我的公众号:Java3y”); } // 实现基本方法 @Override protected void actualContent() { System.out.println(“大家好,我是3y,今天来给大家分享我写的模板方法模式”); }}基本方法:在子类实现,并且在模板方法中被调用模板方法:定义了一个框架,实现对基本方法的调用,完成固定的逻辑。1.4模板方法的优缺点优点:封装不变的部分,扩展可变的部分。把认为是不变的部分的算法封装到父类,可变部分的交由子类来实现!提取公共部分的代码,行为由父类控制,子类实现!缺点:抽象类定义了部分抽象方法,这些抽象的方法由子类来实现,子类执行的结果影响了父类的结果(子类对父类产生了影响),会带来阅读代码的难度!1.5模板方法模式JDK应用最经典的就是JUC包下的AQS(AbstractQueuedSynchronizer)了。AQS是什么?AQS其实就是一个可以给我们实现锁的框架。内部实现的关键是:先进先出的队列、state状态我们可以看一下AQS定义的acquire() public final void acquire(int arg) { if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); } acquire()相当于模板方法,tryAcquire(arg)相当于基本方法。最后模板方法模式也很简单呀,一个抽象类有基本方法(等着被子类实现的方法),有模板方法(对外暴露、调用基本方法、定义了算法的框架),那就完事了。推荐阅读和参考资料:《设计模式之禅》https://blog.csdn.net/carson_ho/article/details/54910518https://blog.csdn.net/hguisu/article/details/7564039乐于分享和输出干货的Java技术公众号:Java3y。关注即可领取海量的视频资源!精彩回顾:2018总结【14本书免费送】觉得我的文章写得不错,不妨点一下赞! ...

January 4, 2019 · 2 min · jiezi

js设计模式--迭代器模式

前言本系列文章主要根据《JavaScript设计模式与开发实践》整理而来,其中会加入了一些自己的思考。希望对大家有所帮助。文章系列js设计模式–单例模式js设计模式–策略模式js设计模式–代理模式概念迭代器模式是指提供一种方法顺序访问一个聚合对象中的各个元素,而又不需要暴露该对象的内部表示。迭代器模式可以把迭代的过程从业务逻辑中分离出来,在使用迭代器模式之后,即使不关心对象的内部构造,也可以按顺序访问其中的每个元素。UML类图场景JavaScript已经内置迭代器,如forEach Iterator等,再如jquery的$.each分类内部迭代器定义内部已经定义好了迭代规则,它完全接手整个迭代过程,外部只需要一次初始调用实现var each = function (ary, callback) { for (var i = 0; i < ary.length; i++) { callback(i, ary[i]) }}each([1, 2, 3, 4, 5], function (i, item) { console.log(i, item)})优缺点优点:内部迭代器在调用的时候非常方便,外界不用关心迭代器内部的实现,跟迭代器的交互也仅 仅是一次初始调用缺点:由于内部迭代器的迭代规则已经被提前规 定,上面的 each 函数就无法同时迭代2个数组,如下代码var compare = function( ary1, ary2 ){ if ( ary1.length !== ary2.length ){ throw new Error ( ‘ary1 和ary2 不相等’ ); } each( ary1, function( i, n ){ if ( n !== ary2[ i ] ){ throw new Error ( ‘ary1 和ary2 不相等’ ); } }); alert ( ‘ary1 和ary2 相等’ );};compare( [ 1, 2, 3 ], [ 1, 2, 4 ] ); // throw new Error ( ‘ary1 和ary2 不相等’ );外部迭代器定义外部迭代器必须显式地请求迭代下一个元素实现我们模拟一个es6迭代器var Iterator = function (ary) { this.ary = ary this.index = 0}Iterator.prototype.isDone = function () { return this.index >= this.ary.length}Iterator.prototype.next = function () { if (!this.isDone()) { var res = this.ary[this.index] this.index++ return { value: res, done: this.isDone() } }}var a = new Iterator([1, 2, 3])while (!a.isDone()) { console.log(a.next())}下面解决一下上面那个问题var a = new Iterator([1, 2, 3, 3])var b = new Iterator([1, 2, 3])function compare(iterator1, iterator2) { while (!iterator1.isDone() || !iterator2.isDone()) { if (iterator1.next().value !== iterator2.next().value) { return false } } return true}compare(a, b)例子文件上传实现文件上传对象var getUploadObj = function () { try { return new ActiveXObject(“TXFTNActiveX.FTNUpload”); } catch (e) { // IE 上传控件 if (supportFlash()) { // supportFlash 函数未提供 var str = ‘<object type=“application/x-shockwave-flash”></object>’; return $(str).appendTo($(‘body’)); } else { var str = ‘<input name=“file” type=“file”/>’; // 表单上传 return $(str).appendTo($(‘body’)); } }};缺点:第一是很难阅读,第二是严重违反开闭原则改进var getActiveUploadObj = function () { try { return new ActiveXObject(“TXFTNActiveX.FTNUpload”); } catch (e) { return false; }};var getFlashUploadObj = function () { if (supportFlash()) { // supportFlash 函数未提供 var str = ‘<object type=“application/x-shockwave-flash”></object>’; return $(str).appendTo($(‘body’)); }; return false;}var getFormUpladObj = function () { var str = ‘<input name=“file” type = “file” class = “ui-file” / > ‘; // 表单上传 return $(str).appendTo($(‘body’));}var iteratorUploadObj = function () { for (var i = 0, fn; fn = arguments[i++];) { var uploadObj = fn(); if (uploadObj !== false) { return uploadObj; } };}var uploadObj = iteratorUploadObj(getActiveUploadObj, getFlashUploadObj, getFormUpladObj);es6基于类实现class Iterator { constructor(conatiner) { this.list = conatiner.list this.index = 0 } next() { if (this.hasNext()) { return this.list[this.index++] } return null } hasNext() { if (this.index >= this.list.length) { return false } return true }}class Container { constructor(list) { this.list = list } getIterator() { return new Iterator(this) }}// 测试代码let container = new Container([1, 2, 3, 4, 5])let iterator = container.getIterator()while(iterator.hasNext()) { console.log(iterator.next())}es6中的Iterator我们都知道Array、Map、Set、类对象(如arguments NodeList等)都有一个Symbol.iterator迭代方法,可以通过以下方式取得var a = [1,2,3]console.log(a[Symbol.iterator])另外generator也会返回迭代器function* gen() { yield 1 yield ‘1’}var a = gen()a.next() ...

January 4, 2019 · 2 min · jiezi

js设计模式--代理模式

前言本系列文章主要根据《JavaScript设计模式与开发实践》整理而来,其中会加入了一些自己的思考。希望对大家有所帮助。概念代理模式是为一个对象提供一个代用品或占位符,以便控制对它的访问。UML类图场景比如,明星都有经纪人作为代理。如果想请明星来办一场商业演出,只能联系他的经纪人。经纪人会把商业演出的细节和报酬都谈好之后,再把合同交给明星签。分类保护代理于控制不同权限的对象对目标对象的访问,如上面明星经纪人的例子虚拟代理把一些开销很大的对象,延迟到真正需要它的时候才去创建。如短时间内发起很多个http请求,我们可以用虚拟代理实现一定时间内的请求统一发送优缺点优点1. 可以保护对象2. 优化性能,减少开销很大的对象3. 缓存结果例子图片预加载加载一张图片 var myImage = (function () { var imgNode = document.createElement(‘img’); document.body.appendChild(imgNode); return { setSrc: function (src) { imgNode.src = src; } } })(); myImage.setSrc(‘https://segmentfault.com/img/bVbmvnB?w=573&h=158');想象一下,如果我们的图片很大,用户就会看到页面很长一段时间是空白我们可以想到的改进是图片加载完成之前都展示loading图片加个loading图片var myImage = (function () { var imgNode = document.createElement(‘img’); document.body.appendChild(imgNode); var img = new Image() img.onload = () => { // 模拟图片加载 setTimeout(() => { imgNode.src = img.src }, 1000) } return { setSrc: function (src) { img.src = src imgNode.src = ‘https://content.igola.com/static/WEB/images/other/loading-searching.gif'; } }})();myImage.setSrc(‘https://segmentfault.com/img/bVbmvnB?w=573&h=158');这段代码违背了单一职责原则,这个对象同时承担了加载图片和预加载图片两个职责同时也违背了开放封闭原则,如果我们以后不需要预加载图片了,那我们不得不修改整个对象用虚拟代理改进var myImage = (function () { var imgNode = document.createElement(‘img’); document.body.appendChild(imgNode); return { setSrc: function (src) { imgNode.src = src } }})();var proxyImage = (function() { var img = new Image() img.onload = function() { myImage.setSrc(img.src) } return { setSrc: function (src) { img.src = src myImage.setSrc(‘https://content.igola.com/static/WEB/images/other/loading-searching.gif') } }})()proxyImage.setSrc(‘https://segmentfault.com/img/bVbmvnB?w=573&h=158');注意:我们的代理和本体接口要保持一致性,如上面proxyImage和myImage都返回一个包含setSrc方法的对象。居于这点我们写代理的时候也有迹可循。虚拟代理合并HTTP请求简单的实现<body> <div id=“wrapper”> <input type=“checkbox” id=“1”></input>1 <input type=“checkbox” id=“2”></input>2 <input type=“checkbox” id=“3”></input>3 <input type=“checkbox” id=“4”></input>4 <input type=“checkbox” id=“5”></input>5 <input type=“checkbox” id=“6”></input>6 <input type=“checkbox” id=“7”></input>7 <input type=“checkbox” id=“8”></input>8 <input type=“checkbox” id=“9”></input>9 </div></body><script type=“text/javascript”> // 模拟http请求 var synchronousFile = function (id) { console.log(‘开始同步文件,id 为: ’ + id); }; var inputs = document.getElementsByTagName(‘input’) var wrapper = document.getElementById(‘wrapper’) wrapper.onclick = function (e) { if (e.target.tagName === ‘INPUT’) { synchronousFile(e.target.id) } }</script>缺点很明显:每点一次就发送一次http请求改进<body> <div id=“wrapper”> <input type=“checkbox” id=“1”></input>1 <input type=“checkbox” id=“2”></input>2 <input type=“checkbox” id=“3”></input>3 <input type=“checkbox” id=“4”></input>4 <input type=“checkbox” id=“5”></input>5 <input type=“checkbox” id=“6”></input>6 <input type=“checkbox” id=“7”></input>7 <input type=“checkbox” id=“8”></input>8 <input type=“checkbox” id=“9”></input>9 </div></body><script type=“text/javascript”> // 模拟http请求 var synchronousFile = function (id) { console.log(‘开始同步文件,id 为: ’ + id); }; var inputs = document.getElementsByTagName(‘input’) var wrapper = document.getElementById(‘wrapper’) wrapper.onclick = function (e) { if (e.target.tagName === ‘INPUT’ && e.target.checked) { proxySynchronousFile(e.target.id) } } var proxySynchronousFile = (function () { var cacheIds = [], timeId = 0 return function (id) { if (cacheIds.indexOf(id) < 0) { cacheIds.push(id) } clearTimeout(timeId) timeId = setTimeout(() => { synchronousFile(cacheIds.join(’,’)) cacheIds = [] }, 1000) } })()</script>缓存代理-计算乘积粗糙的实现var mult = function () { console.log(‘开始计算乘积’); var a = 1; for (var i = 0, l = arguments.length; i < l; i++) { a = a * arguments[i]; } return a; }; mult(2, 3); // 输出:6 mult(2, 3, 4); // 输出:24改进var mult = function () { console.log(‘开始计算乘积’); var a = 1; for (var i = 0, l = arguments.length; i < l; i++) { a = a * arguments[i]; } return a;};// mult(2, 3); // 输出:6// mult(2, 3, 4); // 输出:24var proxyMult = (function() { var cache = {} return function () { let id = Array.prototype.join.call(arguments, ‘,’) if (cache[id]) { return cache[id] } else { return cache[id] = mult.apply(this, arguments) } }})()proxyMult(2, 3); // 输出:6proxyMult(2, 3); // 输出:6我们现在希望加法也能够缓存再改进var mult = function () { console.log(‘开始计算乘积’); var a = 1; for (var i = 0, l = arguments.length; i < l; i++) { a = a * arguments[i]; } return a;};var plus = function () { console.log(‘开始计算和’); var a = 0; for (var i = 0, l = arguments.length; i < l; i++) { a = a + arguments[i]; } return a;};// mult(2, 3); // 输出:6// mult(2, 3, 4); // 输出:24var createProxyFactory = function (fn) { var cache = {} return function () { let id = Array.prototype.join.call(arguments, ‘,’) if (cache[id]) { return cache[id] } else { return cache[id] = fn.apply(this, arguments) } }}var proxyMult = createProxyFactory(mult), proxyPlus = createProxyFactory(plus);proxyMult(1, 2, 3, 4) // 输出:24proxyMult(1, 2, 3, 4) // 输出:24proxyPlus(1, 2, 3, 4) // 输出:10proxyPlus(1, 2, 3, 4) // 输出:10es6的代理模式基于类实现class Car { drive() { return “driving”; };}class CarProxy { constructor(driver) { this.driver = driver; } drive() { return ( this.driver.age < 18) ? “too young to drive” : new Car().drive(); };}class Driver { constructor(age) { this.age = age; }}基于Proxy实现// 明星let star = { name: ‘张XX’, age: 25, phone: ‘13910733521’}// 经纪人let agent = new Proxy(star, { get: function (target, key) { if (key === ‘phone’) { // 返回经纪人自己的手机号 return ‘18611112222’ } if (key === ‘price’) { // 明星不报价,经纪人报价 return 120000 } return target[key] }, set: function (target, key, val) { if (key === ‘customPrice’) { if (val < 100000) { // 最低 10w throw new Error(‘价格太低’) } else { target[key] = val return true } } }})// 主办方console.log(agent.name)console.log(agent.age)console.log(agent.phone)console.log(agent.price)// 想自己提供报价(砍价,或者高价争抢)agent.customPrice = 150000// agent.customPrice = 90000 // 报错:价格太低console.log(‘customPrice’, agent.customPrice) ...

January 3, 2019 · 4 min · jiezi

js设计模式--策略模式

概念策略模式的定义是:定义一系列的算法,把它们一个个封装起来,并且使它们可以相互替换。策略模式指的是定义一系列的算法,把它们一个个封装起来。将不变的部分和变化的部分隔开是每个设计模式的主题,策略模式也不例外,策略模式的目的就是将算法的使用与算法的实现分离开来。一个基于策略模式的程序至少由两部分组成。第一个部分是一组策略类,策略类封装了具体 的算法,并负责具体的计算过程。 第二个部分是环境类Context,Context 接受客户的请求,随后 把请求委托给某一个策略类。要做到这点,说明 Context中要维持对某个策略对象的引用。策略模式的实现并不复杂,关键是如何从策略模式的实现背后,找到封装变化、委托和多态性这些思想的价值。场景从定义上看,策略模式就是用来封装算法的。但如果把策略模式仅仅用来封装算法,未免有一点大材小用。在实际开发中,我们通常会把算法的含义扩散开来,使策略模式也可以用来封装 一系列的“业务规则”。只要这些业务规则指向的目标一致,并且可以被替换使用,我们就可以 用策略模式来封装它们。优缺点优点策略模式利用组合、委托和多态等技术和思想,可以有效地避免多重条件选择语句。策略模式提供了对开放—封闭原则的完美支持,将算法封装在独立的strategy中,使得它们易于切换,易于理解,易于扩展。策略模式中的算法也可以复用在系统的其他地方,从而避免许多重复的复制粘贴工作。在策略模式中利用组合和委托来让 Context 拥有执行算法的能力,这也是继承的一种更轻便的替代方案。缺点增加许多策略类或者策略对象,但实际上这比把它们负责的 逻辑堆砌在 Context 中要好。要使用策略模式,必须了解所有的 strategy,必须了解各个 strategy 之间的不同点, 这样才能选择一个合适的 strategy。但这些缺点并不严重例子计算奖金粗糙的实现 var calculateBonus = function( performanceLevel, salary ){ if ( performanceLevel === ‘S’ ){ return salary * 4; } if ( performanceLevel === ‘A’ ){ return salary * 3; } if ( performanceLevel === ‘B’ ){ return salary * 2; } }; calculateBonus( ‘B’, 20000 ); // 输出:40000 calculateBonus( ‘S’, 6000 ); // 输出:24000缺点:calculateBonus 函数比较庞大,包含了很多 if-else 语句calculateBonus 函数缺乏弹性,如果增加了一种新的绩效等级 C,或者想把绩效 S 的奖金 系数改为 5,那我们必须深入 calculateBonus 函数的内部实现,这是违反开放封闭原则的。算法的复用性差使用组合函数重构代码 var performanceS = function( salary ){ return salary * 4; }; var performanceA = function( salary ){ return salary * 3; }; var performanceB = function( salary ){ return salary * 2; }; var calculateBonus = function( performanceLevel, salary ){ if ( performanceLevel === ‘S’ ){ return performanceS( salary ); } if ( performanceLevel === ‘A’ ){ return performanceA( salary ); } if ( performanceLevel === ‘B’ ){ return performanceB( salary ); } }; calculateBonus( ‘A’ , 10000 ); // 输出:30000问题依然存在:calculateBonus 函数有可能越来越庞大,而且在系统变化的时候缺乏弹性使用策略模式重构代码 var performanceS = function(){}; performanceS.prototype.calculate = function( salary ){ return salary * 4; }; var performanceA = function(){}; performanceA.prototype.calculate = function( salary ){ return salary * 3; }; var performanceB = function(){}; performanceB.prototype.calculate = function( salary ){ return salary * 2; }; //接下来定义奖金类Bonus: var Bonus = function(){ this.salary = null; // 原始工资 this.strategy = null; // 绩效等级对应的策略对象 }; Bonus.prototype.setSalary = function( salary ){ this.salary = salary; // 设置员工的原始工资 }; Bonus.prototype.setStrategy = function( strategy ){ this.strategy = strategy; // 设置员工绩效等级对应的策略对象 }; Bonus.prototype.getBonus = function(){ // 取得奖金数额 return this.strategy.calculate( this.salary ); // 把计算奖金的操作委托给对应的策略对象 }; var bonus = new Bonus(); bonus.setSalary( 10000 ); bonus.setStrategy( new performanceS() ); // 设置策略对象 console.log( bonus.getBonus() ); // 输出:40000 bonus.setStrategy( new performanceA() ); // 设置策略对象 console.log( bonus.getBonus() ); // 输出:30000但这段代码是基于传统面向对象语言的模仿,下面我们用JavaScript实现的策略模式。JavaScript 版本的策略模式在 JavaScript 语言中,函数也是对象,所以更简单和直接的做法是把 strategy 直接定义为函数 var strategies = { “S”: function( salary ){ return salary * 4; }, “A”: function( salary ){ return salary * 3; }, “B”: function( salary ){ return salary * 2; } }; var calculateBonus = function( level, salary ){ return strategies level ; }; console.log( calculateBonus( ‘S’, 20000 ) ); // 输出:80000 console.log( calculateBonus( ‘A’, 10000 ) ); // 输出:30000es6类实现var performanceS = function () {};performanceS.prototype.calculate = function (salary) { return salary * 4;};var performanceA = function () {};performanceA.prototype.calculate = function (salary) { return salary * 3;};var performanceB = function () {};performanceB.prototype.calculate = function (salary) { return salary * 2;};//接下来定义奖金类Bonus:class Bonus { constructor() { this.salary = null; // 原始工资 this.strategy = null; // 绩效等级对应的策略对象 } setSalary(salary) { this.salary = salary; // 设置员工的原始工资 } setStrategy(strategy) { this.strategy = strategy; // 设置员工绩效等级对应的策略对象 } getBonus() { // 取得奖金数额 return this.strategy.calculate(this.salary); // 把计算奖金的操作委托给对应的策略对象 }}var bonus = new Bonus();bonus.setSalary(10000);bonus.setStrategy(new performanceS()); // 设置策略对象console.log(bonus.getBonus()); // 输出:40000bonus.setStrategy(new performanceA()); // 设置策略对象console.log(bonus.getBonus()); // 输出:30000缓动动画目标:编写一个动画类和一些缓动算法,让小球以各种各样的缓动效果在页面中运动分析:首先缓动算法的职责是实现小球如何运动然后动画类(即context)的职责是负责:初始化动画对象在运动开始之前,需要提前记录一些有用的信息,至少包括以下信息:动画开始时的准确时间点;动画开始时,小球所在的原始位置;小球移动的目标位置;小球运动持续的时间。计算小球某时刻的位置更新小球的位置实现:<!DOCTYPE html><html lang=“en”><head> <meta charset=“UTF-8”> <meta name=“viewport” content=“width=device-width, initial-scale=1.0”> <meta http-equiv=“X-UA-Compatible” content=“ie=edge”> <title>Document</title></head><body> <div style=“position:absolute;background:blue” id=“div”>我是div</div></body><script> var tween = { linear: function (t, b, c, d) { return c * t / d + b; }, easeIn: function (t, b, c, d) { return c * (t /= d) * t + b; }, strongEaseIn: function (t, b, c, d) { return c * (t /= d) * t * t * t * t + b; }, strongEaseOut: function (t, b, c, d) { return c * ((t = t / d - 1) * t * t * t * t + 1) + b; }, sineaseIn: function (t, b, c, d) { return c * (t /= d) * t * t + b; }, sineaseOut: function (t, b, c, d) { return c * ((t = t / d - 1) * t * t + 1) + b; } }; var Animate = function (dom) { this.dom = dom; // 进行运动的dom 节点 this.startTime = 0; // 动画开始时间 this.startPos = 0; // 动画开始时,dom 节点的位置,即dom 的初始位置 this.endPos = 0; // 动画结束时,dom 节点的位置,即dom 的目标位置 this.propertyName = null; // dom 节点需要被改变的css 属性名 this.easing = null; // 缓动算法 this.duration = null; // 动画持续时间 }; Animate.prototype.start = function (propertyName, endPos, duration, easing) { this.startTime = +new Date; // 动画启动时间 this.startPos = this.dom.getBoundingClientRect()[propertyName]; // dom 节点初始位置 this.propertyName = propertyName; // dom 节点需要被改变的CSS 属性名 this.endPos = endPos; // dom 节点目标位置 this.duration = duration; // 动画持续事件 this.easing = tween[easing]; // 缓动算法 var self = this; var timeId = setInterval(function () { // 启动定时器,开始执行动画 if (self.step() === false) { // 如果动画已结束,则清除定时器 clearInterval(timeId); } }, 16); }; Animate.prototype.step = function () { var t = +new Date; // 取得当前时间 if (t >= this.startTime + this.duration) { // (1) this.update(this.endPos); // 更新小球的CSS 属性值 return false; } var pos = this.easing(t - this.startTime, this.startPos, this.endPos - this.startPos, this.duration); // pos 为小球当前位置 this.update(pos); // 更新小球的CSS 属性值 }; Animate.prototype.update = function (pos) { this.dom.style[this.propertyName] = pos + ‘px’; }; var div = document.getElementById(‘div’); var animate = new Animate(div); animate.start(’left’, 500, 1000, ’linear’); // animate.start( ’top’, 1500, 500, ‘strongEaseIn’ );</script></html>验证表单简单的实现<html><body> <form action=“http:// xxx.com/register” id=“registerForm” method=“post”> 请输入用户名:<input type=“text” name=“userName” /> 请输入密码:<input type=“text” name=“password” /> 请输入手机号码:<input type=“text” name=“phoneNumber” /> <button>提交</button> </form> <script> var registerForm = document.getElementById(‘registerForm’); registerForm.onsubmit = function () { if (registerForm.userName.value === ‘’) { alert(‘用户名不能为空’); return false; } if (registerForm.password.value.length < 6) { alert(‘密码长度不能少于6 位’); return false; } if (!/(^1[3|5|8][0-9]{9}$)/.test(registerForm.phoneNumber.value)) { alert(‘手机号码格式不正确’); return false; } } </script></body></html>使用策略模式改进<html><body> <form action=“http:// xxx.com/register” id=“registerForm” method=“post”> 请输入用户名:<input type=“text” name=“userName” /> 请输入密码:<input type=“text” name=“password” /> 请输入手机号码:<input type=“text” name=“phoneNumber” /> <button>提交</button> </form> <script> var strategies = { isNonEmpty: function (value, errorMsg) { // 不为空 if (value === ‘’) { return errorMsg; } }, minLength: function (value, length, errorMsg) { // 限制最小长度 if (value.length < length) { return errorMsg; } }, isMobile: function (value, errorMsg) { // 手机号码格式 if (!/(^1[3|5|8][0-9]{9}$)/.test(value)) { return errorMsg; } } }; var validataFunc = function () { var validator = new Validator(); // 创建一个validator 对象 /添加一些校验规则*/ validator.add(registerForm.userName, ‘isNonEmpty’, ‘用户名不能为空’); validator.add(registerForm.password, ‘minLength:6’, ‘密码长度不能少于6 位’); validator.add(registerForm.phoneNumber, ‘isMobile’, ‘手机号码格式不正确’); var errorMsg = validator.start(); // 获得校验结果 return errorMsg; // 返回校验结果 } var registerForm = document.getElementById(‘registerForm’); registerForm.onsubmit = function () { var errorMsg = validataFunc(); // 如果errorMsg 有确切的返回值,说明未通过校验 if (errorMsg) { alert(errorMsg); return false; // 阻止表单提交 } }; var Validator = function () { this.cache = []; // 保存校验规则 }; Validator.prototype.add = function (dom, rule, errorMsg) { var ary = rule.split(’:’); // 把strategy 和参数分开 this.cache.push(function () { // 把校验的步骤用空函数包装起来,并且放入cache var strategy = ary.shift(); // 用户挑选的strategy ary.unshift(dom.value); // 把input 的value 添加进参数列表 ary.push(errorMsg); // 把errorMsg 添加进参数列表 return strategies[strategy].apply(dom, ary); }); }; Validator.prototype.start = function () { for (var i = 0, validatorFunc; validatorFunc = this.cache[i++];) { var msg = validatorFunc(); // 开始校验,并取得校验后的返回信息 if (msg) { // 如果有确切的返回值,说明校验没有通过 return msg; } } }; </script></body></html>缺点:一 个文本输入框只能对应一种校验规则再改进:可以有多个校验规则<html><body> <form action=“http:// xxx.com/register” id=“registerForm” method=“post”> 请输入用户名:<input type=“text” name=“userName” /> 请输入密码:<input type=“text” name=“password” /> 请输入手机号码:<input type=“text” name=“phoneNumber” /> <button>提交</button> </form> <script> /策略对象***/ var strategies = { isNonEmpty: function (value, errorMsg) { if (value === ‘’) { return errorMsg; } }, minLength: function (value, length, errorMsg) { if (value.length < length) { return errorMsg; } }, isMobile: function (value, errorMsg) { if (!/(^1[3|5|8][0-9]{9}$)/.test(value)) { return errorMsg; } } }; /Validator 类***/ var Validator = function () { this.cache = []; }; Validator.prototype.add = function (dom, rules) { var self = this; for (var i = 0, rule; rule = rules[i++];) { (function (rule) { var strategyAry = rule.strategy.split(’:’); var errorMsg = rule.errorMsg; self.cache.push(function () { var strategy = strategyAry.shift(); strategyAry.unshift(dom.value); strategyAry.push(errorMsg); return strategies[strategy].apply(dom, strategyAry); }); })(rule) } }; Validator.prototype.start = function () { for (var i = 0, validatorFunc; validatorFunc = this.cache[i++];) { var errorMsg = validatorFunc(); if (errorMsg) { return errorMsg; } } }; /客户调用代码***/ var registerForm = document.getElementById(‘registerForm’); var validataFunc = function () { var validator = new Validator(); validator.add(registerForm.userName, [{ strategy: ‘isNonEmpty’, errorMsg: ‘用户名不能为空’ }, { strategy: ‘minLength:6’, errorMsg: ‘用户名长度不能小于10 位’ }]); validator.add(registerForm.password, [{ strategy: ‘minLength:6’, errorMsg: ‘密码长度不能小于6 位’ }]); var errorMsg = validator.start(); return errorMsg; } registerForm.onsubmit = function () { var errorMsg = validataFunc(); if (errorMsg) { alert(errorMsg); return false; } }; </script></body></html> ...

January 3, 2019 · 7 min · jiezi

js设计模式--单例模式

概念单例模式的定义是:保证一个类仅有一个实例,并提供一个访问它的全局访问点。UML类图场景单例模式是一种常用的模式,有一些对象我们往往只需要一个,比如线程池、全局缓存、浏 览器中的 window 对象等。在 JavaScript 开发中,单例模式的用途同样非常广泛。试想一下,当我们单击登录按钮的时候,页面中会出现一个登录浮窗,而这个登录浮窗是唯一的,无论单击多少 次登录按钮,这个浮窗都只会被创建一次,那么这个登录浮窗就适合用单例模式来创建。优缺点优点:创建对象和管理单例的职责被分布在两个不同的方法中实现1. 我们的第一个单例var instance = nullvar getInstance = function(arg) { if (!instance) { instance = arg } return instance}var a = getInstance(‘a’)var b = getInstance(‘b’)console.log(a===b)这种定义一个全局变量的方式非常不优雅,也不好复用代码2. 利用闭包实现单例var Singleton = function( name ){ this.name = name;};Singleton.getInstance = (function(){ var instance = null; return function( name ){ if ( !instance ){ instance = new Singleton( name ); } return instance; }})();var a = Singleton.getInstance(‘a’)var b = Singleton.getInstance(‘b’)console.log(a===b)有些同学可能对闭包不大理解,下面用函数实现一下3. 利用函数实现单例function Singleton(name) { this.name = name this.instance = null}Singleton.getInstance = function(name) { if (!this.instance) { this.instance = new Singleton(name) } return this.instance}var a = Singleton.getInstance(‘a’)var b = Singleton.getInstance(‘b’)console.log(a===b)2,3这两种方式也有缺点,就是我们必须调用getInstance来创建对象,一般我们创建对象都是利用new操作符4. 透明的单例模式var Singleton = (function() { var instance Singleton = function(name) { if (instance) return instance this.name = name return instance = this } return Singleton})()var a = new Singleton(‘a’)var b = new Singleton(‘b’)console.log(a===b)这中方法也有点缺点:不符合单一职责原则,这个对象其实负责了两个功能:单例和创建对象下面我们分离这两个职责5. 利用代理实现单例var People = function(name) { this.name = name}var Singleton = (function() { var instance Singleton = function(name) { if (instance) return instance return instance = new People(name) } return Singleton})()var a = new Singleton(‘a’)var b = new Singleton(‘b’)console.log(a===b)这中方法也有点缺点:代码不能复用。如果我们有另外一个对象也要利用单例模式,那我们不得不写重复的代码6. 提供通用的单例var People = function(name) { this.name = name}var Singleton = function(Obj) { var instance Singleton = function() { if (instance) return instance return instance = new Obj(arguments) } return Singleton}var peopleSingleton = Singleton(People)var a = new peopleSingleton(‘a’)var b = new peopleSingleton(‘b’)console.log(a===b)到这里已经比较完美了,等等这只是es5的写法,下面我们用es6来实现一下7. es6单例模式class People { constructor(name) { if (typeof People.instance === ‘object’) { return People.instance; } People.instance = this; this.name = name return this; }}var a = new People(‘a’)var b = new People(‘b’)console.log(a===b)比较以上几种实现用全局变量的第1种方法,应该摒弃用闭包实现的第2种方式,instance 实例对象总是在我们调用 Singleton.getInstance 的时候才被创建,应该摒弃其他方式都是惰性单例(在需要时才创建)js的特殊性我们都知道:JavaScript 其实是一门无类(class-free)语言,,生搬单例模式的概念并无意义。单例模式的核心是确保只有一个实例,并提供全局访问。我们可以用一下几种方式来另类实现1. 全局变量比如var a = {},这时全局就只有一个a对象但全局变量存在很多问题,它很容易造成命名空间污染,我们用以下两种方式解决2.使用命名空间 var namespace1 = { a: function () { alert(1); }, b: function () { alert(2); } };另外我们还可以动态创建命名空间 var MyApp = {}; MyApp.namespace = function (name) { var parts = name.split(’.’); var current = MyApp; for (var i in parts) { if (!current[parts[i]]) { current[parts[i]] = {}; } current = current[parts[i]]; } }; MyApp.namespace(’event’); MyApp.namespace(‘dom.style’); console.dir(MyApp); // 上述代码等价于: var MyApp = { event: {}, dom: { style: {} } };3. 闭包 var user = (function () { var __name = ‘sven’, __age = 29; return { getUserInfo: function () { return __name + ‘-’ + __age; } } })();例子登录框下面我们来实现一个点击登录按钮弹出登录框的例子粗糙的实现<html><body> <button id=“loginBtn”>登录</button></body><script> var loginLayer = (function () { var div = document.createElement(‘div’); div.innerHTML = ‘我是登录浮窗’; div.style.display = ’none’; document.body.appendChild(div); return div; })(); document.getElementById(’loginBtn’).onclick = function () { loginLayer.style.display = ‘block’; };</script></html>上面这种方式如果用户没有点击登录按钮,也会在一开始就创建登录框改进<html><body> <button id=“loginBtn”>登录</button></body><script> var createLoginLayer = function () { var div = document.createElement(‘div’); div.innerHTML = ‘我是登录浮窗’; div.style.display = ’none’; document.body.appendChild(div); return div; }; document.getElementById(’loginBtn’).onclick = function () { var loginLayer = createLoginLayer(); loginLayer.style.display = ‘block’; };</script></html>这种方式每次点击按钮都会创建一个登录框再改进var createLoginLayer = (function () { var div; return function () { if (!div) { div = document.createElement(‘div’); div.innerHTML = ‘我是登录浮窗’; div.style.display = ’none’; document.body.appendChild(div); } return div; } })(); document.getElementById(’loginBtn’).onclick = function () { var loginLayer = createLoginLayer(); loginLayer.style.display = ‘block’; };这种方式不够通用,不符合单一职责原则再再改进 var getSingle = function (fn) { var result; return function () { return result || (result = fn.apply(this, arguments)); } }; var createLoginLayer = function () { var div = document.createElement(‘div’); div.innerHTML = ‘我是登录浮窗’; div.style.display = ’none’; document.body.appendChild(div); return div; }; var createSingleLoginLayer = getSingle(createLoginLayer); document.getElementById(’loginBtn’).onclick = function () { var loginLayer = createSingleLoginLayer(); loginLayer.style.display = ‘block’; }; //下面我们再试试创建唯一的iframe 用于动态加载第三方页面: var createSingleIframe = getSingle(function () { var iframe = document.createElement(‘iframe’); document.body.appendChild(iframe); return iframe; }); document.getElementById(’loginBtn’).onclick = function () { var loginLayer = createSingleIframe(); loginLayer.src = ‘http://baidu.com’; };至此已经完美 ...

January 2, 2019 · 3 min · jiezi

设计模式的知识大纲分享

最近一段时间,通过阅读《head first 设计模式》和其他资料,系统地学习了设计模式的知识。我将自己的思考和总结做成了知识大纲与大家分享:https://mubu.com/doc/kpPXaNRfVC如果你觉得幕布这个知识大纲整理工具很好用,欢迎使用我的邀请链接来注册:https://mubu.com/inv/2060390 。你和我都会获得30天的高级版~阅读建议这个大纲来自我学习时的思考和总结,不适合把它当做学习设计模式的教程。建议先阅读《head first 设计模式》来学习设计模式例子很重要:在学习某个设计模式的时候,如果你脑中没有具体的例子,那么是很难理解它的。例子的来源包括理解学习资料中举的例子编程经验:见过/写过这样的代码如果无法通过上面两种途径来找到例子,建议先不要学习这个设计模式。即使现在花费大量时间钻研透了(来自我的教训),也会有以下坏处:很快就忘记将来遇到这个模式,发现自己当初的理解有误由于编程经验的积累,一段时间以后只需要花费一点点时间就能搞懂,当初浪费那么多时间在这上面不值得不建议一口气读完(你会很快感到无趣)。可以先阅读一点感兴趣的部分。也可以先收藏着,在将来遇到/学习这个模式的时候再看对应的部分随着经验的丰富和理解的加深,我会持续更新这份资料。我在总结的时候,会尽量参考比较权威的资料来保证正确性、尽量囊括所有重要的部分、尽量解释得通俗易懂,但是难免有所疏漏。如果你发现了问题或者希望与我讨论,欢迎发送邮件到 632882184@qq.com !

January 2, 2019 · 1 min · jiezi

《JavaScript设计模式》阅读笔记_part1

JavaScript设计模式阅读第二章:类1、闭包实现类闭包可以理解为’类生成器’闭包代码:var Book = (function(){ var bookNum = 0; function checkBook(name){ } return function(newId,newName,newPrice){ var name,price; this.id = newId; bookNum++; if(bookNum > 100){ throw new Error(‘最多只出版100本书’); } function checkID(id){} this.getName = function(){ console.log(name); return name; }; this.getPrice = function(){ console.log(price); return price; }; this.setName = function (mName) { name = mName; }; this.setPrice = function (mPrice) { price = mPrice; }; this.copy = function () {}; // 构造器 this.setName(newName); this.setPrice(newPrice); }})();Book.prototype = { isJSBook: false, display: function () { }}使用:var myBook = new Book(‘id’,’name’,‘price’);使用方法与普通的是一致的。但是如果不加new关键词的话var myBook = Book(‘id’,’name’,‘price’);当不使用new关键词的时候只会将Book执行一遍并且this指针为window并且所有的值都在可以使用将return的function写为一个私有的类,并且将外部的prototype写在里面,让闭包看起来更加的舒服,更像是一个整体。2、对象的安全模式在使用类的时候可能会忘记使用new关键词。这个时候调用就像上面说的那种。执行一遍代码,并且其中的this指向window。可以使用安全模式避免忘记使用new的情况。列子:var Book = function (title,time,type) { if(this instanceof Book){ this.title = title; this.time = time; this.type = type; }else{ return new Book(title,time,type); }}本质可以看出就是加了一层判断。3、js原型链对引用类型的无力。当原型链上的值为引用的时候:var test = function () {}test.prototype.nums = [1,2,3,4];ins1 = new test();ins2 = new test();console.log(ins2.nums);ins1.nums.push(5);console.log(ins2.nums);这里就可以看出来如果原型链上的值为引用类型的时候会出现问题。4、多继承多继承的实现就是将父类们的所有属性进行拷贝到一个到当前类上。当遇到引用类型的时候应当深拷贝,但是此处我们只讨论浅拷贝的问题。以下代码为多继承:var mix = function () { var len = arguments.length; var target = arguments[1]; var arg; for(var i = 1;i < len;i++){ arg = arguments[i]; for(var property in arg){ target[property] = arg[property]; } } return arg;}5、多态多态是对arguments里面的值得个数进行统计,根据不同的情况给予不同的回应。简单例子var add = function () { var len = arguments.length; switch (len) { case 0: return 10; case 1: return 10 + arguments[0]; case 2: return arguments[0] + arguments[1]; }} ...

January 1, 2019 · 1 min · jiezi

python设计模式-状态模式

问题:有一个糖果公司需要设计一个糖果售卖机,控制流程如下图,需要怎么实现?这是一个状态图,每个圆圈都是一种状态。很明显,有有25分钱、 没有25分钱、 售出糖果、 糖果售罄四个状态,同时也对应四个动作:投入25分钱,退回25分钱,转动曲柄和发放糖果。那如何从状态图得到真正的代码呢?简单代码实现如下:#! -- coding: utf-8 --class GumballMachine: # 找出所有状态,并创建实例变量来持有当前状态,然后定义状态的值 STATE_SOLD_OUT = 0 STATE_NO_QUARTER = 1 STATE_HAS_QUARTER = 2 STATE_SOLD = 3 state = STATE_SOLD_OUT def init(self, count=0): self.count = count if count > 0: self.state = self.STATE_NO_QUARTER def str(self): return “Gumball machine current state: %s” % self.state def insert_quarter(self): # 投入25分钱 if self.state == self.STATE_HAS_QUARTER: # 如果已经投过 print(“You can’t insert another quarter”) elif self.state == self.STATE_NO_QUARTER: # 如果没有投过 self.state = self.STATE_HAS_QUARTER print(“You inserted a quarter”) elif self.state == self.STATE_SOLD_OUT: # 如果已经售罄 print(“You can’t insert a quarter, the machine is sold out”) elif self.state == self.STATE_SOLD: # 如果刚刚买了糖果 print(“Please wait, we’re already giving you a gumball”) def eject_quarter(self): # 退回25分 if self.state == self.STATE_HAS_QUARTER: print(“Quarter returned”) self.state = self.STATE_NO_QUARTER elif self.state == self.STATE_NO_QUARTER: print(“You haven’t inserted a quarter”) elif self.state == self.STATE_SOLD: print(“Sorry, you alread turned the crank”) elif self.state == self.SOLD_OUT: print(“You can’t eject, you haven’t inserted”) def turn_crank(self): # 转动曲柄 if self.state == self.STATE_SOLD: print(“Turning twice doesn’t get you another gumball”) elif self.state == self.STATE_NO_QUARTER: print(“You turned but there’s no quarter”) elif self.state == self.STATE_SOLD_OUT: print(“You turned, but there are no gumballs”) elif self.state == self.STATE_HAS_QUARTER: print(“You turned…”) self.state = self.STATE_SOLD self.dispense() def dispense(self): # 发放糖果 if self.state == self.STATE_SOLD: print(“A gumball comes rolling out the slot”) self.count -= 1 if self.count == 0: self.state = self.STATE_SOLD_OUT else: self.state = self.STATE_NO_QUARTER elif self.state == self.STATE_NO_QUARTER: print(“You need to pay first”) elif self.state == self.STATE_SOLD_OUT: print(“No gumball dispensed”) elif self.state == self.STATE_HAS_QUARTER: print(“No gumball dispensed”)if name == “main”: # 以下是代码测试 gumball_machine = GumballMachine(5) # 装入5 个糖果 print(gumball_machine) gumball_machine.insert_quarter() # 投入25分钱 gumball_machine.turn_crank() # 转动曲柄 print(gumball_machine) gumball_machine.insert_quarter() #投入25分钱 gumball_machine.eject_quarter() # 退钱 gumball_machine.turn_crank() # 转动曲柄 print(gumball_machine) gumball_machine.insert_quarter() # 投入25分钱 gumball_machine.turn_crank() # 转动曲柄 gumball_machine.insert_quarter() # 投入25分钱 gumball_machine.turn_crank() # 转动曲柄 gumball_machine.eject_quarter() # 退钱 print(gumball_machine)这段代码有几个问题:没有遵守开放-关闭原则更像是面向过程的设计状态转化被埋藏在条件语句中未来加入新的需求,需要改动的较多,不易维护,可能会出bug如何改进呢?考虑封装变化,把每个状态的行为都放在各自的类中,每个状态只要实现自己的动作,用加入新类的方式来实现新状态的加入。定义State 父类,在这个类中,糖果机的每个动作都有一个应对的方法为机器中的每个状态实现状态类,这些类将负责在对应的状态下进行机器的行为摆脱旧的条件代码,将动作委托到状态类新的实现代码如下:#! -- coding: utf-8 --class State: # 定义state基类 def insert_quarter(self): pass def eject_quarter(self): pass def turn_crank(self): pass def dispense(self): passclass SoldOutState(State): # 继承State 类 def init(self, gumball_machine): self.gumball_machine = gumball_machine def str(self): return “sold_out” def insert_quarter(self): print(“You can’t insert a quarter, the machine is sold out”) def eject_quarter(self): print(“You can’t eject, you haven’t inserted a quarter yet”) def turn_crank(self): print(“You turned, but ther are no gumballs”) def dispense(self): print(“No gumball dispensed”)class SoldState(State): # 继承State 类 def init(self, gumball_machine): self.gumball_machine = gumball_machine def str(self): return “sold” def insert_quarter(self): print(“Please wait, we’re already giving you a gumball”) def eject_quarter(self): print(“Sorry, you already turned the crank”) def turn_crank(self): print(“Turning twice doesn’t get you another gumball”) def dispense(self): self.gumball_machine.release_ball() if gumball_machine.count > 0: self.gumball_machine.state = self.gumball_machine.no_quarter_state else: print(“Oops, out of gumballs!”) self.gumball_machine.state = self.gumball_machine.soldout_stateclass NoQuarterState(State): # 继承State 类 def init(self, gumball_machine): self.gumball_machine = gumball_machine def str(self): return “no_quarter” def insert_quarter(self): # 投币 并且改变状态 print(“You inserted a quarter”) self.gumball_machine.state = self.gumball_machine.has_quarter_state def eject_quarter(self): print(“You haven’t insert a quarter”) def turn_crank(self): print(“You turned, but there’s no quarter”) def dispense(self): print(“You need to pay first”)class HasQuarterState(State): # 继承State 类 def init(self, gumball_machine): self.gumball_machine = gumball_machine def str(self): return “has_quarter” def insert_quarter(self): print(“You can’t insert another quarter”) def eject_quarter(self): print(“Quarter returned”) self.gumball_machine.state = self.gumball_machine.no_quarter_state def turn_crank(self): print(“You turned…”) self.gumball_machine.state = self.gumball_machine.sold_state def dispense(self): print(“No gumball dispensed”)class GumballMachine: def init(self, count=0): self.count = count # 找出所有状态,并创建实例变量来持有当前状态,然后定义状态的值 self.soldout_state = SoldOutState(self) self.no_quarter_state = NoQuarterState(self) self.has_quarter_state = HasQuarterState(self) self.sold_state = SoldState(self) if count > 0: self.state = self.no_quarter_state else: self.state = self.soldout_state def str(self): return “>>> Gumball machine current state: %s” % self.state def insert_quarter(self): # 投入25分钱 self.state.insert_quarter() def eject_quarter(self): # 退回25分 self.state.eject_quarter() # print(“state”, self.state, type(self.state)) def turn_crank(self): # 转动曲柄 # print(“state”, self.state, type(self.state)) self.state.turn_crank() def release_ball(self): # 发放糖果 print(“A gumball comes rolling out the slot…”) if self.count > 0: self.count -= 1 if name == “main”: # 以下是代码测试 gumball_machine = GumballMachine(5) # 装入5 个糖果 print(gumball_machine) gumball_machine.insert_quarter() # 投入25分钱 gumball_machine.turn_crank() # 转动曲柄 print(gumball_machine) gumball_machine.insert_quarter() #投入25分钱 gumball_machine.eject_quarter() # 退钱 gumball_machine.turn_crank() # 转动曲柄 print(gumball_machine) gumball_machine.insert_quarter() # 投入25分钱 gumball_machine.turn_crank() # 转动曲柄 gumball_machine.insert_quarter() # 投入25分钱 gumball_machine.turn_crank() # 转动曲柄 gumball_machine.eject_quarter() # 退钱 print(gumball_machine)重构后的代码相对于之前的代码做了哪些事情呢?将每个状态的行为局部话到自己的类中删除if 语句将状态类对修改关闭,对糖果季类对扩展开放下图是刚初始状态图示:上面重构部分代码使用的就是状态模式:定义状态模式: 状态模式允许对象在内部状态改变时改变它的行为,对象看起来好像修改了它的类。状态模式的类图如下:状态模式是将多个行为封装在状态对象中, context 的行为随时可委托到其中一个状态中。当前状态在不同的状态对象中改变,以反映出context 内部的状态,context 的行为也会随之改变。扩展如果,现在要在这四个状态的基础上再加一个状态(购买糖果后,有10%的概率再得一个),该如何实现呢?# 添加WinnerState 类,只有dispense 方法不同,可以从SoldState 类继承class WinnerState(SoldState): def str(self): return “winner” def dispense(self): print(“You’re a WINNER! You get two gumballs for your quarter”) self.gumball_machine.release_ball() if gumball_machine.count == 0: self.gumball_machine.state = self.gumball_machine.soldout_state else: self.gumball_machine.release_ball() if gumball_machine.count > 0: self.gumball_machine.state = self.gumball_machine.no_quarter_state else: print(“Oops, out of gumballs!”) self.gumball_machine.state = self.gumball_machine.soldout_state# 修改turn_crank 方法class HasQuarterState(State): … def turn_crank(self): print(“You turned…”) winner = random.randint(0, 9) if winner == 4 and self.gumball_machine.count > 1: # 如果库存大于 1 并且随机数等于4(可以是0到9任意值) self.gumball_machine.state = self.gumball_machine.winner_state else: self.gumball_machine.state = self.gumball_machine.sold_state# 在 GumballMachine 中初始化class GumballMachine: def init(self, count=0): self.count = count # 找出所有状态,并创建实例变量来持有当前状态,然后定义状态的值 … self.winner_state = WinnerState(self) …总结状态模式允许一个对象给予内部状态而拥有不同的行为状态模式用类代表状态Context 会将行为委托给当前状态对象通过将每状态封装进一个类,把改变局部化状态装欢可以由State 类或Context 类控制使用状态模式会增加类的数目状态类可以被多个Context 实例共享本文例子来自《Head First 设计模式》。最后,感谢女朋友支持和包容,比❤️也可以在公号输入以下关键字获取历史文章:公号&小程序 | 设计模式 | 并发&协程 ...

January 1, 2019 · 4 min · jiezi

依赖倒置原则

个人博客原文:依赖倒置原则设计模式六大原则之三:依赖倒置原则。简介姓名 :依赖倒置原则英文名 :Dependence Inversion Principle价值观 :大男子主义的典型代表,什么都得通过老大或者老爸同意伴侣 :一定是个温柔体贴的女子个人介绍 :High level modules should not depend upon low level modules.Both should depend upon abstractions. 高层模块不应该依赖低层模块,两者都应该依赖其抽象(模块间的依赖通过抽象发生,实现类之间不发生直接的依赖关系,其依赖关系是通过接口或抽象类产生的)Abstractions should not depend upon details. 抽象不应该依赖细节(接口或抽象类不依赖于实现类)Details should depend upon abstractions. 细节应该依赖抽象(实现类依赖接口或抽象类)给大家讲个故事,我胡乱想的,如有雷同,肯定是英雄所见略同。那必须交个朋友。一个小村里,有两家饭馆,虽然挂着不同的牌子,挨在一起,但是老板确是表兄弟。这两兄弟抠得很,为了节省成本,密谋了一个想法:在两家饭馆谁家忙的时候,可以让不忙的那家的员工过去支援一下。这样子,本来每家饭馆都需要 2 个洗碗工,总共需要 4 个,他们就只招了 3 个,省了 1 个洗碗工的成本,当然不止洗碗工,还有服务员等等。两兄弟约定了规则:A 饭馆需要支援的时候,B 饭馆老板,让 B 饭馆老板选哪个员工去支援,不能直接让 A 饭馆的员工直接找 B 饭馆的员工去帮忙,但可以让 A 饭馆员工找 B饭馆老板告知需要支援。虽然老板权利大,但是也不能说 A 饭馆老板直接叫 B 饭馆的员工去帮忙。员工没有真实的老板,今天为 A 饭馆工作就是 A 饭馆的员工,没有跟定哪个老板。大概通过这个小故事,描述了依赖倒置原则的基本内容。代码复原下面通过代码来模拟这个故事。错误的示范这个错误的示范将就看哈,可能有些问题没描述清楚。老板和员工抽象abstract class Boss { abstract void support(); abstract void askHelp(Boss boss);}abstract class Staff { private String name; abstract void service(); abstract void askHelp(Boss boss); public String getName() { return name; } public void setName(String name) { this.name = name; }}老板具体类class BossA extends Boss { private StaffA staffA; public BossA(StaffA staffA) { this.staffA = staffA; } @Override void support() { staffA.service(); } @Override void askHelp(Boss boss) { boss.support(); }}class BossB extends Boss { private StaffB staffB; public BossB(StaffB staffB) { this.staffB = staffB; } @Override void support() { staffB.service(); } @Override void askHelp(Boss boss) { boss.support(); }}员工具体类class StaffA extends Staff { public StaffA(String name) { this.setName(name); } @Override void service() { System.out.println(this.getName() + “提供服务”); } @Override void askHelp(Boss boss) { boss.support(); }}class StaffB extends Staff { public StaffB(String name) { this.setName(name); } @Override void service() { System.out.println(this.getName() + “提供服务”); } @Override void askHelp(Boss boss) { boss.support(); }}测试代码/** 初始化老板和员工 /StaffA staffA = new StaffA(“A 员工”);StaffB staffB = new StaffB(" B 员工");Boss bossA = new BossA(staffA);Boss bossB = new BossB(staffB);/* A 老板向 B 老板求支援 /bossA.askHelp(bossB); // 打印出:B 员工提供服务/* B 员工向 A 老板求支援 /staffB.askHelp(bossA); // 打印出:A 员工提供服务好像看起来实现了要求了,但是其实这段代码没有按照上面的 3 点规则编写,破坏了第 3 点规则,老板们的员工没有用员工的抽象类,破坏了细节依赖抽象这一点。设想一下,假如现在 A 老板把 A 员工辞退了,重新招了个 C 员工,那么怎么实现呢?是不是需要再新增一个 StaffC 类,然后再修改 BossA 类代码,把 StaffA 换成 StaffC。这样超级麻烦,在平时写项目中要时刻考虑这一点:在具体实现类使用其他类,是不是可以用其抽象类?代码:DIPErrorTest.java正确的示范看了上面那个憋屈的代码,再来看下面简洁的代码,才会发现依赖倒置原则是多么强大。老板和员工抽象类abstract class Boss2 { private Staff2 staff; public Boss2(Staff2 staff) { this.staff = staff; } abstract void support(); abstract void askHelp(Boss2 boss); public void setStaff(Staff2 staff) { this.staff = staff; } public Staff2 getStaff() { return staff; }}abstract class Staff2 { private String name; abstract void service(); abstract void askHelp(Boss2 boss); public void setName(String name) { this.name = name; } public String getName() { return name; }}老板类class BossImpl extends Boss2 { public BossImpl(Staff2 staff) { super(staff); } @Override void support() { this.getStaff().service(); } @Override void askHelp(Boss2 boss) { boss.support(); }}员工类class StaffImpl extends Staff2{ public StaffImpl(String name) { this.setName(name); } @Override void service() { System.out.println(this.getName() + “提供服务”); } @Override void askHelp(Boss2 boss) { boss.support(); }}测试类/* 正确示范 /Staff2 staffA2 = new StaffImpl(“A 员工”);Staff2 staffB2 = new StaffImpl(“B 员工”);Boss2 bossA2 = new BossImpl(staffA2);Boss2 bossB2 = new BossImpl(staffB2);/* A 老板向 B 老板求支援 /bossA2.askHelp(bossB2); // 打印出:B 员工提供服务/* B 员工向 A 老板求支援 /staffB2.askHelp(bossA2); // 打印出:A 员工提供服务/* A 老板辞退了 A 员工,换成了 C 员工 /Staff2 staffC2 = new StaffImpl(“C 员工”);bossA2.setStaff(staffC2);/* B 员工向 A 老板求支援 */staffB2.askHelp(bossA2); // 打印出:C 员工提供服务这代码相比上面错误的示范,简洁了很多,实现的功能却更灵活,这就是依赖倒置原则强大的地方,它可以将类的耦合性降低,提供灵活的处理。代码:DIPRightTest.java最佳实践变量的表面类型尽量是接口或者是抽象类任何类都不应该从具体类派生尽量不要覆写基类的方法结合里氏替换原则使用(来自《设计模式之禅》)总结总的来说,要实现依赖倒置原则,要有『面向接口编程』这个思维,掌握好这个思维后,就可以很好的运用依赖倒置原则。参考资料:《大话设计模式》、《Java设计模式》、《设计模式之禅》、《研磨设计模式》、《Head First 设计模式》欢迎大家关注公众号 LieBrother,一起学习、进步! ...

January 1, 2019 · 3 min · jiezi

三分钟学会《门面模式》

前言只有光头才能变强回顾前面所写过的设计模式:给女朋友讲解什么是代理模式包装模式就是这么简单啦单例模式你会几种写法?工厂模式理解了没有?策略模式原来就这么简单!无论是面试还是个人的提升,设计模式是必学的。今天来讲解门面(外观)模式上一次分享了一篇好文:《为什么阿里巴巴禁止工程师直接使用日志系统(Log4j、Logback)中的 API》【强制】应用中不可直接使用日志系统(Log4j、Logback)中的 API,而应依赖使用日志框架SLF4J 中的 API,使用门面模式的日志框架,有利于维护和各个类的日志处理方式统一。不知道大家有没有了解过门面模式,我去翻了一下《设计模式之禅》,发现非常简单,所以在这给大家分享一下。一、门面(外观)模式介绍1.1门面模式现实例子一个电源总开关可以控制所有电器的打开或关闭状态。无论是空调、冰箱、电视、电脑、风扇等等,只要是电器都受这个电闸控制。只要这个电闸将关闭,所有的电器都会受到牵连(一同关闭)。电源总开关(电闸)即为该系统的外观模式设计。1.2回到代码世界比如,我们家里现在有空调、冰箱、电脑这么几个电器// 冰箱public class Fridge { // 关闭冰箱 public void turnOff() { } // 开冰箱灯..减低冰箱温度..调高冰箱温度…}// 电视public class Television { // 关闭电视 public void turnOffTV() { System.out.println(“关闭电视”); } // 切换电视节目..减低电视声音..调高电视声音… public void doSomething() { System.out.println(“切换电视节目..减低电视声音..调高电视声音…”); }}// 电脑public class Computer { // 关闭电脑 public void turnOffComputer() { System.out.println(“关闭电脑”); } // 使用电脑干别的事 public void doSomething() { System.out.println(“使用电脑干别的事~”); }} 如果没有电闸的的情况下,我想将上面的电器关闭掉,我需要这样干: // 我要关闭电视、电脑、空调 public static void main(String[] args) { new Computer().turnOffComputer(); new Fridge().turnOffFridge(); new Television().turnOffTV(); // 当然了,一个正常的家庭不单单只有这么点电器的。 // 如果某一天我想关闭家里所有的电器,就需要重复new 个对象,调用其turn offer方法 } 一个一个关是不是很麻烦,所以我们就有了电闸:// 电闸public class ElectricBrake { private Computer computer = new Computer(); private Fridge fridge = new Fridge(); private Television television = new Television(); // 关闭所有电器 public void turnOffAll() { computer.turnOffComputer(); fridge.turnOffFridge(); television.turnOffTV(); }} 当我们想关闭所有电器的时候,我们可以使用电闸来关闭。// 我要关闭所有电器 public static void main(String[] args) { ElectricBrake brake = new ElectricBrake(); brake.turnOffAll(); } 有经验的同学可能就会想,这不就再封装了一层吗??这就是门面模式啦??嗯,没错,这就是门面模式1.3门面模式介绍《设计模式之禅》:要求一个子系统的外部与其内部的通信必须通过一个统一的对象进行。门面模式提供一个高层次的接口,使得子系统更易于使用。门面模式的通用类图十分简单:按照我们的例子,子系统就相当于电脑、冰箱、电视。统一的对象就相当于我们的电闸。我们通过电闸来对所有电器进行关闭(使得不用逐个逐个找电脑、冰箱、电视来关闭)使用了门面模式,使客户端调用变得更加简单!1.4门面模式的优缺点优点:减少系统的相互依赖。使用门面模式,所有的依赖都是对门面对象的依赖,与子系统无关提高了灵活性。不管子系统内部如何变化,只要不影响门面对象,任你自由活动。缺点:不符合开闭原则,对修改关闭,对扩展开放。比如我们上面的例子,如果有新电器要想要加入一次关闭的队伍中,只能在门面对象上修改 turnOffAll()方法的代码。最后是不是觉得门面设计模式就那么一回事了?说白了就是对子系统封装了一层,给予了一个高层次的接口(门面对象),进而方便客户端调用。推荐阅读和参考资料:《设计模式之禅》https://blog.csdn.net/hguisu/…https://www.cnblogs.com/lthIU…乐于分享和输出干货的Java技术公众号:Java3y。关注即可领取海量的视频资源!文本已收录至我的GitHub仓库,欢迎Star:https://github.com/ZhongFuCheng3y/3y ...

December 30, 2018 · 1 min · jiezi

里氏替换原则

个人博客原文:里氏替换原则设计模式六大原则之二:里氏替换原则。简介姓名 :里氏替换原则英文名 :Liskov Substitution Principle座右铭 :If for each object o1 of type S there is an object o2 of type T such that for all programs P defined in terms of T,the behavior of P is unchanged when o1 is substituted for o2 then S is a subtype of T. 如果对每一个类型为S的对象o1,都有类型为T的对象o2,使得以T定义的所有程序P在所有的对象o1都代换成o2时,程序P的行为没有发生变化,那么类型S是类型T的子类型。Functions that use pointers or references to base classes must be able to use objects of derived classes without knowing it. 所有引用基类的地方必须能透明地使用其子类的对象。这 2 个定义来自《设计模式之禅》,比较干巴巴,不认真思考起来可能不太容易懂。简单来说就是定义了什么是父子。在现实生活中,什么是父子?就是生你的那个男人和你的关系就是父子(父女)。而这里定义的就是假如 A 能胜任 B 干的所有事情,那 B 就是 A 的父亲,也就是儿子要会父亲的所有能活,儿子活得再烂也要有父亲的水平。价值观 :很显然,比较传统,严父出孝子。儿子必须要有父亲的能耐,最好青出于蓝胜于蓝。伴侣 :估计有个贤惠的老婆,才能有这么优秀的儿子。个人介绍 :我比较严厉,也是为了生存没办法,只有一辈一辈地变优秀,一直坚持下去,家族就会越来越好。这样就可以富过三代,你看你们人类不是经常说富不过三代。。。扎心了老铁,老子还是富零代。老爹开车,前方注意里氏替换原则定义了什么是父子,还有一点要注意的,就是儿子不能在父亲会的技能上搞“创新”。比如父亲会做红烧排骨,儿子在新东方烹饪学校中学到了一招,在红烧排骨里面加糖和醋,变成红烧糖醋排骨,更加美味,看代码,儿子在父亲的基础红烧排骨上加了糖醋,好像没啥问题。class Father1 { public void braisedRibs(){ System.out.println(“红烧排骨”); }}class Son1 extends Father1 { public void braisedRibs(){ System.out.println(“红烧糖醋排骨”); }}运行下面代码,会打印:红烧排骨Father1 father1 = new Father1();father1.braisedRibs();我们上面说过,所有在使用父亲的地方,都能够替换成儿子,并且效果是一样的,那接下来我们改一下代码。Son1 son1 = new Son1();son1.braisedRibs();结果是啥?打印出:红烧糖醋排骨,出乎意料吧。。。这结果完全不一样。想一下上面说的:老爸会的老子也要会,很明显,上面的例子老子不会红烧排骨,只会红烧糖醋排骨,所以这根本不是父子关系。那应该怎么实现呢?其实红烧排骨和红烧糖醋排骨这压根就是 2 道菜,你去餐馆吃饭的时候,你点红烧排骨服务员给你送来红烧糖醋排骨,或者你点红烧糖醋排骨服务员给你送来红烧排骨,你这时候不生气,算我输。来看看 Son2,Son2 将红烧糖醋改为 braisedSweetAndSourPorkRibs (翻译不好找 Google 算账去哈,反正不是我翻译的)。class Son2 extends Father1 { public void braisedSweetAndSourPorkRibs(){ System.out.println(“红烧糖醋排骨”); } }测试一下是不是好儿子Son2 son2 = new Son2();son2.braisedRibs();son2.braisedSweetAndSourPorkRibs();打印出:红烧排骨红烧糖醋排骨这才是 Father1 的好儿子嘛,不仅会红烧排骨,还会红烧糖醋排骨。所以说里氏替换原则就是在定义父子关系,大家都遵守这个定义,就会一代比一代好,不遵守大家也看到了,把前辈传下来的都毁于一旦了。代码见:LSPTest.java优缺点下面再贴一下书本上的一些优缺点优点代码共享,减少创建类的工作量,每个子类都拥有父类的方法和属性;提高代码的重用性;子类可以形似父类,但又异于父类,“龙生龙,凤生凤,老鼠生来会打洞”是说子拥有父的“种”,“世界上没有两片完全相同的叶子”是指明子与父的不同;提高代码的可扩展性,实现父类的方法就可以“为所欲为”了,君不见很多开源框架的扩展接口都是通过继承父类来完成的;提高产品或项目的开放性。缺点继承是侵入性的。只要继承,就必须拥有父类的所有属性和方法;降低代码的灵活性。子类必须拥有父类的属性和方法,让子类自由的世界中多了些约束;增强了耦合性。当父类的常量、变量和方法被修改时,需要考虑子类的修改,而且在缺乏规范的环境下,这种修改可能带来非常糟糕的结果————大段的代码需要重构。(来自《设计模式之禅》)总结好了,里氏替换原则的大概原理讲得差不多,大家只要记住是在定义“父子关系”,就像游戏规则一样,定义后让大家遵守,会让大家的程序在后面越来越复杂的时候也能清晰,而不会越来越乱。参考资料:《大话设计模式》、《Java设计模式》、《设计模式之禅》、《研磨设计模式》、《Head First 设计模式》 ...

December 29, 2018 · 1 min · jiezi

前端框架中 “类mixin” 模式的思考

“类 mixin” 指的是 Vue 中的 mixin,Regular 中的 implement使用 Mixin 的目的首先我们需要知道为什么会有 mixin 的存在?为了扩展对象(组件)的功能扩展对象功能的需求是很容易的理解的。比如业务开发时会碰到跨模块传递消息的需求,常用的方法是使用 “发布-订阅模式” 来创建一个全局的EventEmitter。不使用 mixin 时的使用方法如下// global/eventEmitterclass EventEmitter { on(eventName, handle) { } emit(eventName) { }}// ComponentAimport EventEmitter from ‘global/eventEmitter’; // 全局的 EventEmitter 对象class ComponentA { constructor() { EventEmitter.on(’event’, () => {}); }}// ComponentBimport EventEmitter from ‘global/eventEmitter’;class ComponentB { constructor() { EventEmitter.emit(’event’); }}我们需要在不同的组件中引入 EventEmitter 来使用它如果使用 mixin 的话,我们可以这样做// eventEmitter mixinimport EventEmitter from ‘global/eventEmitter’; // 全局的 EventEmitter 对象const eventEmitterMixin = { on: (eventName, handle) => {EventEmitter.on(eventName, handle)}, fire: () => {EventEmitter.emit(eventName)}}// Root ComponentComponent.mixin(eventEmitterMixin);// ComponentAclass ComponentA { constructor() { this.on(’event’, () => {}); }}// ComponentBclass ComponentB { constructor() { this.emit(’event’); }}这样通过mixin来扩展了组件的功能,是每个组件都可以方便的使用 EventEmitter 的功能为了复用代码软件开发中的 DRY 原则还是有必要遵守的。过多的重复代码会导致维护上的麻烦,通过 mixin ,我们可以在不同的对象上使用同一份代码来完成相同的功能,减轻我们维护的压力。复用 VS 扩展其实这两个没有可比性,但在我们决定是否需要将一个对象通过 mixin 的方式混入到其他对象时就应该考虑这个问题。原则上 复用 > 扩展。如果一个 mixin 只是为了扩展单个对象的功能,而扩展的功能并不能复用到其他的对象时,就不应该使用 mixin,而是直接写在那个对象上更好。如果扩展的功能可以被复用的话,那么可以考虑使用 mixin。mixin 的缺点mixins-considered-harmful 这篇文章已经列举了一些问题,我简单的列举下mixin 会引入隐性的依赖关系mixin 会导致命名冲突mixin 会增加项目的复杂性如何正确的使用 Mixin虽然 mixin 有一些缺点,但正确的使用还是可以方便我们的开发没有不合理的设计模式,只有不合理的使用设计模式首先我们需要解决混入的 mixin 可能会造成隐性的依赖关系,而形成这种依赖关系多半是 mixin 中的扩展功能依赖了被扩展对象想内部数据,例如:// 一个数据库查看组件class DatabaseView { state = { database: [{name: ‘db1’, id: 1, tables: [{name: ’table1’, id: 1}]}] }}DatabaseView.mixin(viewTabelDetails);// 把查看表详情弹窗作为 mixin 混入const mixin = { viewTabelDetails: (dbId, tableId) => { const db = this.state.database.filter(db => db.id === dbId)[0]; if (db) { const table = db.tables.filter(table => table.id === tableId)[0]; if (table) { Model.show(table); } } }}上面的例子中,mixin 依赖的 DarabaseView 中的 database 数据。这就导致了如果页面中其他的组件也需要一个查看表详情弹窗的功能,那么这个组件也必须有类似的 database 数据,形成了一个隐性的依赖关系。避免的方式就是 mixin 对象中的功能不要与被扩展对象发生依赖,而在组件开发中这个依赖多半就是使用了被扩展对象的 state 产生的至于命名冲突和增加项目的复杂性可以通过其他的方式解决,相比于上面的问题还算简单总结mixin 只是用来扩展对象功能的,而且这个扩展功能是可以被复用的,否则你应该直接写在对象里面扩展功能的同时,mixin提供的函数不应该依赖被扩展对象的内部数据。因为如果依赖的被扩展对象的内部数据,会使这个 mixin 只能被包含特定数据对象的对象复用,影响 mixin 的复用个人主观观点其实我觉得在前端的开发中,我们应该避免使用 mixin 去扩展组件的功能例如我们完全可以使用函数调用(显性的)去调用 mixin 对象中的方法,而不是一股脑的将 mixin 对象混合到组件中每次修改使用了 implement 组件都会特别痛苦(这样导入的方法 WebStorm 根本不识别),极大了增加维护的工作量。同时由于有的 mixin 对象依赖的被扩展对象的内部数据,导致想复用的话还得有相同的数据结构(那还复用个锤子啊)所以,不要用 mixin !!!更多文章可以查看本人的博客 https://lleohao.github.io ...

December 28, 2018 · 1 min · jiezi

策略模式原来这么简单!

前言只有光头才能变强回顾前面:给女朋友讲解什么是代理模式包装模式就是这么简单啦单例模式你会几种写法?工厂模式理解了没有?无论是面试还是个人的提升,设计模式是必学的。今天来讲解策略模式~一、策略模式介绍我一次听到策略模式这个词,是在我初学JDBC的时候。不知道大家有没有用过DBUtils这个组件。当时初学跟着视频学习,方立勋老师首先是让我们先自己封装一下JDBC的一些常用的操作(实际上就是模仿DBUtils这个组件)。当时候的问题是这样的:我们打算封装一下query()查询方法,传入的参数有String sql , Object[] objects(指定SQL语句和对应的参数)。我们想根据不同的业务返回不同的值。比如说,有的时候我们返回的是一条数据,那我们想将这条数据封装成一个Bean对象比如说,有的时候我们返回的是多条数据,那我们想将这多条数据封装成一个List<Bean> 集合比如说,有的时候我们返回的是xxxx数据,那我们想将这多条数据封装成一个Map<Bean> 集合……..等等等当时解决方案是这样的:先定义一个接口:ResultSetHandler(调用者想要对结果集进行什么操作,只要实现这个接口即可)这个接口定义了行为。Object hanlder(ResultSet resultSet);然后实现上面的接口,比如我们要封装成一个Bean对象,就是public class BeanHandler implements ResultSetHandler调用的时候,实际上就是query()查询方法多一个参数 query(String sql, Object[] objects, ResultSetHandler rsh)。调用者想要返回什么类型,只要传入相对应的ResultSetHandler实现类就是了。代码如下: query方法: //这个方法的返回值是任意类型的,所以定义为Object。 public static Object query(String sql, Object[] objects, ResultSetHandler rsh) { Connection connection = null; PreparedStatement preparedStatement = null; ResultSet resultSet = null; try { connection = getConnection(); preparedStatement = connection.prepareStatement(sql); //根据传递进来的参数,设置SQL占位符的值 if (objects != null) { for (int i = 0; i < objects.length; i++) { preparedStatement.setObject(i + 1, objects[i]); } } resultSet = preparedStatement.executeQuery(); //调用调用者传递进来实现类的方法,对结果集进行操作 return rsh.hanlder(resultSet); } 接口: /* * 定义对结果集操作的接口,调用者想要对结果集进行什么操作,只要实现这个接口即可 * / public interface ResultSetHandler { Object hanlder(ResultSet resultSet); } 接口实现类(Example): //接口实现类,对结果集封装成一个Bean对象 public class BeanHandler implements ResultSetHandler { //要封装成一个Bean对象,首先要知道Bean是什么,这个也是调用者传递进来的。 private Class clazz; public BeanHandler(Class clazz) { this.clazz = clazz; } @Override public Object hanlder(ResultSet resultSet) { try { //创建传进对象的实例化 Object bean = clazz.newInstance(); if (resultSet.next()) { //拿到结果集元数据 ResultSetMetaData resultSetMetaData = resultSet.getMetaData(); for (int i = 0; i < resultSetMetaData.getColumnCount(); i++) { //获取到每列的列名 String columnName = resultSetMetaData.getColumnName(i+1); //获取到每列的数据 String columnData = resultSet.getString(i+1); //设置Bean属性 Field field = clazz.getDeclaredField(columnName); field.setAccessible(true); field.set(bean,columnData); } //返回Bean对象 return bean; }这就是策略模式??就这??这不是多态的使用吗??1.1策略模式讲解《设计模式之禅》:定义一组算法,将每个算法都封装起来,并且使他们之间可以互换策略模式的类图是这样的:策略的接口和具体的实现应该很好理解:策略的接口相当于我们上面所讲的ResultSetHandler接口(定义了策略的行为)具体的实现相当于我们上面所讲的BeanHandler实现(接口的具体实现)具体的实现一般还会有几个,比如可能还有ListBeanHandler、MapBeanHandler等等令人想不明白的可能是:策略模式还有一个Context上下文对象。这对象是用来干什么的呢?《设计模式之禅》:Context叫做上下文角色,起承上启下封装作用,屏蔽高层模块对策略、算法的直接访问,封装可能存在的变化。在知乎上也有类似的问题(为什么不直接调用,而要通过Person?):说白了,通过Person来调用更符合面向对象(屏蔽了直接对具体实现的访问)。首先要明白一个道理,就是——到底是 “人” 旅游,还是火车、汽车、自行车、飞机这些交通工具旅游?如果没有上下文的话,客户端就必须直接和具体的策略实现进行交互了,尤其是需要提供一些公共功能或者是存储一些状态的时候,会大大增加客户端使用的难度;引入上下文之后,这部分工作可以由上下文来完成,客户端只需要和上下文进行交互就可以了。这样可以让策略模式更具有整体性,客户端也更加的简单具体的链接:https://www.zhihu.com/question/31162942所以我们再说回上文的通用类图,我们就可以这样看了:1.2策略模式例子现在3y拥有一个公众号,名称叫做Java3y。3y想要这让更多的人认识到Java3y这个公众号。所以每天都在想怎么涨粉(hahah于是3y就开始想办法了(操碎了心),同时3y在这一段时间下来发现涨粉的方式有很多。为了方便,定义一个通用的接口方便来管理和使用呗。接口:/* * 增加粉丝策略的接口(Strategy) /interface IncreaseFansStrategy { void action();}涨粉的具体措施,比如说,请水军:/* * 请水军(ConcreteStrategy) /public class WaterArmy implements IncreaseFansStrategy { @Override public void action() { System.out.println(“3y牛逼,我要给你点赞、转发、加鸡腿!”); }}涨粉的具体措施,比如说,认真写原创:/* * 认真写原创(ConcreteStrategy) /public class OriginalArticle implements IncreaseFansStrategy{ @Override public void action() { System.out.println(“3y认真写原创,最新一篇文章:《策略模式,就这?》”); }}3y还想到了很多涨粉的方法,比如说送书活动啊、商业互吹啊等等等…(这里就不细说了)说到底,无论是哪种涨粉方法,都是通过3y去执行的。/* * 3y(Context) /public class Java3y { private IncreaseFansStrategy strategy ; public Java3y(IncreaseFansStrategy strategy) { this.strategy = strategy; } // 3y要发文章了(买水军了、送书了、写知乎引流了…)。 // 具体执行哪个,看3y选哪个 public void exec() { strategy.action(); }}所以啊,每当到了发推文的时候,3y就可以挑用哪种方式涨粉了:public class Main { public static void main(String[] args) { // 今天2018年12月24日 Java3y java3y = new Java3y(new WaterArmy()); java3y.exec(); // 明天2018年12月25日 Java3y java4y = new Java3y(new OriginalArticle()); java4y.exec(); // …… }}执行结果:1.3策略模式优缺点优点:算法可以自由切换改一下策略很方便扩展性良好增加一个策略,就多增加一个类就好了。缺点:策略类的数量增多每一个策略都是一个类,复用的可能性很小、类数量增多所有的策略类都需要对外暴露上层模块必须知道有哪些策略,然后才能决定使用哪一个策略1.4JDK的策略模式应用不知道大家还能不能想起ThreadPoolExecutor(线程池):线程池你真不来了解一下吗?学习ThreadPoolExecutor(线程池)就肯定要知道它的构造方法每个参数的意义: /* * Handler called when saturated or shutdown in execute. / private volatile RejectedExecutionHandler handler; public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler) { //…. this.handler = handler; } /* * Invokes the rejected execution handler for the given command. * Package-protected for use by ScheduledThreadPoolExecutor. */ final void reject(Runnable command) { handler.rejectedExecution(command, this); }其中我们可以找到RejectedExecutionHandler,这个参数代表的是拒绝策略(有四种具体的实现:直接抛出异常、使用调用者的线程来处理、直接丢掉这个任务、丢掉最老的任务)其实这就是策略模式的体现了。最后看完会不会觉得策略模式特别简单呀?就一个算法接口、多个算法实现、一个Context来包装一下,就完事了。推荐阅读和参考资料:https://www.cnblogs.com/lewis0077/p/5133812.html《设计模式之禅》乐于分享和输出干货的Java技术公众号:Java3y。文章的目录导航:https://github.com/ZhongFuCheng3y/3y ...

December 28, 2018 · 2 min · jiezi

Java编程之设计模式之工厂方法模式全解

1 日志记录器的设计Sunny软件公司欲开发一个系统运行日志记录器(Logger),该记录器可以通过多种途径保存系统的运行日志,如通过文件记录或数据库记录,用户可以通过修改配置文件灵活地更换日志记录方式。在设计各类日志记录器时,Sunny公司的开发人员发现需要对日志记录器进行一些初始化工作,初始化参数的设置过程较为复杂,而且某些参数的设置有严格的先后次序,否则可能会发生记录失败。如何封装记录器的初始化过程并保证多种记录器切换的灵活性是Sunny公司开发人员面临的一个难题。Sunny公司的开发人员通过对该需求进行分析,发现该日志记录器有两个设计要点:(1) 需要封装日志记录器的初始化过程,这些初始化工作较为复杂,例如需要初始化其他相关的类,还有可能需要读取配置文件(例如连接数据库或创建文件),导致代码较长,如果将它们都写在构造函数中,会导致构造函数庞大,不利于代码的修改和维护;(2) 用户可能需要更换日志记录方式,在客户端代码中需要提供一种灵活的方式来选择日志记录器,尽量在不修改源代码的基础上更换或者增加日志记录方式。Sunny公司开发人员最初使用简单工厂模式对日志记录器进行了设计,初始结构如图1所示:基于简单工厂模式设计的日志记录器结构图在上图中,LoggerFactory充当创建日志记录器的工厂,提供了工厂方法createLogger()用于创建日志记录器,Logger是抽象日志记录器接口,其子类为具体日志记录器。其中,工厂类LoggerFactory代码片段如下所示://日志记录器工厂class LoggerFactory { //静态工厂方法public static Logger createLogger(String args) { if(args.equalsIgnoreCase(“db”)) { //连接数据库,代码省略 //创建数据库日志记录器对象 Logger logger = new DatabaseLogger(); //初始化数据库日志记录器,代码省略 return logger; } else if(args.equalsIgnoreCase(“file”)) { //创建日志文件 //创建文件日志记录器对象 Logger logger = new FileLogger(); //初始化文件日志记录器,代码省略 return logger; } else { return null; }}}为了突出设计重点,我们对上述代码进行了简化,省略了具体日志记录器类的初始化代码。在LoggerFactory类中提供了静态工厂方法createLogger(),用于根据所传入的参数创建各种不同类型的日志记录器。通过使用简单工厂模式,我们将日志记录器对象的创建和使用分离,客户端只需使用由工厂类创建的日志记录器对象即可,无须关心对象的创建过程,但是我们发现,虽然简单工厂模式实现了对象的创建和使用分离,但是仍然存在如下两个问题:(1) 工厂类过于庞大,包含了大量的if…else…代码,导致维护和测试难度增大;(2) 系统扩展不灵活,如果增加新类型的日志记录器,必须修改静态工厂方法的业务逻辑,违反了“开闭原则”。如何解决这两个问题,提供一种简单工厂模式的改进方案?这就是本文所介绍的工厂方法模式的动机之一。2 工厂方法模式概述在简单工厂模式中只提供一个工厂类,该工厂类处于对产品类进行实例化的中心位置,它需要知道每一个产品对象的创建细节,并决定何时实例化哪一个产品类。简单工厂模式最大的缺点是当有新产品要加入到系统中时,必须修改工厂类,需要在其中加入必要的业务逻辑,这违背了“开闭原则”。此外,在简单工厂模式中,所有的产品都由同一个工厂创建,工厂类职责较重,业务逻辑较为复杂,具体产品与工厂类之间的耦合度高,严重影响了系统的灵活性和扩展性,而工厂方法模式则可以很好地解决这一问题。在工厂方法模式中,我们不再提供一个统一的工厂类来创建所有的产品对象,而是针对不同的产品提供不同的工厂,系统提供一个与产品等级结构对应的工厂等级结构。工厂方法模式定义如下:工厂方法模式(Factory Method Pattern):定义一个用于创建对象的接口,让子类决定将哪一个类实例化。工厂方法模式让一个类的实例化延迟到其子类。工厂方法模式又简称为工厂模式(Factory Pattern),又可称作虚拟构造器模式(Virtual Constructor Pattern)或多态工厂模式(Polymorphic Factory Pattern)。工厂方法模式是一种类创建型模式。工厂方法模式提供一个抽象工厂接口来声明抽象工厂方法,而由其子类来具体实现工厂方法,创建具体的产品对象。工厂方法模式结构如图2所示:图2 工厂方法模式结构图在工厂方法模式结构图中包含如下几个角色:● Product(抽象产品):它是定义产品的接口,是工厂方法模式所创建对象的超类型,也就是产品对象的公共父类。● ConcreteProduct(具体产品):它实现了抽象产品接口,某种类型的具体产品由专门的具体工厂创建,具体工厂和具体产品之间一一对应。● Factory(抽象工厂):在抽象工厂类中,声明了工厂方法(Factory Method),用于返回一个产品。抽象工厂是工厂方法模式的核心,所有创建对象的工厂类都必须实现该接口。● ConcreteFactory(具体工厂):它是抽象工厂类的子类,实现了抽象工厂中定义的工厂方法,并可由客户端调用,返回一个具体产品类的实例。与简单工厂模式相比,工厂方法模式最重要的区别是引入了抽象工厂角色,抽象工厂可以是接口,也可以是抽象类或者具体类,其典型代码如下所示:interface Factory { public Product factoryMethod();}在抽象工厂中声明了工厂方法但并未实现工厂方法,具体产品对象的创建由其子类负责,客户端针对抽象工厂编程,可在运行时再指定具体工厂类,具体工厂类实现了工厂方法,不同的具体工厂可以创建不同的具体产品,其典型代码如下所示:class ConcreteFactory implements Factory { public Product factoryMethod() { return new ConcreteProduct(); }}在实际使用时,具体工厂类在实现工厂方法时除了创建具体产品对象之外,还可以负责产品对象的初始化工作以及一些资源和环境配置工作,例如连接数据库、创建文件等。在客户端代码中,只需关心工厂类即可,不同的具体工厂可以创建不同的产品,典型的客户端类代码片段如下所示:……Factory factory;factory = new ConcreteFactory(); //可通过配置文件实现Product product;product = factory.factoryMethod();……可以通过配置文件来存储具体工厂类ConcreteFactory的类名,更换新的具体工厂时无须修改源代码,系统扩展更为方便。3 完整解决方案Sunny公司开发人员决定使用工厂方法模式来设计日志记录器,其基本结构如图3所示:图3 日志记录器结构图在图3中,Logger接口充当抽象产品,其子类FileLogger和DatabaseLogger充当具体产品,LoggerFactory接口充当抽象工厂,其子类FileLoggerFactory和DatabaseLoggerFactory充当具体工厂。完整代码如下所示://日志记录器接口:抽象产品interface Logger {public void writeLog();}//数据库日志记录器:具体产品class DatabaseLogger implements Logger {public void writeLog() { System.out.println(“数据库日志记录。”);}}//文件日志记录器:具体产品class FileLogger implements Logger {public void writeLog() { System.out.println(“文件日志记录。”);}}//日志记录器工厂接口:抽象工厂interface LoggerFactory {public Logger createLogger();}//数据库日志记录器工厂类:具体工厂class DatabaseLoggerFactory implements LoggerFactory {public Logger createLogger() { //连接数据库,代码省略 //创建数据库日志记录器对象 Logger logger = new DatabaseLogger(); //初始化数据库日志记录器,代码省略 return logger;} }//文件日志记录器工厂类:具体工厂class FileLoggerFactory implements LoggerFactory {public Logger createLogger() {//创建文件日志记录器对象 Logger logger = new FileLogger(); //创建文件,代码省略 return logger;} } 编写如下客户端测试代码:class Client {public static void main(String args[]) { LoggerFactory factory; Logger logger; factory = new FileLoggerFactory(); //可引入配置文件实现 logger = factory.createLogger(); logger.writeLog();}}编译并运行程序,输出结果如下:文件日志记录。4 反射与配置文件为了让系统具有更好的灵活性和可扩展性,Sunny公司开发人员决定对日志记录器客户端代码进行重构,使得可以在不修改任何客户端代码的基础上更换或增加新的日志记录方式。在客户端代码中将不再使用new关键字来创建工厂对象,而是将具体工厂类的类名存储在配置文件(如XML文件)中,通过读取配置文件获取类名字符串,再使用Java的反射机制,根据类名字符串生成对象。在整个实现过程中需要用到两个技术:Java反射机制与配置文件读取。软件系统的配置文件通常为XML文件,我们可以使用DOM (Document Object Model)、SAX (Simple API for XML)、StAX (Streaming API for XML)等技术来处理XML文件。关于DOM、SAX、StAX等技术的详细学习大家可以参考其他相关资料,在此不予扩展。Java反射(Java Reflection)是指在程序运行时获取已知名称的类或已有对象的相关信息的一种机制,包括类的方法、属性、父类等信息,还包括实例的创建和实例类型的判断等。在反射中使用最多的类是Class,Class类的实例表示正在运行的Java应用程序中的类和接口,其forName(String className)方法可以返回与带有给定字符串名的类或接口相关联的 Class对象,再通过Class对象的newInstance()方法创建此对象所表示的类的一个新实例,即通过一个类名字符串得到类的实例。如创建一个字符串类型的对象,其代码如下://通过类名生成实例对象并将其返回 Class c=Class.forName(“String”); Object obj=c.newInstance(); return obj;此外,在JDK中还提供了java.lang.reflect包,封装了其他与反射相关的类,此处只用到上述简单的反射代码,在此不予扩展。Sunny公司开发人员创建了如下XML格式的配置文件config.xml用于存储具体日志记录器工厂类类名:<!— config.xml –><?xml version=“1.0”?><config><className>FileLoggerFactory</className></config>为了读取该配置文件并通过存储在其中的类名字符串反射生成对象,Sunny公司开发人员开发了一个名为XMLUtil的工具类,其详细代码如下所示://工具类XMLUtil.javaimport javax.xml.parsers.;import org.w3c.dom.;import org.xml.sax.SAXException;import java.io.*;public class XMLUtil {//该方法用于从XML配置文件中提取具体类类名,并返回一个实例对象public static Object getBean() { try { //创建DOM文档对象 DocumentBuilderFactory dFactory = DocumentBuilderFactory.newInstance(); DocumentBuilder builder = dFactory.newDocumentBuilder(); Document doc; doc = builder.parse(new File(“config.xml”)); //获取包含类名的文本节点 NodeList nl = doc.getElementsByTagName(“className”);Node classNode=nl.item(0).getFirstChild(); String cName=classNode.getNodeValue();//通过类名生成实例对象并将其返回 Class c=Class.forName(cName); Object obj=c.newInstance();return obj; } catch(Exception e) { e.printStackTrace(); return null;} }}有了XMLUtil类后,可以对日志记录器的客户端代码进行修改,不再直接使用new关键字来创建具体的工厂类,而是将具体工厂类的类名存储在XML文件中,再通过XMLUtil类的静态工厂方法getBean()方法进行对象的实例化,代码修改如下:class Client {public static void main(String args[]) { LoggerFactory factory; Logger logger; factory = (LoggerFactory)XMLUtil.getBean(); //getBean()的返回类型为Object,需要进行强制类型转换 logger = factory.createLogger(); logger.writeLog();}}引入XMLUtil类和XML配置文件后,如果要增加新的日志记录方式,只需要执行如下几个步骤:(1) 新的日志记录器需要继承抽象日志记录器Logger;(2) 对应增加一个新的具体日志记录器工厂,继承抽象日志记录器工厂LoggerFactory,并实现其中的工厂方法createLogger(),设置好初始化参数和环境变量,返回具体日志记录器对象;(3) 修改配置文件config.xml,将新增的具体日志记录器工厂类的类名字符串替换原有工厂类类名字符串;(4) 编译新增的具体日志记录器类和具体日志记录器工厂类,运行客户端测试类即可使用新的日志记录方式,而原有类库代码无须做任何修改,完全符合“开闭原则”。通过上述重构可以使得系统更加灵活,由于很多设计模式都关注系统的可扩展性和灵活性,因此都定义了抽象层,在抽象层中声明业务方法,而将业务方法的实现放在实现层中。5 重载的工厂方法Sunny公司开发人员通过进一步分析,发现可以通过多种方式来初始化日志记录器,例如可以为各种日志记录器提供默认实现;还可以为数据库日志记录器提供数据库连接字符串,为文件日志记录器提供文件路径;也可以将参数封装在一个Object类型的对象中,通过Object对象将配置参数传入工厂类。此时,可以提供一组重载的工厂方法,以不同的方式对产品对象进行创建。当然,对于同一个具体工厂而言,无论使用哪个工厂方法,创建的产品类型均要相同。如图4所示:图4 重载的工厂方法结构图引入重载方法后,抽象工厂LoggerFactory的代码修改如下:interface LoggerFactory {public Logger createLogger();public Logger createLogger(String args);public Logger createLogger(Object obj);}具体工厂类DatabaseLoggerFactory代码修改如下:class DatabaseLoggerFactory implements LoggerFactory {public Logger createLogger() { //使用默认方式连接数据库,代码省略 Logger logger = new DatabaseLogger(); //初始化数据库日志记录器,代码省略 return logger;}public Logger createLogger(String args) { //使用参数args作为连接字符串来连接数据库,代码省略 Logger logger = new DatabaseLogger(); //初始化数据库日志记录器,代码省略 return logger;} public Logger createLogger(Object obj) { //使用封装在参数obj中的连接字符串来连接数据库,代码省略 Logger logger = new DatabaseLogger(); //使用封装在参数obj中的数据来初始化数据库日志记录器,代码省略 return logger;} }//其他具体工厂类代码省略在抽象工厂中定义多个重载的工厂方法,在具体工厂中实现了这些工厂方法,这些方法可以包含不同的业务逻辑,以满足对不同产品对象的需求。6 工厂方法的隐藏有时候,为了进一步简化客户端的使用,还可以对客户端隐藏工厂方法,此时,在工厂类中将直接调用产品类的业务方法,客户端无须调用工厂方法创建产品,直接通过工厂即可使用所创建的对象中的业务方法。如果对客户端隐藏工厂方法,日志记录器的结构图将修改为图5所示:图5 隐藏工厂方法后的日志记录器结构图在图5中,抽象工厂类LoggerFactory的代码修改如下://改为抽象类abstract class LoggerFactory { //在工厂类中直接调用日志记录器类的业务方法writeLog()public void writeLog() { Logger logger = this.createLogger(); logger.writeLog();}public abstract Logger createLogger(); } 客户端代码修改如下:class Client {public static void main(String args[]) { LoggerFactory factory; factory = (LoggerFactory)XMLUtil.getBean(); factory.writeLog(); //直接使用工厂对象来调用产品对象的业务方法}}通过将业务方法的调用移入工厂类,可以直接使用工厂对象来调用产品对象的业务方法,客户端无须直接使用工厂方法,在某些情况下我们也可以使用这种设计方案。7 工厂方法模式总结工厂方法模式是简单工厂模式的延伸,它继承了简单工厂模式的优点,同时还弥补了简单工厂模式的不足。工厂方法模式是使用频率最高的设计模式之一,是很多开源框架和API类库的核心模式。主要优点工厂方法模式的主要优点如下:(1) 在工厂方法模式中,工厂方法用来创建客户所需要的产品,同时还向客户隐藏了哪种具体产品类将被实例化这一细节,用户只需要关心所需产品对应的工厂,无须关心创建细节,甚至无须知道具体产品类的类名。(2) 基于工厂角色和产品角色的多态性设计是工厂方法模式的关键。它能够让工厂可以自主确定创建何种产品对象,而如何创建这个对象的细节则完全封装在具体工厂内部。工厂方法模式之所以又被称为多态工厂模式,就正是因为所有的具体工厂类都具有同一抽象父类。(3) 使用工厂方法模式的另一个优点是在系统中加入新产品时,无须修改抽象工厂和抽象产品提供的接口,无须修改客户端,也无须修改其他的具体工厂和具体产品,而只要添加一个具体工厂和具体产品就可以了,这样,系统的可扩展性也就变得非常好,完全符合“开闭原则”。主要缺点工厂方法模式的主要缺点如下:(1) 在添加新产品时,需要编写新的具体产品类,而且还要提供与之对应的具体工厂类,系统中类的个数将成对增加,在一定程度上增加了系统的复杂度,有更多的类需要编译和运行,会给系统带来一些额外的开销。(2) 由于考虑到系统的可扩展性,需要引入抽象层,在客户端代码中均使用抽象层进行定义,增加了系统的抽象性和理解难度,且在实现时可能需要用到DOM、反射等技术,增加了系统的实现难度。适用场景在以下情况下可以考虑使用工厂方法模式:(1) 客户端不知道它所需要的对象的类。在工厂方法模式中,客户端不需要知道具体产品类的类名,只需要知道所对应的工厂即可,具体的产品对象由具体工厂类创建,可将具体工厂类的类名存储在配置文件或数据库中。(2) 抽象工厂类通过其子类来指定创建哪个对象。在工厂方法模式中,对于抽象工厂类只需要提供一个创建产品的接口,而由其子类来确定具体要创建的对象,利用面向对象的多态性和里氏代换原则,在程序运行时,子类对象将覆盖父类对象,从而使得系统更容易扩展。 ...

December 27, 2018 · 2 min · jiezi

单一职责原则

个人博客原文:单一职责原则设计模式六大原则之一:单一职责原则简介姓名 :单一职责原则英文名 :Single Responsibility Principle座右铭 :There should never be more than one reason for a class to change. 应当有且仅有一个原因引起类的变更。。。意思就是不管干啥,我都只干一件事,你叫我去买菜,我就只买菜,叫我顺便去倒垃圾就不干了,就这么拽脾气 :一个字“拽”,两个字“特拽“伴侣 :老子职责单一,哪来的伴侣?个人介绍 :在这个人兼多责的社会里,我显得那么的特立独行,殊不知,现在社会上发生的很多事情都是因为没有处理好职责导致的,比如,经常有些父母带着小孩,一边玩手机,导致小孩弄丢、发生事故等等单一职责应用范围单一职责原则适用的范围有接口、方法、类。按大家的说法,接口和方法必须保证单一职责,类就不必保证,只要符合业务就行。方法设想一下这个场景:假设我们要做一个用户修改名字以及修改密码的功能,可以有多种实现方案,比如下面列举 2 种实现方式代码:SrpOfMethod.java第一种实现方式/** * 错误的示范 /enum OprType { /* * 更新密码 / UPDATE_PASSWORD, /* * 更新名字 / UPDATE_NAME;}interface UserOpr { boolean updateUserInfo(User user, OprType oprType);}class UserOprImpl implements UserOpr { @Override public boolean updateUserInfo(User user, OprType oprType) { if (oprType == OprType.UPDATE_NAME) { // update name } else if (oprType == OprType.UPDATE_PASSWORD) { // update password } return true; }}第二种实现方式/* * 正确的示范 /interface UserOpr2 { boolean updatePassword(User user, String password); boolean updateUserInfo(User user);}class UserOprImpl2 implements UserOpr2 { @Override public boolean updatePassword(User user, String password) { user.setPassword(password); // update password return true; } @Override public boolean updateUserInfo(User user) { // update user info return true; }}2 种实现有什么区别呢? 第一种实现通过 OprType 类型的不同来做不同的事情,把修改密码和修改名字耦合在一起,容易引起问题,只要稍不注意,传错枚举值就悲剧了,在代码中也没法很直接看到是做什么操作,也就是这个方法的职责不明确。而第二种实现,把修改密码和修改名字分离开来,也就是把修改密码和修改名字都当做独自的职责处理,这样子就很清晰明了,你调用哪个方法,就很明确的知道这个方法是实现什么逻辑。结论是啥呢?用第二种方式实习才符合单一职责原则。现实中看到很多像第一种实现的代码,而且是枚举有十来个的情况,看代码真费劲。接口设想一下这个场景,假设我们让小明去倒垃圾,小红去买菜,小红回来后再叫小红去洗碗。下面也举 2 个实现的例子。代码:SrpOfInterface.java第一种实现方式/* * 错误的示范 /interface Housework { void shopping(); void pourGarbage();}class XiaoMing implements Housework { @Override public void shopping() { // 不购物 } @Override public void pourGarbage() { System.out.println(“pourGarbage …”); }}class XiaoHong implements Housework { @Override public void shopping() { System.out.println(“shopping …”); } @Override public void pourGarbage() { // 从不倒垃圾 }}中途回来小红去洗碗,要怎么实现?按这个写法,就在 Housework 接口添加 washingUp() 方法,然后小明和小红依次都实现洗碗这个方法,只是小明不做具体实现代码,这样子是不是觉得很别扭,不符合单一职责原则的,修改一个地方,不影响其他不需要改变的地方,只对需要用到的地方做修改。小明本来就不用洗碗,却要去实现洗碗这个方法。第二种实现方式/* * 正确的示范 */interface Shopping { void doShopping();}interface PourGarbage { void doPourGarbage();}interface WashingUp { void doWashingUp();}class XiaoMing2 implements PourGarbage { @Override public void doPourGarbage() { System.out.println(“pourGarbage …”); }}class XiaoHong2 implements Shopping, WashingUp { @Override public void doShopping() { System.out.println(“shopping …”); } @Override public void doWashingUp() { System.out.println(“washing up …”); }}可以看到,这种实现把不同的家务都当做不同的职责,分离开来,这种实现可以按需实现做家务的类型,小明只需要去倒垃圾,就实现 PourGarbage 接口,小红去购物和洗碗,就实现 Shopping 和 WashingUp 接口,完全不会影响到对方,这才是完美的根据单一职责原则编写出来的代码。类类这个看了一些资料都说没法硬性要求一定按单一职责原则分,或者说类的职责可大可小,没有很明确的像上面接口那样按照单一职责原则分就很清晰也很有道理。设想一下这个场景:我们要实现一个用户注册、登录、注销操作,可以像如下 2 种实现方式代码:SrpOfClass.java第一种实现方式从用户的角度考虑,这些操作都是用户的行为,可以放在一个统一的类 UserBizclass UserBiz { public boolean register(User user){ // 注册操作 return true; } public boolean login(User user) { // 登录操作 return true; } public boolean logout(User user) { // 注销操作 return true; }}第二种实现方式有人又说,不是说单一职责么?从业务操作考虑,需要把注册、登录、注销分开class UserRegisterBiz { public boolean register(User user){ // 注册操作 return true; }}class UserLoginBiz { public boolean login(User user) { // 登录操作 return true; }}class UserLogoutBiz { public boolean logout(User user) { // 注销操作 return true; }}感觉像是在抬杠,其实这个没有好坏之分,根据具体业务具体分析,你说你的登录、注册、注销操作代码很多,需要分开,那就分开,无可厚非。好处类的复杂性降低,实现什么职责都有清晰明确的定义可读性提高,复杂性降低,那当然可读性提高了可维护性提高,可读性提高,那当然更容易维护了变更引起的风险降低,变更是必不可少的,如果接口的单一职责做得好,一个接口修改只对相应的实现类有影响,对其他的接口无影响,这对系统的扩展性、维护性都有非常大的帮助(来自《设计模式之禅》)总结这个单一职责原则,目的就是提高代码的可维护性、可读性、扩展性,如果为了单一职责而破坏了这 3 个特性,可能会得不偿失。参考资料:《大话设计模式》、《Java设计模式》、《设计模式之禅》、《研磨设计模式》、《Head First 设计模式》 ...

December 27, 2018 · 2 min · jiezi

什么是SOLID原则(第3部分)

让我们从最后一个 SOLID 原则开始吧,即依赖倒置原则(Dependency Inversion Principle,简称 DIP)(不要和依赖注入Dependency Injection ,DI 弄混淆了)。这个原则所说的是高级模块不应该依赖具象的低级模块,它们都应该依赖相应模块的抽象层。我仍将使用自行车的示例来尝试给你解释这个原则。首选看下这个 Bike 接口:interface Bike { void pedal() void backPedal()}MountainBike 和 ClassicBike 这两个类实现了上面的接口:// 山地车class MountainBike implements Bike { override void pedal() { // complex code that computes the inner workings of what happens // when pedalling on a mountain bike, which includes taking into // account the gear in which the bike currently is. } override void backPedal() { // complex code that computes what happens when we back pedal // on a mountain bike, which is that you pedal in the wrong // direction with no discernible effect on the bike }}// 传统自行车class ClassicBike implements Bike { override void pedal() { // the same as for the mountain bike with the distinction that // there is a single gear on a classic bike } override void backPedal() { // complex code that actually triggers the brake function on the // bike }}正如你所看到的,踩脚踏板(pedal)会让自行车向前行驶,但是山地车 MountainBike 因为有多个齿轮,所以它的 pedal 会更加复杂。另外,向后踩脚踏板(back pedal)时,山地车不会做任何事,而传统自行车 ClassicBike 则会触发刹车操作。我之所以在每个方法的注释中都有提到“complex code”,是因为我想指出我们应该把上述代码移动到不同的模块中。我们这样做是为了简化自行车类以及遵循单一职责原则(自行车类不应该担起在你向前或向后踩脚踏板时究竟发生了什么的计算工作,它们应该处理有关自行车的更高级别的事情)。为了做到这一点,我们将为每种类型的 pedalling 创建一些行为类。class MountainBikePedalBehaviour { void pedal() { //complex code }}class MountainBikeBackPedalBehaviour { void backPedal() { // complex code }}class ClassicBikePedalBehaviour { void pedal() { // complex code }}class ClassicBikeBackPedalBehaviour { void backPedal() { // complex code }}然后像下面这样使用这些类:// 山地车class MountainBike implements Bike { override void pedal() { var pedalBehaviour = new MountainBikePedalBehaviour() pedalBehaviour.pedal() } override void backPedal() { var backPedalBehaviour = new MountainBikeBackPedalBehaviour() backPedalBehaviour.backPedal() }}// 传统自行车class ClassicBike implements Bike { override void pedal() { var pedalBehaviour = new ClassicBikePedalBehaviour() pedalBehaviour.pedal() } override void backPedal() { var backPedalBehaviour = new ClassicBikeBackPedalBehaviour() backPedalBehaviour.backPedal() }}这个时候,我们可以很清楚地看到高级模块 MountainBike 依赖于某些具体的低级模块 MountainBikePedalBehaviour 和 MountainBikeBackPedalBehaviour。ClassicBike 以及它的低级模块同样如此。根据依赖倒置原则,高级模块和低级模块都应该依赖抽象。为此,我们需要以下接口:interface PedalBehaviour { void pedal()}interface BackPedalBehaviour { void backPedal()}除了需要实现上面的接口外,行为类的代码与之前无异:class MountainBikePedalBehaviour implements PedalBehaviour { override void pedal() { // same as before }}剩下的其他行为类同上。现在我们需要一种方法将 PedalBehaviour 和 BackPedalBehaviour 传递给 MountainBike 和 ClassicBike 类。我们可以选择在构造方法、pedal() 、pedalBack() 中完成这件事。本例中,我们使用构造方法。class MountainBike implements Bike { PedalBehaviour pedalBehaviour; BackPedalBehaviour backPedalBehaviour; public MountainBike(PedalBehaviour pedalBehaviour, BackPedalBehaviour backPedalBehaviour) { this.pedalBehaviour = pedalBehaviour; this.backPedalBehaviour = backPedalBehaviour; } override void pedal() { pedalBehaviour.pedal(); } override void backPedal() { backPedalBehaviour.backPedal(); }}ClassicBike 类同上。我们的高级模块(MountainBike 和 ClassicBike)不再依赖于具体的低级模块,而是依赖于抽象的 PedalBehaviour 和 BackPedalBehaviour。在我们的例子中,我们应用的主模块可能看起来向下面这样:class MainModule { MountainBike mountainBike; ClassicBike classicBike; MountainBikePedalBehaviour mountainBikePedalBehaviour; ClassicBikePedalBehaviour classicBikePedalBehaviour; MountainBikeBackPedalBehaviour mountainBikeBackPedalBehaviour; ClassicBikeBackPedalBehaviour classicBikeBackPedalBehaviour; public MainModule() { mountainBikePedalBehaviour = new MountainBikePedalBehaviour(); mountainBikeBackPedalBehaviour = new MountainBikeBackPedalBehaviour(); mountainBike = new MountainBike(mountainBikePedalBehaviour, mountainBikeBackPedalBehaviour); classicBikePedalBehaviour = new ClassicBikePedalBehaviour(); classicBikeBackPedalBehaviour = new ClassicBikeBackPedalBehaviour(); classicBike = new ClassicBike(classicBikePedalBehaviour, classicBikeBackPedalBehaviour); } public void pedalBikes() { mountainBike.pedal() classicBike.pedal() } public void backPedalBikes() { mountainBike.backPedal(); classicBike.backPedal(); }}可以看到,我们的 MainModule 依赖了具体的低级模块而不是抽象层。我们可以通过向构造方法中传递依赖来改善这种情况:public MainModule(Bike mountainBike, Bike classicBike, PedalBehaviour mBikePB, BackPedalBehaviour mBikeBPB, PedalBehaviour cBikePB, BackPedalBehaviour cBikeBPB)…现在,MainModule 部分依赖了抽象层,部分依赖了低级模块,这些低级模块也依赖了那些抽象层。所有这些模块之间的关系不再依赖于实现细节。在我们到达应用程序中的最高模块之前,为了尽可能地延迟一个具体类的实例化,我们通常要靠依赖注入和实现了依赖注入的框架。你可以在 这里 找到更多有关依赖注入的信息。我们可以将依赖注入视为帮助我们实现依赖倒置的工具。我们不断地向依赖链中传递依赖关系以避免具体类的实例化。那么为什么要经历这一切呢?不依赖于具象的一个优点就是我们可以模拟一个类,从而使测试更容易进行。我们来看一个简单的例子。interface Network { public String getServerResponse(URL serverURL);}class NetworkRequestHandler implements Network { override public String getServerResponse(URL serverURL) { // network code implementation }}假设我们还有一个 NetworkManager 类,它有一个公共方法,通过使用一个 Network 的实例返回服务器响应:public String getResponse(Network networkRequestHandler, URL url) { return networkRequestHandler.getServerResponse(url)}因为这样的代码结构,我们可以测试代码如何处理来自服务器的“404”响应。为此,我们将创 NetworkRequestHandler的模拟版本。我们之所以可以这么做,是因为 NetworkManager 依赖于抽象层,即 Network,而不是某个具体的 NetworkRequestHandler。class Mock404 implements Network { override public String getServerResponse(URL serverURL) { return “404” }}通过调用 getResponse 方法,传递 Mock404 类的实例,我们可以很容易地测试我们期望的行为。像 Mockito 这样的模拟库可以帮助你模拟某些类,而无需编写单独的类来执行此操作。除了易于测试,我们的应用在多变情景下也能应对自如。因为模块之间的关系是基于抽象的,我们可以更改具体模块的实现,而无需大范围地更改代码。最后同样重要的是这会让事情变得更简单。如果你有留意自行车的示例,你会发现 MountainBike 和 ClassicBike 类非常相似。这就意味着我们不再需要单独的类了。我们可以创建一个简单的实现了 Bike 接口的类 GenericBike,然后山地车和传统自行车的实例化就像下面这样:GenericBike mountainBike = new GenericBike(mbPedalB, mbBackPedalB);GenericBike classicBike = new GenericBike(cbPedalB, cbBackPedalB);我们减少了一半数量的具体自行车类的实现,这意味着我们的代码更容易管理。总结所有这些原则可能看起来有点矫枉过正,你可能会排斥它们。在很长的一段时间里,我和你一样。随着时间的推移,我开始逐渐把我的代码向增强可测试性和更易于维护的方向转变。渐渐地,我开始这样来思考事情:“如果只有一种方法可以把两个部分的内容分开,并将其放在不同的类中,以便我能……”。通常,答案是的确存在这样的一种方法,并且别人已经实现过了。大多数时候,这种方法都受到 SOLID 原则的启发。当然,紧迫的工期和其他现实生活中的因素可能会不允许你遵守所有这些原则。虽然很难 100% 实现 SOLID 原则,但是有比没有强吧。也许你可以尝试只在那些当需求变更时最容易受影响的部分遵守这些原则。你不必过分遵循它们,可以把这些原则视为你提高代码质量的指南。如果你不得不需要制作一个快速原型或者验证一个概念应用的可行性,那么你没有必要尽力去搭一个最佳架构。SOLID更像是一个长期策略,对于必须经得起时间考验的软件非常有用。在这篇由三部分组成的文章中,我试图给你展示了一些有关 SOLID的我发现比较有趣的东西。关于 SOLID,还有很多看法和解释,为了更好地理解和从多个角度获取知识,请多阅读些其他文章。我希望这篇文章对你有所帮助。……如果你还没有看过另两个部分,这里是它们的链接,第1部分 和 第2部分 。如果你喜欢这篇文章,你可以在我们的 官方站点 上找到更多信息。 ...

December 23, 2018 · 3 min · jiezi

什么是SOLID原则(第2部分)

翻译自:What’s the deal with the SOLID principles? (part 2)在文章的 第1部分,我们主要讨论了前两个 SOLID 原则,它们分别是单一职责原则和开闭原则。在这一部分,我们将按照首字母缩略词中的顺序来处理接下来的两个原则。让我们启程吧!L在 SOLID 原则中,最具神秘色彩的就是里氏替换原则(Liskov Substitution Principle,简称 LSP)了。此原则以 Barbara Liskov 的名字命名,他在 1987年 首次提出了这一原则。里氏替换原则要阐述的内容是:如果对象 A 是对象 B 的子类,或者对象 A 实现了接口 B(本质上讲,A 就是 B 的一个实例),那么我们应该能够在不做任何特殊处理的情况下,像使用一个对象 B 或者 B 的一个实例那样使用对象 A。为了理清思路,让我们看一个关于多个自行车的示例。Bike 基类如下:class Bike { void pedal() { // pedal code } void steer() { // steering code } void handBrakeFront() { // hand braking front code } void handBrakeBack() { // hand braking back code }}山地自行车类 MountainBike 继承自基类 Bike (译者注:山地车有通过齿轮的机械原理调整档位的特性):class MountainBike extends Bike { void changeGear() { // change gear code }}MountainBike 类遵循了里氏替换原则,因为它能够被当作一个 Bike 类的对象使用。如果我们有一个自行车类型数组,并用 Bike 和 MountainBike 的实例对象来填充它,那么我们完全可以正确无误地调用 steer() 、pedal() 等 Bike 基类的所有方法。所以,我们可以在不经过特殊处理的情况下,把 MountainBike 类型的元素当作 Bike 类型的元素来使用。现在想象一下,我们添加了一个名为 ClassicBike 的类,如下所示:class ClassicBike extends Bike { void footBrake() { // foot braking code }}这个类代表了一种经典自行车,你可以通过向后踩踏板来进行制动。这种自行车没有手刹。基于此,如果我们有一个 ClassicBike 类型的元素混在了上述的自行车数组中,我们仍然能够无误地调用 steer 和 pedal 方法。但是,当我们尝试调用 handBrakeFront 或者 handBrakeBack 的时候,问题就暴露出来了。取决于具体的实现,调用这些方法可能导致系统崩溃或者什么也不会做。我们可以通过检查当前元素是否是 ClassicBike 的实例来解决这个问题:foreach(var bike in bikes) { bike.pedal() bike.steer() if(bike is ClassicBike) { bike.footBrake() } else { bike.handBrakeFront() bike.handBrakeBack() }}如你所见,假如没有类似上面的类型判断,我们就不能再把一个 ClassicBike 的实例看作一个 Bike 实例了。这显然违背了里氏替换原则。有多种方法可以解决这个问题,当我们讨论到 SOLID 中的 I 原则时,就会看到一个解决之道。遵循里氏替换原则的一个有趣的后果就是你编写的代码将很难不符合开闭原则。【译者注】原文中,上图有个标题“There is no spoon”,这句话是电影《黑客帝国》的一句台词。面向对象编程存在的一个问题就是我们常常忘记正在打交道的数据,以及处理这些数据和真实世界里对象的关系。现实生活中的有些事情无法在代码中直接建立模型,因此我们必须牢记:抽象本身并不神奇,底层的数据仅是数据而已(并不是一个真正的自行车)。忽视里氏替换原则可能会让你遇到各种麻烦。拿 Donald (之前一篇文章 中的一个开发者) 来说,他写了一个 String 的子类,名叫 SuperSmartString 。这个子类做了所有的事情并覆写了父类 String 中的一些方法。他的这种编码方式显然违背了里氏替换原则。之后,他在他的代码中全都使用子类 SuperSmartString 的实例,而且还把这些实例视同 String 实例。不久,Donald 就注意到了一些“奇怪”、“神秘”的 bug 开始四处出现。当这些问题出现时,程序员就该开始抱怨之旅了,编程语言、编译器,编码平台、操作系统,甚至是市长和上帝都要跟着挨批评了。这些“神奇的” bug 其实可以通过遵守里氏替换原则来避免。就算不是为了代码质量,单单是为了程序员应该头脑清晰的职业属性,这个原则也应该受人尊敬。如果你的工作项目稍有复杂,那么只需少许的 SuperSmartStrings 和 ClassicBike 们就能让你工作不堪忍受。【译者注】关于里氏替换原则的相关内容远不止上文提到的这些,比如覆写(override)与重载(overload)的区别、子类需要个性化时该怎么做等等都需要我们关注。I至此,我们还剩下两个原则。I 代表的是接口隔离原则(Interface Segregation Principle,简称 ISP)。这个很容易理解。它说的是我们应该保持接口短小,在实现时选择实现多个小接口而不是庞大的单个接口。我会再次使用自行车的例子,但是这次我用一个 Bike 接口而不是 Bike 基类:interface Bike { void pedal() void steer() void handBrakeFront() void handBrakeBack()}MountainBike 类必须实现这个接口的所有方法:class MountainBike implements Bike { override void pedal() { // pedal implementation } override void steer() { // steer implementation } override void handBrakeFront() { // front hand brake implementation } override void handBrakeBack() { // back hand brake implementation } void changeGear() { // change gear code }}目前尚好。对于有问题的带有脚刹功能的 ClassicBike 类,我们可以采用下面这种笨拙的实现:class ClassicBike implements Bike { override pedal() { // pedal implementation } override steer() { // steer implementation } override handBrakeFront() { // no code or throw an exception } override handBrakeBack() { // no code or throw an exception } void brake() { // foot brake code }}在这个例子中,我们不得不重写手刹的两个方法,尽管不需要它们。正如前所述,我们打破了里氏替换原则。比较好的一个做法就是重构这个接口:interface Bike() { void pedal() void steer()}interface HandBrakeBike { void handBrakeFront() void handBrakeBack()}interface FootBrakeBike { void footBrake()}MountainBike 类将实现 Bike 和 HandBrakeBike 接口,如下所示:class MountainBike implements Bike, HandBrakeBike { // same code as before}ClassicBike 将实现 Bike 和 FootBrakeBike ,如下所示:class ClassicBike implements Bike, FootBrakeBike { override pedal() { // pedal implementation } override steer() { // steer implementation } override footBrake() { // code that handles foot braking }}接口隔离原则的优势之一就是我们可以在多个对象上组合匹配接口,这提高了我们代码的灵活性和模块化。我们也可以有一个 MultipleGearsBike 接口,在它里面添加 changeGear() 方法。现在,我们就可以构建一个拥有脚刹和换挡功能的自行车了。此外,我们的类现在也遵循了里氏替换原则,ClassicBike 与 MountainBike 都能够看作 Bike 而毫无问题了。如前所述,遵循里氏替换原则也有助于开闭原则的实现。……如果你还没看过第 1 部分的内容,可以在 这里 查看。在 第3部分 我们将探讨最后一个 SOLID 原则。如果你喜欢这篇文章,你可以在我们的 官方站点 上找到更多信息。 ...

December 22, 2018 · 2 min · jiezi

Javascript设计模式

设计原则(SOLID)单一职责模式李式替换原则开放封闭原则接口隔离原则依赖倒置原则设计模式工厂模式将new操作单独封装遇到new时,就要考虑是否该使用工厂模式了示例你去购买汉堡,直接点餐、取餐,不会自己亲手做商店要“封装”做汉堡的工作,做好直接给买者UML类图:代码示例:class Product { constructor(name) { this.name = name; } init() { console.log(‘init’) } fn1() { console.log(‘fn1’) } fn2() { console.log(‘fn2’) }}class Creator { create(name) { return new Product(name) }}let create = new Creator();let p = create.create(‘p’)p.init()p.fn1()p.fn2()应用场景1、jQuery:$(‘div’)和new $(‘div’)有何区别?第一:书写麻烦,jQuery的链式操作将成为噩梦第二:一旦jQuery名字变化,将是灾难性的//仿jQuery代码class jQuery { constructor(selector) { let slice = Array.prototype.slice; let dom = slice.call(document.querySelectorAll(selector)) let len = dom ? dom.length : 0 for (let i = 0; i < len; i++) { this[i] = dom[i] } this.length = len this.selector = selector || ’’ } append() { console.log(‘append’); } addClass() { console.log(‘addClass’) }}window.$ = function(selector) { return new jQuery(selector);}var $p = $(‘p’)console.log($p)console.log($p.addClass)2、React.crateElement:var profile = <div> <img src=“avater.png” className=“profile”/> <h3>{[user.firstName,user.lastName].join(’’)}</h3> </div>;编译完之后:var profile = React.createElement(“div”,null, React.createElement(“img”,{src:“avater.png”,className:“profile”}), React.createElement(“h3”,null,[user.firstName,user.lastName].join(" “)));//源码实现class vnode(tag, attrs, children) { //…省略内部代码…}React.createElement = function(tag,attrs,children){ return new vnode(tag,attrs,children)}3、Vue的异步组件:Vue.component(‘async-example’, funciton(resolve, reject) { setTimeout(function() => { resolve({ template: ‘<div>I am async!</div>’ }) }, 1000);})设计原则验证:构造函数和创建者分离符合开放封闭原则单例模式系统中被唯一使用一个类中只有一个实例 实例:登录框、购物车代码演示java版的单例模式演示public class SingleObject{ //注意:私有化构造函数,外部不能new,只能内部new!!!! private SingleObject(){} //唯一被new出来的对象 private SingleObject getInstance(){ if(instance == null){ //只new一次 instance = new SingleObject(); } return instance; } //对象方法 public void login(username,password){ System.out.println(“login…”) } } public class SingletonPatternDemo{ public static void main(String[] args){ //不合法的构造函数 //编译时报错:构造函数 SingleObject()是不可见的!!! //SingleObject object = new SingleObject(); //获取唯一可用的对象 SingleObject object = SingleObject.getInstance(); } }Javascript版的单例模式演示class SingleObject { login() { console.log(’login…’) }}//静态方法SingleObject.getInstance = (function() { let instance return function() { if (!instance) { instance = new SingleObject(); } return instance; }})()var login = SingleObject.getInstance().login();javascript的单例模式缺点:如果强制new也不会报错:var loginnew = new SingleObject();loginnew.login()测试://注意这里只能用静态函数getInstance,不能new SingleObject()!!!let obj1 = SingleObject.getInstance()obj1.login()let obj2 = SingleObject.getInstance()obj2.login()console.log(obj1 === obj2); //两者必须完全相同适配器模式装饰器模式代理模式外观模式观察者模式迭代器模式状态模式未完待续,每日更新 ...

December 22, 2018 · 2 min · jiezi

什么是SOLID原则(第1部分)

翻译自:What’s the deal with the SOLID principles?(part 1)即使你是一个初级开发人员,你也可能听说过 SOLID 原则。它们无处不在。工作面试时,你也许听过这样的问题:“你是如何评估代码质量的呢?又是如何区分代码的好坏呢?”一般的答案类似这样:“我尽量保持文件足够小。当文件变得很大时,我将移动部分代码到其他文件中。”。最糟糕的答案之一是:“我看到了就知道了。”当你用此类的描述回答之后,面试官通常会问:“你听说过 SOLID 原则吗?”。那么,什么是 SOLID 原则呢?它为什么会被经常提到呢?维基百科 - SOLID 是这样描述的:“在面向对象的计算机编程中,术语 SOLID 是对五大软件设计原则的助记缩写,这五个原则旨在让软件设计更易理解、更灵活、更易于维护。它与 GRASP 软件设计原则并无关系。这些软件设计原则是 Robert C. Martin 提出的众多原则中的一个子集。虽然它们适用于任何面向对象的设计,但是 SOLID 原则也可以形成诸如敏捷开发或者自适应软件开发等方法论的核心理念。Martin 在 2000年 的论文《设计原则与设计模式》中介绍了 SOLID 原则的理论。”。如果正确地遵循了 SOLID 原则,那么它将引导你写出维护性和可测试性更好的高质量代码。如果需求变更,你也可以轻松地修改代码(与不遵守 SOLID 的代码相比)。我认为向你提及这些内容是非常重要的:这些原则关注的是可维护性、可测试性以及和人类思维相关的更高质代码,而不是你为之编码的计算机。工作在以性能为主的领域的开发者各自有着不同的编程方法。由于在我们生活的世界里,人工时间成本比机器时间成本更昂贵,所以大多数开发者都不在高性能需求的领域里工作,而且他们常常被鼓励去使用面向对象的编程方法。在这种情况下,大部分开发者都能很好地应用 SOLID 原则。从本文开始,我将按照单词首字母缩写的顺序尽力向你解释每一个原则。为了简洁起见,我会把这个主题分为 3 个部分。本文是我将向你介绍的前两个原则的第 1 部分。好了,让我们从字母 S 开始吧。SSOLID 中的 “S” 表示的是单一职责原则(Single Responsibility Principle,简称 SRP),它是最容易理解也可能是最容易让人忽视的一个原则。此原则意味着一个类应该只做且只能做一件事情。如果一个类未能达到它的目的,那么你就可以看着它并说“你做了一件事……”。举例来说,假如我们需要从网络上获取一些 JSON 数据,然后解析它,并把结果保存在本地数据库中。根据我们正在编码的平台,这种工作可以使用为数不多的代码来实现。由于代码量不多,我们可能会想把所有的逻辑全部扔到一个类中。但是,根据单一职责原则,这将会是一个糟糕的做法。我们可以清楚地区分 3 个不同的职责:从网络上获取 JSON 数据,解析数据,保存解析的结果到数据库中。基于此,我们应该有 3 个类。第 1 个类应该只处理网络。我们给它提供一个 URL,然后接收 JSON 数据或者在出现问题时,收到一个错误信息。第 2 个类应该只解析它接收到的 JSON 数据并以相应的格式返回结果。第 3 个类应该以相应的格式接收 JSON 数据,并把它保存在本地数据库中。为什么非要这么麻烦呢?通过这样分离代码,我们能获得什么好处呢?其中一个好处就是可测试性。对于网络请求类,我们可以使用一个测试的 URL 分别在请求成功和发生错误的测试用例下来观察它的正确行为。为了测试 JSON 模块,我们可以提供一个模拟的 JSON 数据,然后查看它生成的正确数据。同样的测试原则也适用于数据库类(提供模拟数据,在模拟的数据库上测试结果)。有了这些测试,如果我们的程序出了问题,我们可以运行测试并查看问题发生在哪个地方。可能是服务器上的某些部分发生了改变,导致我们接收了有损数据。或者数据是正常的,但是我们在 JSON 解析模块中遗漏了什么,致使我们不能正确地解析数据。又或者可能我们正尝试插入数据的数据库中不存在某个列。通过这些测试,我们不必猜测问题出在哪个地方。看到了问题所在,我们就努力地去解决它。除了可测试性,我们还拥抱了模块化。如果项目需求变更,服务器返回数据的格式是 XML 或者其他的自定义格式而非 JSON,那么我们所要做的就是编写一个处理数据解析的新模块,然后用这个新的代替 JSON 模块。或者可能因为一些奇葩的理由,上述两者我们都需要,而网络模块再根据一些规则调用正确的模块。如果我们有一些本地的 JSON 文件需要解析并将解析的数据发给其他模块时该怎么办呢?那么,我们可以把本地的 JSON 发送给我们的解析模块,然后获取结果并把它用在需要的地方。如果我们需要以 Flat 的格式(译者注:Flat File)而不是数据库的形式本地保存数据呢?同样没问题,我们可以用一个新的模块来替换数据库模块。如你所见,这个看似简单的原则有很多优势。通过遵守这个原则,我们已经能够想象的到我们的代码库在可维护性方面会有重大改进。O字母“O”表示的是开闭原则( Open-Closed Principle,简称 OCP)。常言道,我们的类应该对扩展开发,对修改关闭。什么意思呢?我的理解是,我们应该以插件式的方式来编写类和模块。如果我们需要额外的功能,我们不应该修改类,而是能够嵌入一个提供这个额外功能的不同类。为了解释我的理解,我将使用一个经典的计算器示例。这个计算器在最开始只能执行两种运算:加法和减法。计算器类看起来像下面这样(本文中的代码不是用特定语言编写的):class Calculator { public float add(float a, float b) { return a + b } public float subtract(float a, float b) { return a — b }}我们像下面这样使用这个类:Calculator calculator = new Calculator()float sum = calculator.add(10, 2) //the value of sum is 12float diff = calculator.subtract(10, 2) //the value of diff is 8现在,我们假设客户希望为这个计算器添加乘法功能。为了添加这个额外的功能,我们必须编辑计算器类并添加乘法方法:public float multiply(float a, float b) { return a * b}如果需求又一次改变,客户又需要除法,sin,cos,pow以及众多的其他数学函数,我们不得不一次又一次编辑这个类来添加这些需求。根据开闭原则,这并不是一个明智的做法。因为这意味着我们的类可以修改。我们需要让它屏蔽修改,而对扩展开放,那么我们该怎么做呢?首先,我们定义一个名为 Operation 的接口,这个接口只有一个名为 compute 的方法:interface Operation { float compute(float a, float b)}之后,我们可以通过实现 Operation 接口来创建操作类(本文中提供的大多数示例也可以通过继承和抽象类来完成,但我更喜欢使用接口)。为了重建简单的计算器示例,我们将编写加法和减法类:class Addition implements Operation { public float compute(float a, float b) { return a + b }}class Subtraction implements Operation { public float compute(float a, float b) { return a — b }}我们的新计算器类只有一个名叫 calculate 的方法,在这个方法中,我们可以传递操作数与操作类:class Calculator { public float calculate(float a, float b, Operation operation) { return operation.compute(a, b) }}我们将像下面这样使用我们的新类:Calculator calculator = new Calculator()Addition addition = new Addition()Subtraction subtraction = new Subtraction()float sum = calculator.calculate(10, 2, addition) //the value of sum is 12float diff = calculator.calculate(10, 2, subtraction) //the value of diff is 8现在如果我们需要添加乘法,我们将创建这样的一个乘法运算类:class Multiplication implements Operation { public float compute(float a, float b) { return a * b }}然后通过添加以下内容在上面的示例中使用它:Multiplication multiplication = new Multiplication()float prod = calculator.calculate(10, 2, multiplication) // the value of prod is 20我们终于可以说我们的计算器类对修改关闭,对扩展开放了。看一下这个简单的例子,你可能会说将这些额外的方法添加到原始的计算器类中也没什么大问题,还有就是可能更好的实现也就意味着编写更多的代码。诚然,在这个简单的情景中,我更赞同你的说法。但是,在现实生活里的复杂情景下,遵守开闭原则编码将大有裨益。也许你需要为每个新功能添加远不止那三个方法,也许这些方法非常复杂。然而通过遵循开闭原则,我们可以用不同的类外化新的功能。它将有助于我们以及他人更好地理解我们的代码,而这主要是因为我们必须专注于较小的代码块而不是滚动无休止的文件。为了更好地可视化这个概念,我们可以把计算器类视为第三方库的一部分,并且无法访问其源码。好的实现就是编写它的善良的人们遵守了开闭原则,使它对扩展开放。因此,我们可以使用自己的代码扩展其功能,并轻松地在我们的项目中使用它。如果这听起来仍让人犯傻,那就这样想吧:你刚刚为客户编写了一个很棒的软件,它完成了客户想要的一切。你尽最大能力编写了所有的内容,并且代码质量令人惊叹。数周后,客户想要新的功能。为了实现它们,你必须潜心投入到你的漂亮代码中,修改各种文件。这样做,有可能代码质量会受到影响,特别是在截止日期紧张时。如果你已经为你的代码编写了测试(这也是你应该做的),那么这些修改可能会破坏一些测试,你还必须修改这些测试。这与遵守了开闭原则编写的代码形成了鲜明的对比。要实现新功能,你只需编写新代码即可。旧代码保持不变。你所有的旧测试仍然有效。因为我们不是生活在一个完美的世界中,所以在某些跑偏的情况下,你可能仍然会对旧代码的某些部分进行细微的更改,但这与非开闭原则带来的修改相比则可以忽略不计。除此之外,遵循开闭原则的编码方式还能让你在心理上获得极大的愉悦体验。其中一个就是,你只需要编写新代码,而无须为了实现新功能对你引以为傲的代码痛下杀手。通过为新功能编写新代码,而不是修改旧代码,高涨的团队士气将随之而来。这可以提高生产效率,从而减少工作压力,改善生活质量。我希望你能看到这个原则的重要性。不过令人沮丧的是,在一个真实的项目中,主要是由于缺乏魔法水晶球的能力,我们很难预见未来以及应该如何、在哪里应用这个原则。但是,知道了开闭原则的确有助于在需求来临时识别出可能的用例。在一开始的实现中,当客户想要给这个计算器添加乘法和除法功能时,我们随手将这两个方法添加到了 Calculator 类中。接下来,当他还要 sin、cos 时,我们也许会对自己说:“等会儿……”。等待过后,我们开始重构代码以适配开闭原则来避免将来可能遇到的麻烦。现在,当客户还想要 tan、pow 以及其他功能时,我们早就搞定了。……你可以在 什么是SOLID原则(第2部分) 阅读下两个 SOLID 原则。如果你喜欢这篇文章,你可以在我们的 官方站点 上找到更多信息。 ...

December 21, 2018 · 2 min · jiezi

13 个设计 REST API 的最佳实践

原文 RESTful API Design: 13 Best Practices to Make Your Users Happy写在前面之所以翻译这篇文章,是因为自从成为一名前端码农之后,调接口这件事情就成为了家常便饭,并且,还伴随着无数的争论与无奈。编写友好的 restful api 不论对于你的同事,还是将来作为第三方服务调用接口的用户来说,都显得至关重要。关于 restful api 本身以及设计原则,我陆陆续续也看过很多的文章和书籍,在读过原文后,感觉文中指出的 13 点最佳实践还是比较全面的且具有参考意义的,因此翻译出来分享给大家。如有错误,还望指正。由于我一般倾向于意译,关于原文中的开头语或者一些与之无关的内容,我就省略掉了,毕竟时间是金钱,英语好并且能科学上网的朋友我建议还是看原文,以免造成理解上的误差。1. 了解应用于 REST 之上的 HTTP 知识如果你想要构建设计优良的 REST API,了解一些关于 HTTP 协议的基础知识是很有帮助的,毕竟磨刀不误砍材工。在 MDN 上有很多质量不错的文档介绍 HTTP。但是,就 REST API 设计本身而言,所涉及到的 HTTP 知识要点大概包含以下几条:HTTP 中包含动词(或方法): GET、POST、PUT、PATCH 还有 DELETE 是最常用的。REST 是面向资源的,一个资源被一个 URI 所标识,比如 /articles/。端点(endpoint),一般指动词与 URI 的组合,比如 GET: /articles/。一个端点可以被解释为对某种资源进行的某个动作。比如, POST: /articles 可能代表“创建一个新的 article”。在业务领域,我们常常可以将动词和 CRUD(增删查改)关联起来:GET 代表查,POST代表增,PUT 和 PATCH 代表改(注: PUT 通常代表整体更新,而 PATCH 代表局部更新),而 DELETE 代表删。当然了,你可以将 HTTP 协议中所提供的任何东西应用于 REST API 的设计之中,但以上这些是比较基础的,因此时刻将它们记在脑海中是很有必要的。2. 不要返回纯文本虽然返回 JSON 数据格式的数据不是 REST 架构规范强制限定的,但大多 REST API 都遵循这条准则。但是,仅仅返回 JSON 数据格式的数据还是不够的,你还需要指定返回 body 的头部,比如 Content-Type,它的值必须指定为 application/json。这一点对于程序化客户端尤为重要(比如通过 python 的 requests 模块来与 api 进行交互)—— 这些程序是否对返回数据进行正确解码取决于这个头部。注:通常而言,对于浏览器来说,这似乎不是问题,因为浏览器一般都自带内容嗅探机制,但为了保持一致性,还是在响应中设置这个头部比较妥当。3. 避免在 URI 中使用动词如果你理解了第 1 条最佳实践所传达的意思,那么你现在就会明白不要将动词放入 REST API 的 URI 中。这是因为 HTTP 的动词已经足以描述执行于资源的业务逻辑操作了。举个例子,当你想要提供一个针对某个 article 提供 banner 图片并返回的接口时,可能会实现如下格式的接口:GET: /articles/:slug/generateBanner/这里 GET 已经说明了这个接口是在做读的操作,因此,可以简化为:GET: /articles/:slug/banner/类似的,如果这个端口是要创建一个 article:// 不要这么做POST: /articles/createNewArticle/// 这才是最佳实践POST: /articles/尝试用 HTTP 的动词来描述所涉及的业务逻辑操作。4. 使用复数的名词来描述资源一些时候,使用资源的复数形式还是单数形式确实存在一定的困扰,比如使用 /article/:id/ 更好还是使用 /articles/:id/ 更好呢?这里我推荐使用后者。为什么呢?因为复数形式可以满足所有类型端点的需求。单数形式的 GET /article/2/ 看起来还是不错的,但是如果是 GET /article/ 呢?你能够仅通过字面信息来区分这个接口是返回某个 article 还是多个呢?因此,为了避免有单数命名造成的歧义性,并尽可能的保持一致性,使用复数形式,比如:GET: /articles/2/POST: /articles/…5. 在响应中返回错误详情当 API 服务器处理错误时,如果能够在返回的 JSON body 中包含错误信息,对于接口调用者来说,会一定程度上帮助他们完成调试。比如对于常见的提交表单,当遇到如下错误信息时:{ “error”: “Invalid payoad.”, “detail”: { “surname”: “This field is required.” }}接口调用者很快就是明白发生错误的原因。6. 小心 status code这一点可能是最重要、最重要、最重要的一点,可能也是这篇文章中,唯一你需要记住的那一点。你可能知道,HTTP 中你可以返回带有 200 状态码的错误响应,但这是十分糟糕的。不要这么做,你应当返回与返回错误类型相一致的具有一定含义的状态码。聪明的读者可能会说,我按照第 5 点最佳实践来提供足够详细的信息,难道不行吗?当然可以,不过让我讲一个故事:我曾经使用过一个 API,对于它返回的所有响应的状态码均是 200 OK,同时通过响应数据中的 status 字段来表示当前的请求是否成功,比如:{ “status”: “success”, “data”: {}}所以,虽然状态码是 200 OK,但我却不能绝对确定请求是否成功,事实上,当错误发生时,这个 API 会按如下代码片段返回响应:HTTP/1.1 200 OKContent-Type: text/html{ “status”: “failure”, “data”: { “error”: “Expected at least two items in list.” }}头部还是 text/html,因为它同时返回了一些 HTML 片段。正因为这样,我不得不在检查响应状态码正确的同时,还需校验这个具有特殊含义的 status 字段的值,才可以放心的处理响应返回的 data。这种设计的一个真正坏处在于,它打破了接口与调用者之间的“信任”,因为你可能会担心这个接口对你撒谎(注:言外之意就是,由于特设的字段可能会改变,因此增加了不可靠性)。所以,使用正确的状态码,同时仅在响应的 body 中返回错误信息,并设置正确的头部,比如:HTTP/1.1 400 Bad RequestContent-Type: application/json{ “error”: “Expected at least two items in list."}7. 保持 status code 的一致性当你掌握了正确使用状态码之后,就应该努力使它们具有一致性。比如,如果一个 POST 类型的端点返回 201 Created,那么所有的 POST 端点都应返回同样的状态码。这样做的好处在于,调用者无需在意端点返回的状态码取决于某种特殊条件,也就形成了一致性。如果有特殊情况,请在文档中显著地说明它们。下面是我推荐的与动词相对应的状态码:GET: 200 OKPOST: 201 CreatedPUT: 200 OKPATCH: 200 OKDELETE: 204 No Contenthttps://blog.florimondmanca.c…8. 不要嵌套资源使用 REST API 获取资源数据,通常情况下会直接获取多个或者单个,但当我们需要获取相关联的资源时,该怎么做呢?比如说,我们期望获取作者为某个 author 的 article 列表 —— 假设 authro 的 id=12。这里提供两种方案:第一种方案通过在 URI 中,将嵌套的资源放在所关联的资源后边来进行描述,比如:GET: /authors/12/articles/一些人推荐这种方案的理由是,这种形式的 URI 一定程度上描述了 author 与 article 之间的一对多关系。但与此同时,结合第 4 点最佳实践,我们就不太能够分清当前端点返回的数据到底是 author 类型还是 article 类型。这里有一篇文章,详细阐述了扁平化形式优于嵌套形式,因此一定有更好的方法,这就是下面的第二种方案:GET: /articles/?author_id=12直接将筛选 article 的逻辑抽离为 querystring 即可,这样的 URI 相比之前,更加清晰地描述了“获取所有 author(id=12) 的 article”的意思。9. 优雅地处理尾部斜杠一个好的 URI 中是否应当包含尾部斜杠,并不具有探讨价值,选择一种更倾向的风格并保持一致性即可,同时当客户端误用尾部斜杠时,提供重定向响应。我再来讲我自己的一个故事。某天,我在将某个 API 端点集成到项目中,但是我总是收到 500 Internal Error 的错误,我调用的端点差不多看起来这样:POST: /entities调试一段时间之后,我几乎崩溃了,因为我根本不知道我哪里做错了,直到我发现服务器之所以报 500 的错误,是因为我粗心丢掉了尾部斜杠(注:这种经历人人都会遇到,我在 SF 上遇过无数次类似的问题),当我把 URI 改成:POST: /entities/之后,一切正常运转。当然,大多数的 web 框架都针对 URL 是否包含尾部斜杠,进行了优雅地处理并提供定制选项,如果可以的话,找到它并开启这项功能。10. 使用 querystring 来完成筛选和分页功能大部分情况下,一个简单的端点没有办法满足负责业务场景。你的用户可能想要获取满足一定条件下的某些数据集合 ,同时为了保证性能,仅仅获取这个集合下的一个子集。换言之,这通常叫作筛选功能和分页功能:筛选:用户可以提供额外的属性来控制返回的数据集合分页:获取数据集合的子集,最简单的分页是基于分页个数的分页,它由 page 和 page_size 来决定那么问题来了,我们如何将这两项功能与 RESTful API 结合在一起呢?答案当然是通过 querystring。对于分页,很显然使用这种方式再合适不过了,比如:GET: /articles/?page=1&page_size=10但对于筛选,你可能会犯第 8 点最佳实践中所指出的问题,比如获取处于 published 状态的 article 列表:GET: /articles/published/除了之前提出的问题外,这里还涉及一个设计上的问题,就是 published 本身不是资源,它仅仅是资源的特征,类似这种特征字段,应该将它们放到 querystring 中:GET: /articles/?published=true&page=2&page_size=20更加优雅、清晰,不是吗?11. 分清 401 和 403当我们遇到 API 中关于安全的错误提示时,很容易混淆这两个不同类型的错误,认证和授权(比如权限相关)—— 老实讲,我自己也经常搞混。这里是我自己总结的备忘录,它阐述了我如何在实际情况下,区分它们:用户是否未提供身份验证凭据?认证是否还有效?这种类型的错误一般是未认证(401 Unauthorized)。用户经过了正常的身份验证,但没有访问资源所需的权限?这种一般是未授权(403 Forbidden)12. 巧用 202 Accepted我发现 202 Accepted 在某些场合是 201 Created 的一个非常便捷的替代方案,这个状态码的含义是:服务器已经接受了你的请求,但是到目前为止还未创建新的资源,但一切仍处于正常状态。我分享两种特别适合使用 202 Accepted 状态码的业务场景:如果资源是经过位于将来一系列处理流程之后才创建的,比如当某项作业完成时如果资源已经存在,但这是理想状态,因此不应该被识别为一个错误时13. 采用 REST API 定制化的框架作为最后一个最佳实践,让我们来探讨这样一个问题:你如何在 API 的实施中,实践最佳实践呢?通常的情况是这样的,你想要快速创建一个 API 以便一些服务可以互相访问彼此。Python 开发者可能马上掏出了 Flask,而 JS 开发者也不甘示弱,祭出了 Express,他们会使用实现一些简单的 routes 来处理 HTTP 请求。但这样做的问题是,通常,web 框架并不是针对构建 REST API 服务而专门存在的,换言之,Flask 和 Express 是两个十分通用的框架,但它们并非特别适合用于构建 REST API 服务。因此,你必须采取额外的步骤来实施 API 中的最佳实践,但大多数情况下,由于懒惰或者时间紧张等因素,意味着你不会投入过多精力在这些方面 —— 然后给你的用户提供了一个古怪的 API 端点。解决方案十分简单:工欲善其事,必先利其器,掌握并使用正确的工作才是最好的方案。在各种语言中,许多专门用于构建 REST API 服务的新框架已经出现了,它们可以帮助你在不牺牲生产力的情况下,轻松地完成工作,同时遵循最佳实践。在 Python 中,我发现的最好的 API 框架之一是 Falcon。它与 Flask 一样简单,非常高效,十分适合构建 REST API 服务。如果你更喜欢 Django 的话,使用 Django REST Framework就足够了,虽然框架不是那么直观(注:按我的理解应该是说不太容易上手,但是我不这么认为),但功能非常强大。在 NodeJS 中,Restify 似乎也是一个不错的选择,尽管我还没有尝试过。我强烈建议你给这些框架一个机会!它们将帮助你构建规范,优雅且设计良好的 REST API 服务。总结我们都应致力于让调用 API 这件事成为一种乐趣。希望本文能使你了解到在构建更好的 REST API 服务的过程中,涉及到的一些建议和技巧。对我而言,应该把这些最佳实践归结为三点,分别是良好的语义,简洁和合理性。关注公众号 全栈101,只谈技术,不谈人生 ...

December 20, 2018 · 2 min · jiezi

通俗易懂的设计模式

<!– TOC –>通俗易懂的设计模式零、使用1、安装2、测试一、什么是设计模式二、设计模式的类型三、设计模式的六大原则四、UML类图1、看懂UML类图2、解释五、资料<!– /TOC –>前言:花了一些时间再次熟悉了一遍设计模式,主要是参考design-patterns-for-humans,也有了一些感悟,本人能力有限,如遇到什么不对的地方还望指出修正,谢谢有收获的话请加颗小星星,没有收获的话可以 反对 没有帮助 举报三连仓库地址零、使用1、安装composer create-project -s dev omgzui/design-patternorgit clone https://github.com/OMGZui/DesignPatterncomposer install2、测试vendor/bin/phpunit tests/一、什么是设计模式摘自wiki:在软件工程中,设计模式(design pattern)是对软件设计中普遍存在(反复出现)的各种问题,所提出的解决方案。这个术语是由埃里希·伽玛(Erich Gamma)等人在1990年代从建筑设计领域引入到计算机科学的。设计模式并不直接用来完成代码的编写,而是描述在各种不同情况下,要怎么解决问题的一种方案。面向对象设计模式通常以类别)或对象)来描述其中的关系和相互作用,但不涉及用来完成应用程序的特定类别或对象。设计模式能使不稳定依赖于相对稳定、具体依赖于相对抽象,避免会引起麻烦的紧耦合,以增强软件设计面对并适应变化的能力。并非所有的软件模式都是设计模式,设计模式特指软件“设计”层次上的问题。还有其他非设计模式的模式,如架构模式。同时,算法不能算是一种设计模式,因为算法主要是用来解决计算上的问题,而非设计上的问题。随着软件开发社群对设计模式的兴趣日益增长,已经出版了一些相关的专著,定期召开相应的研讨会,而且沃德·坎宁安(Ward Cunningham)为此发明了WikiWiki用来交流设计模式的经验。设计模式(Design pattern)代表了最佳的实践,通常被有经验的面向对象的软件开发人员所采用。设计模式是软件开发人员在软件开发过程中面临的一般问题的解决方案。这些解决方案是众多软件开发人员经过相当长的一段时间的试验和错误总结出来的。简而言之设计模式就是一套解决方案,目的是为了重用代码,同时也能保证代码的可靠性二、设计模式的类型创建型模式(Creational):这些设计模式提供了一种在创建对象的同时隐藏创建逻辑的方式,而不是使用 new 运算符直接实例化对象。这使得程序在判断针对某个给定实例需要创建哪些对象时更加灵活简单工厂模式(Simple Factory Pattern) 传送门抽象方法模式(Factory Method Pattern) 传送门抽象工厂模式(Abstract Factory Pattern) 传送门单例模式(Singleton Pattern) 传送门建造者模式(Builder Pattern) 传送门原型模式(Prototype Pattern) PHP使用clone即可实现,目的是减少开销结构型模式(Structural):这些设计模式关注类和对象的组合。继承的概念被用来组合接口和定义组合对象获得新功能的方式适配器模式(Adapter Pattern) 传送门桥接模式(Bridge Pattern) 传送门装饰器模式(Decorator Pattern) 传送门外观模式(Facade Pattern) 传送门享元模式(Flyweight Pattern) 传送门代理模式(Proxy Pattern) 传送门行为型模式(Behavioral):这些设计模式特别关注对象之间的通信责任链模式(Chain of Responsibility Pattern) 传送门命令模式(Command Pattern) 传送门迭代器模式(Iterator Pattern) 传送门备忘录模式(Memento Pattern) 传送门观察者模式(Observer Pattern) 传送门状态模式(State Pattern) 传送门策略模式(Strategy Pattern) 传送门访问者模式(Visitor Pattern) 传送门模板方法模式(Template Method Pattern) 传送门三、设计模式的六大原则开闭原则(Open Close Principle)开闭原则的意思是:对扩展开放,对修改关闭。在程序需要进行拓展的时候,不能去修改原有的代码,实现一个热插拔的效果。简言之,是为了使程序的扩展性好,易于维护和升级。想要达到这样的效果,我们需要使用接口和抽象类,后面的具体设计中我们会提到这点。里氏代换原则(Liskov Substitution Principle)里氏代换原则是面向对象设计的基本原则之一。 里氏代换原则中说,任何基类可以出现的地方,子类一定可以出现。LSP 是继承复用的基石,只有当派生类可以替换掉基类,且软件单位的功能不受到影响时,基类才能真正被复用,而派生类也能够在基类的基础上增加新的行为。里氏代换原则是对开闭原则的补充。实现开闭原则的关键步骤就是抽象化,而基类与子类的继承关系就是抽象化的具体实现,所以里氏代换原则是对实现抽象化的具体步骤的规范。依赖倒转原则(Dependence Inversion Principle)这个原则是开闭原则的基础,具体内容:针对接口编程,依赖于抽象而不依赖于具体。接口隔离原则(Interface Segregation Principle)这个原则的意思是:使用多个隔离的接口,比使用单个接口要好。它还有另外一个意思是:降低类之间的耦合度。由此可见,其实设计模式就是从大型软件架构出发、便于升级和维护的软件设计思想,它强调降低依赖,降低耦合。迪米特法则,又称最少知道原则(Demeter Principle)最少知道原则是指:一个实体应当尽量少地与其他实体之间发生相互作用,使得系统功能模块相对独立。合成复用原则(Composite Reuse Principle)合成复用原则是指:尽量使用合成/聚合的方式,而不是使用继承。四、UML类图1、看懂UML类图一个简单的栗子车的类图结构为abstract,表示车是一个抽象类它有两个继承类:小汽车和自行车,它们之间的关系为实现关系,使用带空心箭头的虚线表示小汽车为与SUV之间也是继承关系,它们之间的关系为泛化关系,使用带空心箭头的实线表示小汽车与发动机和轮胎之间是组合关系,使用带实心箭头的实线表示学生与班级之间是聚合关系,使用带空心箭头的实线表示学生与身份证之间为关联关系,使用一根实线表示学生上学需要用到自行车,与自行车是一种依赖关系,使用带箭头的虚线表示2、解释实现关系是继承自抽象类,泛化关系是继承自非抽象类组合关系是强依赖的,比如上图中的小汽车由发动机和轮胎组成,小汽车不在了,发动机和轮胎也是不存在了聚合是不是强依赖的,比如上图中的一个班级由学生构成,班级不在了,学生还在关联关系是强关联的,比如上图中学生跟身份证关联了,可以说是身份证可以代表学生,学生也能代表身份证依赖关系一般是单向的,双向依赖是非常糟糕的,比如上图中学生依赖自行车,但是自行车依赖学生是说不通的,同时学生也可以依赖价值百万的公交车或价值上亿的地铁,只是临时性的依赖五、资料图说设计模式design-patterns-for-humansDesignPatternsPHPUML软件 ...

December 20, 2018 · 1 min · jiezi

每天一个设计模式之享元模式

作者按:《每天一个设计模式》旨在初步领会设计模式的精髓,目前采用javascript和python两种语言实现。诚然,每种设计模式都有多种实现方式,但此小册只记录最直截了当的实现方式 :)个人技术博客-godbmw.com 欢迎来玩! 每周至少 1 篇原创技术分享,还有开源教程(webpack、设计模式)、面试刷题(偏前端)、知识整理(每周零碎),欢迎长期关注!本篇博客地址是:《每天一个设计模式之享元模式》。如果您也想进行知识整理 + 搭建功能完善/设计简约/快速启动的个人博客,请直接戳theme-bmw0. 项目地址享元模式·代码《每天一个设计模式》地址1. 什么是“享元模式”?享元模式:运用共享技术来减少创建对象的数量,从而减少内存占用、提高性能。享元模式提醒我们将一个对象的属性划分为内部和外部状态。内部状态:可以被对象集合共享,通常不会改变外部状态:根据应用场景经常改变享元模式是利用时间换取空间的优化模式。2. 应用场景享元模式虽然名字听起来比较高深,但是实际使用非常容易:只要是需要大量创建重复的类的代码块,均可以使用享元模式抽离内部/外部状态,减少重复类的创建。为了显示它的强大,下面的代码是简单地实现了大家耳熟能详的“对象池”,以彰显这种设计模式的魅力。3. 代码实现这里利用python和javascript实现了一个“通用对象池”类–ObjectPool。这个类管理一个装载空闲对象的数组,如果外部需要一个对象,直接从对象池中获取,而不是通过new操作。对象池可以大量减少重复创建相同的对象,从而节省了系统内存,提高运行效率。为了形象说明“享元模式”在“对象池”实现和应用,特别准备了模拟了File类,并且模拟了“文件下载”操作。通过阅读下方代码可以发现:对于File类,内部状态是pool属性和download方法;外部状态是name和src(文件名和文件链接)。借助对象池,实现了File类的复用。注:为了方便演示,Javascript实现的是并发操作,Python实现的是串行操作。输出结果略有不同。3.1 Python3 实现from time import sleepclass ObjectPool: # 通用对象池 def init(self): self.__pool = [] # 创建对象 def create(self, Obj): # 对象池中没有空闲对象,则创建一个新的对象 # 对象池中有空闲对象,直接取出,无需再次创建 return self.__pool.pop() if len(self.__pool) > 0 else Obj(self) # 对象回收 def recover(self, obj): return self.__pool.append(obj) # 对象池大小 def size(self): return len(self.__pool)class File: # 模拟文件对象 def init(self, pool): self.__pool = pool def download(self): # 模拟下载操作 print(’+ 从’, self.src, ‘开始下载’, self.name) sleep(0.1) print(’-’, self.name, ‘下载完成’) # 下载完毕后,将对象重新放入对象池 self.__pool.recover(self)if name == ‘main’: obj_pool = ObjectPool() file1 = obj_pool.create(File) file1.name = ‘文件1’ file1.src = ‘https://download1.com’ file1.download() file2 = obj_pool.create(File) file2.name = ‘文件2’ file2.src = ‘https://download2.com’ file2.download() file3 = obj_pool.create(File) file3.name = ‘文件3’ file3.src = ‘https://download3.com’ file3.download() print(’*’ * 20) print(‘下载了3个文件, 但其实只创建了’, obj_pool.size(), ‘个对象’)输出结果(这里为了方便演示直接使用了sleep方法,没有再用多线程模拟):+ 从 https://download1.com 开始下载 文件1- 文件1 下载完成+ 从 https://download2.com 开始下载 文件2- 文件2 下载完成+ 从 https://download3.com 开始下载 文件3- 文件3 下载完成下载了3个文件, 但其实只创建了 1 个对象3.2 ES6 实现// 对象池class ObjectPool { constructor() { this._pool = []; // } // 创建对象 create(Obj) { return this._pool.length === 0 ? new Obj(this) // 对象池中没有空闲对象,则创建一个新的对象 : this._pool.shift(); // 对象池中有空闲对象,直接取出,无需再次创建 } // 对象回收 recover(obj) { return this._pool.push(obj); } // 对象池大小 size() { return this._pool.length; }}// 模拟文件对象class File { constructor(pool) { this.pool = pool; } // 模拟下载操作 download() { console.log(+ 从 ${this.src} 开始下载 ${this.name}); setTimeout(() => { console.log(- ${this.name} 下载完毕); // 下载完毕后, 将对象重新放入对象池 this.pool.recover(this); }, 100); }}/ 以下是测试函数 **********************/let objPool = new ObjectPool();let file1 = objPool.create(File);file1.name = “文件1”;file1.src = “https://download1.com”;file1.download();let file2 = objPool.create(File);file2.name = “文件2”;file2.src = “https://download2.com”;file2.download();setTimeout(() => { let file3 = objPool.create(File); file3.name = “文件3”; file3.src = “https://download3.com”; file3.download();}, 200);setTimeout( () => console.log( ${"*".repeat(50)}\n下载了3个文件,但其实只创建了${objPool.size()}个对象 ), 1000);输出结果如下:+ 从 https://download1.com 开始下载 文件1+ 从 https://download2.com 开始下载 文件2- 文件1 下载完毕- 文件2 下载完毕+ 从 https://download3.com 开始下载 文件3- 文件3 下载完毕************************************************下载了3个文件,但其实只创建了2个对象4. 参考《JavaScript 设计模式和开发实践》 ...

December 20, 2018 · 2 min · jiezi

从源码学习设计模式之模板方法

Photo by Tomáš Malík on Unsplash什么是模板方法模式?摘录 wiki 的介绍。模板方法模式定义了一个算法的步骤,并允许子类别为一个或多个步骤提供其实践方式。让子类别在不改变算法架构的情况下,重新定义算法中的某些步骤。在软件工程中,它是一种软件设计模式,和C++模板没有关连。模板设计方法存在目的在于某些算法逻辑存在一些相同处,而具体细节却不同。这样使用模板方法,可以抽取共用逻辑到父类,在子类实现具体算法细节,这样减少了重复代码。 模板方法充分运用了多态与继承。使用抽象父类定义抽象操作,然后在公共逻辑调用抽象方法。子类方法只要继承父类关注自身实现细节。Talk is cheap. Show me the code下面拿支付接入支付渠道例子来使用模板方法。假设银行卡支付需要实现两家银行的支付功能。不同银行提供的接口,在参数,调用方式等肯定存在很大区别。这个时候我们就可以使用模板设计方法,父类实现支付前通用逻辑,用子类实现交互的不同。系统类结构如下。系统类结构AgreementPay 提供支付功能,AgreementBasePay 为抽象类实现通用逻辑,AgreementCCBPay 与 AgreementCMBPay 实现具体的渠道支付方法。具体源码如下。AgreementPay 接口public interface AgreementPay {PayResponse payInChannel(PayReauest reauest);}AgreementBasePay 抽象方法实现通用逻辑。public abstract class AgreementBasePay implements AgreementPay {public PayResponse pay(PayReauest reauest) { checkRequest(reauest); return this.payInChannel(reauest);}private void checkRequest(PayReauest reauest) { System.out.println(“具体方法参数检查”);}}具体实现类,实现具体渠道支付细节。public class AgreementCCBPay extends AgreementBasePay {@Overridepublic PayResponse payInChannel(PayReauest reauest) { System.out.println(“去建设银行支付”); return new PayResponse();}}public class AgreementCMBPay extends AgreementBasePay {@Overridepublic PayResponse payInChannel(PayReauest reauest) { System.out.println(“去招商银行支付”); return new PayResponse();}}实现模板方法的细节,我们来看 client 使用逻辑。public class Client {public static void main(String[] args) { System.out.println(“使用招商银行支付”); AgreementPay agreementPay = new AgreementCMBPay(); PayRequest request = new PayRequest(); agreementPay.payInChannel(request); System.out.println(“使用建设银行支付”); agreementPay = new AgreementCCBPay(); agreementPay.payInChannel(request);}}上面 client 逻辑,其实看起来还是有一些死板,且需要外部知道调用哪个渠道接口。但是如果真正提供一个对外接口,外部调用方法是不关心你具体使用那个子类支付。所以这里我们可以改进一下,public static Map<String, AgreementPay> payCache = new HashMap<>();static { payCache.put(“CMB”, new AgreementCMBPay()); payCache.put(“CCB”, new AgreementCCBPay());}public static void main(String[] args) { PayRequest request = new PayRequest(); AgreementPay pa; switch (request.getBankCode()) { case “CMB”: pa = payCache.get(“CMB”); pa.payInChannel(request); case “CCB”: pa = payCache.get(“CCB”); pa.payInChannel(request); default: throw new RuntimeException(); }}改造之后我们先将其 AgreementPay 实例放入 map 中,然后调用时根据一个标志来选择具体实现类。从上面的细节我们可以看到模板方法其实设计思路与实现细节都比较简单。看完我们的示例代码,我们去看下 mybatis 如何使用模板方法。mybatis 模板方法应用在看源码之前,我们先看下我们不使用 mybatis 之前,如何查询数据。 Class.forName(“com.mysql.jdbc.Driver”); //2.获得数据库的连接 Connection conn = DriverManager.getConnection(URL, NAME, PASSWORD); //3.通过数据库的连接操作数据库,实现增删改查 PreparedStatement pstmt = conn.prepareStatement(“select user_name,age from imooc_goddess where id=?”); pstmt.setInt(1, 21); ResultSet rs = pstmt.execute(); while (rs.next()) {//如果对象中有数据,就会循环打印出来 System.out.println(rs.getString(“user_name”) + “,” + rs.getInt(“age”)); }我们可以看到直接使用 JDBC 查询,十分麻烦,且需要我们自己将 java 类型转换成 jdbc 数据类型。ORM 框架重要作用在于把数据库表与 java,ORM 框架省去我们自己将 java 类型转化成 JDBC 类型的麻烦。JDBC 存在有那么多类型,如何做到转换的那?其实关键就是应用模板设计方法。mybatis 中存在一个接口 TypeHandler,该接口方法主要如下:public interface TypeHandler<T> {void setParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType) throws SQLException;T getResult(ResultSet rs, String columnName) throws SQLException;T getResult(ResultSet rs, int columnIndex) throws SQLException;T getResult(CallableStatement cs, int columnIndex) throws SQLException;}从方法上看,这个接口主要的方法为 PreparedStatement 设置列参数,或者从 ResultSet 获取列的值然后转换成相应的 java 数据类型。我们看下这个接口实现的类图。TypeHandler 实现类图可以看到 BaseTypeHandler 为 TypeHandler 的具体抽象类,我们具体看下 TypeHandler getResult 在抽象类中实现细节。@Override public T getResult(ResultSet rs, String columnName) throws SQLException {T result;try { result = getNullableResult(rs, columnName);} catch (Exception e) { throw new ResultMapException(“Error attempting to get column ‘” + columnName + “’ from result set. Cause: " + e, e);}if (rs.wasNull()) { return null;} else { return result;}}public abstract T getNullableResult(ResultSet rs, String columnName) throws SQLException;可以看到其最后调用抽象方法 getNullableResult。其由具体的子类的实现。我们具体找一个子类 DateTypeHandler 来查看具体实现。public class DateTypeHandler extends BaseTypeHandler<Date> {// 忽略其他方法@Override public Date getNullableResult(ResultSet rs, String columnName) throws SQLException {Timestamp sqlTimestamp = rs.getTimestamp(columnName);if (sqlTimestamp != null) { return new Date(sqlTimestamp.getTime());}return null;}}可见其具体从 ResultSet 取出 JDBC 类型为 Timestamp,然后转换成 java 类型的 Date。实现具体的子类,那么在哪里使用了那?其实 mybatis 框架会把所有 TypeHandler 在 TypeHandlerRegistry 注册。具体类方法如图TypeHandlerRegistry。其提供了相关 register 方法注册 TypeHandler,然后又提供了相关 getTypeHandler 方法取出具体 TypeHandler 实现类。总结使用模板方法,将公共逻辑抽取出来,将具体实现细节交给子类。参考Mybatis源代码分析之类型转换什么是模板方法模式?摘录 wiki 的介绍。模板方法模式定义了一个算法的步骤,并允许子类别为一个或多个步骤提供其实践方式。让子类别在不改变算法架构的情况下,重新定义算法中的某些步骤。在软件工程中,它是一种软件设计模式,和C++模板没有关连。模板设计方法存在目的在于某些算法逻辑存在一些相同处,而具体细节却不同。这样使用模板方法,可以抽取共用逻辑到父类,在子类实现具体算法细节,这样减少了重复代码。模板方法充分运用了多态与继承。使用抽象父类定义抽象操作,然后在公共逻辑调用抽象方法。子类方法只要继承父类关注自身实现细节。Talk is cheap. Show me the code下面拿支付接入支付渠道例子来使用模板方法。假设银行卡支付需要实现两家银行的支付功能。不同银行提供的接口,在参数,调用方式等肯定存在很大区别。这个时候我们就可以使用模板设计方法,父类实现支付前通用逻辑,用子类实现交互的不同。系统类结构如下。AgreementPay 提供支付功能,AgreementBasePay 为抽象类实现通用逻辑,AgreementCCBPay 与 AgreementCMBPay 实现具体的渠道支付方法。具体源码如下。AgreementPay 接口public interface AgreementPay { PayResponse payInChannel(PayReauest reauest);}AgreementBasePay 抽象方法实现通用逻辑。public abstract class AgreementBasePay implements AgreementPay { public PayResponse pay(PayReauest reauest) { checkRequest(reauest); return this.payInChannel(reauest); } private void checkRequest(PayReauest reauest) { System.out.println(“具体方法参数检查”); }}具体实现类,实现具体渠道支付细节。public class AgreementCCBPay extends AgreementBasePay { @Override public PayResponse payInChannel(PayReauest reauest) { System.out.println(“去建设银行支付”); return new PayResponse(); }}public class AgreementCMBPay extends AgreementBasePay { @Override public PayResponse payInChannel(PayReauest reauest) { System.out.println(“去招商银行支付”); return new PayResponse(); }}实现模板方法的细节,我们来看 client 使用逻辑。public class Client { public static void main(String[] args) { System.out.println(“使用招商银行支付”); AgreementPay agreementPay = new AgreementCMBPay(); PayRequest request = new PayRequest(); agreementPay.payInChannel(request); System.out.println(“使用建设银行支付”); agreementPay = new AgreementCCBPay(); agreementPay.payInChannel(request); }}上面 client 逻辑,其实看起来还是有一些死板,且需要外部知道调用哪个渠道接口。但是如果真正提供一个对外接口,外部调用方法是不关心你具体使用那个子类支付。所以这里我们可以改进一下, public static Map<String, AgreementPay> payCache = new HashMap<>(); static { payCache.put(“CMB”, new AgreementCMBPay()); payCache.put(“CCB”, new AgreementCCBPay()); } public static void main(String[] args) { PayRequest request = new PayRequest(); AgreementPay pa; switch (request.getBankCode()) { case “CMB”: pa = payCache.get(“CMB”); pa.payInChannel(request); case “CCB”: pa = payCache.get(“CCB”); pa.payInChannel(request); default: throw new RuntimeException(); } }改造之后我们先将其 AgreementPay 实例放入 map 中,然后调用时根据一个标志来选择具体实现类。从上面的细节我们可以看到模板方法其实设计思路与实现细节都比较简单。看完我们的示例代码,我们去看下 mybatis 如何使用模板方法。mybatis 模板方法应用在看源码之前,我们先看下我们不使用 mybatis 之前,如何查询数据。 Class.forName(“com.mysql.jdbc.Driver”); //2.获得数据库的连接 Connection conn = DriverManager.getConnection(URL, NAME, PASSWORD); //3.通过数据库的连接操作数据库,实现增删改查 PreparedStatement pstmt = conn.prepareStatement(“select user_name,age from imooc_goddess where id=?”); pstmt.setInt(1, 21); ResultSet rs = pstmt.execute(); while (rs.next()) {//如果对象中有数据,就会循环打印出来 System.out.println(rs.getString(“user_name”) + “,” + rs.getInt(“age”)); }我们可以看到直接使用 JDBC 查询,十分麻烦,且需要我们自己将 java 类型转换成 jdbc 数据类型。ORM 框架重要作用在于把数据库表与 java,ORM 框架省去我们自己将 java 类型转化成 JDBC 类型的麻烦。JDBC 存在有那么多类型,如何做到转换的那?其实关键就是应用模板设计方法。mybatis 中存在一个接口 TypeHandler,该接口方法主要如下:public interface TypeHandler<T> { void setParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType) throws SQLException; T getResult(ResultSet rs, String columnName) throws SQLException; T getResult(ResultSet rs, int columnIndex) throws SQLException; T getResult(CallableStatement cs, int columnIndex) throws SQLException;}从方法上看,这个接口主要的方法为 PreparedStatement 设置列参数,或者从 ResultSet 获取列的值然后转换成相应的 java 数据类型。我们看下这个接口实现的类图。可以看到 BaseTypeHandler 为 TypeHandler 的具体抽象类,我们具体看下 TypeHandler getResult 在抽象类中实现细节。 @Override public T getResult(ResultSet rs, String columnName) throws SQLException { T result; try { result = getNullableResult(rs, columnName); } catch (Exception e) { throw new ResultMapException(“Error attempting to get column ‘” + columnName + “’ from result set. Cause: " + e, e); } if (rs.wasNull()) { return null; } else { return result; } } public abstract T getNullableResult(ResultSet rs, String columnName) throws SQLException;可以看到其最后调用抽象方法 getNullableResult。其由具体的子类的实现。我们具体找一个子类 DateTypeHandler 来查看具体实现。public class DateTypeHandler extends BaseTypeHandler<Date> { // 忽略其他方法 @Override public Date getNullableResult(ResultSet rs, String columnName) throws SQLException { Timestamp sqlTimestamp = rs.getTimestamp(columnName); if (sqlTimestamp != null) { return new Date(sqlTimestamp.getTime()); } return null; }}可见其具体从 ResultSet 取出 JDBC 类型为 Timestamp,然后转换成 java 类型的 Date。实现具体的子类,那么在哪里使用了那?其实 mybatis 框架会把所有 TypeHandler 在 TypeHandlerRegistry 注册。具体类方法如图。其提供了相关 register 方法注册 TypeHandler,然后又提供了相关 getTypeHandler 方法取出具体 TypeHandler 实现类。总结使用模板方法,将公共逻辑抽取出来,将具体实现细节交给子类。参考Mybatis源代码分析之类型转换 ...

December 19, 2018 · 4 min · jiezi

每天一个设计模式之命令模式

作者按:《每天一个设计模式》旨在初步领会设计模式的精髓,目前采用javascript和python两种语言实现。诚然,每种设计模式都有多种实现方式,但此小册只记录最直截了当的实现方式 :)原文地址是:《每天一个设计模式之命令模式》欢迎关注个人技术博客:godbmw.com。每周 1 篇原创技术分享!开源教程(webpack、设计模式)、面试刷题(偏前端)、知识整理(每周零碎),欢迎长期关注!如果您也想进行知识整理 + 搭建功能完善/设计简约/快速启动的个人博客,请直接戳theme-bmw0. 示例代码此节全部代码《每天一个设计模式》地址1. 什么是“命令模式”?命令模式是一种数据驱动的设计模式,它属于行为型模式。请求以命令的形式包裹在对象中,并传给调用对象。调用对象寻找可以处理该命令的合适的对象,并把该命令传给相应的对象。该对象执行命令。在这三步骤中,分别有 3 个不同的主体:发送者、传递者和执行者。在实现过程中,需要特别关注。2. 应用场景有时候需要向某些对象发送请求,但是又不知道请求的接受者是谁,更不知道被请求的操作是什么。此时,命令模式就是以一种松耦合的方式来设计程序。3. 代码实现3.1 python3 实现命令对象将动作的接收者设置在属性中,并且对外暴露了execute接口(按照习惯约定)。在其他类设置命令并且执行命令的时候,只需要按照约定调用Command对象的execute()即可。到底是谁接受命令,并且怎么执行命令,都交给Command对象来处理!author = ‘godbmw.com’# 接受到命令,执行具体操作class Receiver(object): def action(self): print(“按钮按下,执行操作”)# 命令对象class Command: def init(self, receiver): self.receiver = receiver def execute(self): self.receiver.action()# 具体业务类class Button: def init(self): self.command = None # 设置命令对戏那个 def set_command(self, command): self.command = command # 按下按钮,交给命令对象调用相关函数 def down(self): if not self.command: return self.command.execute()if name == “main”: receiver = Receiver() command = Command(receiver) button = Button() button.set_command(command) button.down()3.2 ES6 实现setCommand方法为按钮指定了命令对象,命令对象为调用者(按钮)找到了接收者(MenuBar),并且执行了相关操作。而按钮本身并不需要关心接收者和接受操作。// 接受到命令,执行相关操作const MenuBar = { refresh() { console.log(“刷新菜单页面”); }};// 命令对象,execute方法就是执行相关命令const RefreshMenuBarCommand = receiver => { return { execute() { receiver.refresh(); } };};// 为按钮对象指定对应的 对象const setCommand = (button, command) => { button.onclick = () => { command.execute(); };};let refreshMenuBarCommand = RefreshMenuBarCommand(MenuBar);let button = document.querySelector(“button”);setCommand(button, refreshMenuBarCommand);下面是同级目录的 html 代码,在谷歌浏览器中打开创建的index.html,并且打开控制台,即可看到效果。<!DOCTYPE html><html lang=“en”> <head> <meta charset=“UTF-8” /> <meta name=“viewport” content=“width=device-width, initial-scale=1.0” /> <meta http-equiv=“X-UA-Compatible” content=“ie=edge” /> <title>命令模式</title> </head> <body> <button>按钮</button> <script src="./main.js"></script> </body></html>4. 参考《JavaScript 设计模式和开发实践》如何实现命令模式 ...

December 13, 2018 · 1 min · jiezi

每天一个设计模式之订阅-发布模式

博主按:《每天一个设计模式》旨在初步领会设计模式的精髓,目前采用javascript(靠这吃饭)和python(纯粹喜欢)两种语言实现。诚然,每种设计模式都有多种实现方式,但此小册只记录最直截了当的实现方式 :)0. 项目地址每天一个设计模式之订阅-发布模式·原文地址本节课代码《每天一个设计模式》地址1. 什么是“订阅-发布模式”?订阅-发布模式定义了对象之间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖它的对象都可以得到通知。了解过事件机制或者函数式编程的朋友,应该会体会到“订阅-发布模式”所带来的“时间解耦”和“空间解耦”的优点。借助函数式编程中闭包和回调的概念,可以很优雅地实现这种设计模式。2. “订阅-发布模式” vs 观察者模式订阅-发布模式和观察者模式概念相似,但在订阅-发布模式中,订阅者和发布者之间多了一层中间件:一个被抽象出来的信息调度中心。但其实没有必要太深究 2 者区别,因为《Head First 设计模式》这本经典书都写了:发布+订阅=观察者模式。其核心思想是状态改变和发布通知。在此基础上,根据语言特性,进行实现即可。3. 代码实现3.1 python3 实现python 中我们定义一个事件类Event, 并且为它提供 事件监听函数、(事件完成后)触发函数,以及事件移除函数。任何类都可以通过继承这个通用事件类,来实现“订阅-发布”功能。class Event: def init(self): self.client_list = {} def listen(self, key, fn): if key not in self.client_list: self.client_list[key] = [] self.client_list[key].append(fn) def trigger(self, *args, **kwargs): fns = self.client_list[args[0]] length = len(fns) if not fns or length == 0: return False for fn in fns: fn(*args[1:], **kwargs) return False def remove(self, key, fn): if key not in self.client_list or not fn: return False fns = self.client_list[key] length = len(fns) for _fn in fns: if _fn == fn: fns.remove(_fn) return True# 借助继承为对象安装 发布-订阅 功能class SalesOffice(Event): def init(self): super().init()# 根据自己需求定义一个函数:供事件处理完后调用def handle_event(event_name): def _handle_event(*args, **kwargs): print(“Price is”, *args, “at”, event_name) return _handle_eventif name == “main”: # 创建2个回调函数 fn1 = handle_event(“event01”) fn2 = handle_event(“event02”) sales_office = SalesOffice() # 订阅event01 和 event02 这2个事件,并且绑定相关的 完成后的函数 sales_office.listen(“event01”, fn1) sales_office.listen(“event02”, fn2) # 当两个事件完成时候,触发前几行绑定的相关函数 sales_office.trigger(“event01”, 1000) sales_office.trigger(“event02”, 2000) sales_office.remove(“event01”, fn1) # 打印:False print(sales_office.trigger(“event01”, 1000))3.2 ES6 实现JS 中一般用事件模型来代替传统的发布-订阅模式。任何一个对象的原型链被指向Event的时候,这个对象便可以绑定自定义事件和对应的回调函数。const Event = { clientList: {}, // 绑定事件监听 listen(key, fn) { if (!this.clientList[key]) { this.clientList[key] = []; } this.clientList[key].push(fn); return true; }, // 触发对应事件 trigger() { const key = Array.prototype.shift.apply(arguments), fns = this.clientList[key]; if (!fns || fns.length === 0) { return false; } for (let fn of fns) { fn.apply(null, arguments); } return true; }, // 移除相关事件 remove(key, fn) { let fns = this.clientList[key]; // 如果之前没有绑定事件 // 或者没有指明要移除的事件 // 直接返回 if (!fns || !fn) { return false; } // 反向遍历移除置指定事件函数 for (let l = fns.length - 1; l >= 0; l–) { let _fn = fns[l]; if (_fn === fn) { fns.splice(l, 1); } } return true; }};// 为对象动态安装 发布-订阅 功能const installEvent = obj => { for (let key in Event) { obj[key] = Event[key]; }};let salesOffices = {};installEvent(salesOffices);// 绑定自定义事件和回调函数salesOffices.listen( “event01”, (fn1 = price => { console.log(“Price is”, price, “at event01”); }));salesOffices.listen( “event02”, (fn2 = price => { console.log(“Price is”, price, “at event02”); }));salesOffices.trigger(“event01”, 1000);salesOffices.trigger(“event02”, 2000);salesOffices.remove(“event01”, fn1);// 输出: false// 说明删除成功console.log(salesOffices.trigger(“event01”, 1000));4. 参考维基百科·订阅-发布模式观察者模式和订阅-发布模式的不同《JavaScript 设计模式和开发实践》 ...

December 10, 2018 · 2 min · jiezi

这次,彻底弄懂接口及抽象类

作者:伯特出处:github.com/ruicbAndroid/LoulanPlan声明:本文出自伯特的《楼兰计划》,转载务必注明作者及出处。本文旨在讨论抽象类和接口的作用、实例及使用场景,都是我的理解和总结。更多关于接口和抽象类的概念知识,可自行查阅相关文档。1. 抽象类及其作用抽象类,顾名思义,即类的抽象。在介绍面向对象概念时,我们知道类是客观事物的抽象,而抽象类又是类的进一步抽象,该怎么理解呢?举个例子,我们定义若干个类 class BMW、class Benz、class Audi,分别对客观事物“宝马”、“奔驰”、“奥迪”三种汽车进行抽象,包含相关属性和行为(即方法)。但是我们知道,汽车都有通用的属性和行为,比如品牌、发动机、方向盘、轮胎等属性,前进、后退、转弯等行为,所以我们可以在宝马、奔驰等汽车之上,进一步抽象出“汽车”类 abstract class Car,包含通用的特性(属性和方法)。让 BMW、Benz、Audi 等继承抽象类 extends Car,便拥有了汽车的通用特性,然后在抽象类基础上定义各自的特殊属性及方法。这里的 abstract class Car 即抽象类,可以看出,抽象类是用来捕捉子类的通用特性的,包括属性及行为。2. 接口及其作用下面我们来看看接口,假使我研发出来一台会飞的汽车“伯特莱斯”(Bote-Royce),在程序中定义如下:class BoteRoyce extends Car { //…省略通用特性 /** * 可以飞 / void fly() { System.out.println(“假装会飞~”); }}看起来没问题:BoteRoyce extends Car:表达这是一辆汽车;fly() 方法:体现这车可以飞。但是,随着技术发展,出现了众多可以制造飞行汽车的厂商,难道每一个可以飞的汽车都去定义一个 fly() 方法?心想这还不简单,在抽象类 Car 中定义一个抽象方法 abstract void fly() 让子类去实现,不就可以了吗?No No No… 正如不是所有牛奶都叫特仑苏一样,不是所有汽车都会飞,飞行功能不是汽车的通用特性。将 fly() 方法定义在 Car 中,显然违背了“抽象类用来捕捉子类的通用特性”这一原则。在这种场景下,解决方案之一就是使用接口,如下:/* * 飞行器接口 /public interface Aircraft { //定义抽象方法 void fly();}类 BoteRoyce 的定义修改如下:/ * 实现 Aircraft 接口,表示具备飞行器能力 /class BoteRoyce extends Car implements Aircraft { /* * 覆写接口方法,实现飞行能力 / @Override void fly() { System.out.println(“假装会飞~”); }}再有其他品牌的飞行汽车,都可以通过 extends Car implements Aircraft 实现飞行能力。上述定义的 interface Aircraft 即为接口,我们通常使用接口对行为进行抽象。3. 接口和抽象类的区别关于二者的区别,可以结合前面的例子,来加深理解。抽象类是对类本质的抽象,表达的是 is a 的关系,比如:BMW is a Car。抽象类包含并实现子类的通用特性,将子类存在差异化的特性进行抽象,交由子类去实现。而接口是对行为的抽象,表达的是 like a 的关系。比如:Bote-Royce like a Aircraft(像飞行器一样可以飞),但其本质上 is a Car。接口的核心是定义行为,即实现类可以做什么,至于实现类主体是谁、是如何实现的,接口并不关心。4. 接口与抽象类的使用场景熟悉 Java 的同学可能会质疑,上述关于接口的使用,完全可以通过再次抽象 Car 去实现:/* * 会飞的汽车 /abstract class FlyCar extends Car { //定义抽象方法 public abstract void fly();}普通的汽车依然 extends Car,可以飞行的汽车 extends FlyCar 即可:/ * 继承 FlyCar,表示是可以飞行的汽车 /class BoteRoyce extends FlyCar { /* * 覆写抽象方法,实现飞行能力 */ @Override public void fly() { System.out.println(“假装会飞~”); }}如果你也这么想,表示你 get 到了抽象类的点。不过话说回来,这样的话接口岂不是没有存在的意义了?当然不是了。就 BoteRoyce 而言,如果你关心的是“飞行汽车”这个整体,那么定义抽象类 FlyCar 是个不错的选择;如果你关心的是汽车具备“飞行”的行为,那不妨继续沿用前面使用 Aircraft 接口的方案。这一点与设计模式中六大原则之一的“里氏替换原则”不谋而合,该原则指出:所有引用基类(抽象类或接口)的地方必须能透明地使用其子类的对象。也就是说,当你遵循该原则时,你必须要考虑你关心的是“飞行汽车”实体,还是“飞行”行为,并将其作为基类,从而决定程序所能接受的子类对象。同时,“接口隔离原则”指导我们,一个类对另一个类的依赖应该建立在最小的接口上。相比于抽象类 FlyCar,接口 Aircraft 能最大限度的减少对外暴露的接口,并隐藏细节,更符合这一原则。所以说啊,面向对象只是指导我们编程的思想,而非条条框框。在实际开发中,具体使用抽象类还是接口,并没有绝对限制,而是取决于你的业务场景和架构设计。5. 总结好了,本次关于接口与抽象类的总结就到这儿,你彻底弄懂了吗?下期分享再见~欢迎关注我的公众号“伯特说”: ...

December 10, 2018 · 1 min · jiezi

设计模式应用举例

纸上得来终觉浅,学习设计模式,看了很多书,但是始终还是觉得不如直接看例子来的更加客观具体,下面主要记录了js中的几个常见的设计模式举例,供自己以后复习的时候可以直接通过例子更快更好的理解设计模式。单例模式保证一个类仅有一个实例,并提供一个全局访问入口var getSingleton = function(fn){ var result; return function(){ return result || (result = fn.apply(this, arguments)); }}var createLoginLayer = function(){ var div; return function(){ if(!div){ div = document.createElement(‘div’); div.innerText = ‘这是弹窗’; div.style.display = ’none’; document.body.appendChild(div); } return div; }});var singletonCreateLoginLayer = getSingleton(createLoginLayer);document.getElementById(’loginBtn’).onclick = function(){ var layer = singletonCreateLoginLayer(); layer.style.display = ‘block’;}策略模式定义一系列算法,并使之可以相互替换。目的就是使算法的使用和定义分离出来。var Strategy = { S: function(salary){ return salary * 4; }, A: function(salary){ return salary * 3; }, B: function(salary){ return salary * 2; }};var getBouns = function(strategy, salary){ return Strategystrategy;}getBouns(‘B’, 1000); // 2000表单校验var strategies = { isNonEmpty: function(value, errorMsg){ …. }, minLength: function(value, length, errorMsg){ …. }, isMobile: function(value, errorMsg){ … }};var Validator = function(){ this.cache = [];};Validator.prototype.add = function(dom, rule, errorMsg){ var ary = [rule]; this.cache.push(function(){ var strategy = ary.shift(); ary.unshift(dom.value); ary.push(errorMsg); return strategies[strategy].apply(dom, ary); });}Validator.prototype.start = function(){ for(var i=0, validatorFunc; validatorFunc = this.cache[i++]){ var msg = validatorFunc(); if(msg){ return msg; } }}var validatorFunc = function(){ var validator = new Validator(); // 创建一个对象 validator.add(…); var errorMsg = validator.start(); // 获取校验结果}var registerForm = document.getElementById(‘form’);registerForm.onsubmit = function(){ var errorMsg = validatorFunc(); if(errorMsg){ return false; }}代理模式为一个对象提供一个代用品或占位符,以便控制对它的访问。当客户不方便直接访问一个对象或者不满足需要的时候提供一个替身对象来控制对这个对象的访问,客户实际上访问的是替身对象。单一职责原则指的是,就一个类(通常也包括对象和函数等)而言,应该仅有一个引起它变 化的原因。如果一个对象 了多 职责,就意味着这个对象将变得 大,引起它变化的原因可 能会有多个。面向对象设计 将行为分 到细 度的对象之中,如果一个对象 的职责过多, 等于把这些职责耦合到了一起,这种耦合会 致 和低内聚的设计。当变化发生时,设计可能 会 到意外的 。虚拟代理图片懒加载const myImg = ( const node = documnet.createElement(‘img’) document.body.appendChild(node) return { setSrc(src) { node.src= src } })()const proxy = ( const img = new Image() img.onload = () => { myImg.setSrc(this.src) } return { setImg(src) { img.src = src myImg.setSrc(’loading.gif’) } })()观察者模式发布订阅模式又叫观察者模式,它定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都将得到通知。在JavaScript开发中,我们一般用事件模型来替代传统的发布订阅模式。const Event = ( function() { var eventList = {} var addEventListen var trigger var remove addEventListen = function(eventName, fn) { eventList[eventName] = eventList[eventName] || [] eventList[eventName].push(fn) } trigger = function() { var key = Array.prototype.shift.call(arguments) var fns = eventList[key] if (!fns || !fns.length) { return } fns.forEach((fn, index) => { fn.apply(this, arguments) }) } remove = function(eventName, fn) { var fns = eventList[eventName] if (!fns || !fns.length) { return false } if (!fn) { fns.length = 0 } else { fns.forEach((_fn, index) => { if(fn === _fn) { fns.splice(index, 1) } }) } } return { addEventListen, trigger, remove } })()var testFn = () => { console.log(‘you have click a cancel btn’)}Event.addEventListen(‘click’, () => { console.log(‘you have click a button’)})Event.addEventListen(‘click’, () => { console.log(‘you have click button2’)})Event.addEventListen(‘click’, () => { console.log(‘you have click button3’)})Event.addEventListen(‘click’, testFn)Event.remove(‘click’, testFn)Event.trigger(‘click’)享元模式享元模式是为性能优化而生的,在一个存在大量相似对象的系统中,享元模式可以很好地解决大量对象带来的性能问题文件上传// uploadType作为内部状态,再抽离外部状态var Upload = function(uploadType){ this.uploadType = uploadType;};// 定义删除文件的方法Upload.prototype.delFile = function(id){ uploadManager.setExternalState(id, this); // 设置外部状态 if(this.fileSize < 3000){ return this.dom.parentNode.removeChild(this.dom); } if(window.confirm(‘确定要删除文件吗?’+ file.fileName)){ return this.dom.parentNode.removeChild(this.dom); }};// 工厂进行对象实例化var UploadFactory = (function(){ var createFlyWeightObjs = {}; return { create: function(uploadType){ if(createFlyWeightObjs[uploadType]){ return createFlyWeightObjs[uploadType]; } return createFlyWeightObjs[uploadType] = new Upload(uploadType); } }})();// 管理器封装外部状态var uploadManager = (function(){ var uploadDataBase = {}; // 存储外部状态 return { add: function(id, uploadType, fileName, fileSize){ var flyWeightObj = UploadFactory.create(uploadType); var dom = document.createElement(‘div’); dom.innerHTML = ‘…’; document.body.appendChild(dom); uploadDataBase[id] = { // 添加外部状态 fileName: fileName, fileSize: fileSize, dom: dom }; return flyWeightObj; }, setExternalState: function(id, flyWeightObj){ // 设置外部状态 var uploadData = uploadDataBase[id]; for(var i in uploadData){ flyWeightObj[i] = uploadData[i]; } } }})();var id = 0;window.startUpload = function(uploadType, files){ for(var i = 0, file; file = files[i++];){ var uploadObj = uploadManager.add(++id, uploadType, file.fileName, file.fileSize); }};startUpload(‘plugin’, [ { fileName: ‘1.txt’, fileSize: 1000 }, …]);对象池对象池维护一个装载空闲对象的池子,如果需要对象的时候,不是直接new,而是转从对象池里获取。如果对象池没有空闲对象,则创建一个新的对象,当获取出的对象完成它的职责之后,再进入池子等待被下次获取。var objectPoolFactory = function(createObjFn){ var objectPool = []; return { create: function(){ var obj = objectPool.length === 0 ? createObjFn.apply(this, arguments) : objectPool.shift(); return obj; }, recover: function(obj){ objectPool.push(obj); } };};// 现在利用ObjectPoolFactory来创建一个装载一些iframe的对象池var iframeFactory = objectPoolFactory(function(){ var iframe = document.createElement(‘iframe’); document.body.appendChild(iframe); iframe.onload = function(){ iframe.onload = null; iframeFactory.recover(iframe); } return iframe;});var iframe1 = iframeFactory.create();iframe1.src = ‘http://baidu.com’;var iframe2 = iframeFactory.create();iframe2.src = ‘http://qq.com’;setTimeout(function(){ var iframe3 = iframeFactory.create(); iframe3.src = ‘http://163.com’;}, 3000);中介者模式用一个中介者对象来封装一系列的对象交互。中介者使各个对象之间不会相互引用。从而使其达到松散耦合的目的。与观察者模式对比来看,中介者模式是观察者模式中的共享被观察者对象。在这个系统中的对象之间直接的发布/订阅关系被牺牲掉了,取而代之的是维护一个通信的中心节点。写程序是为了快速完成项目交付生产,而不是堆砌模式和过渡设计。关键就在于如何去衡量对象之间的耦合程度。如果对象之间的复杂耦合确实导致调用和维护出现了困难,而且这些耦合度随项目的变化呈指数增长曲线,那就可以考虑用中介者模式来重构代码。function Player(name, teamColor){ this.name = name; // 角色名字 this.teamColor = teamColor; // 队伍颜色 this.state = ‘alive’; // 玩家生存状态}Player.prototype.win = function(){ console.log(‘winner:’ + this.name);};Player.prototype.lose = function(){ console.log(’loser:’ + this.name);};Player.prototype.die = function(){ this.state = ‘dead’; playerDirector.ReceiveMessage(‘playerDead’, this); // 给中介者发送消息,玩家死亡};Player.prototype.remove = function(){ playerDirector.ReceiveMessage(‘removePlayer’, this); // 给中介者发送消息,移除一个玩家};Player.prototype.changeTeam = function(){ playerDirector.ReceiveMessage(‘changeTeam’, this); // 给中介者发送消息,玩家换队};var playerFactory = function(name, teamColor){ var newPlayer = new Player(name, teamColor); playerDirector.ReceiveMessage(‘addPlayer’, newPlayer); // 给中介者发送消息,新增玩家 return newPlayer;};// 实现playerDirector对象var playDirector = (function(){ var players = {}; // 保存所有玩家 var operations = {}; // 中介者可以执行的操作 // 新增一个玩家 operations.add = function(player){ var teamColor = player.teamColor; players[teamColor] = players[teamColor] || []; players[teamColor].push(player); }; // 移除一个玩家 operations.removePlayer = function(player){ var teamColor = player.teamColor; var teamPlayers = players[teamColor] || []; for(var i=teamPlayers.length - 1; i >= 0 ;i –){ if(teamPlayers[i] === player){ teamPlayers.splice(i, 1); } } }; // 玩家换队 operations.changeTeam = function(player, newTeamColor){ operations.removePlayer(player); // 从原队伍中删除 player.teamColor = newTeamColor; // 换颜色 operations.addPlayer(player); // 新增玩家到新的队伍 } operations.playerDead = function(player){ var teamColor = player.teamColor; var teamPlayer = players[teamColor]; var all_dead = true; // 遍历队友列表 for(var i=0, player; player = teamPlayer[i++];){ if(player.state !== ‘dead’){ all_dead = false; break; } } // 如果队友全部死亡 if(all_dead === true){ this.lose(); // 通知所有队友玩家游戏失败 for(var i=0, player; player = teamPlayer[i++];){ player.lose(); } // 通知所有敌人游戏胜利 for(var color in players){ if(color !== teamColor){ var teamPlayers = players[color]; for(var i=0, player; player = teamPlayers[i++];){ player.win(); } } } } } var ReceiveMessage = function(){ var message = Array.prototype.shift.call(arguments); operations[message].apply(this, arguments); }; return { ReciveMessage: ReceiveMessage }})();// 创建8个玩家对象var player1 = playerFactory(‘a’, ‘red’);var player2 = playerFactory(‘b’, ‘red’);var player3 = playerFactory(‘c’, ‘red’);var player4 = playerFactory(’d’, ‘red’);var player5 = playerFactory(’e’, ‘blue’);var player6 = playerFactory(‘f’, ‘blue’);var player7 = playerFactory(‘g’, ‘blue’);var player8 = playerFactory(‘h’, ‘blue’);装饰者模式给对象动态地增加职责的方式称为装饰者模式。装饰者模式能够在不改变对象自身的基础上,在程序运行期间给对象动态地添加职责。跟继承相比,装饰者是一种更轻便灵活的做法,这是一种“即用即付”的方式。函数通过Function.prototype.before或者Function.prototype.after被装饰之后,返回的实际上是一个新的函数,如果在原函数上保存了一些属性,那么这些属性会丢失。这种装饰方式也叠加了函数的作用域,如果装饰的链条过长,性能上也会受到一些影响。Function.prototype.before = function(beforeFn){ var _self = this; return function(){ if(beforefn.apply(this, arguments) === false){ return; }; return _self.apply(this, arguments); }};var validata = function(){ if(username.value === ‘’){ alert(‘不能为空’); return false; } if(password.value === ‘’){ alert(‘不能为空’); return false; }};var formSubmit = function(){ var param = { username: username.value, password: password.value }; ajax(’….’);};formSubmit = formSubimt.before(validata);submitBtn.onclick = function(){ formSubmit();};状态模式状态模式的关键是把事物的每种状态都封装成单独的类,跟此种状态有关的行为都被封装在这个类的内部,只需要在上下文中,把某个请求委托给当前的状态对象即可,该状态对象会福州渲染它自身的行为。// Light 类var Light = function(){ this.offLightState = new OffLightState(this); this.weekLightState = new WeekLightState(this); this.strongLightState = new StrongLightState(this); this.button = null;};Light.prototype.init = function(){ var button = document.createElement(‘button’); var self = this; this.button = document.body.appendChild(button); this.button.innerHTML = ‘开关’; this.currState = this.offLightState; this.button.onclick = function(){ self.currState.buttonWasPressed(); };};// offLightStatevar OffLightState = function(light){ this.light = light;};OffLightState.prototype.buttonWasPressed = function(){ console.log(‘弱光’) // offLightState 对应的行为 this.light.setState(this.light.weekLightState); // 切换状态到 weekLightState};文中代码主要来自曾探老师的《JavaScript设计模式与开发实践》,书中的内容关于设计模式写的更加详实细致,如果想学习设计模式推荐这本书入门哈! ...

December 6, 2018 · 5 min · jiezi

面试官问:能否模拟实现JS的call和apply方法

之前写过两篇《面试官问:能否模拟实现JS的new操作符》和《面试官问:能否模拟实现JS的bind方法》其中模拟bind方法时是使用的call和apply修改this指向。但面试官可能问:能否不用call和apply来实现呢。意思也就是需要模拟实现call和apply的了。附上之前写文章写过的一段话:已经有很多模拟实现call和apply的文章,为什么自己还要写一遍呢。学习就好比是座大山,人们沿着不同的路登山,分享着自己看到的风景。你不一定能看到别人看到的风景,体会到别人的心情。只有自己去登山,才能看到不一样的风景,体会才更加深刻。先通过MDN认识下call和applyMDN 文档:Function.prototype.call()语法fun.call(thisArg, arg1, arg2, …)thisArg在fun函数运行时指定的this值。需要注意的是,指定的this值并不一定是该函数执行时真正的this值,如果这个函数处于非严格模式下,则指定为null和undefined的this值会自动指向全局对象(浏览器中就是window对象),同时值为原始值(数字,字符串,布尔值)的this会指向该原始值的自动包装对象。arg1, arg2, …指定的参数列表返回值返回值是你调用的方法的返回值,若该方法没有返回值,则返回undefined。MDN 文档:Function.prototype.apply()func.apply(thisArg, [argsArray])thisArg可选的。在 func 函数运行时使用的 this 值。请注意,this可能不是该方法看到的实际值:如果这个函数处于非严格模式下,则指定为 null 或 undefined 时会自动替换为指向全局对象,原始值会被包装。argsArray可选的。一个数组或者类数组对象,其中的数组元素将作为单独的参数传给 func 函数。如果该参数的值为 null 或 undefined,则表示不需要传入任何参数。从ECMAScript 5 开始可以使用类数组对象。返回值调用有指定this值和参数的函数的结果。直接先看例子1call 和 apply 的异同相同点:1、call和apply的第一个参数thisArg,都是func运行时指定的this。而且,this可能不是该方法看到的实际值:如果这个函数处于非严格模式下,则指定为 null 或 undefined 时会自动替换为指向全局对象,原始值会被包装。2、都可以只传递一个参数。不同点:apply只接收两个参数,第二个参数可以是数组也可以是类数组,其实也可以是对象,后续的参数忽略不计。call接收第二个及以后一系列的参数。看两个简单例子1和2**:// 例子1:浏览器环境 非严格模式下var doSth = function(a, b){ console.log(this); console.log([a, b]);}doSth.apply(null, [1, 2]); // this是window // [1, 2]doSth.apply(0, [1, 2]); // this 是 Number(0) // [1, 2]doSth.apply(true); // this 是 Boolean(true) // [undefined, undefined]doSth.call(undefined, 1, 2); // this 是 window // [1, 2]doSth.call(‘0’, 1, {a: 1}); // this 是 String(‘0’) // [1, {a: 1}]// 例子2:浏览器环境 严格模式下’use strict’;var doSth2 = function(a, b){ console.log(this); console.log([a, b]);}doSth2.call(0, 1, 2); // this 是 0 // [1, 2]doSth2.apply(‘1’); // this 是 ‘1’ // [undefined, undefined]doSth2.apply(null, [1, 2]); // this 是 null // [1, 2]typeof 有7种类型(undefined number string boolean symbol object function),笔者都验证了一遍:更加验证了相同点第一点,严格模式下,函数的this值就是call和apply的第一个参数thisArg,非严格模式下,thisArg值被指定为 null 或 undefined 时this值会自动替换为指向全局对象,原始值则会被自动包装,也就是new Object()。重新认识了call和apply会发现:它们作用都是一样的,改变函数里的this指向为第一个参数thisArg,如果明确有多少参数,那可以用call,不明确则可以使用apply。也就是说完全可以不使用call,而使用apply代替。也就是说,我们只需要模拟实现apply,call可以根据参数个数都放在一个数组中,给到apply即可。模拟实现 apply既然准备模拟实现apply,那先得看看ES5规范。ES5规范 英文版,ES5规范 中文版。apply的规范下一个就是call的规范,可以点击打开新标签页去查看,这里摘抄一部分。Function.prototype.apply (thisArg, argArray) 当以 thisArg 和 argArray 为参数在一个 func 对象上调用 apply 方法,采用如下步骤:1.如果 IsCallable(func) 是 false, 则抛出一个 TypeError 异常。2.如果 argArray 是 null 或 undefined, 则返回提供 thisArg 作为 this 值并以空参数列表调用 func 的 [[Call]] 内部方法的结果。3.返回提供 thisArg 作为 this 值并以空参数列表调用 func 的 [[Call]] 内部方法的结果。4.如果 Type(argArray) 不是 Object, 则抛出一个 TypeError 异常。5~8 略9.提供 thisArg 作为 this 值并以 argList 作为参数列表,调用 func 的 [[Call]] 内部方法,返回结果。apply 方法的 length 属性是 2。在外面传入的 thisArg 值会修改并成为 this 值。thisArg 是 undefined 或 null 时它会被替换成全局对象,所有其他值会被应用 ToObject 并将结果作为 this 值,这是第三版引入的更改。结合上文和规范,如何将函数里的this指向第一个参数thisArg呢,这是一个问题。这时候请出例子3:// 浏览器环境 非严格模式下var doSth = function(a, b){ console.log(this); console.log(this.name); console.log([a, b]);}var student = { name: ‘轩辕Rowboat’, doSth: doSth,};student.doSth(1, 2); // this === student // true // ‘轩辕Rowboat’ // [1, 2]doSth.apply(student, [1, 2]); // this === student // true // ‘轩辕Rowboat’ // [1, 2]可以得出结论1:在对象student上加一个函数doSth,再执行这个函数,这个函数里的this就指向了这个对象。那也就是可以在thisArg上新增调用函数,执行后删除这个函数即可。知道这些后,我们试着容易实现第一版本:// 浏览器环境 非严格模式function getGlobalObject(){ return this;}Function.prototype.applyFn = function apply(thisArg, argsArray){ // apply 方法的 length 属性是 2。 // 1.如果 IsCallable(func) 是 false, 则抛出一个 TypeError 异常。 if(typeof this !== ‘function’){ throw new TypeError(this + ’ is not a function’); } // 2.如果 argArray 是 null 或 undefined, 则 // 返回提供 thisArg 作为 this 值并以空参数列表调用 func 的 [[Call]] 内部方法的结果。 if(typeof argsArray === ‘undefined’ || argsArray === null){ argsArray = []; } // 3.如果 Type(argArray) 不是 Object, 则抛出一个 TypeError 异常 . if(argsArray !== new Object(argsArray)){ throw new TypeError(‘CreateListFromArrayLike called on non-object’); } if(typeof thisArg === ‘undefined’ || thisArg === null){ // 在外面传入的 thisArg 值会修改并成为 this 值。 // ES3: thisArg 是 undefined 或 null 时它会被替换成全局对象 浏览器里是window thisArg = getGlobalObject(); } // ES3: 所有其他值会被应用 ToObject 并将结果作为 this 值,这是第三版引入的更改。 thisArg = new Object(thisArg); var __fn = ‘__fn’; thisArg[__fn] = this; // 9.提供 thisArg 作为 this 值并以 argList 作为参数列表,调用 func 的 [[Call]] 内部方法,返回结果 var result = thisArg__fn; delete thisArg[__fn]; return result;};实现第一版后,很容易找出两个问题:[ ] 1.fn 同名覆盖问题,thisArg对象上有__fn,那就被覆盖了然后被删除了。针对问题1解决方案一:采用ES6 Sybmol() 独一无二的。可以本来就是模拟ES3的方法。如果面试官不允许用呢。解决方案二:自己用Math.random()模拟实现独一无二的key。面试时可以直接用生成时间戳即可。// 生成UUID 通用唯一识别码// 大概生成 这样一串 ‘18efca2d-6e25-42bf-a636-30b8f9f2de09’function generateUUID(){ var i, random; var uuid = ‘’; for (i = 0; i < 32; i++) { random = Math.random() * 16 | 0; if (i === 8 || i === 12 || i === 16 || i === 20) { uuid += ‘-’; } uuid += (i === 12 ? 4 : (i === 16 ? (random & 3 | 8) : random)) .toString(16); } return uuid;}// 简单实现// ‘’ + new Date().getTime();如果这个key万一这对象中还是有,为了保险起见,可以做一次缓存操作。比如如下代码:var student = { name: ‘轩辕Rowboat’, doSth: ‘doSth’,};var originalVal = student.doSth;var hasOriginalVal = student.hasOwnProperty(‘doSth’);student.doSth = function(){};delete student.doSth;// 如果没有,originalVal则为undefined,直接赋值新增了一个undefined,这是不对的,所以需判断一下。if(hasOriginalVal){ student.doSth = originalVal;}console.log(‘student:’, student); // { name: ‘轩辕Rowboat’, doSth: ‘doSth’ }[ ] 2.使用了ES6扩展符…解决方案一:采用eval来执行函数。eval把字符串解析成代码执行。MDN 文档:eval语法eval(string)参数string表示JavaScript表达式,语句或一系列语句的字符串。表达式可以包含变量以及已存在对象的属性。返回值执行指定代码之后的返回值。如果返回值为空,返回undefined解决方案二:但万一面试官不允许用eval呢,毕竟eval是魔鬼。可以采用new Function()来生成执行函数。MDN 文档:Function语法new Function ([arg1[, arg2[, …argN]],] functionBody)参数arg1, arg2, … argN被函数使用的参数的名称必须是合法命名的。参数名称是一个有效的JavaScript标识符的字符串,或者一个用逗号分隔的有效字符串的列表;例如“×”,“theValue”,或“A,B”。functionBody一个含有包括函数定义的JavaScript语句的字符串。接下来看两个例子:简单例子:var sum = new Function(‘a’, ‘b’, ‘return a + b’);console.log(sum(2, 6));// 稍微复杂点的例子:var student = { name: ‘轩辕Rowboat’, doSth: function(argsArray){ console.log(argsArray); console.log(this.name); }};// var result = student.doSth([‘Rowboat’, 18]);// 用new Function()生成函数并执行返回结果var result = new Function(‘return arguments[0][arguments[1]](arguments[2][0], arguments[2][1])’)(student, ‘doSth’, [‘Rowboat’, 18]);// 个数不定// 所以可以写一个函数生成函数代码:function generateFunctionCode(argsArrayLength){ var code = ‘return arguments[0][arguments[1]](’; for(var i = 0; i < argsLength; i++){ if(i > 0){ code += ‘,’; } code += ‘arguments[2][’ + i + ‘]’; } code += ‘)’; // return arguments[0][arguments[1]](arg1, arg2, arg3…) return code;}你可能不知道在ES3、ES5中 undefined 是能修改的可能大部分人不知道。ES5中虽然在全局作用域下不能修改,但在局部作用域中也是能修改的,不信可以复制以下测试代码在控制台执行下。虽然一般情况下是不会的去修改它。function test(){ var undefined = 3; console.log(undefined); // chrome下也是 3}test();所以判断一个变量a是不是undefined,更严谨的方案是typeof a === ‘undefined’或者a === void 0;这里面用的是void,void的作用是计算表达式,始终返回undefined,也可以这样写void(0)。更多可以查看韩子迟的这篇文章:为什么用「void 0」代替「undefined」解决了这几个问题,比较容易实现如下代码。使用 new Function() 模拟实现的apply// 浏览器环境 非严格模式function getGlobalObject(){ return this;}function generateFunctionCode(argsArrayLength){ var code = ‘return arguments[0][arguments[1]](’; for(var i = 0; i < argsArrayLength; i++){ if(i > 0){ code += ‘,’; } code += ‘arguments[2][’ + i + ‘]’; } code += ‘)’; // return arguments[0][arguments[1]](arg1, arg2, arg3…) return code;}Function.prototype.applyFn = function apply(thisArg, argsArray){ // apply 方法的 length 属性是 2。 // 1.如果 IsCallable(func) 是 false, 则抛出一个 TypeError 异常。 if(typeof this !== ‘function’){ throw new TypeError(this + ’ is not a function’); } // 2.如果 argArray 是 null 或 undefined, 则 // 返回提供 thisArg 作为 this 值并以空参数列表调用 func 的 [[Call]] 内部方法的结果。 if(typeof argsArray === ‘undefined’ || argsArray === null){ argsArray = []; } // 3.如果 Type(argArray) 不是 Object, 则抛出一个 TypeError 异常 . if(argsArray !== new Object(argsArray)){ throw new TypeError(‘CreateListFromArrayLike called on non-object’); } if(typeof thisArg === ‘undefined’ || thisArg === null){ // 在外面传入的 thisArg 值会修改并成为 this 值。 // ES3: thisArg 是 undefined 或 null 时它会被替换成全局对象 浏览器里是window thisArg = getGlobalObject(); } // ES3: 所有其他值会被应用 ToObject 并将结果作为 this 值,这是第三版引入的更改。 thisArg = new Object(thisArg); var fn = ‘’ + new Date().getTime(); // 万一还是有 先存储一份,删除后,再恢复该值 var originalVal = thisArg[__fn]; // 是否有原始值 var hasOriginalVal = thisArg.hasOwnProperty(__fn); thisArg[__fn] = this; // 9.提供 thisArg 作为 this 值并以 argList 作为参数列表,调用 func 的 [[Call]] 内部方法,返回结果。 // ES6版 // var result = thisArg__fn; var code = generateFunctionCode(argsArray.length); var result = (new Function(code))(thisArg, __fn, argsArray); delete thisArg[__fn]; if(hasOriginalVal){ thisArg[__fn] = originalVal; } return result;};利用模拟实现的apply模拟实现callFunction.prototype.callFn = function call(thisArg){ var argsArray = []; var argumentsLength = arguments.length; for(var i = 0; i < argumentsLength - 1; i++){ // argsArray.push(arguments[i + 1]); argsArray[i] = arguments[i + 1]; } console.log(‘argsArray:’, argsArray); return this.applyFn(thisArg, argsArray);}// 测试例子var doSth = function (name, age){ var type = Object.prototype.toString.call(this); console.log(typeof doSth); console.log(this === firstArg); console.log(’type:’, type); console.log(’this:’, this); console.log(‘args:’, [name, age], arguments); return ’this–’;};var name = ‘window’;var student = { name: ‘轩辕Rowboat’, age: 18, doSth: ‘doSth’, __fn: ‘doSth’,};var firstArg = student;var result = doSth.applyFn(firstArg, [1, {name: ‘Rowboat’}]);var result2 = doSth.callFn(firstArg, 1, {name: ‘Rowboat’});console.log(‘result:’, result);console.log(‘result2:’, result2);细心的你会发现注释了这一句argsArray.push(arguments[i + 1]);,事实上push方法,内部也有一层循环。所以理论上不使用push性能会更好些。面试官也可能根据这点来问时间复杂度和空间复杂度的问题。// 看看V8引擎中的具体实现:function ArrayPush() { var n = TO_UINT32( this.length ); // 被push的对象的length var m = %_ArgumentsLength(); // push的参数个数 for (var i = 0; i < m; i++) { this[ i + n ] = %_Arguments( i ); // 复制元素 (1) } this.length = n + m; // 修正length属性的值 (2) return this.length;};行文至此,就基本结束了,你可能还发现就是写的非严格模式下,thisArg原始值会包装成对象,添加函数并执行,再删除。而严格模式下还是原始值这个没有实现,而且万一这个对象是冻结对象呢,Object.freeze({}),是无法在这个对象上添加属性的。所以这个方法只能算是非严格模式下的简版实现。最后来总结一下。总结通过MDN认识call和apply,阅读ES5规范,到模拟实现apply,再实现call。就是使用在对象上添加调用apply的函数执行,这时的调用函数的this就指向了这个thisArg,再返回结果。引出了ES6 Symbol,ES6的扩展符…、eval、new Function(),严格模式等。事实上,现实业务场景不需要去模拟实现call和apply,毕竟是ES3就提供的方法。但面试官可以通过这个面试题考察候选人很多基础知识。如:call、apply的使用。ES6 Symbol,ES6的扩展符…,eval,new Function(),严格模式,甚至时间复杂度和空间复杂度等。读者发现有不妥或可改善之处,欢迎指出。另外觉得写得不错,可以点个赞,也是对笔者的一种支持。// 最终版版 删除注释版,详细注释看文章// 浏览器环境 非严格模式function getGlobalObject(){ return this;}function generateFunctionCode(argsArrayLength){ var code = ‘return arguments[0][arguments[1]](’; for(var i = 0; i < argsArrayLength; i++){ if(i > 0){ code += ‘,’; } code += ‘arguments[2][’ + i + ‘]’; } code += ‘)’; return code;}Function.prototype.applyFn = function apply(thisArg, argsArray){ if(typeof this !== ‘function’){ throw new TypeError(this + ’ is not a function’); } if(typeof argsArray === ‘undefined’ || argsArray === null){ argsArray = []; } if(argsArray !== new Object(argsArray)){ throw new TypeError(‘CreateListFromArrayLike called on non-object’); } if(typeof thisArg === ‘undefined’ || thisArg === null){ thisArg = getGlobalObject(); } thisArg = new Object(thisArg); var fn = ‘’ + new Date().getTime(); var originalVal = thisArg[__fn]; var hasOriginalVal = thisArg.hasOwnProperty(__fn); thisArg[__fn] = this; var code = generateFunctionCode(argsArray.length); var result = (new Function(code))(thisArg, __fn, argsArray); delete thisArg[__fn]; if(hasOriginalVal){ thisArg[__fn] = originalVal; } return result;};Function.prototype.callFn = function call(thisArg){ var argsArray = []; var argumentsLength = arguments.length; for(var i = 0; i < argumentsLength - 1; i++){ argsArray[i] = arguments[i + 1]; } return this.applyFn(thisArg, argsArray);}扩展阅读《JavaScript设计模式与开发实践》- 第二章 第 2 章 this、call和applyJS魔法堂:再次认识Function.prototype.call不用call和apply方法模拟实现ES5的bind方法JavaScript深入之call和apply的模拟实现关于作者:常以轩辕Rowboat为名混迹于江湖。前端路上 | PPT爱好者 | 所知甚少,唯善学。个人博客segmentfault个人主页掘金个人主页知乎github ...

November 30, 2018 · 6 min · jiezi

解读 IoC 框架 InversifyJS

InversityJS 是一个 IoC 框架。IoC(Inversion of Control) 包括依赖注入(Dependency Injection) 和依赖查询(Dependency Lookup)。相比于类继承的方式,控制反转解耦了父类和子类的联系。案例解析import ‘reflect-metadata’import { inject, injectable, Container } from ‘inversify’const container = new Container()@injectable()class PopMusic { getName() { return ‘流行音乐’ }}container.bind(‘request1’).to(PopMusic)@injectable()class ClassicalMusic { getName() { return ‘古典音乐’ }}container.bind(‘request2’).to(ClassicalMusic)@injectable()class Music { pm: any cm: any constructor( @inject(‘request1’) popMusic: any, @inject(‘request2’) classicalMusic: any) { this.pm = popMusic this.cm = classicalMusic } getName() { const result = this.pm.getName() + this.cm.getName() return result }}container.bind(‘Plan’).to(Music)const music: any = container.get(‘Plan’)console.log(music.getName()) // 流行音乐古典音乐上述案例可以抽象为下图:虚线表示可以注入,但在代码中没有表现出来。代码流程可概括如下:1.将所有相关类(这里指 Music、popMusic、classicMusic) 通过 @injectable 声明进 container 容器;2.通过 container.get() 获取 container.bind().to(target) 中的目标对象(这里指 Music);3.如果目标对象中的 constructor() 里有 @inject(), 则将相应的实例(这里指 PopMusic 与 classicalMusic 的实例)当作构造函数的参数’注入’;inject/injectable 相关源码inject 源码简化如下:// 这是一个属性装饰器function inject(serviceIdentifier) { return function (target, targetKey) { const metadataValue = { [targetKey]: [Metadata { key: ‘inject’, value: serviceIdentifier })] } Reflect.defineMetadata(‘inversify:tagged_props’, metadataValue, target.constructor); }}injectable 源码简化如下:// 这是一个类装饰器function injectable() { return function (target) { const metadataValue = [] Reflect.defineMetadata(‘inversify:paramtypes’, metadataValue, target) return target }}从简化版源码中可以看到 inject/injectable 最终是对 Reflect.defineMetadata() 的一个使用。可以将 metadata 看成是一种相对高效的数据结构。reflect-metadataInversityJS 深度结合了 reflect-metadata, reflect-metadata 在 Reflect 基础上对其 api 进行了扩展。metadata 本质上是一个 WeakMap 对象。扩展:Map 和 WeakMap 的区别Reflect.defineMetadata(metadataKey, metadataValue, target[, propertyKey]) 简化版实现如下:const Metadata = new WeakMap()function defineMetadata(metadataKey, metadataValue, target, propertyKey) { metadataMap = new Map() metadataMap.set(metadataKey, metadataValue) targetMetadata = new Map() targetMetadata.set(propertyKey, metadataMap) Metadata.set(target, targetMetadata)}Reflect.getOwnMetadata(metadataKey, target[, propertyKey]) 简化版实现如下:function getOwnMetadata(metadataKey, target, propertyKey) { var targetMetadata = Metadata.get(target) var metadataMap = targetMetadata.get(propertyKey) return metadataMap.get(metadataKey)}其数据结构可表示如下:WeakMap { target: Map { propertyKey: Map { metadataKey: metadataValue } }}相关链接Architecture overview ...

November 8, 2018 · 2 min · jiezi

JS 发布订阅模式

首先声明,本文并非原创。原文请点击这里,本文是在原文的基础上加入一些自己的一些东西,方便以后自己理解与查看。发布订阅模式事件发布/订阅模式 (PubSub) 在异步编程中帮助我们完成更松的解耦,甚至在 MVC、MVVC 的架构中以及设计模式中也少不了发布-订阅模式的参与。优点:在异步编程中实现更深的解耦缺点:如果过多的使用发布订阅模式,会增加维护的难度实现发布订阅模式var Event = function() { this.obj = {}}Event.prototype.on = function(eventType,fn) { if(!this.obj[eventType]) { this.obj[eventType] = [] } this.obj[eventType].push(fn)}Event.prototype.emit = function() { // 取第一个参数,作为eventType var eventType = Array.prototype.shift.call(arguments); // 获取事件数组 var arr = this.obj[eventType]; var len = arr.length; // 循环数组,一次执行其中的函数 for(var i=0;i<len;i++) { // 直接调用arr[i],其this指向为undefined(严格模式下) // 因此用apply将this指向arr[i] // 数组shift函数取出第一个参数,将剩下的参数传入函数中 arr[i].apply(arr[i],arguments) }}var ev = new Event()ev.on(‘click’,function(a) { // 订阅 console.log(a)})ev.emit(‘click’,1) // 发布以上代码只能实现先订阅,再发布。直接发布就会报错。如何实现可以先发布,然后订阅?var Event = function() { this.obj = {}; this.cacheList = [];}Event.prototype.emit = function() { const args = arguments; //函数参数 const that = this; //this指向,保持cache函数的this指向 function cache() { var eventType = Array.prototype.shift.call(arg) var arr = that.obj[eventType] for (let i = 0; i < arr.length; i++) { arr[i].apply(arr[i], arg) } } this.cacheList.push(cache) // 采用闭包,保持对emit函数中参数和that的引用}Event.prototype.on = function(eventType,fn) { if(!this.obj[eventType]) { this.obj[eventType] = [] } this.obj[eventType].push(fn) // 在订阅函数中执行emit函数中缓存的函数 for (let i = 0; i < this.cacheList.length; i++) { this.cacheListi }}改成这样后就实现了先发布函数,再订阅的过程。但是也只能先发布,然后再订阅,反过来就行不通。 ...

October 26, 2018 · 1 min · jiezi

[探索]在开发中尽量提高代码的复用性

ctrl+c 和 ctrl+v 给我们带来了很多的便利,但是也使我们变得懒惰,不愿思考。1.前言相信很多人和我一样,在开发项目的时候,因为项目赶,或者一时没想到等原因。频繁使用 ctrl+c 和 ctrl+v ,导致代码很多都是重复的。这几天,也看了自己以前写的代码,简单的探索了一下,挑选几个实例,分享下如何在特定场景下,保证代码质量前提下,提高代码复用性。提高代码的复用性,应该是不同场景,不同解决方案的。同时也要保证代码质量。不建议强制提高代码复用性,如果提高代码复用性会大大的降低代码的可读性,维护性,可能会得不偿失。2.HTML+CSS在做项目的时候,相信页面上总会有很多相似的按钮,比如下图,项目上几个相似的按钮。面对这样的需求,之前是直接写三个按钮<button type=“button” class=‘u-btn-yes-samll’>确定</button><button type=“button” class=‘u-btn-yes’>确定</button><button type=“button” class=‘u-btn-yes-big’>确定</button>cssbutton{ border:none; font-family:‘微软雅黑’; }.u-btn-yes{ width:80px; height:36px; font-size:14px; color:#fff; border-radius:10px; background:#09f;}.u-btn-yes-samll{ width:60px; height:30px; font-size:12px; color:#fff; border-radius:10px; background:#09f;}.u-btn-yes-big{ width:100px; height:40px; font-size:16px; color:#fff; border-radius:10px; background:#09f;}这样相当于每增加一种按钮,就增加了几行的 css 代码,实际上改变的只有 width ,height ,font-size 这三个属性。实际上可以根据大小进行代码复用。.u-btn-yes{ width:80px; height:36px; font-size:14px; color:#fff; border-radius:10px; background:#09f;}.u-btn-yes.u-btn-samll{ width:60px; height:30px; font-size:12px;}.u-btn-yes.u-btn-big{ width:100px; height:40px; font-size:16px;}页面调用<button type=“button” class=‘u-btn-yes u-btn-samll’>确定</button><button type=“button” class=‘u-btn-yes’>确定</button><button type=“button” class=‘u-btn-yes u-btn-big’>确定</button>页面上可能还有很多按钮类似的,但是不同颜色的(如下图),也可以灵活处理,这样还可以自由组合。这个也是社区上很多 UI 库所使用的方式。.u-btn{ width:80px; height:36px; font-size:14px; color:#fff; border-radius:10px; background:#09f;}.u-btn-samll{ width:60px; height:30px; font-size:12px;}.u-btn-big{ width:100px; height:40px; font-size:16px;}.u-btn-red{ background:#f33;}.u-btn-yellow{ background:#f90;}html<button type=“button” class=‘u-btn u-btn-samll’>确定</button><button type=“button” class=‘u-btn u-btn-red’>确定</button><button type=“button” class=‘u-btn u-btn-big u-btn-yellow’>确定</button>对于这些按钮,不建议设置 margin ,positon 等样式,因为不同的按钮在不同的地方,上面这几个属性基本不会一样。3.JavaScript关于提高代码复用性的好处,在上面 HTML+CSS的实例里面并没有很明显的优势,但在 JS 里面提高代码的复用性优势就比较明显了,下面简单列举几个例子。3-1.使用装饰者模式进行封装在上家公司的项目里面有这样代码,目的也很明显,就是用户填写表单的时候,还没有填写完整就提交,前端这里就需要给一个简单的提示。//当信息没填写完整的时候弹出提示layer.alert(‘请检查信息是否填写完整’,{ title:‘提示’, icon:2}) 基于 layer 这个开源库,代码看得特别的简单。但是随着项目的开发,用户填写表单的地方有多个,那么上面的代码就会被复制到多个地方,这样难免会有有点多余。另外,这样做最大的一个问题就是:如果上面的代码在项目上有20个地方在用,有一天需求变了,title 这个属性值要从‘提示’变成‘警告’。那就麻烦了,要找20个地方,即使编辑器有全局替换的功能,这样的改动出问题的概率也比较大。面对这样的情况。之前处理的方法是对这个弹窗进行简单粗暴的封装,方便复用。function openTips(){ //当信息没填写完整的时候弹出提示 layer.alert(‘请检查信息是否填写完整’,{ title:‘提示’, icon:2 });}在需要的地方,需要的时候进行调用就好,这样可以写少很多代码!修改起来,也只需要修改 openTips 这一个地方就完事了。openTips();3-2.使用策略模式代替 switch下面再看一个实例。借用下之前群友的发的代码。模拟数据let listWarnConf = [ { warnType: 1, warnCondition: 220, }, { warnType: 2, warnCondition: 36, }, { warnType: 3, warnCondition: 45, }, { warnType: 4, warnCondition: 110, }, { warnType: 5, warnCondition: 380, }]业务逻辑代码listWarnConf.forEach(item => { switch(item.warnType) { case 1: item.warnTypeText = ‘超压’; item.warnConditionText = 电压高于${item.warnCondition}V break; case 2: item.warnTypeText = ‘欠压’; item.warnConditionText = 电压低于${item.warnCondition}V break case 3: item.warnTypeText = ‘超载’; item.warnConditionText = 电流高于${item.warnCondition}A break case 4: item.warnTypeText = ‘电压不平衡’; item.warnConditionText = 电压不平衡高于${item.warnCondition}% break case 5: item.warnTypeText = ‘电流不平衡’; item.warnConditionText = 电流不平衡${item.warnCondition}% break }})这样看着结果是没问题的,但是看着那么多 case 执行的都是赋值操作。而且最大的问题和上面一样,如果多个地方使用,需求变了,那么还是要修改这么多的地方,下面优化下,让代码的复用性提高下。//设置配置数据let warnConfig={ 1:{ warnTypeText:‘超压’, warnConditionText:‘电压高于replaceTextV’ }, 2:{ warnTypeText:‘欠压’, warnConditionText:‘电压低于replaceTextV’ }, 3:{ warnTypeText:‘超载’, warnConditionText:‘电流高于replaceTextV’ }, 4:{ warnTypeText:‘电压不平衡’, warnConditionText:‘电压不平衡高于replaceText%’ }, 5:{ warnTypeText:‘电流不平衡’, warnConditionText:‘电流不平衡高于replaceText%’ }}//业务逻辑–根据配置数据设置warnTypeText和warnConditionTextlistWarnConf.forEach(item => { item.warnTypeText=warnConfig[item.warnType].warnTypeText; item.warnConditionText=warnConfig[item.warnType].warnConditionText.replace(‘replaceText’,item.warnCondition);})这样改代码量没减少,可读性比 switch 差,但能读懂。但是这样做就是重复的代码少了,配置数据和业务逻辑分离了,如果以后要修改配置数据或者业务逻辑,就修改其中一项即可,互相不影响。把配置数据抽出来公用,那么在需要修改的时候,直接修改就好。关于提高代码的复用性,或者说减少重复的代码,个人觉可以往以下目标努力–当需求发生改变,需要修改代码的时候,同样的代码不要修改两次。3-3.保持函数单一职责,灵活组合保持函数的单一职责,保证一个函数只执行一个动作,每个动作互不影响,可以自由组合,就可以提高代码的复用性。比如下面的代码,从服务端请求回来的订单数据如下,需要进行以下处理1.根据 status 进行对应值得显示(0-进行中,1-已完成,2-订单异常)2.把 startTime 由时间戳显示成 yyyy-mm-dd3.如果字段值为空字符串 ,设置字段值为 ‘–’let orderList=[ { id:1, status:0, startTime:1538323200000, }, { id:2, status:2, startTime:1538523200000, }, { id:3, status:1, startTime:1538723200000, }, { id:4, status:’’, startTime:’’, },];需求似乎很简单,代码也少let _status={ 0:‘进行中’, 1:‘已完成’, 2:‘订单异常’}orderList.forEach(item=>{ //设置状态 item.status=item.status.toString()?_status[item.status]:’’; //设置时间 item.startTime=item.startTime.toString()?new Date(item.startTime).toLocaleDateString().replace(///g,’-’):’’; //设置– for(let key in item){ if(item[key]===’’){ item[key]=’–’; } }})运行结果也正常,但是这样写代码重复性会很多,比如下面,另一组重服务端请求回来的用户数据,用户数据没有 status,startTime,两个字段,而且需要根据 type 对应显示用户的身份(0-普通用户,1-vip,2-超级vip)。let userList=[ { id:1, name:‘守候’, type:0 }, { id:2, name:‘浪迹天涯’, type:1 }, { id:3, name:‘曾经’, type:2 }]出现这样的需求,之前写的代码无法重用,只能复制过来,再修改下。let _type={ 0:‘普通用户’, 1:‘vip’, 2:‘超级vip’}userList.forEach(item=>{ //设置type item.type=item.type.toString()?_type[item.type]:’’; //设置– for(let key in item){ if(item[key]===’’){ item[key]=’–’; } }})结果正常,想必大家已经发现问题了,代码有点多余。下面就使用单一职责的原则改造下操作函数,设置 status,startTime,type,– 。这里拆分成四个函数。let handleFn={ setStatus(list){ let _status={ 0:‘进行中’, 1:‘已完成’, 2:‘订单异常’ } list.forEach(item=>{ item.status=item.status.toString()?_status[item.status]:’’; }) return list }, setStartTime(list){ list.forEach(item=>{ item.startTime=item.startTime.toString()?new Date(item.startTime).toLocaleDateString().replace(///g,’-’):’’; }) return list; }, setInfo(list){ list.forEach(item=>{ for(let key in item){ if(item[key]===’’){ item[key]=’–’; } } }) return list; }, setType(list){ let _type={ 0:‘普通用户’, 1:‘vip’, 2:‘超级vip’ } list.forEach(item=>{ item.type=item.type.toString()?_type[item.type]:’’; }) return list; }}下面直接调用函数就好//处理订单数据orderList=handleFn.setStatus(orderList);orderList=handleFn.setStartTime(orderList);orderList=handleFn.setInfo(orderList);console.log(orderList);//处理用户数据userList=handleFn.setType(userList);userList=handleFn.setInfo(userList);console.log(userList);运行结果也正常如果嫌弃连续赋值麻烦,可以借用 jQuery 的那个思想,进行链式调用。let ec=(function () { let handle=function (obj) { //深拷贝对象 this.obj=JSON.parse(JSON.stringify(obj)); }; handle.prototype={ /** * @description 设置保密信息 / setInfo(){ this.obj.map(item=>{ for(let key in item){ if(item[key]===’’){ item[key]=’–’; } } }); return this; }, /* * @description 设置状态 / setStatus(){ let _status={ 0:‘进行中’, 1:‘已完成’, 2:‘订单异常’ } this.obj.forEach(item=>{ item.status=item.status.toString()?_status[item.status]:’’ }); return this; }, /* * @description 设置时间 / setStartTime(){ this.obj.forEach(item=>{ item.startTime=item.startTime.toString()?new Date(item.startTime).toLocaleDateString().replace(///g,’-’):’’; }); return this; }, /* * @description 设置type / setType(){ let _type={ 0:‘普通用户’, 1:‘vip’, 2:‘超级vip’ } this.obj.forEach(item=>{ item.type=item.type.toString()?_type[item.type]:’’; }) return this; }, /* * @description 返回处理结果 * @return {Array|} / end(){ return this.obj; } } //暴露构造函数接口 return function (obj) { return new handle(obj); }})();这样就可以链式调用了//处理订单数据orderList=ec(orderList).setStatus().setStartTime().setInfo().end();console.log(orderList);//处理用户数据userList=ec(userList).setType().end();console.log(userList);事情到这里了,相信大家发现一个很严重的问题就是循环的次数增加了。没优化之前,只需要循环一次,就可以把设置状态,设置时间,设置–这些步骤都完成,但是现在 setStatus().setStartTime().setInfo() 这里的代码,每执行一个函数,都遍历了一次数组,这个就得优化下。处理的方式就是在每一个函数里面,只记录要处理什么,但是不进行处理,等到执行到 end 的时候再统一处理,以及返回。let ec=(function () { let handle=function (obj) { //深拷贝对象 this.obj=JSON.parse(JSON.stringify(obj)); //记录要处理的步骤 this.handleFnList=[]; }; handle.prototype={ /* * @description 设置保密信息 / handleSetInfo(item){ for(let key in item){ if(item[key]===’’){ item[key]=’–’; } } return this; }, setInfo(){ this.handleFnList.push(‘handleSetInfo’); return this; }, /* * @description 设置状态 / handleSetStatus(item){ let _status={ 0:‘进行中’, 1:‘已完成’, 2:‘订单异常’ } item.status=item.status.toString()?_status[item.status]:’’ return item; }, setStatus(){ this.handleFnList.push(‘handleSetStatus’); return this; }, /* * @description 设置时间 / handleSetStartTime(item){ item.startTime=item.startTime.toString()?new Date(item.startTime).toLocaleDateString().replace(///g,’-’):’’; return item; }, setStartTime(){ this.handleFnList.push(‘handleSetStartTime’); return this; }, /* * @description 设置type / handleSetType(item){ let _type={ 0:‘普通用户’, 1:‘vip’, 2:‘超级vip’ } item.type=item.type.toString()?_type[item.type]:’’; return item; }, setType(){ this.handleFnList.push(‘handleSetType’); return this; }, /* * @description 返回处理结果 * @return {Array|} */ end(){ //统一处理操作 this.obj.forEach(item=>{ this.handleFnList.forEach(fn=>{ item=thisfn; }) }) return this.obj; } } //暴露构造函数接口 return function (obj) { return new handle(obj); }})();这样改,之前的调用方式不需要改变,然后结果也是正确的可能大家会觉得很简单一个需求,却搞得这么复杂。如果这样想是正确的,因为这个的确搞复杂了,可读性也差了,但想到项目遇到的处理数据不止这一些,还有比如金额的格式显示,其它数据的各种状态码解析显示,银行卡号每隔4位分割,电话号码的显示等等。所以就先封装一下,以后用的时候,直接使用。不知道算不算是先苦后甜?如果需求比较简单,可能真的没必要这么样封装。4.小结假期看代码,提高代码复用性的总结,差不多就是这些了,当然还有一些实例,但是在之前已经写过了,和该文章提及的实例也是大同小异,就不再重复提及。提高代码的复用性是一个很大的话题,如果大家有什么好的建议,实例,欢迎分享。————————-华丽的分割线——————–想了解更多,和我交流,内推职位,请添加我微信。或者关注我的微信公众号:乐趣区 ...

October 8, 2018 · 3 min · jiezi

练就Java24章真经—你所不知道的工厂方法

前言最近一直在Java方向奋斗《终于,我还是下决心学Java后台了》,今天抽空开始学习Java的设计模式了。计划有时间就去学习,你这么有时间,还不来一起上车吗?之所以要学习Java模式,是因为面试的时候有时间回答的不是太完整,面试过后才想起来如何回答。所以,我说了: 只有总结才是王道,只有总结才能提高设计模式其实正规的来说Java其实是23中设计模式,不过网上也有说是24种或者是26中的!设计模式不过是前人对代码的一种封装。用专业的话来讲:设计模式是一套被反复使用、多数人知晓的、经过分类编目的、代码设计经验的总结创建型模式,共五种:1.工厂方法模式、2.抽象工厂模式、3.单例模式、4.建造者模式、5.原型模式。结构型模式,共七种:6.适配器模式、7.装饰器模式、8.代理模式、9.外观模式、10.桥接模式、11.组合模式、12.享元模式。行为型模式,共十一种:13.策略模式、14.模板方法模式、15.观察者模式、16.迭代子模式、17.责任链模式、18.命令模式、19.备忘录模式、20.状态模式、21.访问者模式、22.中介者模式、23.解释器模式。今日重点:工厂方法模式工厂模式是创建型模式之一,又称为静态工厂方法模式!优点:1.良好的封装性,代码结构清晰。一个对象创建是有条件约束的,如一个调用者需要一个具体的产品对象,只要知道这个产品的类名(或约束字符串)就可以了,不用知道创建对象的艰辛过程,减少模块间的耦合。2.工厂方法模式的扩展性非常优秀。在增加产品类的情况下,只要适当地修改具体的工厂类或扩展一个工厂类,就可以完成“拥抱变化”。例如在我们的例子中,需要增加一个棕色人种,则只需要增加一个BrownHuman类,工厂类不用任何修改就可完成系统扩展。3.屏蔽产品类。这一特点非常重要,产品类的实现如何变化,调用者都不需要关心,它只需要关心产品的接口,只要接口保持不表,系统中的上层模块就不要发生变化,因为产品类的实例化工作是由工厂类负责,一个产品对象具体由哪一个产品生成是由工厂类决定的。在数据库开发中,大家应该能够深刻体会到工厂方法模式的好处:如果使用JDBC连接数据库,数据库从MySql切换到Oracle,需要改动地方就是切换一下驱动名称(前提条件是SQL语句是标准语句),其他的都不需要修改,这是工厂方法模式灵活性的一个直接案例。4.工厂方法模式是典型的解耦框架。高层模块值需要知道产品的抽象类,其他的实现类都不用关心,符合迪米特原则,我不需要的就不要去交流;也符合依赖倒转原则,只依赖产品类的抽象;当然也符合里氏替换原则,使用产品子类替换产品父类,没问题!缺点:每次增加一个产品时,都需要增加一个具体类和对象实现工厂,是的系统中类的个数成倍增加,在一定程度上增加了系统的复杂度,同时也增加了系统具体类的依赖。这并不是什么好事。用途:第一种情况是对于某个产品,调用者清楚地知道应该使用哪个具体工厂服务,实例化该具体工厂,生产出具体的产品来。Java Collection中的iterator() 方法即属于这种情况。第二种情况,只是需要一种产品,而不想知道也不需要知道究竟是哪个工厂为生产的,即最终选用哪个具体工厂的决定权在生产者一方,它们根据当前系统的情况来实例化一个具体的工厂返回给使用者,而这个决策过程这对于使用者来说是透明的。典型例子:车子继承vehicle(车)类,有小汽车卡,公交车bus等,车子工厂实现工厂接口,工厂接口有抽象方法vehicle produce vehicle(String type)方法,车子工厂中实现工厂方法vehicle produce vehicle(String Type),方法中根据需要new新的车子。示例代码:注意事项有人把工厂模式分为: 简单工厂模式 ,工厂方法模式,抽象工厂模式,所以多出一种模式,这里简单工厂模式比较简单,实际中用的的很少,只在很简单的情况下用,没啥好说的,据说这不是一个真正的设计模式。在这里我就不做讨论了。希望 大家也不用纠结!项目地址:https://github.com/androidsta…总结学习一个知识点要知道是什么,为什么,怎么办,要知其然。也要知其所以然!阅读更多终于,我还是下决心学Java后台了来谈一下android中的MVVM金9银10的面试黄金季节,分享几个重要的面试题身为程序员写一百万行代码的感觉相信自己,没有做不到的,只有想不到的在这里获得的不仅仅是技术!

September 5, 2018 · 1 min · jiezi