日期:2021年8月17日

设计模式

面向对象的目的、方法:

  • 业务逻辑与界面逻辑分离,减小耦合。这样业务逻辑很可能再未来被复用。
  • 个人感觉面向对象的终极目标就是开闭原则(open for extensions, close for modifications)

一.简单工厂模式

简单工厂模式uml图

  • Client: 产品使用者,有一个抽象类型Product的变量。
  • Factory: 创建一个具体的产品,可供client使用。
  • Product: 产品抽象类,定义了一些抽象方法。
  • Concrete Product: 具体的某个产品。

Client可以使用抽象的Product的方法,而不用关心这个product的具体实现和具体类型。

class Client {
    public Product product;
    ...
}
abstract class Product
{
    public abstract Product createProduct();
    ...
}

class OneProduct extends Product
{
    ...
    static
    {
        ProductFactory.instance().registerProduct("ID1", new OneProduct());
    }
    public OneProduct createProduct()
    {
        return new OneProduct();
    }
    ...
}

class ProductFactory
{
    public Product createProduct(String ProductID) {
        ...
    }
    ...
}

createProduct

条件方式

ProductFactory负责上产product,常用的方式可以通过switch/case(if/else)语句判断:

class ProductFactory
{
    public Product createProduct(String id) {
        if (id == ID1) {
            return new OneProduct();
        }
        if (id == ID2) {
            return new AnotherProduct();
        }

        return null;
    }
    ...
}

这个方式违背了开闭原则,每次有新的product类,都需要更改createProduct方法。

Class注册方式

可以通过反射去实现,但是先跳过反射的方法。

ProductFactory类中使用一个HashMap存储产品的type和产品Id。具体实现如下。

abstract class Product
{
    public abstract Product createProduct();
    ...
}

class OneProduct extends Product
{
    ...
    static
    {
        ProductFactory.instance().registerProduct("ID1", new OneProduct()); // 每次添加一个产品类,都需要在工厂进行注册。
    }
    public OneProduct createProduct()
    {
        return new OneProduct();
    }
    ...
}

class ProductFactory {
    private HashMap m_RegisteredProducts = new HashMap(); // 存储产品id和对应的product

    public void registerProduct(String productID, Product p) {
        this.m_RegisteredProducts.put(productID, p);
    }

    public Product createProduct(String productID) {
        ((Product)m_RegisteredProducts.get(productID)).createProduct();
    }
}

这种方式的确保证了开闭原则,但是每次有新的product类, 都需要主动注册,也有点不方便。

二.策略模式

策略模式uml图

  • Strategy: 定义了策略的接口,所有具体的策略都要去实现这个接口
  • ConcreteStrategy: 具体的策略,实现了具体的算法
  • Context: 保存了一个Strategy对象的引用,可能定义了一个接口可以获取Strategy类中的数据。通过Strategy成员可以使用不同的ConcreteStrategy。Context不需要知道策略是如何实现的。

Context对象接受客户端的请求,为客户端做代理,使用Strategy。

我的理解:策略模式与工厂模式有些类似,使用工厂模式的话,客户端最终获得的是Product,然后通过Product类执行Product中的方法。而使用策略模式,客户端通过使用Context使用具体的算法行为。

何时使用

当你遇到某些类,仅他们的行为(算法)不同时,将不同的算法分离在不同的类中,在使用的时候根据需要去选择不同的类来使用对应的算法。

目的

定义一些列算法,独立封装这些算法中的每一个,并且可以自由切换算法。策略模式使算法独立于使用这些算法的用户(client),用户端可以自由切换算法,保证了业务逻辑和界面分离,减少代码耦合性。

代码实现

/* 以机器人的行为为例 */

// Strategy——抽象行为接口
public interface IBehaviour {
    public int moveCommand();
}

