1.引入

1.0 引入

使用模式最棒的方式,就是把它们从家里找出来同其他模式展开交互。你越多地使用模式就越容易发现它们一同现身在你的设计中。对于这些在设计中携手合作征服许多问题的模式,我们给它一个特别的名字:复合模式 (Compound Pattern)。没错!我们说的正是一种由模式所构成的模式

在本章,将重访SimUDuck鸭子模拟器中那些熟悉的鸭子。复合模式必须够一般性,适合解决许多问题才行。因此,在本章的后半段,将会拜访一个真正的复合模式,就是MVC(Model-View-Control).

模式通常被一起使用,并被组合在同一个设计解决方案中。

复合模式在一个解决方案中结合两个或多个模式,以解决一般货重复发生的问题

1.1 需求引入-初始版本

从头重建我们的鸭子模拟器,并通过使用一堆模式来赋予它一些有趣的能力。

1.首先,来创建一个Quackable接口

public interface Quackable {public void quack();//Quackable只需做好一件事,Quack呱呱叫
}

2.某些鸭子实现Quackable接口

如果没有类实现某个接口,那么这个接口的存在就没有意义。因此,来设计一些具体的鸭子。

//绿头鸭
public class MallardDuck implements Quackable {@Overridepublic void quack() {System.out.println("Quack");}@Overridepublic String toString() {return "Mallard Duck";}
}//红头鸭
public class RedheadDuck implements Quackable {@Overridepublic void quack() {System.out.println("Quack");}@Overridepublic String toString() {return "Redhead Duck";}
}//鸭鸣器
public class DuckCall implements Quackable {public void quack() {System.out.println("Kwak");}public String toString() {return "Duck Call";}
}//橡皮鸭
public class RubberDuck implements Quackable {public void quack() {System.out.println("Squeak");}public String toString() {return "Rubber Duck";}
}

3.有了鸭子,还需要一个模拟器

让我们来制造一个会产生一些鸭子,还要确认鸭子会嘎嘎叫的模拟器

