1. Facade 外观模式
  2. 练习
  3. Mediator 仲裁者模式
  4. 练习

Facade 模式

这个模式其实很简单, 就是如果一个程序内部很复杂, 与其对外要求程序的使用者先做这个在做这个, 不如用一个东西包装一下, 用一个窗口对外即可.

用术语就是将互相关联在一起的类整理出高层接口API, Facade角色来具备这个API, 对外只需要暴露Facade角色即可.

作者的例子也很简单, 是一个生成HTML页面的类. 类里有一个生成段落和生成HTML结构的两个类, 这里隐含着必须先生成title等内容, 才能再生成正文的顺序.

如果将类交给用户使用, 用户就必须注意其中的顺序, 然而我们可以包装一个类, 只需要名字和文件名, 就可以隐藏其中的细节, 直接生成HTML文件.

先来看生成名字的类:

import java.io.FileInputStream;
import java.io.IOException;
import java.util.Properties;

public class Database {

    private Database() {

    }

    public static Properties getProperties(String name) {
        String filename = name + ".txt";
        Properties properties = new Properties();

        try {
            properties.load(new FileInputStream(filename));

        } catch (IOException e) {
            System.out.println("加载文件失败");
        }
        return properties;
    }

}

这个类是一个通过文件加载Properties的类, 很简单. 然后是写HTML的类, 之前已经很多次编写过类似的类了:

import java.io.IOException;
import java.io.Writer;

public class HtmlWriter {

    private Writer writer;

    public HtmlWriter(Writer writer) {
        this.writer = writer;
    }

    public void title(String title) throws IOException {
        writer.write("<html>");
        writer.write("<head>");
        writer.write("<title>");
        writer.write(title);
        writer.write("</title>");
        writer.write("</head>");
        writer.write("<body>\n");
        writer.write("<h1>\n");
        writer.write(title);
        writer.write("</h1>");

    }

    public void paragraph(String msg) throws IOException {
        writer.write("<p>");
        writer.write(msg);
        writer.write("</p>");
    }

    public void link(String href, String caption) throws IOException {
        paragraph("<a href=\"" + href + "\">" + caption + "</a>");
    }

    public void mailto(String address, String username) throws IOException {
        link("mailto:" + address, username);
    }

    public void close() throws IOException {
        writer.write("</body>");
        writer.write("</html>\n");
        writer.close();
    }
}

其中拼接HTML都是传统艺能了, 这里主要是看一个顺序, 即必须先生成头部信息, 然后填充正文才行. 如果交给用户自己使用这个类, 则用户必须按照这个顺序调用.

现在就该Facade角色出场了, 我们编写一个PageMaker类, 使用用户名然后在其中正确的调用这些方法并且生成文件:

import java.io.IOException;
import java.io.PrintWriter;
import java.util.Properties;

public class PageMaker {

    private PageMaker() {

    }