// ConcreteStrategy——具体的行为:agressive behaviour
public class AgressiveBehaviour implements IBehaviour{
    public int moveCommand()
    {
        System.out.println("\tAgressive Behaviour: if find another robot attack it");
        return 1;
    }
}

// ConcreteStrategy——具体的行为:defensive behaviour
public class DefensiveBehaviour implements IBehaviour{
    public int moveCommand()
    {
        System.out.println("\tDefensive Behaviour: if find another robot run from it");
        return -1;
    }
}

// ConcreteStrategy——具体的行为:normal behaviour
public class NormalBehaviour implements IBehaviour{
    public int moveCommand()
    {
        System.out.println("\tNormal Behaviour: if find another robot ignore it");
        return 0;
    }
}

// Context-机器人
public class Robot {
    IBehaviour behaviour; // Strategy的引用
    String name;

    public Robot(String name)
    {
        this.name = name;
    }

    public void setBehaviour(IBehaviour behaviour)
    {
        this.behaviour = behaviour;
    }

    public IBehaviour getBehaviour()
    {
        return behaviour;
    }

    public void move()
    {
        System.out.println(this.name + ": Based on current position" +
                     "the behaviour object decide the next move:");
        int command = behaviour.moveCommand();
        // ... send the command to mechanisms
        System.out.println("\tThe result returned by behaviour object " +
                    "is sent to the movement mechanisms " + 
                    " for the robot '"  + this.name + "'");
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

// client
public class Main {

    public static void main(String[] args) {

        Robot r1 = new Robot("Big Robot");
        Robot r2 = new Robot("George v.2.1");
        Robot r3 = new Robot("R2");

        r1.setBehaviour(new AgressiveBehaviour());
        r2.setBehaviour(new DefensiveBehaviour());
        r3.setBehaviour(new NormalBehaviour());

        r1.move();
        r2.move();
        r3.move();

        System.out.println("\r\nNew behaviours: " +
                "\r\n\t'Big Robot' gets really scared" +
                "\r\n\t, 'George v.2.1' becomes really mad because" +
                "it's always attacked by other robots" +
                "\r\n\t and R2 keeps its calm\r\n");

        r1.setBehaviour(new DefensiveBehaviour());
        r2.setBehaviour(new AgressiveBehaviour());

        r1.move();
        r2.move();
        r3.move();
    }
}

三.单一职责

这里的职责可以认为是为改变一个类的因子的。单一职责表示,如果我们有两个因子可能会改变一个类,那么我们需要将这个类分离成两个类。每个类分别对这两个因子负责。当其中一个因子发生变化,需要去修改类时,只需要修改其中一个类,而不会影响另一个类,这个过程叫做职责分离。

目的

一个类仅有一个需要改变的因子。

案例

假设有如下发送邮件的程序:

// single responsibility principle - bad example
interface IEmail {
    public void setSender(String sender);
    public void setReceiver(String receiver);
    public void setContent(String content);
}

class Email implements IEmail {
    public void setSender(String sender) {// set sender; }
    public void setReceiver(String receiver) {// set receiver; }
    public void setContent(String content) {// set content; }
}

Email类其实包含了两个职责,一个是处理邮件协议(sender和receiver),另一个是需要处理内容(content)。现在这个内容是字符串形式,假如以后要支持html格式或者markdown格式,那么修改setContent可能会影响到其他部分。所以最好的方式是将职责单一化,再创建一个类用于处理content。

// single responsibility principle - good example
interface IEmail {
    public void setSender(String sender);
    public void setReceiver(String receiver);
    public void setContent(IContent content);
}

interface Content {
    public String getAsString(); // used for serialization
}

class Email implements IEmail {
    public void setSender(String sender) {// set sender; }
    public void setReceiver(String receiver) {// set receiver; }
    public void setContent(IContent content) {// set content; }
}

一般我们写软件时,习惯将界面操作部分和逻辑部分的代码分离,这也是遵守职责单一原则的例子。将职责分离,更有利于代码的维护,增加代码的复用性。

如果一个类承担的职责过多,就等于把这些职责耦合在一起,一个职责的变化可能会削弱或抑制这个类完成其他职责的能力。这种耦合会导致脆弱的设计,当变化发生时,设计会遭受意想不到的破坏。

四.开闭原则

开闭原则(Open Close Principle):对扩展是开放的,对修改是封闭的。软件设计或者写代码必须遵循这一原则,新功能的添加必须最小化的影响旧代码,允许我们使用添加类的方式添加新功能,保持旧代码不变。因为旧代码都是已经测试过的代码,如果修改可能会产生新的错误。

目的

类、模块和函数都应该遵守对扩展是开放的,对修改是封闭的这一原则。

案例

首先看一个错误的例子:

// Open-Close Principle - Bad example
class GraphicEditor {

     public void drawShape(Shape s) {
         if (s.m_type==1)
             drawRectangle(s);
         else if (s.m_type==2)
             drawCircle(s);
     }
     public void drawCircle(Circle r) {....}
     public void drawRectangle(Rectangle r) {....}
}

class Shape {
     int m_type;
}

class Rectangle extends Shape {
     Rectangle() {
         super.m_type=1;
     }
}

class Circle extends Shape {
     Circle() {
         super.m_type=2;
     }
} 

上例中由如下几个缺点:

  • 每次添加一个新的ShapeGraphicEditor的单元测试必须重新执行
  • 当一个新的Shape添加,写代码的人必须要理解GraphicEditor的代码逻辑,因为他需要修改drawShape函数。
  • 添加一个新的Shape可能会影响已经存在的功能。

将上面的例子修改如下,遵守开闭原则:

// Open-Close Principle - Good example
class GraphicEditor {
     public void drawShape(Shape s) {
         s.draw();
     }
}

class Shape {
     abstract void draw();
}

class Rectangle extends Shape  {
     public void draw() {
         // draw the rectangle
     }
}

每次添加新的Shape,只需要增加一个类,继承Shape类,完成draw函数即可。

五.依赖倒转原则

抽象不应该依赖细节,细节应该依赖于抽象。即,针对接口编程,不要对实现编程。

  • 高层模块不应该依赖底层模块。两个都应该依赖抽象。
  • 抽象不应该依赖细节,细节应该依赖抽象。

目的

大部分程序员在写代码时,为了使代码得以复用,一般都会把这些代码写成许许多多函数的程序,这样在做新项目时,去调用这些底层的函数就可以了。比如很多项目需要访问数据库,所以就把访问数据库的代码写成了函数,每次做新项目时就去调用这些函数,这就叫高层模块依赖低层模块。但是当要做新项目时,法线业务逻辑的高层模块都是一样的,但是客户希望使用不同的数据库或存储信息方式,这时麻烦来了,我们希望复用这些高层模块,但高层模块都是与低层的访问数据库绑定在一起的,没办法复用这些高层模块,这就非常糟糕了。所以应该针对接口编程,不要对实现编程, 高层模块和低层模块都应该依赖抽象。

所以,我们需要在高层模块和低层模块之间添加一个抽象层。

High Level Classes --> Abstraction Layer --> Low Level Classes

案例

首先,分析下面的错误代码示范。它违背了依赖倒转原则。有一个Manager类(高层类)和一个Worker类(低层类),可以看到Manager此时是依赖于Worker类的。假设我们需要添加一个新模块,叫SuperWorker。这时我们不得不去更改Manager的代码,如果Manager代码特别复杂,那这个工作量就很大了,而且很容易出错。

所以这段代码,有如下几个缺点:

  • 我们必须去更改Manager类(假设Manager类的逻辑很复杂,这是一个很花时间的工作)。
  • Manager已有的功能可能会受到影响。
  • 更改完成后,Manager类的单元测试需要重新来一遍。
// Dependency Inversion Principle - Bad example
class Worker {
    public void work() {
        // ....working
    }
}

class Manager {
    Worker worker;
    public void setWorker(Worker w) {
        worker = w;
    }

    public void manage() {
        worker.work();
    }
}

class SuperWorker {
    public void work() {
        //.... working much more
    }
}

下面是优化后的代码。我们添加了一个IWorker接口,解决了上面存在的问题。

  • 添加SuperWorkers时,Manager类无需做出更改。
  • 因为Manager类没有更改,已有的功能不会受到影响。
  • Manager类的测试工作,不必重新去做。
// Dependency Inversion Principle - Good example
interface IWorker {
    public void work();
}

class Worker implements IWorker{
    public void work() {
        // ....working
    }
}

class SuperWorker  implements IWorker{
    public void work() {
        //.... working much more
    }
}

class Manager {
    IWorker worker;

    public void setWorker(IWorker w) {
        worker = w;
    }

    public void manage() {
        worker.work();
    }
}

六.装饰模式(Decorator Pattern)

动机

通常我们可以通过继承的方式,扩展一个类的功能。但是有时候我们需要动态的扩展一个类的功能形成一个新的object。

想像一个比较经典的图形窗口的例子。为了向图形窗口添加一个特定的边框,需要继承Window类,创建一个FrameWindow类,为了创建一个有边框的窗口,我们需要使用FrameWindow创建一个新的object。然而通过这种方式,我们无法扩展一个普通的图形窗口,从而得到一个有特定边框的窗口。

目的

此模式的目的是动态地向对象添加额外的职责。

实现

装饰模式(decorator pattern)

  • Component: 一个接口,可以向实现该接口的类对象,动态的添加职责(功能)。
  • ConcreteComponent:定义可以向其添加额外职责的对象。
  • Decorator:保留一个Component对象的引用,并定义Component的接口。
  • Concrete Decorators: 具体的装饰类,通过添加状态和行为扩展Component Object的功能(职责)。

描述

当需要动态地向类添加和删除责任,以及由于可能产生大量子类而无法进行子类化时,可以使用装饰器模式。

案例

用Java语言,利用装饰模式实现一个图形窗口程序,给窗口动态的添加scroll。 装饰模式(Decorator Pattern) 下面的代码实现了一个简单的Window接口:

package decorator;

/**
 * Window Interface 
 * 
 * Component window
 */
public interface Window {

    public void renderWindow();

}

创建 一个具体的window实现类:

package decorator;

/**
 * Window implementation 
 * 
 * Concrete implementation
 */
public class SimpleWindow implements Window {

    @Override
    public void renderWindow() {
        // implementation of rendering details

    }
}

接下来创建DecorateWindow类,表示一个window的装饰类。值得注意的是类里面有一个privateWindowReference成员,表示需要装饰(增加功能)的窗口。

package decorator;

/**
 *
 */
public class DecoratedWindow implements Window{

    /**
     * private reference to the window being decorated 
     */
    private Window privateWindowRefernce = null;

    public DecoratedWindow( Window windowRefernce) {

        this.privateWindowRefernce = windowRefernce;
    }

    @Override
    public void renderWindow() {

        privateWindowRefernce.renderWindow();

    }
}

继承DecoratedWindow,实现一个为窗口添加滚动功能的装饰窗口子类:

package decorator;

/**
 * Concrete Decorator with extended state 
 * 
 * Scrollable window creates a window that is scrollable
 */
public class ScrollableWindow extends DecoratedWindow{
    /**
     * Additional State 
     */
    private Object scrollBarObjectRepresentation = null;

    public ScrollableWindow(Window windowRefernce) {
        super(windowRefernce);
    }

    @Override
    public void renderWindow() {

        // render scroll bar 
        renderScrollBarObject();

        // render decorated window
        super.renderWindow();
    }

    private void renderScrollBarObject() {

        // prepare scroll bar 
        scrollBarObjectRepresentation = new  Object();

        // render scrollbar 

    }   
}

最后,客户端程序创建一个可滚动的窗口:

package decorator;

public class GUIDriver {

    public static void main(String[] args) {
        // create a new window 
        Window window = new ConcreteWindow();
        window.renderWindow();

        // at some point later 
        // maybe text size becomes larger than the window 
        // thus the scrolling behavior must be added 
        // decorate old window 
        window = new ScrollableWindow(window);

        //  now window object 
        // has additional behavior / state 
        window.renderWindow();
    }
}

总结

问:装饰功能有什么用?

答:装饰模式可以在程序运行时,动态的给被装饰的类对象添加其他功能(通常为装饰性功能,非核心功能)。

问:什么时候应该用装饰模式?

答:当一个类需要新的功能的时候,你可以向旧类添加新的代码,或者继承旧类创建一个新的类。第一种方式需要更改旧代码,很可能影响原有的核心功能,显然这不符合我们的预期。第二种方式是一种静态的,无法达到动态增加原有类对象功能的效果,就像上面的例子,我们需要动态的增加ConcreteWindow对象的功能,显然第二种方式也无法满足我们的需求。这时就需要使用装饰者模式了。

问:装饰者模式有什么有点?

答:装饰模式可以将类中的装饰性功能从类中移除,从而在运行时动态的添加,这样做有效的把核心职责和装饰功能区分开了,而且可以去除相关类中的重复逻辑。装饰模式还有一个重要的优点,就是当有很多个装饰类时,你可以自由的组合这些装饰类的顺序,还是以上面的图形窗口为例,我们有一个ScrollableWindow装饰类,可以使window具有滚动功能,还可以增加ToolbarWindow装饰类,使window具有工具栏,ScrollableWindowToolbarWindow的装饰顺序是任意的,你可以自由安排。

Window window = new ConcreteWindow();
window.renderWindow();
window = new ScrollableWindow(window);
window.renderWindow();

window = new ToolbarWindow(window);
window.renderWindow();

七.代理模式

动机

某些时候我们需要可以控制对象访问的能力。例如,如果我们只需要使用一个复杂对象(该对象内部比较复杂,方法很多,成员很多)的几个方法,我们不需要直接实例化这个对象,可以使用一些轻量级的对象作为代理,代替复杂对象,通过这些轻量级对象访问我们需要的方法。这些轻量级的对象就是代理(proxy)

需要使用代理的场景有很多:控制何时需要实例化和初始化开销较大的对象。可以使用多个代理访问复杂对象的不同成员,来控制复杂对象的访问权限。代理还提供了一种访问运行在其他程序或进程中甚至是其他机器上的对象的方法。

以图像查看程序,思考下。 一个图像查看程序必须能够列出和显示文件夹中的高分辨率照片对象,但人们多久打开文件夹并查看里面的所有图像。 有时您将寻找一个特定的照片,有时您只希望看到一个图像名称。图像查看器必须能够列出所有的照片对象,但是照片对象只有在需要渲染时才能加载到内存中。

目的

此模式的目的是为对象提供一个占位符(代理),以控制对该对象的引用。

实现

uml of proxy pattern

  • SubjectRealSubject和它的代理需要实现的接口。
  • ProxyRealSubject的代理,内部保留了RealSubject的引用。可以在任何可以使用RealSubject的地方,作为RealSubject的替代对象。控制RealSubject的访问,并且可以控制RealSubject的创建和删除。
  • RealSubjectProxy代理的真实Object。

类型

代理可以分为如下几种类型:

  • 虚拟代理(Virtual Proxies):用于延迟复杂对象(实例化耗费比较大的对象)的创建和初始化,直到真的需要使用该对象的方法时才去创建该对象。例如当打开一个很大的HTML网页时,里面可能有很多文字和图片,但是还是可以很快打开它,此时你能看到的所有的文字,但是图片却是一张一张的下载后才能看到,那些未打开的图片框,就是通过虚拟代理来替代了真实的图片,此时代理存储了真实图片的路径和尺寸。
  • 远程代理(Remote Proxies):为远程对象(运行在其他进程、机器上的对象)提供一个本地代理,可以通过本地代理访问远程对象。典型的例子有.Net中的WebService应用。
  • 安全代理(Protection Proxies):为安全对象提供不同的代理控制安全对象的访问权限。
  • 智能引用(Smart References):当调用真实对象时,代理处理另外一些事。例如计算真实对象的引用次数,可以在代理对象中设置一个引用计数,当引用计数达到某个值时,禁制访问真实对象。

案例

一个虚拟代理的例子。

需要构建一个图片查看器程序,该程序需要列出所有图片并且展示高分辨率图片。再打开该程序时,需要立刻看到所有图片列表,但是并不需要立刻展示高分辨率图片,仅在需要绘制高分辨率图片时才会加载高分辨率图片,例如当用户点击某个图片查看,此时需要真正的渲染高分辨率图片。

proxy pattern例子

Image接口,表示Subject,接口有一个ShowImage方法。

package proxy;

/**
 * Subject Interface
 */
public interface Image {

    public void showImage();
}

虚拟代理类,该类只有在showImage方法被调用时,才会实例化高分辨率图片,这样节省了加载图片的消耗。

package proxy;

/**
 * Proxy
 */
public class ImageProxy implements Image {

    /**
     * Private Proxy data 
     */
    private String imageFilePath;

    /**
     * Reference to RealSubject
     */
    private Image proxifiedImage;


    public ImageProxy(String imageFilePath) {
        this.imageFilePath= imageFilePath;  
    }

    @Override
    public void showImage() {

        // create the Image Object only when the image is required to be shown
        proxifiedImage = new HighResolutionImage(imageFilePath);

        // now call showImage on realSubject
        proxifiedImage.showImage();
    }
}

高分辨率图片RealSubject的实现:

package proxy;

/**
 * RealSubject
 */
public class HighResolutionImage implements Image {

    public HighResolutionImage(String imageFilePath) {

        loadImage(imageFilePath);
    }

    private void loadImage(String imageFilePath) {
        // load Image from disk into memory
        // this is heavy and costly operation
    }

    @Override
    public void showImage() {

        // Actual Image rendering logic
    }

}

客户端代码,ImageViewer

package proxy;

/**
 * Image Viewer program
 */
public class ImageViewer {
    public static void main(String[] args) {
        // assuming that the user selects a folder that has 3 images    
        //create the 3 images     
        Image highResolutionImage1 = new ImageProxy("sample/veryHighResPhoto1.jpeg");
        Image highResolutionImage2 = new ImageProxy("sample/veryHighResPhoto2.jpeg");
        Image highResolutionImage3 = new ImageProxy("sample/veryHighResPhoto3.jpeg");

        // assume that the user clicks on Image one item in a list
        // this would cause the program to call showImage() for that image only
        // note that in this case only image one was loaded into memory
        highResolutionImage1.showImage();

        // consider using the high resolution image object directly
        Image highResolutionImageNoProxy1 = new HighResolutionImage("sample/veryHighResPhoto1.jpeg");
        Image highResolutionImageNoProxy2 = new HighResolutionImage("sample/veryHighResPhoto2.jpeg");
        Image highResolutionImageBoProxy3 = new HighResolutionImage("sample/veryHighResPhoto3.jpeg");


        // assume that the user selects image two item from images list
        highResolutionImageNoProxy2.showImage();

        // note that in this case all images have been loaded into memory 
        // and not all have been actually displayed
        // this is a waste of memory resources
    }
}

(未完待续)

留言(0
发表评论
邮箱地址不会被公开。*表示必填项。
评论(支持部分html标签)*
姓名*
站点