美文网首页
[Common] Head First 设计模式 (单例 + 命

[Common] Head First 设计模式 (单例 + 命

作者: 木小易Ying | 来源:发表于2020-04-10 08:00 被阅读0次

    Chapter 5 单件模式

    单例模式比全局变量好的是不用程序一开始就初始化全局变量(有的东西的初始化很费事儿最好不要都一开始就初始化),可以用到单例的时候再初始化,主要是将构造方法private并且只提供一个对外的getInstance实现只初始化一次:

    public class Singleton
    {
        private static Singleton uniqueInstance = null;
     
        //其他有用的实例变量
        
        //构造方法是私有的,所以在类外不能new出多个实例
        private Singleton()
        {
            //初始化其他实例变量
        }
     
        public static Singleton getInstance()
        {
            if (uniqueInstance == null)
            {
                uniqueInstance = new Singleton();
            }
            return uniqueInstance;
        }
    }
    

    但注意哦,上面的方式会有线程问题,可能还是会产生两个实例。

    单件模式(又称单例模式),确保一个类只有一个实例,并提供一个全局访问点。

    加锁保证单例

    Chapter 6 命令模式

    设计一个家电自动化遥控器的API。这个遥控器具有七个可编程的插槽,每个插槽都有对应的开关按钮,这个遥控器还具备一个整体的撤销按钮。希望你能够创建一组控制遥控器的API,让每个插槽都能够控制一个或一组装置,能够控制目前的装置和任何未来可能出现的装置,这一点很重要。(这里有一组Java类,这些类时由多个厂商开发出来的,用来控制家电自动化装置,例如点灯,风扇,热水器,音响设备和其他类似的可控制装置。)

    家电类们

    因为每个家电的接口都不一样,我们总不好先判断是什么家电,然后调用不同的接口,但是我们可以使用命令模式,把请求例如按下打开电灯按钮封装成一个命令对象,这样遥控器就不需要知道电灯是怎么做的了。


    这里用一个餐厅下单的例子改造举例:

    餐厅订单

    然后改成命令模式:


    命令模式的餐厅

    就是将菜品的制作改成实现了command接口,这样厨师拿到的时候,可以直接调用command.execute(),它不会管具体里面是做什么的,菜品自己会执行制作。

    --

    遥控器的改造也是酱紫,首先定义command接口,然后实现不同电器的command:

    interface Command{
        public void execute();
    }
    
    class LightOnCommand implements Command{
        Light light;
        public LightOnCommand(Light light){
            this.light=light;
        }
        @Override
        public void execute() {
            light.on();
        }
    }
    

    假设有一个只有一个按钮的遥控器,就是酱紫的:

    class SimpleRemoteControl{
        Command slot;
        public SimpleRemoteControl(){}
        public void setCommand(Command command){
            slot=command;
        }
        public void buttonWasPressed(){
            slot.execute();
        }
    }
    
    // test code
    class RemoteControlTest{
        public static void main(String[] args){
            SimpleRemoteControl remote=new SimpleRemoteControl();
            Light light=new Light();
            LightOnCommand lightOn=new LightOnCommand(light);
            remote.setCommand(lightOn);
            remote.buttonWasPressed();
        }
    }
    

    命令模式:将“请求”封装成对象,一边使用不同的请求、队列或者日志来来参数化其他对象。命令模式也支持可撤销的操作。

    类图
    空对象

    为了不用每次调用插槽command的时候都先检查command是不是空,可以创建一个空command什么都不做来占位,这样就可以避免了判断null了~

    public class NoCommand implements Command {
        public void execute() { }
        public void undo() { }
    }
    

    这里插一句,命令模式的优点,如果通过让家具们都实现某个接口,然后直接把自己给遥控器,遥控器调用接口也可以,缺点是:

    • 如果有一个按钮控制两个家具呢?(如果通过命令模式可以新建一个初始化传入一组command,然后execute的时候调用这组command的每一个命令)
    • 如果需要增加减少功能,接口就要变,每个家具类都要变
    • 如果又有个什么遥控,家具类就要再增加一套接口,但是家具类没有职责应该这么做吖

    命令模式在thread队列中应用 命令模式在死机恢复时候的应用

    Chapter 7 适配器模式与外观模式

    现实世界适配器

    适配器模式:将一个类的接口转换成客户期望的另一个接口。适配器让原本接口不相融的类可以相互合作

    例如,当我们换了一个新厂商,不能修改厂商的代码,自己的旧代码由于已经提供了接口给别人也不能修改,就可以加一层适配性,我们可以自己提供,也可以让厂商提供哈

    适配器
    • 设计:
      实现想要转换的接口
      取得要适配的对象的引用,作为局部变量
      用要适配的对象的方法实现接口中的方法

    如果有两个不同的interface分别是火鸡和鸭子,但我们只有火鸡却要提供鸭子给外部:

    用火鸡冒充鸭子
    • 客户使用适配器的过程:
      通过目标接口调用适配器方法
      适配器使用被适配者的接口转换成对被适配者的调用
      客户端接受调用结果,但并未察觉适配器在中转的作用

    适配器模式一般只适配(持有)一个类,如果适配多个类也可以啦,但你可以用多个适配器适配一个接口哈;如果旧接口和新接口共存,最好让适配器同时满足这两种接口不要用两套。

    类图

    之前我们讲的是对象适配器,而类适配器是通过多重继承代替组合的方式调用的被适配者。

    类适配器

    类适配器的好处就是他不用新增一个持有的被适配对象,以及不会实现所有被适配者的接口,因为它是继承的是有默认行为的。

    这里装饰者是不改变接口加入更多责任,而适配器是改变接口的哈


    外观模式

    外观模式提供了一个统一的接口,用来访问子系统中的一群接口。外观定义了一个高层接口,让子系统更容易使用。

    外观模式不只是简化了接口,也将客户从组件的子系统中解耦。

    外观和适配器可以包装许多类,但是外观强调的是简化接口,而适配器是为了将接口转换成不同的接口。

    假设我们设计了一个家庭影院,类图是下面酱紫的,然后如果我们想要看一场电影,需要打开爆米花、打开音效、打开……最后打开dvd,要做一系列的事情非常麻烦,关的时候也是要反着一系列操作。

    家庭影院类图

    外观只是提供了一些简化的操作,没有把子系统的高级操作隔离起来,依然将子系统完整的暴露出来,因此如果你需要更高级的操作也可以访问子系统的接口

    外观并没有实现新的行为,只是将子系统的操作合理的组合。一个子系统可以有多个外观,并可以创造分层次的外观,外观不只简化了接口,也将用户从复杂的子系统中解耦出来。

    外观vs适配器:
    外观的目的是简化接口,适配器的目的是转换接口满足客户预期,和包装几个类没有关系

    类图
    OO原则:最少知识

    最少知识原则:只和你的密友谈话。

    减少对象之间的交互,只留下几个密友。不要让太多的类耦合在一起以至于修改系统中的一部分会影响到其他部分。

    反例:
    public float getTemp() {
        return station.getThermometer().getTemperature();
    }
    
    正例:
    public float getTemp() {
        return station. getTemperature();
    }
    
    • 在对象方法内,我们允许调用哪些对象的方法,简单理解为以下4个小原则:
      该对象本身
      方法参数传入对象
      方法内实例的对象
      对象组件
    最少知识原则示例

    外观模式其实帮助了我们实现最少知识原则。


    Chapter 8 封装算法

    煮茶和咖啡

    煮茶和咖啡的步骤几乎是一致的,所以可以通过提取基类来避免重复。

    public abstract class CaffeineBeverage {
        final void prepareRecipe(){
            boilWater();
            brew();
            addCondimennts();
            pourInCup();
        }
        abstract void brew();
        abstract void addCondimennts();
        public void boilWater(){
            System.out.println("把水煮沸");
        }
        public void pourInCup(){
            System.out.println("倒进杯子");
        }
    }
    public class Coffee extends CaffeineBeverage{
        public void brew(){
            System.out.println("用沸水冲泡咖啡");
        }
        public void addCondimennts(){
            System.out.println("添加糖和牛奶");
        }
    }
    public class Tea extends CaffeineBeverage{
        public void brew(){
            System.out.println("用沸水冲泡茶");
        }
        public void addCondimennts(){
            System.out.println("添加柠檬");
        }
    }
    

    模板方法定义了一个算法的步骤,并容许子类为一个或多个步骤提供突现。

    当我们将共同的地方抽出作为父类的时候,将来如果有类似的饮料就更容易拓展,减少了重复代码。同时修改的话也只要改父类就好。

    在一个方法中定义一个算法的骨架,而将一些步骤延迟到子类中。模版方法使得子类可以在不改变算法结构的情况下,重新定义算法中的某些步骤。

    类图

    钩子是一种被声明在抽象类中的方法,但只有空的或者默认的实现。钩子的存在, 可以让子类有能力对算法的不同点进行挂钩。要不要挂钩,由子类自行决定。

    public abstract class CaffeineBeverage {
        final void prepareRecipe(){
            boilWater();
            brew();
            pourInCup();
            /**
             * 残们加上了一个小的条件语句,而该条件是否成立,是由一个
             * 具体方法customerWantsCondiments()決定的。
             * 如果顾客“想要”调料,有这时我们才调用addCondimennts()
             */
            if (customerWantsCondiments()){
                addCondimennts();
            }
        }
        /**
         * 残们在这里定义了-个方法, (通常)是空的缺省实现。
         * 这个方法会返回true,不做别的事。
         * 这就是一个钩子,子类可以覆盖这个方法,但不见得一定要这么做。
         * @return
         */
        boolean customerWantsCondiments(){
            return true;
        }
        abstract void brew();
        abstract void addCondimennts();
        public void boilWater(){
            System.out.println("把水煮沸");
        }
        public void pourInCup(){
            System.out.println("倒进杯子");
        }
    }
    子类中
    
    public class Coffee extends CaffeineBeverage{
    //    用户输入的值
        private String answer;
        public void brew(){
            System.out.println("用沸水冲泡咖啡");
        }
        public void addCondimennts(){
            System.out.println("添加糖和牛奶");
        }
        //覆盖钩子,提供自己的功能
        @Override
        boolean customerWantsCondiments() {
    //        让用户根据他们的输入来判断是否需要添加配料
            if (answer.toLowerCase().startsWith("y")){
                return true;
            }else {
                return false;
            }
        }
    }
    

    如果是算法中必须的一步就可以用抽象方法,如果是可选的就可以用钩子来实现。钩子是可以实现也可以不实现,不强求的做法。

    可选的步骤作为钩子的话(空或者默认实现)就可以让子类减少必须实现的抽象方法的数量啦。

    钩子

    钩子就类似声明周期函数感觉,你可以选择覆盖,也可以选择不覆盖。


    好莱坞原则

    好莱坞原则:别调用(打电话给)我们,我们会调用(打电话给)你。

    好莱坞原则可以给我们一种防止“依赖腐败”的方法。

    当高层组件依赖低层组件,而低层组件又依赖高层组件,而高层组件又依赖边侧组件,而边侧组件又依赖低层组件时, 依赖腐败就发生了。在这种情况下,没有人可以轻易地搞懂系统是如何设计的。

    在好莱坞原则之下,我们允许低层组件将自己挂钩到系统上,但是高层组件会决定什么时候和怎样使用这些低层组件。换句话说,高层组件对待低层组件的方式是“别调用我们,我们会调用你”

    模板方法里的好莱坞原则
    • 好莱坞原则和依赖倒置原则之间的关系如何?
      依赖倒置原则教我们尽量避免使用具体类,而多使用抽象。而好菜坞原则是用在创建框架或组件上的一种技巧,好让低层组件能够被挂钩进计算中,而且又不会让高层组件依赖低层组件。两者的目标都是在于解耦,但是依赖倒置原则更加注重如何在设计中避免依赖。

    好菜坞原则教我们一个技巧,创建个有弹性的设计,允许低层结构能够互相操作,而又防止其他类太过依赖它们。

    低层组件结束的时候经常会调用super的方法,这个是可以的,只是我们要避免环状调用。


    ![java排序中的模板方法images.jianshu.io/upload_images/5219632-deebac020dd36180.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

    策略模式会实现整个算法,而模板模式的子类只实现部分算法;而且策略是不依赖上层类的,而模板胡依赖顶层父类,模板会共享一部分代码例如共通的步骤;策略模式封装算法的方式是组合,通过持有不同的实例实现,模板方法通过继承封装不同的算法。

    策略模式可以不继承只是实现接口,因为它不需要继承默认的实现,这点和模板模式是不一样的。

    而工程模式其实就是模板模式的一种特殊版本,如果模板里面有一个createXX的方法下放到子类实现,然后模板里直接调用生成一个instance,那么其实就是工厂方法。


    Chapter 9 迭代器与组合模式

    假设有两个集合类,一个是ArrayList,一个是[],那么当一个对象需要操作这两个集合实例的时候,需要分别用不同方式遍历,这样是很不方便的。

    不同类型一起集合处理

    所以Java才有迭代器iterator接口,无论是数组还是ArrayList都可以创建iterator来遍历。

    迭代器接口是酱紫的:

    public interface Iterator{
      boolean hasNext();  //返回一个布尔值,判断是否还有更多的元素
      Object next();   //返回下一个元素
    }
    

    我们来改写一下print菜单这个事儿:

    //实现迭代器接口  DinerMenu
    public class DinerMenuIterator implements Iterator{
      MenuItem[] items;
      int position = 0;   //position记录当前数组遍历的位置
      
      public DinerMenuIterator(MenuItem[] items){  //构造器需要被传入一个菜单项的数组当作参数
        this.items=items;
      }
      
      public Object next(){  //返回数组内的下一项,并递增其位置
        MenuItem menuItem= items[position];
        position=position+1;
        return menuItem;
      }
      
      public boolean hasNext(){
        /*检查是否已经取得数组内所有的元素,如果还有元素待遍历则返回true;
         由于使用的是固定长度的数组,所以不但要检查是否超出了数组长度,也必须检查是否下一项是null,如果是null,就没有其他项了
        */
        if(position>=items.length||items[position]==null){ 
          return false;
        }else{
          return true;
        }
      }
    
    public class Waitress { 
        Menu dinerMenu; 
        public Waitress( Menu dinerMenu) { 
            this.dinerMenu = dinerMenu; 
        } 
        public void printMenu() {  
            Iterator dinerIterator = dinerMenu.createIterator();
    
            System.out.println("/nLUNCH"); 
            printMenu(dinerIterator); 
        } 
        private void printMenu(Iterator iterator) { 
            while (iterator.hasNext()) { 
                MenuItem menuItem = (MenuItem)iterator.next(); 
                System.out.print(menuItem.getName() + ", "); 
                System.out.print(menuItem.getPrice() + " -- "); 
                System.out.println(menuItem.getDescription()); 
            } 
        }
    

    这样以后我们的waitress就不需要知道两种不同的菜单的具体用什么实现的菜单存储了,只要调用iterator接口即可,把菜单内部对外封闭了。

    迭代器模式提供一种方法顺序访问一个聚合对象中各个元素, 而又无须暴露该对象的内部表示。


    单一责任

    单一责任原则:一个类应该只有一个引起变化的原因。

    类的每个责任都有改变的潜在区域。超过一个责任则意味着多个改变的区域。该原则说明了应该尽量让每个类保持单一责任。


    通过iterator我们改变了菜单的遍历,让一切看起来比较方便,但现在其中一家餐厅提出了子菜单(甜品),这个时候我们就需要一个类似树的结构来存储了。

    子菜单

    组合模式:允许将对象组合成树形结构来表现“整体/部分”层次结构。组合能够让客户以抑制的方式处理个别对象以及对象组合。

    组合模式类图

    组合模式包含组件,组件有两种:一种是单纯的叶节点,一种是持有一群孩子的组合,这些孩子可以是叶节点也还可以是组合。

    组合&菜单

    这里meneComponent接口定义了叶节点 & 组合的方法,相当于有了两种responsibility,叶节点只要实现它需要的方法即可。

    类似frame panel界面也有这种关系。

    如果你还想方便一点也可以让子节点持有parent指针

    这里让一个接口实现两种(节点以及组合)的特性,其实是用单一原则换取了透明性,也就是对用户而言,每个节点都是一致的,它可以不用关心这个点是单纯的一个叶节点还是一个组合的使用。

    组合遍历器

    这里用stack实现了根叶节点的遍历


    • 策略模式:封装可互换的行为,并使用委托决定使用哪一个。

    • 适配器模式:改变一个或多个类的接口。

    • 迭代器模式:提供一个方式来遍历集合,而无需暴露集合的实现。

    • 外观模式:简化一群类的接口。

    • 组合模式:客户可以将对象的集合以及个别的对象一视同仁。

    • 观察者模式:当某个状态改变是,允许一群对象能被通知到。

    相关文章

      网友评论

          本文标题:[Common] Head First 设计模式 (单例 + 命

          本文链接:https://www.haomeiwen.com/subject/bjkmuhtx.html