    public static void makeWelcomePage(String mailaddr, String filename) {
        try {
            Properties mailprop = Database.getProperties("maildata.txt");
            String username = mailprop.getProperty(mailaddr);
            HtmlWriter writer = new HtmlWriter(new PrintWriter(filename));
            writer.title("Welcome to" + username + "'s page!");
            writer.paragraph("waiting....");
            writer.mailto(mailaddr, username);
            writer.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

这个类也很简单, 重点看其中只用了一个API就可以操作了.

public class Main {
    public static void main(String[] args) {
        PageMaker.makeWelcomePage("conyli@conyli.cc", "welcome.html");
    }
}

这个模式我感觉其实就是最基本的封装, 而且Facade角色可以嵌套, 一层套一层来使用.

练习

习题15-1

这个很简单,如果不想要外边的包访问Database和HtmlWriter, 只需要将其权限改为包访问权限即可.


习题15-2

这个也很简单, 只不过是再添加一个方法即可. 其中的title部分可以复用. 从Properties中取出所有的邮件地址和用户名来复用生成link的方法即可.

public static void makeLinkPage(String filename) {
    try {
        Properties mailprop = Database.getProperties("maildata");
        HtmlWriter writer = new HtmlWriter(new PrintWriter(filename));
        writer.title("Link Page");

        for (String name : mailprop.stringPropertyNames()) {
            writer.mailto(name, mailprop.getProperty(name) );
        }
        writer.close();
    } catch (IOException e) {
        e.printStackTrace();
    }
}

Mediator 仲裁者模式

这个模式我想了一下, 应该适用那些整体状态非常多的时候, 每个组件都需要向一个集中管理状态的组件进行报告的时候使用. 以后自己如果写前端的话, 应该可以使用到这个模式.

简单的说, 调整多个对象之间的关系, 就可以用到仲裁者模式, 即不让各个组件之间通信, 而是增加一个角色负责管理.

这里作者举了一个AWT的例子, 其本质和Web开发的理念是一样的. 其中有两个接口, 一个表示仲裁者, 一个表示成员对象.

public interface Mediator {

    //创建符合仲裁者模式要求的成员对象
    void createColleagues();

    //让组员向仲裁者报告变化的方法, 由组员调用
    void colleagueChanged();
}
public interface Colleague {

    //设置自己的仲裁者
    void setMediator(Mediator mediator);

    //设置自己的状态为是否可用, 这个方法教给仲裁者来调用, 而不是自己设置
    void setColleagueEnabled(boolean enabled);

}

在这两个接口的状态下, 我们就可以来编写仲裁者和团队成员类了:

import java.awt.Button;

public class ColleagueButton  extends Button implements Colleague {

    private Mediator mediator;

    public ColleagueButton(String caption) {
        super(caption);
    }

    @Override
    public void setMediator(Mediator mediator) {
        this.mediator = mediator;
    }

    //这个其实是利用了awt包里的特性, 比如setEnabled表示是否禁用
    @Override
    public void setColleagueEnabled(boolean enabled) {
        setEnabled(enabled);
    }
}
import java.awt.*;
import java.awt.event.TextEvent;

public class ColleagueTextField extends TextField implements Colleague, TextListener {

    private Mediator mediator;

    public ColleagueTextField(String text, int columns) {
        super(text, columns);
    }

    @Override
    public void setMediator(Mediator mediator) {
        this.mediator = mediator;
    }

    @Override
    public void setColleagueEnabled(boolean enabled) {
        setEnabled(enabled);
        setBackground(enabled ? Color.white : Color.lightGray);
    }

    //自己的文字发生变化的时候通知仲裁者
    //这是一个接口, 会在文字发生变化的时候被awt框架调用
    public void textValueChanged(TextEvent event) {
        mediator.colleagueChanged();
    }

}

这里注意红字的实现了一个额外的接口, 这是awt框架用来检测变化事件的.下边的Checkbox也类似:

import java.awt.*;
import java.awt.event.ItemEvent;
import java.awt.event.ItemListener;

public class ColleagueCheckbox extends Checkbox implements Colleague, ItemListener {

    private Mediator mediator;

    public ColleagueCheckbox(String caption, CheckboxGroup checkboxGroup, boolean state) {
        super(caption, checkboxGroup, state);
    }

    @Override
    public void setMediator(Mediator mediator) {
        this.mediator = mediator;
    }

    @Override
    public void setColleagueEnabled(boolean enabled) {
        setEnabled(enabled);
    }

    public void itemStateChanged(ItemEvent event) {
        mediator.colleagueChanged();
    }
}

最后就可以来编写仲裁者类了, 会根据各个组件的通知来管理状态:

import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;

public class LoginFrame extends Frame implements ActionListener, Mediator {


    //这是为了实现界面所需要的各个组件
    private ColleagueCheckbox checkGuest;
    private ColleagueCheckbox checkLogin;
    private ColleagueTextField textUser;
    private ColleagueTextField textPass;
    private ColleagueButton buttonOk;
    private ColleagueButton buttonCancel;

    public LoginFrame(String title) {

        //绘制框体和创建组件
        super(title);
        setBackground(Color.lightGray);
        setLayout(new GridLayout(4, 2));
        createColleagues();

        //添加所有组件
        add(checkGuest);
        add(checkLogin);
        add(new Label("Username: "));
        add(textUser);
        add(new Label("Password: "));
        add(textPass);
        add(buttonOk);
        add(buttonCancel);

        //设置初始状态
        colleagueChanged();

        pack();
        show();
    }

    @Override
    public void createColleagues() {
        CheckboxGroup g = new CheckboxGroup();
        //这个猜着就能看懂, 一组两个checkbox, 一个默认选中 , 一个默认不选
        checkGuest = new ColleagueCheckbox("Guest", g, true);
        checkLogin = new ColleagueCheckbox("Login", g, false);

        textUser = new ColleagueTextField("", 10);
        textPass = new ColleagueTextField("", 10);
        //设置密码框的覆盖字符
        textPass.setEchoChar('*');

        buttonOk = new ColleagueButton("OK");
        buttonCancel = new ColleagueButton("Cancel");

        //设置仲裁者为当前对象
        checkGuest.setMediator(this);
        checkLogin.setMediator(this);
        textUser.setMediator(this);
        textPass.setMediator(this);
        buttonOk.setMediator(this);
        buttonCancel.setMediator(this);

        //添加事件监听器, 这个应该是套路
        checkGuest.addItemListener(checkGuest);
        checkLogin.addItemListener(checkLogin);
        textUser.addTextListener(textUser);
        textPass.addTextListener(textPass);
        buttonOk.addActionListener(this);
        buttonCancel.addActionListener(this);
    }

    @Override
    public void colleagueChanged() {
        //如果客户被选中, 禁止两个文本框, OK按钮可以选中
        if (checkGuest.getState()) {
            textUser.setColleagueEnabled(false);
            textPass.setColleagueEnabled(false);
            buttonOk.setColleagueEnabled(true);
        } else {
            textUser.setColleagueEnabled(true);
            //这是抽取出来的方法, 用于改变状态
            userpassChanged();
        }
    }

    private void userpassChanged() {
        buttonOk.setEnabled(false);
        //如果用户名中输入了字符就让密码框可以使用. 密码框可用的时候, 如果有字符就让OK键可以用, 否则OK键无法使用
        if (textUser.getText().length() > 0) {
            textPass.setColleagueEnabled(true);
            if (textPass.getText().length() > 0) {
                buttonOk.setColleagueEnabled(true);
            } else {
                buttonOk.setColleagueEnabled(false);

            }
        }
    }


    @Override
    public void actionPerformed(ActionEvent e) {
        System.out.println(e.toString());
        System.exit(0);

    }
}

这个逻辑还是比较好懂的, 先创建一系列控件, 然后添加好, 之后每次监听到任何事件, 比如输入文字和单选框发生变化, 都跳到colleagueChanged()方法中对逻辑进行统一处理.

使用起来就是使用awt框架:

public class Main {
    public static void main(String[] args) {
        new LoginFrame("Mediator Sample");
    }
}

回头看看这个模式, 这个模式的关键在于, 各个控件没有更改自身的状态, 而是统一交给仲裁者来管理, 这样有一大好处就是如果出现BUG, 几乎可以确定是仲裁者编写出现问题, 而无需一个一个到各个组件中寻找错误所在.

一般仲裁者由于利用了其他类的一些具体功能特性, 使得难以复用. 不过组件一般都是可以复用的.

这个模式特别适合使用在GUI模式中, 也就意味着在编写Web前端应用的时候, 可以考虑使用该模式.

练习

练习16-1

修改成当用户名和密码的长度都大于等于4个字符的时候OK按钮才有效.

很显然, 在修改需求的时候, 仲裁者模式只需要修改仲裁者类:

private void userpassChanged() {
    buttonOk.setEnabled(false);
    //如果用户名中输入了字符就让密码框可以使用. 密码框可用的时候, 如果有字符就让OK键可以用, 否则OK键无法使用
    if (textUser.getText().length() > 0) {
        textPass.setColleagueEnabled(true);
        if (textPass.getText().length() >=4  && textUser.getText().length()>=4) {
            buttonOk.setColleagueEnabled(true);
        } else {
            buttonOk.setColleagueEnabled(false);

        }
    }
}

练习16-2

无法在接口中定义, 因为接口中如果定义域, 只能定义静态域.