本篇文章主要讲的是设计模式的前置知识,但是不包含具体设计模式,希望阅读后能有所收获。
动态类型语言
编程语言按照数据类型大体可以分为两类,一类是静态类型语言,另一类是动态类型语言。
静态类型语言在编译前就确定了数据的类型,如果数据类型不符合就不能通过编译。而动态类型语言是在程序运行时,当赋值给变量时才确定变量的数据类型。
静态类型语言相对于动态类型语言来说更加稳定和安全。但是为了实现这样的安全,需要在变量赋值的时候花费更多的精力。同时在声明变量的时候也需要使用更多的代码。所以静态类型语言相对繁琐、安全。动态类型语言相对容易出错但灵活、快速。
由于在使用 Javascript 给变量赋值时,我们不需要考虑数据类型,所以 JavaScript 是一门动态类型的语言,我们可以任意的调用任何对象的任何方法而无需考虑它原本是否有这个方法。
面向接口编程
鸭子类型和多态
鸭子类型通俗的说法是:当看到一只鸟走起来像鸭子、游泳起来像鸭子、叫起来也像鸭子,那么这只鸟就可以被称为鸭子。即只要拥有指定的全部行为那么他们就能被归为一类。鸭子类型中无需关注类型,它只关注对象的行为(HAS-A)而不是对象的本身(IS-A)。
由于动态语言没有类型的判断,所以实现鸭子类型只要有特定的行为即可。
var dog = {
quackSing :function(){console.log( '汪汪汪')};
}
var cat = {
quackSing :function(){console.log( '喵喵喵')};
}
function quackSing(animal){
if(animal && typeof animal.quackSing == 'function'){
console.log('由于我有 quckSing 方法,所以我可以加入动物大家庭')
}
}
quackSing(cat) //由于我有 quckSing 方法,所以我可以加入动物大家庭
quackSing(dog) //由于我有 quckSing 方法,所以我可以加入动物大家庭
在静态类型语言中和鸭子类型相对应的是多态,多态的实际含义是同个操作对同类型的不同的对象有不同的表现。它一般通过继承(向上转型)来实现。通常把具体需要的行为放到超类或接口之中,然后子类继承超类,获得超类方法。而这些子类就属于”超类接口“。例如下图 Cat、Dog 属于 Animal 接口。
public abstract class Animal {
abstract void quackSing(){}
}
public class Cat extends Animal {
void quackSing(){
System.out.printf('喵喵喵')
}
}
public class Dat extends Animal {
void quackSing(){
System.out.printf('汪汪汪')
}
}
public class AnimalbeHavior{
Animal animal;
//这里使用了向上转型,如果只是使用Animal下的某个子类,那么其他子类无法执行。
//例如类型从 Animal 换成 Cat 时,由于类型校验参数无法放入 Dog。
public AnimalbeHavior (Animal a){
animal = a;
}
public void quackSing(){
animal.quackSing
}
}
AnimalbeHavior aa = new AnimalbeHavior(new Cat());
aa.quackSing() //'喵喵喵'
AnimalbeHavior bb = new AnimalbeHavior(new Dog());
bb.quackSing() //'汪汪汪'
多态最根本的好处在于,你不必再向对象询问”你是什么类型“而后根据得到的答案调用对象的某个行为——你只管调用该行为就是了,其他一切多态机制都会帮你安排妥当 ————《重构:改善既有代码的设计》
借用鸭子类型和多态的思想,我们可以实现一个原则:“面向接口编程”,而不是”面向实现编程“。在上面的例子中,我们把只要有quackSing方法的都认为是一个接口(Animal)。当我们调用 Animal 接口的 quackSing 方法时。由于 Cat、Dog 都有同样的行为,所以我们可以代理到具体实现(Cat、Dog)的 quackSing 方法。多态和鸭子类型的优点在于可以将行为分布到各个对象上,让他们自己负责,从而消除了分支语句。
如果不抽象出这样的接口,那么我们只能在执行同样的行为时判断出是哪个实现(Cat、Dog)触发这个行为,违背了开闭原则。面向接口编程,统一管理这一类接口,可以让代码更有弹性。
function quackSing(animal){
if(isCat){
console.log('喵喵喵')
}else if (isDog){
console.log('汪汪汪')
}
}
多用组合,少用继承
现在小明有几只猫,他们都会跑、吃饭、叫,他们拥有一样的方法。所以设计时我们一般会用继承来创建这几只小猫,都拥有run、eat、makeSound 方法。
突然有一天,小明买了一直玩具猫(ToyCat),这只猫吃不了东西,即没有 eat 方法。由于使用了继承,所以需要 ToyCat 重写 eat 方法来覆盖这个超类的 eat 方法。甚至这只玩具猫还可能是一只会招手招财猫,那么就需要在超类中加一个 beckon 的方法。这样,没有招手能力的小猫都需要重写方法覆盖掉超类的 beckon 方法。所以继承会导致子类如果不需要该方法时也要实现这个方法,造成浪费。
实际上除了一股脑将所有方法加到超类上,还可以将具体的行为作为接口(Interface),然后子类继承接口后实现这个行为。例如有吃饭能力的小猫继承 Eatable 接口、有招手能力的继承 Beckonable 接口,并在当前子类实现对应行为。
但是接口不具有实现的代码,所以继承的接口无法达到代码复用。这意味着,当你需要修改一类行为,你需要在每一个定义了这个行为中的类修改它,很容易出错。
下面我们可以根据下面 2 个设计原则解决这些问题。
设计原则:找出应用中可能需要变化的部分,把他们独立出来,不要和那些不需要变化的代码混在一起。
对于这些小猫,我们先找出可能变化的行为 (eat、beckon),它们会根据小猫的不同而有不同的变化。而其他方法不需要经常改动的,我们暂时不处理。
可以在设计之初就开始考虑哪些会发生变化,或者是在需求更新时发现有些地方经常会变动,此时就可以把这些变动的地方"封装"从而独立出来。这样会使得代码更有弹性。
设计原则:针对接口编程,而不是针对实现编程。
针对接口编程,这里的接口不是上文中的语法自带的(Interface)。这里的接口代表着每一种行为,可以看成是一类具体行为实现的集合即动作的集合。例如:吃的这种行为是一个接口(Eatbehavior),而具体的吃法( Eatmuch 、Eatlittile、Eatnothing) 就是行为具体的实现。同理也可以将 beckon 作为接口(Beckonbechvior)。
此时猫的子类无需实现具体的行为,而是使用接口(Beckonbechvior、Eatbehavior)表示的行为。具体的动作被委托到接口(Beckonbechvior、Eatbehavior)来实现。
而之前使用继承时,猫的行为由超类继承或者自己实现,子类都和具体的实现绑定(具体行为由子类实现)。简单来说之前猫的子类绑定着具体动作,现在猫的子类绑定着接口,然后接口才动作。
这样设计的好处是可以让猫的子类和具体的动作解耦开来。这些动作可以给其他的对象复用,因为已经解耦了动作和对象。同时可以添加一些新的动作而不会影响到猫的子类。
// 猫的抽象类
public abstract class Cat {
Eatbehavior eatbehavior;
Beckonbehavior beckonbehavior;
public Cat {}
public run (){
System.out.printf('一个猫步就跑过去了')
}
public makeSound (){
System.out.printf('喵喵喵')
}
public void eat(){
// 吃的方法委托到 Eatbehavior 的 eat 方法
eatbehavior.eat()
}
public void beckon() {
// 招手方法委托到 Eatbehavior 的 beckon 方法
beckonbehavior.beckon()
}
public abstract void otherThings(){
// ...
}
}
//吃的接口和继承接口的方法
public interface Eatbehavior{
public void eat() {}
}
public class Eatmuch implements Eatbehavior {
public void eat(){
System.out.printf('我吃贼多!')
}
}
public class Eatlittle implements Eatbehavior {
public void eat(){
System.out.printf('我吃一丢丢!')
}
}
public class Eatnothing implements Eatbehavior {
public void eat(){
System.out.printf('我啥都没吃!')
}
}
//招手的接口和继承接口的方法
public interface Beckonbehavior {
public void beckon(){}
}
public nobeckon implements Beckonbehavior {
public void beckon(){
System.out.printf('我不招手的欧')
}
}
public isbeckon implements Beckonbehavior {
public void beckon(){
System.out.printf('我招手贼6')
}
}
// 玩具猫~
public class ToyCat extends Cat {
public void otherThings(){
// ...
}
public ToyCat() {
// 多个类结合在一起使用就是组合,和继承不同的地方在于行为是通过行为对象组合而来的而不是继承而来的。
eatbehavior = new Eatnothing();
beckonbehavior = new isbeckon();
}
// 可以动态的修改行为,使得系统更加灵活
public void setEatbehavior (Eatbehavior behavior){
eatbehavior = new behavior();
}
public void setBebeckonbehavior (Beckonbehavior behavior){
beckonbehavior = new behavior();
}
}
Cat toyCat = new ToyCat();
toyCat.eat(); //我啥都没吃!
toyCat.setEatbehavior(new Eatmuch());
toyCat.eat(); //我吃贼多!
回过头来看使用了”面向接口“的设计,我们解决了继承带来的无用方法的问题,还解决了接口带来的无法复用的问题。同时在转换一下思路,实际上接口中不一定只能是“一组行为”,还可以是“一族算法”,这族算法现在代表着鸭子能做的事。但是假如换成小球不同的运动,这族算法就代表着小球多个不同的移动动画的算法。
最后也大概了解了设计模式的前置知识,可以开始进入设计模式的大门。可以从简单的策略模式、命令模式开始,看看”面向接口编程“和”多用组合少用继承“在实际中怎么使用。推荐看一下最后面的参数书籍~
到这里就结束啦,文章通过阅读和查阅资料总结出来。如果存在错误麻烦指出,立马修正! 最后求个赞👍👍👍👍👍👍👍👍👍👍呗!!大帅比和大漂亮们!
参考书籍
《 JavaScript 设计模式与开发实践 》
《 HeadFirst 设计模式 》