package desginPattern.compoundPattern.exe537Combing;public class DuckSimulator {public static void main(String[] args) {DuckSimulator simulator = new DuckSimulator();simulator.simulate();}void simulate() {//首先,创建一些鸭子Quackable mallardDuck = new MallardDuck();Quackable redheadDuck = new RedheadDuck();Quackable duckCall = new DuckCall();Quackable rubberDuck = new RubberDuck();System.out.println("\nDuck Simulator");//开始模拟各种鸭子simulate(mallardDuck);simulate(redheadDuck);simulate(duckCall);simulate(rubberDuck);}//重载了simulate方法,来模拟鸭子来叫void simulate(Quackable duck) {//通过多态,不管传入的是哪一种呱呱叫对象,多态都可以调用到正确的方法duck.quack();}
}

输出:

4.当鸭子出现在这里,鹅也应该在这附近

public class Goose {public void honk(){System.out.println("Honk");//鹅的叫声是咯咯,不是呱呱}
}

1.2 问题1引入--将鹅引入

如果想在所有使用鸭子的地方使用鹅,毕竟鹅也会飞、会叫、会游,和鸭子差不多,在这个模拟器中使用鹅。

那么需要使用适配器轻易地将鸭子和鹅掺杂在一起。

5.需要鹅适配器

我们的模拟器期望看到Quackable接口。既然鹅不会呱呱叫,那么我们可以利用适配器将鹅适配成鸭子。

6.在模拟器中使用鹅

package desginPattern.compoundPattern.exe537Combing;public class DuckSimulator {public static void main(String[] args) {DuckSimulator simulator = new DuckSimulator();simulator.simulate();}void simulate() {//首先,创建一些鸭子Quackable mallardDuck = new MallardDuck();Quackable redheadDuck = new RedheadDuck();Quackable duckCall = new DuckCall();Quackable rubberDuck = new RubberDuck();//使用适配器模式,将Goose包装进GooseAdapter中Quackable gooseDuck=new GooseAdapter(new Goose());System.out.println("\nDuck Simulator");//开始模拟各种鸭子simulate(mallardDuck);simulate(redheadDuck);simulate(duckCall);simulate(rubberDuck);simulate(gooseDuck);//对鹅进行仿真}//重载了simulate方法,来模拟鸭子来叫void simulate(Quackable duck) {//通过多态,不管传入的是哪一种呱呱叫对象,多态都可以调用到正确的方法duck.quack();}
}

输出:

1.3 问题引入2-计算叫的次数

呱呱叫学家为所有拥有可呱呱叫行为的事物着迷。其中一件他们经常研究的事是:在一群鸭子中,会有多少呱呱叫声?

我们要如何在不变化鸭子类的情况下,计算呱呱叫的次数呢?

8.让呱呱叫学家满意,让他们知道叫声的次数

创建一个装饰者,通过把鸭子包装进装饰者对象,给鸭子一些新行为(计算次数的行为)。而不必修改鸭子的代码。

package desginPattern.compoundPattern.exe537Combing;/*** @author myl* @create 2022-03-16 9:07*/public class QuackCounter implements  Quackable {Quackable duck;static int numberOfQuacks;//使用静态变量跟踪所有呱呱叫的次数public QuackCounter(Quackable duck) {this.duck = duck;}@Overridepublic void quack() {duck.quack();numberOfQuacks++;}//给装饰者加入一个静态方法以返回在所以Quackable中发生的叫声次数public static int getQuacks(){return numberOfQuacks;}
}

9.更新模拟器,以便创建被装饰的鸭子

现在,我们必须更新包装在QuackCounter装饰者中被实例化的每个Quackable对象。如果不这么做,鸭子就会导出乱跑而使得我们无法统计叫声次数。

package desginPattern.compoundPattern.exe537Combing;public class DuckSimulator {public static void main(String[] args) {DuckSimulator simulator = new DuckSimulator();simulator.simulate();}void simulate() {//每创建一个Quackable,就用一个新的装饰者包装它Quackable mallardDuck = new QuackCounter(new MallardDuck());Quackable redheadDuck = new QuackCounter(new RedheadDuck());Quackable duckCall = new QuackCounter(new DuckCall());Quackable rubberDuck = new QuackCounter(new RubberDuck());Quackable gooseDuck=new QuackCounter(new GooseAdapter(new Goose()));System.out.println("\nDuck Simulator");//开始模拟各种鸭子simulate(mallardDuck);simulate(redheadDuck);simulate(duckCall);simulate(rubberDuck);simulate(gooseDuck);//对鹅进行仿真System.out.println("The Ducks quacked "+QuackCounter.getQuacks()+"  times");}//重载了simulate方法,来模拟鸭子来叫void simulate(Quackable duck) {//通过多态,不管传入的是哪一种呱呱叫对象,多态都可以调用到正确的方法duck.quack();}
}

输出:

1.4 引入问题3--引入工厂模式

必须装饰对象来获得被装饰过的行为,有包装才有效果,没包装就没效果

为什么不讲创建鸭子的程序集中在一个地方?换句话说,我们将创建和和装饰的部分包装起来

10.需要工厂生产鸭子

需要一些质量控制来确保鸭子一定是被包装起来的。要建造一个工厂,创建装饰过的鸭子。此工厂应该生产各种不同类型的鸭子的产品家族,因此用到抽象工厂模式

先从AbstractDuckFactory的定义开始:

public abstract class AbstractDuckFactory {//先定义一个抽象工厂,它的子类们会创建不同的家族//每个方法创建一种鸭子public  abstract Quackable createMallardDuck();public abstract Quackable createRedheadDuck();public abstract Quackable createDuckCall();public abstract Quackable createRubberDuck();
}

从创建一个工厂开始,此工厂创建没有装饰者的鸭子

package desginPattern.compoundPattern.exe537Combing;/*** @author myl* @create 2022-03-16 11:19*/
public class DuckFactory extends AbstractDuckFactory {//每个方法创建一个产品:一种特定种类的Quackable。// 模拟器并不知道实际的产品是什么,只知道它实现了Quackable接口@Overridepublic Quackable createMallardDuck() {return new MallardDuck();}@Overridepublic Quackable createRedheadDuck() {return new RedheadDuck();}@Overridepublic Quackable createDuckCall() {return new DuckCall();}@Overridepublic Quackable createRubberDuck() {return new RubberDuck();}
}

现在,要创建我们真正需要的工厂,CountingDuckFactory:

package desginPattern.compoundPattern.exe537Combing;/*** @author myl* @create 2022-03-16 13:34*/
public class CountingDuckFactory extends AbstractDuckFactory {//每个方法都会先用叫声计数器装饰者将Quackable包装起来 //模拟器并不知道有何不同,只知道它实现了Quackable接口@Overridepublic Quackable createMallardDuck() {return new QuackCounter(new MallardDuck());}@Overridepublic Quackable createRedheadDuck() {return new QuackCounter(new RedheadDuck());}@Overridepublic Quackable createDuckCall() {return new QuackCounter(new DuckCall());}@Overridepublic Quackable createRubberDuck() {return new QuackCounter(new RubberDuck());}
}

11.设置模拟器来使用这个工厂

还记得抽象工厂是怎么工作的吗?我们创建一个多态的方法,此方法需要一个用来创建对象的工厂。通过传入不同的工厂,我们就会得到不同的产品家族。
我们要修改一下simulate()方法,让它利用传进来的工厂来创建鸭子。

package desginPattern.compoundPattern.exe537Combing;/*** @author myl* @create 2022-03-16 13:39*/
public class DuckSimulator2 {public static void main(String[] args) {DuckSimulator2 duckSimulator=new DuckSimulator2();AbstractDuckFactory duckFactory=new CountingDuckFactory();duckSimulator.simulator(duckFactory);}void simulator(AbstractDuckFactory duckFactoryfactory){Quackable mallardDuck=duckFactoryfactory.createMallardDuck();Quackable redheadDuck = duckFactoryfactory.createRedheadDuck();Quackable duckCall = duckFactoryfactory.createDuckCall();Quackable rubberDuck = duckFactoryfactory.createRubberDuck();System.out.println("\n Duck Simulator: With Abstract Factory");simulator(mallardDuck);simulator(redheadDuck);simulator(duckCall);simulator(rubberDuck);System.out.println("The ducks quacked"+QuackCounter.getQuacks()+" times");}void simulator(Quackable duck){duck.quack();}
}

输出:

1.5 引入问题4--统一管理会叫的,包括鹅

上面的代码仍然是依赖具体类来实例化鹅。那想为鹅写一个抽象工厂,创建“内鹅外鸭”。

我们需要将鸭子视为一个集合,甚至是子集合( subcollection),为了满足巡逻员想管理鸭子家族的要求)。如果我们下一次命令,就能让整个集合的鸭子听命行事,那就太好了。

12.让我们创建一群鸭子(实际上是一群Quackable)

组合模式允许我们像对待单个对象一样对待对象集合。还有什么模式能比组合模式创建一群Quackable更好呢!

当发现需求中是体现部分与整体层次结构时,以及你希望用户可以忽略组合对象与单个对象的不同,统一地使用组合结构中的所有对象时,就应该考虑组合模式了

public class Flock implements Quackable {//在每一个Flock内,使用ArrayList记录属于这个Flock的Quackable对象ArrayList<Quackable> quackers=new ArrayList<>();public void add(Quackable quacker){quackers.add(quacker);}//Flock也是Quackable,因此也要具备quack()方法,此方法会对整群产生作用// 我们遍历ArrayList调用每一个元素上的quack()@Overridepublic void quack() {Iterator iterator=quackers.iterator();while (iterator.hasNext()){//使用到了迭代器模式Quackable quacker=(Quackable)iterator.next();quacker.quack();}}
}

13.修改模拟器

组合已经准备好了,需要一些让鸭子能进入组合结构的代码

package desginPattern.compoundPattern.exe537Combing;/*** @author myl* @create 2022-03-17 13:34*/
public class DuckSimulator3 {public static void main(String[] args) {DuckSimulator3 simulator=new DuckSimulator3();AbstractDuckFactory duckFactory = new CountingDuckFactory();simulator.simulate(duckFactory);}void simulate(AbstractDuckFactory duckFactory){Quackable redheadDuck = duckFactory.createRedheadDuck();Quackable duckCall = duckFactory.createDuckCall();Quackable rubberDuck = duckFactory.createRubberDuck();Quackable gooseDuck = new GooseAdapter(new Goose());//创建主群Flock,里面有许多QuackableFlock flockOfDucks=new Flock();flockOfDucks.add(redheadDuck);flockOfDucks.add(duckCall);flockOfDucks.add(rubberDuck);flockOfDucks.add(gooseDuck);//创建绿头鸭小集群Flock flockOfMallards = new Flock();Quackable mallardOne = duckFactory.createMallardDuck();Quackable mallardTwo = duckFactory.createMallardDuck();Quackable mallardThree = duckFactory.createMallardDuck();Quackable mallardFour = duckFactory.createMallardDuck();flockOfMallards.add(mallardOne);flockOfMallards.add(mallardTwo);flockOfMallards.add(mallardThree);flockOfMallards.add(mallardFour);//将绿头鸭小集群加入到主群中flockOfDucks.add(flockOfMallards);//测试主群System.out.println("\nDuck Simulator: Whole Flock Simulation");simulate(flockOfDucks);//测试 绿头鸭小集群System.out.println("\nDuck Simulator: Mallard Flock Simulation");simulate(flockOfMallards);//最后,把数据显示给呱呱叫学家System.out.println("\nThe ducks quacked " +QuackCounter.getQuacks() +" times");}void simulate(Quackable quacker){quacker.quack();}}

输出:

安全性VS透明性

你或许还记得,在组合模式章节中,组合(菜单)和叶节点(菜单项)具有一组相同的方法,其中包括了add()方法。就因为有一组相同的方法,我们才能在菜单项上调用不起作用的方法(像通过调用add()来在菜单项内加入些东西)。这么设计的好处是,叶节点和组合之间是“透明的”。客户根本不用管究竟是组合还是叶节点,客户只是调用两者的同一个方法。
但是在这里,我们决定把组合维护孩子的方法和叶节点分开,也就是说,我们打算只让Flock具有add()方法。我们知道给一个Duck添加某些东西是无意义的。这样的设计比较“安全”,你不会调用无意义的方法,但是透明性比较差。现在,客户如果想调用add(),得先确定该Quackable对象是Flock才行。
在OO设计的过程中,折衷一直都是免不了的,在创建你自己的组合时,你需要考虑这些。

1.6 引入问题5--观察个别鸭子的行为

新的需求:要观察个别鸭子的行为,可以使用观察者模式来观察对象的行为。

14.首先,需要一个Observable接口

所谓的Observable就是被观察的对象。Observable需要注册和通知观察者的方法。

//QuackObservable是一个接口,任何想被观察的Quackable都必须实现QuackObservable接口
public interface QuackObservable {//该接口具有注册观察者的方法,任何实现了Observer接口的对象,都可以监听呱呱叫public void registerObserver(Observer observer);public void notifyObservers();
}

我们需要确定所有的Quackable都实现此接口,这样让每个会叫的鸭\鹅,都成为可以被观察的对象。

public interface Quackable extends QuackObservable {public void quack();
}

15.现在我们必须确定所有实现Quackable的具体类都能够扮演QuackObservable的角色

我们需要在每一个类中实现注册和通知(同在第2章我们所做的一样)。但是这次我们要用稍微不一样的做法:我们要在另一个被称为Observable的类中封装注册和通知的代码,然后将它和QuackObservable组合在一起。这样,我们只需要一份代码即可,QuackObservable所有的调用都委托给Observable辅助类。

我们先从Observable辅助类开始下手吧……

package desginPattern.compoundPattern.exe552Observer;import java.util.Iterator;
import java.util.List;
import java.util.ArrayList;//辅助类 Observable
//该类实现了所有必要的功能。我们只要把它插进一个类,就可以让该类将工作委托给Observable//Observable必须实现QuackObservable,因为他们具有一组相同的方法
//QuackObservable会将这些方法的调用转给Observable的方法
public class Observable implements QuackObservable {//用于存储 观察者  的队列List<Observer> observers = new ArrayList<Observer>();QuackObservable duck;//在构造器中,传入QuackObervable的duck,也就是被观察对象//看下面的notify()方法,就会发现当通知发生时,观察者会把此对象传过去,好让观察者知道是哪个对象在呱呱叫public Observable(QuackObservable duck) {this.duck = duck;}//注册观察者的代码@Overridepublic void registerObserver(Observer observer) {observers.add(observer);}//通知观察者的代码@Overridepublic void notifyObservers() {Iterator<Observer> iterator = observers.iterator();while (iterator.hasNext()) {Observer observer = iterator.next();observer.update(duck);}}public Iterator<Observer> getObservers() {return observers.iterator();}
}

16.整合Observable辅助类和Quackable类

这应该不算太糟,我们只是要确定Quackable类是和Observable组合在一起的,并且它们知道怎样来委托工作。然后,它们就准备好成为Observable了。下面是MallardDuck的实现,其他的鸭子实现也类似。

package desginPattern.compoundPattern.exe552Observer;public class MallardDuck implements Quackable {//每个Quackable都有一个Obserrvable实例变量//用于准备 被监听Observable observable;public MallardDuck() {//在构造器中,创建一个Observable并将自身实例的引用传入//如果被监听,则就是监听自身observable = new Observable(this);}@Overridepublic void quack() {System.out.println("Quack");notifyObservers();//一旦叫了,就通知所有观察者}@Overridepublic void registerObserver(Observer observer) {//将观察者的注册 委托给辅助类observableobservable.registerObserver(observer);}@Overridepublic void notifyObservers() {//将观察者的通知 委托给辅助类observableobservable.notifyObservers();}public String toString() {return "Mallard Duck";}
}

17.将模式的Observer端完成

首先实现观察者的接口。从Observer接口开始:

package desginPattern.compoundPattern.exe552Observer;//观察者,就是用于观察   被观察对象的
public interface Observer {public void update(QuackObservable duck);
}

同时,还需要一个观察者:呱呱叫学家。

package desginPattern.compoundPattern.exe552Observer;//需要实现Observer接口,否则就无法在QuackObservable中注册
public class Quackologist implements Observer {@Overridepublic void update(QuackObservable duck) {//update()用于打印出正在呱呱叫的Quack对象System.out.println("Quackologist: " + duck + " just quacked.");}@Overridepublic String toString() {return "Quackologist";}
}

18.开始观察了,更新模拟器

package desginPattern.compoundPattern.exe552Observer;public class DuckSimulator {public static void main(String[] args) {DuckSimulator simulator = new DuckSimulator();AbstractDuckFactory duckFactory = new CountingDuckFactory();simulator.simulate(duckFactory);}void simulate(AbstractDuckFactory duckFactory) {Quackable redheadDuck = duckFactory.createRedheadDuck();Quackable duckCall = duckFactory.createDuckCall();Quackable rubberDuck = duckFactory.createRubberDuck();Quackable gooseDuck = new GooseAdapter(new Goose());Flock flockOfDucks = new Flock();flockOfDucks.add(redheadDuck);flockOfDucks.add(duckCall);flockOfDucks.add(rubberDuck);flockOfDucks.add(gooseDuck);Flock flockOfMallards = new Flock();Quackable mallardOne = duckFactory.createMallardDuck();Quackable mallardTwo = duckFactory.createMallardDuck();Quackable mallardThree = duckFactory.createMallardDuck();Quackable mallardFour = duckFactory.createMallardDuck();flockOfMallards.add(mallardOne);flockOfMallards.add(mallardTwo);flockOfMallards.add(mallardThree);flockOfMallards.add(mallardFour);flockOfDucks.add(flockOfMallards);System.out.println("\nDuck Simulator: With Observer");Quackologist quackologist = new Quackologist();//呱呱叫学家来监听一整个Quacker群flockOfDucks.registerObserver(quackologist);simulate(flockOfDucks);System.out.println("\nThe ducks quacked " + QuackCounter.getQuacks() + " times");}void simulate(Quackable duck) {duck.quack();}
}

输出:

1.7 设计总结

问1:这就是复合模式?

答1:这只是一群模式携手而后作。所谓的复合模式,是指一群模式被结合起来使用,以解决一般性问题。很快就会看到Model-View-Controller(模型-视图-控制器)复合模式,它是由数个模式结合起来而形成的新模式,一再地被用于解决许多设计问题。

2.复合模式之王:MVC(Model-View-Control)

2.1 认识模型-视图-控制器

想象你正在使用你最喜欢的MP3播放器,比方说iTune。你可以用它的界面加入新的歌曲、管理播放清单、将歌曲改名。播放器有一个小型数据库,记录所有的歌曲和相关的名字和数据。播放器也可以播歌,而播歌时用户界面会显示当时的歌曲标题、运行时间等信息。

其实,底下用的就是 模型-视图-控制器

MP3播放器的描述给了我们一个MVC的高层视图,但是仍然无法让我们知道复合模式内的运作细节、无法创建自己的复合模式、无法认识复合模式好在哪里。让我们从模型、视图、控制器三者的关系开始入手,然后再从设计模式的角度来看一看

视图:用来呈现模型。视图通常直接从模型中取出它需要显示的状态与数据。

控制器:取得用户的输入并解读其对模型的意思

模型:模型持有所有的数据、状态和程序逻辑。模型没有注意到视图和控制器,虽然它提供了操纵和检索状态的接口,并发送状态改变通知给观察者。它处理所有应用数据和逻辑。

  • 你是用户—―你和视图交互

视图是模型的窗口。当你对视图做一-些事时(比方说,按下“播放”按钮),视图就告诉控制器你做了什么。控制器会负责处理。

  • 控制器要求模型改变状态

控制器解读你的动作。如果你按下某个按钮,控制器会理解这个动作的意义,并告知模型如何做出对应的动作。

  • 控制器也可能要求视图做改变

当控制器从视图接收到某一动作,结果可能是它也需要告诉视图改变其结果。比方说,控制器可以将界面上的某些按钮或菜单项变成有效或无效。

  • 当模型状态改变时,模型会通知视图

不管是你做了某些动作(比方说按下按钮)还是内部有了某些改变(比方说播放清单的下一首歌开始),只要当模型内的东西改变时,模型都会通知视图它的状态改变了。

  • 视图向模型询问状态

视图直接从模型取得它显示的状态。比方说,当模型通知视图新歌开始播放,视图向模型询问歌名并显示出来。当控制器请求视图改变时,视图也可能向模型询问某些状态。

问: 控制器可以变成模型的观察者吗?
答: 当然。在某些设计中,控制器会向模型注册,模型一有改变就通知控制器。当模型直接影响到用户界面时,就会这么做。比方说,模型内的某些状态可以支配界面的某些项目变成有效或无效,如果这样,要求视图更新相应显示其实就是控制器的事。

问:控制器所做的事情就是把用户的输入从视图发送到模型,对不对?如果只是做这些事,其实控制器没有必要存在呀!为何不把这样的代码放在视图中?大多数情况下,控制器不是只调用模型的方法吗?
答:控制器做的事情不只有“发送给模型”,还会解读输入,并根据输入操纵模型。你真正想问的问题可能是“为何不能把这样的代码放在视图中?”你当然可以这么做,但是你不想这么做,有两个原因:首先,这会让视图的代码变得更复杂,因为这样一来视图就有两个责任,不但要管理用户界面、还要处理如何控制模型的逻辑。第二个原因,这么做将造成模型和视围之间紧耦合,如果你想复用此视图来处理其他模型、根本不可能。控制器把控制逻辑从视图中分离,让模型和视图之间解耦。通过保持控制器和视图之间松耦合,设计就更有弹性而且容易扩展,足以容纳以后的改变。

2.2 通过设计模式看MVC

让我们先从模型开始。你可能也猜到了,模型利用“观察者”让控制器和视图可以随最新的状态改变而更新。另一方面,视图和控制器则实现了“策略模式”。控制器是视图的行为,如果你希望有不同的行为,可以直接换一个控制器。视图内部使用组合模式来管理窗口、按钮以及其他显示组件。

关于策略模式回顾:策略模式定义了算法族,分别封装起来,让他们之间可以互相替换,此模式让算法的变化独立于使用算法的客户。

视图和控制器实现了经典的策略模式:视图是一个对象,可以被调整使用不同的策略,而控制器提供了策略。视图只关心系统中可视的部分,对于任何界面行为,都委托给控制器处理。使用策略模式也可以让视图和模型之间的关系解耦.因为控制器负责和模型交互来传递用户的请求。对于工作是怎么完成的,视图毫不知情。

 显示包括了窗口、面板、按钮、文本标签等。每个显示组件如果不是组合节点(例如窗口),就是叶节点(例如按钮)。当控制器告诉视图更新时,只需告诉视图最顶层的组件即可,组合会处理其余的事。

模型实现了观察者模式,当状态改变时,相关对象将持续更新。使用观察者模式,可以让模型完全独立于视图和控制器。同一个模型可以使用不同的视图,甚至可以同时使用多个视图。

2.2.1 观察者模式

2.2.2 策略模式

2.2.3 组合模式

2.3 引入需求-使用MVC来控制节拍

现在让你来当DJ。当DJ,节拍是头等大事,你一开始可能会用95BPM(每分钟95拍)的downtempo groove,然后转换到140BPM的trance techno,最后是80BPM的ambient mix。
这要怎么做呢?你必须控制节拍并建造工具来帮你的忙。

认识Java DJ View:

还有一些DJ View的方法:

控制器在中间:控制器位于视图和模型之间。它将用户的输入(比方说:从DJ控制菜单中选择“Start”) ,转给模型做动作,启动节拍的产生。

别忘了在下面的模型:模型是这个系统的核心。它实现了节拍开始与停止的逻辑,管理BPM并产生声音。

2.4 流程分析

2.5 创建碎片

现在你已经知道模型是负责维护所有的数据、状态和应用逻辑。那么BeatModel又如何呢?

它的主要工作是管理节拍,所以它具有维护当前BPM的状态和许多产生MIDI(音乐数字接口”(英文:Musical Instrument Digital Interface,简称MIDI))事件的代码,以便产生我们听到的节拍。它也暴露一个接口,让控制器操纵节拍,让视图和控制器获得模型的状态。还有,别忘了模型使用观察者模式,所以我们也需要一些方法,让对象注册为观察者并送出通知。

2.5.1 BeatModelInterface接口

package desginPattern.compoundPattern.exe573DjViewer;public interface BeatModelInterface {/*** initialize()、on()、off()、setBPM()是让控制器调用的* 控制器根据用户的操作而对模型做出适当的处理*///在BeatModel被初始化之后,就会调用此方法void initialize();//打开节拍产生器void on();//关闭节拍产生器void off();//设定BPM。调用此方法后,节拍频率马上改变void setBPM(int bpm);/*** 下面的方法 允许视图和控制器取得状态,并变成观察者* *///返回当前BPMint getBPM();//下面分为两种观察者,一种观察者希望每个节拍都被通知//另一种只希望BPM改变时被通知void registerObserver(BeatObserver o);void removeObserver(BeatObserver o);void registerObserver(BPMObserver o);void removeObserver(BPMObserver o);
}

2.5.2 具体的BeatModel类

package desginPattern.compoundPattern.exe573DjViewer;import javax.sound.sampled.AudioSystem;
import javax.sound.sampled.Clip;
import javax.sound.sampled.Line;
import java.io.File;
import java.util.ArrayList;public class BeatModel implements BeatModelInterface, Runnable {//节拍与BPM的关系,BPM是120,视图每0.5秒得到一次节拍通知ArrayList<BeatObserver> beatObservers=new ArrayList();//观察节拍改变ArrayList<BPMObserver> bpmObservers=new ArrayList<>();//观察BPM改变int bpm=90;Thread thread;boolean stop = false;Clip clip;@Overridepublic void initialize() {try {File resource = new File("F:\\MyJava_ws\\HeadFirstLearning\\0951.wav");//打开的文件需要设置clip = (Clip) AudioSystem.getLine(new Line.Info(Clip.class));clip.open(AudioSystem.getAudioInputStream(resource));}catch(Exception ex) {System.out.println("Error: Can't load clip");System.out.println(ex);}}@Overridepublic void on() {bpm = 90;//notifyBPMObservers();thread = new Thread((Runnable) this);stop = false;thread.start();}@Overridepublic void off() {stopBeat();stop = true;}@Overridepublic void run() {while (!stop) {playBeat();notifyBeatObservers();try {Thread.sleep(60000/getBPM());} catch (Exception e) {}}}//设置BPM@Overridepublic void setBPM(int bpm) {this.bpm = bpm;//1.设置BPM实例变量notifyBPMObservers();//2,通知所有BPM观察者,BPM改变了}@Overridepublic int getBPM() {return bpm;}@Overridepublic void registerObserver(BeatObserver o) {beatObservers.add(o);}public void notifyBeatObservers() {for(int i = 0; i < beatObservers.size(); i++) {BeatObserver observer = (BeatObserver)beatObservers.get(i);observer.updateBeat();}}@Overridepublic void registerObserver(BPMObserver o) {bpmObservers.add(o);}public void notifyBPMObservers() {for(int i = 0; i < bpmObservers.size(); i++) {BPMObserver observer = (BPMObserver)bpmObservers.get(i);observer.updateBPM();}}@Overridepublic void removeObserver(BeatObserver o) {int i = beatObservers.indexOf(o);if (i >= 0) {beatObservers.remove(i);}}@Overridepublic void removeObserver(BPMObserver o) {int i = bpmObservers.indexOf(o);if (i >= 0) {bpmObservers.remove(i);}}public void playBeat() {clip.setFramePosition(0);clip.start();}public void stopBeat() {clip.setFramePosition(0);clip.stop();}}

2.6 视图

现在有趣的事情开始了,我们要把视图挂接上,使BeatModel可视化!

关于视图,我们要注意的第一件事就是实现时要用两个分离的窗口:一个窗口包含当前的BPM和脉动柱,另一个则包含界面控制。为何要这样设计?

因为我们要强调包含模型视图的界面和包含其他用户控制的界面两者之间的差异。让我们详细看看视图的这两个部分:

这两个视图都是在一个类中,但是为了区分显示,先展示创建模型状态的视图

public class DJView implements ActionListener,  BeatObserver, BPMObserver {//视图持有模型和控制器的引用BeatModelInterface model;ControllerInterface controller;//用于显示的组件-2个窗口的组件全在这边JFrame viewFrame;JPanel viewPanel;BeatBar beatBar;JLabel bpmOutputLabel;//构造器得到控制器和模型的引用,将他们存储在实例变量中public DJView(ControllerInterface controller, BeatModelInterface model) {  this.controller = controller;this.model = model;//在被观察者--模型中,注册自己,分别注册 节拍观察者  和 BPM观察者model.registerObserver((BeatObserver)this);model.registerObserver((BPMObserver)this);}//创建所有的Swing组件public void createView() {viewPanel = new JPanel(new GridLayout(1, 2));viewFrame = new JFrame("View");viewFrame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);viewFrame.setSize(new Dimension(100, 80));bpmOutputLabel = new JLabel("offline", SwingConstants.CENTER);beatBar = new BeatBar();beatBar.setValue(0);JPanel bpmPanel = new JPanel(new GridLayout(2, 1));bpmPanel.add(beatBar);bpmPanel.add(bpmOutputLabel);viewPanel.add(bpmPanel);viewFrame.getContentPane().add(viewPanel, BorderLayout.CENTER);viewFrame.pack();viewFrame.setVisible(true);}//当模型发生状态改变时,updateBPM()方法会被调用。// 这时我们更新当前BPM的显示,可以通过直接请求模型而得到这个值@Overridepublic void updateBPM() {if (model != null) {int bpm = model.getBPM();if (bpm == 0) {if (bpmOutputLabel != null) {bpmOutputLabel.setText("offline");}} else {if (bpmOutputLabel != null) {bpmOutputLabel.setText("Current BPM: " + model.getBPM());}}}}//当模型开始一个新的节拍时,updateBeat()方法会被调用。// 这时,我们必须让脉动柱跳一下。//做法是,将脉动柱设为最大值(100),让它自行处理动画部分@Overridepublic void updateBeat() {if (beatBar != null) {beatBar.setValue(100);}}
}

再来看看,视图用户控制界面部分的代码。这个视图,通过告诉控制器做什么来让你控制模型。

package desginPattern.compoundPattern.exe573DjViewer;import java.awt.*;
import java.awt.event.*;
import javax.swing.*;public class DJView implements ActionListener,  BeatObserver, BPMObserver {//视图持有模型和控制器的引用BeatModelInterface model;ControllerInterface controller;//用于显示的组件-2个窗口的组件全在这边JFrame controlFrame;JPanel controlPanel;JLabel bpmLabel;JTextField bpmTextField;JButton setBPMButton;JButton increaseBPMButton;JButton decreaseBPMButton;JMenuBar menuBar;JMenu menu;JMenuItem startMenuItem;JMenuItem stopMenuItem;//构造器得到控制器和模型的引用,将他们存储在实例变量中public DJView(ControllerInterface controller, BeatModelInterface model) {    this.controller = controller;this.model = model;//在被观察者--模型中,注册自己,分别注册 节拍观察者  和 BPM观察者model.registerObserver((BeatObserver)this);model.registerObserver((BPMObserver)this);}//这份方法控制所有的空间,并将它们放在界面上。此方法也会处理菜单//当菜单中的Start或者Stop被选中时,控制器的相应方法就会被调用public void createControls() {// Create all Swing components hereJFrame.setDefaultLookAndFeelDecorated(true);controlFrame = new JFrame("Control");controlFrame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);controlFrame.setSize(new Dimension(100, 80));controlPanel = new JPanel(new GridLayout(1, 2));menuBar = new JMenuBar();menu = new JMenu("DJ Control");startMenuItem = new JMenuItem("Start");menu.add(startMenuItem);startMenuItem.addActionListener((event) -> controller.start());// was..../*startMenuItem.addActionListener(new ActionListener() {public void actionPerformed(ActionEvent event) {controller.start();}});*/stopMenuItem = new JMenuItem("Stop");menu.add(stopMenuItem); stopMenuItem.addActionListener((event) -> controller.stop());// was.../*stopMenuItem.addActionListener(new ActionListener() {public void actionPerformed(ActionEvent event) {controller.stop();}});*/JMenuItem exit = new JMenuItem("Quit");exit.addActionListener((event) -> System.exit(0));// was.../*exit.addActionListener(new ActionListener() {public void actionPerformed(ActionEvent event) {System.exit(0);}});*/menu.add(exit);menuBar.add(menu);controlFrame.setJMenuBar(menuBar);bpmTextField = new JTextField(2);bpmLabel = new JLabel("Enter BPM:", SwingConstants.RIGHT);setBPMButton = new JButton("Set");setBPMButton.setSize(new Dimension(10,40));increaseBPMButton = new JButton(">>");decreaseBPMButton = new JButton("<<");setBPMButton.addActionListener(this);increaseBPMButton.addActionListener(this);decreaseBPMButton.addActionListener(this);JPanel buttonPanel = new JPanel(new GridLayout(1, 2));buttonPanel.add(decreaseBPMButton);buttonPanel.add(increaseBPMButton);JPanel enterPanel = new JPanel(new GridLayout(1, 2));enterPanel.add(bpmLabel);enterPanel.add(bpmTextField);JPanel insideControlPanel = new JPanel(new GridLayout(3, 1));insideControlPanel.add(enterPanel);insideControlPanel.add(setBPMButton);insideControlPanel.add(buttonPanel);controlPanel.add(insideControlPanel);bpmLabel.setBorder(BorderFactory.createEmptyBorder(5,5,5,5));bpmOutputLabel.setBorder(BorderFactory.createEmptyBorder(5,5,5,5));controlFrame.getRootPane().setDefaultButton(setBPMButton);controlFrame.getContentPane().add(controlPanel, BorderLayout.CENTER);controlFrame.pack();controlFrame.setVisible(true);}//下面的四个方法,将菜单中的Start和Stop项变成enable或者disablepublic void enableStopMenuItem() {stopMenuItem.setEnabled(true);}public void disableStopMenuItem() {stopMenuItem.setEnabled(false);}public void enableStartMenuItem() {startMenuItem.setEnabled(true);}public void disableStartMenuItem() {startMenuItem.setEnabled(false);}//点击按钮时,调用此方法@Overridepublic void actionPerformed(ActionEvent event) {if (event.getSource() == setBPMButton) {int bpm = 90;String bpmText = bpmTextField.getText();if (bpmText == null || bpmText.contentEquals("")) {bpm = 90;} else {bpm = Integer.parseInt(bpmTextField.getText());}controller.setBPM(bpm);//当Set按钮被点击,控制器就会把BPM设置为新的值} else if (event.getSource() == increaseBPMButton) {controller.increaseBPM();} else if (event.getSource() == decreaseBPMButton) {controller.decreaseBPM();}}}

2.7 DJ控制器

控制器是策略,我们要把控制器插入视图中,让视图变得聪明

因为要实现策略模式,所以从可以插进 DJ View的任何策略的接口开始。我们称此接口为ControllerInterface.

package desginPattern.compoundPattern.exe573DjViewer;public interface ControllerInterface {//视图所能调用的控制器方法全在这void start();void stop();void increaseBPM();void decreaseBPM();void setBPM(int bpm);
}

控制器的实现

package desginPattern.compoundPattern.exe573DjViewer;public class BeatController implements ControllerInterface {//控制器是MVC夹心饼中的奶油,所以它必须同时和模型以及视图接触,来当两者的粘黏剂BeatModelInterface model;DJView view;public BeatController(BeatModelInterface model) {this.model = model;view = new DJView(this, model);//当构造器当成参数传入创建视图的构造器中view.createView();view.createControls();view.disableStopMenuItem();view.enableStartMenuItem();model.initialize();}//当用户从用户界面中选择“Start” 控制器调用模型的on()//然后改变用户界面(将start菜单项disable,将Stop菜单enable)@Overridepublic void start() {model.on();view.disableStartMenuItem();view.enableStopMenuItem();}@Overridepublic void stop() {model.off();view.disableStopMenuItem();view.enableStartMenuItem();}//如果被点击的按钮增加,控制器就从模型取得当前的BPM,加1,然后设置一个新的BPM@Overridepublic void increaseBPM() {int bpm = model.getBPM();model.setBPM(bpm + 1);}@Overridepublic void decreaseBPM() {int bpm = model.getBPM();model.setBPM(bpm - 1);}//如果用户界面被用来设定任意的BPM值,控制器指示模型设置它的BPM@Overridepublic void setBPM(int bpm) {model.setBPM(bpm);}
}

2.8 全部结合在一起

一切都OK了,已经有了模型、视图和控制器。现在就将他们整合成MVC。然后能够听到指定的音频文件,并且根据设置的BPM不同能够明显感受到音频内容的播放差异。

public class DJTestDrive {public static void main (String[] args) {//1.先建立一个模型BeatModelInterface model = new BeatModel();//2.然后创建一个控制器,并将模型传给他//记住,控制器创建视图,因此不需要将 控制器介绍给视图认识ControllerInterface controller = new BeatController(model);}
}

点击运行之后,就可以根据弹出的两个窗口来开始播放,并设置BPM

2.9 探索策略--显示节拍速率和脉动

在上面的测试输出中,没有对脉动和节拍速率的显示做出差别配置。

想一下DJView做了什么:它显示了节拍速率和脉动。这听起来会不会让你联想到其他事情呢?心跳?碰巧我们有一个心脏监视类,类图是这样的:

如果能在HeartModel中复用我们当前的视图,这会省下不少功夫。但我们需要一个控制器和这个模型一同运作。还有,HeartModel的接口并不符合视图的期望,因为它的方法是getHeartRate(),而不是getBPM()。你如何设计一些类,让视图和HeartModel能够搭配使用呢?

使用适配器模式,实现适配模型

一开始,我们希望将HeartModel适配成BeatModel。如果不这么做,视图就无法和此模型合作,因为视图只知道getBPM(),不知道其实getHeartRate()就等于getBPM()。要怎么做?我们打算使用适配器模式,当然了!适配器其实是使用MVC时经常附带用到的技巧:使用适配器将模型适配成符合现有视图和控制器的需要的模型。
下面将HeartModel适配成BeatModel的代码:

package desginPattern.compoundPattern.exe573DjViewer;//要引用 需要实现的目标接口
public class HeartAdapter implements BeatModelInterface {HeartModelInterface heart;public HeartAdapter(HeartModelInterface heart) {this.heart = heart;}//对于不想让这些方法对HeartRate做的事情,直接使之“无操作”即可@Overridepublic void initialize() {}@Overridepublic void on() {}@Overridepublic void off() {}//当getBPM()被调用时,将它转换到HeartModel的getHeartRate()@Overridepublic int getBPM() {return heart.getHeartRate();}@Overridepublic void setBPM(int bpm) {}//这是观察者方法,直接委托给所包装的HeartModel即可@Overridepublic void registerObserver(BeatObserver o) {heart.registerObserver(o);}@Overridepublic void removeObserver(BeatObserver o) {heart.removeObserver(o);}@Overridepublic void registerObserver(BPMObserver o) {heart.registerObserver(o);}@Overridepublic void removeObserver(BPMObserver o) {heart.removeObserver(o);}
}

2.10 编写HeartController

写完了HeartAdapter,然后来创建控制器,并让视图和HeartModel整合起来,这就是复用。

package desginPattern.compoundPattern.exe573DjViewer;//HeartController也需要实现ControllerInterface
public class HeartController implements ControllerInterface {HeartModelInterface model;DJView view;//控制器创建了视图,并让所有东西你那合在一起public HeartController(HeartModelInterface model) {this.model = model;//此处传入的是HeartModel,并且使用Adapter转换为 BeatModel//HeartModel不能直接交给视图,必须先经过适配器包装过才可以view = new DJView(this, new HeartAdapter(model));//最后,HeartController将菜单项disable,因为这些菜单项都不是需要的view.createView();view.createControls();view.disableStopMenuItem();view.disableStartMenuItem();}//下面的这些方法都是没有实际作用的@Overridepublic void start() {}@Overridepublic void stop() {}@Overridepublic void increaseBPM() {}@Overridepublic void decreaseBPM() {}@Overridepublic void setBPM(int bpm) {}
}

2.11 编写测试代码

来编写测试代码:

package desginPattern.compoundPattern.exe573DjViewer;public class HeartTestDrive {public static void main (String[] args) {HeartModel heartModel = new HeartModel();//需要做的就是 创建一个控制器,传入一个HeartModelControllerInterface model = new HeartController(heartModel);}
}

运行测试程序:

3.MVC与Web

3.1 Web与MVC的适配“Model 2”

Web开发人员也都在适配MVC,使它符合浏览器/服务器模型。我们称这样的适配为“Model 2”,并使用Servlet和JSP枝术的结合,来达到MVC的分离效果,就像传统的GUI。

来看看"Model2"的工作流程

1.你发出一个会被Servlet收到的HTTP请求。
你利用网页浏览器,发出HTTP请求。这通常牵涉到送出表单数据,例如用户名和密码。Servlet收到这样的数据,并解析数据。

2.servlet扮演控制器。
Servlet扮演控制器的角色,处理你的请求,通常会向模型(一般是数据库)发出请求。处理结果往往以JavaBean的形式打包。

3.控制器将控制权交给视图。
视图就是JSP,而JSP唯一的工作就是产生页面,表现模型的视图(④模型通过JavaBean中取得)以及进一步动作所需要的所有控件。

4.视图通过HTTP将页面返回浏览器。
页面返回浏览器,作为视图显示出来。用户提出进一步的请求,以同样的方式处理。

这使得“制作责任”有了进一步的分割,使做网页的做网页,该编程的就编程。

3.2 制作网页版的DJ程序

将BearModel制作成Web版。

计划

1.修正模型。
其实,不需要修改。现在的模型完全没问题!

2.创建Servlet控制器。
我们需要一个简单的Servlet,可以接收HTTP请求,并对模型执行一些操作。它所需要做的是停止、开始和改变BPM。

3.创建HTML视图。
我们用JSP创建一个简单的视图。它会从控制器中收到一个JavaBean,从这个Bean就可以得知它所有需要显示的东西。然后JSP将产生一个HTML界面。

步骤一:模型

请记得在MVC中,模型对视图和控制器一无所知。换句话说,它们之间是完全解耦的。模型只知道,有一些观察者它需要通知。这正是观察者模式美妙的地方。模型还提供一些接口,供视图和控制器获得并设置状态。
我们现在需要修改它以用于Web环境,但是由于它不依赖任何外部类,所以实在是没有什么需要修改的地方。我们可以直接使用BeatModcl,真高效。直接进入步骤二吧!

步骤二:控制器Servlet

别忘了,Servlet将扮演控制器。它将收到来自Web浏览器的请求,并将其转换成作用于模型的动作。

然后,由于Web工作的方式,我们需要将一个视图返回给浏览器。所以我们需要把控制权交给视图(也就是JSP)。我们把这部分留到步骤三。

这部分就不再向下看并实现了。。。。

<!--因为是从后端转入Android,因此不再对代码进行调试与实现,自行看电子书PDF第590开始-->

3.3 设计模式与Model2

利用Model 2实现Web版本的DI控制之后,你可能想知道模式去哪里了。我们的视图是JSP产生的HTML,而这个视图不再是模型的监听者。我们的控制器是Servlet,它会接收HTTP请求,但是策略模式好像不见了。至于组合模式,好像也没个影子。我们有HTML的视图显示在网页浏览器上,这还算是组合模式吗?

12 模式的模式:复合模式相关推荐

  1. 【Qt】2D绘图之复合模式

    00. 目录 文章目录 00. 目录 01. 概述 02. 开发环境 03. 程序示例 04. 附录 01. 概述 QPainter提供了复合模式(Composition Modes)来定义如何完成数 ...

  2. java 无法执行export 命令_模块中的export、import以及复合模式的使用方法

    export:规定模块对外接口 ①默认导出:export default Test(导入时可指定模块任意名称,无需知晓内部真实名称) ②单独导出:export const name = "B ...

  3. 结构设计模式:复合模式

    以前,我们检查了适配器模式的用例和桥接模式. 我们将在本文中研究的模式是复合模式. 通过遵循复合模式,我们可以将对象"组成"为代表整个部分层次结构的树结构,因此客户可以统一对待各个 ...

  4. 设计模式复合使用_结构设计模式:复合模式

    设计模式复合使用 以前,我们检查了适配器模式的用例和桥接模式. 我们将在本文中研究的模式是复合模式. 通过遵循复合模式,我们可以将对象"组成"为代表整个部分层次结构的树结构,因此客 ...

  5. python设计模式【8】-模型·视图·控制器-复合模式

    UML类图简介 设计模式的分类 面向对象的设计原则 python设计模式[1]-单例模式 python设计模式[2]-工厂模式 python设计模式[3]-门面模式 python设计模式[4]-代理模 ...

  6. Android 12.0 Settings 去掉打开开发者模式和USB调试模式的广播

    1.概述 在12.0的系统产品rom定制化开发中,在系统Settings的开发者模式中,打开开发者模式和usb调试模式都会发出开发者模式改变广播和usb调试模式改变广播, 项目开发功能需要要求去掉这两 ...

  7. swift 听筒模式_Swift中的“复合”模式

    swift 听筒模式 定义 (Definition) 'Composite' pattern is a structural design pattern that is useful for com ...

  8. 设计模式之组合模式(复合模式)

    组合模式的特点 首先在实际开发中,会遇到那种具有层次性的那种需求. 就比如我需要开发一个公司系统,里面有总公司邓哥中国集团,也有财务室(财务室每个分公司也必须具备),河南分公司A,河南分公司下面又有郑 ...

  9. Builder Design模式和Factory Design模式之间有什么区别?

    Builder设计模式和Factory设计模式有什么区别? 哪一个更有利,为什么? 如果我想测试和比较/对比这些模式,如何将我的发现表示为图形? #1楼 建筑商和抽象工厂 在某种程度上,Builder ...

最新文章

  1. backboneJs 导图
  2. SAP Spartacus 404 Not found页面的显示机制 - canActivateNotFoundPage
  3. 工业以太网交换机几点常见故障解析
  4. 正则查找倒数第二个符合条件的字符串_EXCEL正则表达式的基础语法
  5. leetcode738. 单调递增的数字(贪心)
  6. 联级阴影贴图CSM(Cascaded shadow map)原理与实现
  7. IDEA中import自己的python包方法
  8. 分布式系统关注点(20)——阻塞与非阻塞有什么区别?
  9. 封装的ini文件类。保存为unicode的。解决delphi xe的TiniFile保存后不为unicode的问题...
  10. java下载视频_怎么用Java从网上下载一个视频下来
  11. win10原版操作系统安装过程【超详细】
  12. 强烈推荐:创业起步 八种赢利模式
  13. 微信接入之获取用户头像
  14. wim linux u盘启动,在U盘启动中安装CDLinux
  15. 数据结构算法学习 之 红黑树
  16. LQ0266 巧排扑克牌【模拟】
  17. pyqt5中利用搜索框和按钮,搜索表中内容
  18. python第三方库matplotlib绘制简单折线图
  19. 什么是MECE 分析法?
  20. VBA 实例:Word 转 TXT

热门文章

  1. (用函数解决)Python报数游戏,输入有n个人按顺序编号,从第一个人报数,输入报数k,从1到k,报到k的退出游戏,从下一个人继续游戏,并求最后剩下的人编号是几号。
  2. idea debug图解
  3. SNPE教程一:基本概念
  4. 安装oracle12f 闪退,安装oracle ,调用图形界面java卡死,
  5. Louvain 算法
  6. 编程一年,我学会了什么?
  7. c语言编程 人造卫星的高度,C语言实验教学教案2008
  8. 中标麒麟系统u盘安装_中标麒麟学习笔记1:安装7.0桌面版操作系统
  9. Eclipse插件安装最简单方式--以Eclipse中文语言包汉化为例(附汉化包)
  10. python网络课程答案_Python语言应用知到网课答案