Java 设计模式(五)
二十二、责任链模式
本章涵盖了责任链模式。
GoF 定义
通过给多个对象一个处理请求的机会,避免将请求的发送方耦合到接收方。链接接收对象,并沿着链传递请求,直到有对象处理它。
概念
在这个模式中,您形成了一个对象链,链中的每个对象都处理一种特定的请求。如果一个对象不能完全处理请求,它会将请求传递给链中的下一个对象。同样的过程可以继续,直到到达链的末端。这种请求处理机制为您提供了在链的末端添加新处理对象(处理程序)的灵活性。图 22-1 描绘了这样一个有 N 个处理器的链。
图 22-1
责任链模式的概念
真实世界的例子
-
每个组织都雇佣了客户服务主管,他们直接接收来自客户的反馈或投诉。如果员工不能正确回答客户的问题,他们会将这些问题/上报转发给组织中的相应部门。这些部门不会试图同时解决一个问题。在调查的第一阶段,看似负责的部门会对案件进行分析,如果他们认为问题应该移交给另一个部门,他们就会这样做。
-
当病人去医院时,也会发生类似的情况。一个部门的医生可以将病人转到另一个部门做进一步诊断。
计算机世界的例子
考虑一个可以发送电子邮件和传真的软件应用程序(例如打印机)。因此,客户可以报告传真问题或电子邮件问题。让我们假设这些问题是由处理程序处理的。因此,您引入了两种不同类型的错误处理程序:EmailErrorHandler和FaxErrorHandler。你可以假设EmailErrorHandler只处理邮件错误;它不能修复传真错误。以类似的方式,FaxErrorHandler处理传真错误,而不关心电子邮件错误。
因此,您可能会形成这样的一个链:每当应用程序发现一个错误时,它都会发出一个标签并转发该错误,希望其中一个处理程序会处理它。让我们假设请求首先到达FaxErrorhandler。如果这个处理程序同意错误是传真问题,它就处理它;否则,处理程序将问题转发给EmailErrorHandler。
注意,链以EmailErrorHandler结束。但是如果您需要处理不同类型的问题——例如,身份验证问题(可能由于安全漏洞而发生),您可以创建一个名为AuthenticationErrorHandler的处理程序,并将其放在EmailErrorHandler之后。现在,如果一个EmailErrorHandler不能完全解决问题,它会将问题转发给AuthenticationErrorHandler,这个链就此结束。
注意
您可以在您的应用程序中自由地以任何顺序放置这些处理程序。
因此,底线是处理链可能会在以下任何场景中结束。
-
这些处理程序中的任何一个都可以完全处理请求,然后控制权就回来了。
-
一个处理程序不能完全处理请求,所以它将请求传递给下一个处理程序。这样,你就到达了链条的末端。因此,请求在那里处理。但是如果请求不能在那里得到处理,就不能进一步传递。(对于这种情况,您可能需要特别小心。)
当您在 Java 应用程序中使用多个 catch 块实现异常处理机制时,您会注意到类似的机制。如果一个异常发生在一个try块中,第一个 catch 块试图处理它。如果它不能处理那种类型的异常,下一个catch块会尝试处理它,并且遵循相同的机制,直到异常被处理程序(catch块)正确处理。如果应用程序中的最后一个 catch 块无法处理它,就会在这个链之外引发一个异常。
注意
在 java.util.logging.Logger 中,您可以看到支持类似概念的 log()方法的不同重载版本。
另一个内置支持可以在 javax.Servlet.Filter 中的 doFilter (ServletRequest 请求、ServletResponse 响应、FilterChain 链)接口方法中看到。
说明
让我们考虑一下在计算机世界的例子中讨论的场景。让我们进一步假设,在下面的例子中,您可以处理来自电子邮件或传真支柱的普通和高优先级问题。
类图
图 22-2 为类图。
图 22-2
类图
包资源管理器视图
图 22-3 显示了程序的高层结构。
图 22-3
包资源管理器视图
履行
下面是实现。
package jdp2e.chainofresponsibility.demo;
enum MessagePriority
{
NORMAL,
HIGH
}
class Message
{
public String text;
public MessagePriority priority;
public Message(String msg, MessagePriority p)
{
text = msg;
this.priority = p;
}
}
interface Receiver
{
boolean handleMessage(Message message);
void nextErrorHandler(Receiver nextReceiver);
}
class IssueRaiser
{
public Receiver setFirstReceiver;
public void setFirstErrorHandler(Receiver firstErrorHandler)
{
this.setFirstReceiver = firstErrorHandler;
}
public void raiseMessage(Message message)
{
if (setFirstReceiver != null)
setFirstReceiver.handleMessage(message);
}
}
// FaxErrorHandler class
class FaxErrorHandler implements Receiver
{
private Receiver nextReceiver;
@Override
public void nextErrorHandler(Receiver nextReceiver)
{
this.nextReceiver = nextReceiver;
}
@Override
public boolean handleMessage(Message message)
{
if (message.text.contains("Fax"))
{
System.out.println(" FaxErrorHandler processed " +message.priority +" priority issue :"+ message.text);
return true;
}
else
{
if (nextReceiver != null)
nextReceiver.handleMessage(message);
}
return false;
}
}
// EmailErrorHandler class
class EmailErrorHandler implements Receiver
{
private Receiver nextReceiver;
@Override
public void nextErrorHandler(Receiver nextReceiver)
{
this.nextReceiver = nextReceiver;
}
@Override
public boolean handleMessage(Message message)
{
if (message.text.contains("Email"))
{
System.out.println(" EmailErrorHandler processed "+message.priority+ " priority issue: "+message.text);
return true;
}
else
{
if (nextReceiver != null)
nextReceiver.handleMessage(message);
}
return false;
}
}
//Client code
public class ChainofResponsibilityPattern {
public static void main(String[] args) {
System.out.println("\n ***Chain of Responsibility Pattern Demo***\n");
/* Forming the chain as IssueRaiser->FaxErrorhandler->EmailErrorHandler
*/
Receiver faxHandler, emailHandler;
//Objects of the chains
IssueRaiser issueRaiser = new IssueRaiser();
faxHandler = new FaxErrorHandler();
emailHandler = new EmailErrorHandler();
//Making the chain
//Starting point:IssueRaiser will raise issues and set the first
//handler
issueRaiser.setFirstErrorHandler(faxHandler);
//FaxErrorHandler will pass the error to EmailHandler if needed.
faxHandler.nextErrorHandler(emailHandler);
//EmailErrorHandler will be placed at the last position in the chain
emailHandler.nextErrorHandler(null);
Message m1 = new Message("Fax is going slow.", MessagePriority.NORMAL);
Message m2 = new Message("Emails are not reaching.", MessagePriority.HIGH);
Message m3 = new Message("In Email, CC field is disabled always.", MessagePriority.NORMAL);
Message m4 = new Message("Fax is not reaching destinations.", MessagePriority.HIGH);
issueRaiser.raiseMessage(m1);
issueRaiser.raiseMessage(m2);
issueRaiser.raiseMessage(m3);
issueRaiser.raiseMessage(m4);
}
}
输出
这是输出。
***Chain of Responsibility Pattern Demo***
FaxErrorHandler processed NORMAL priority issue :Fax is going slow.
EmailErrorHandler processed HIGH priority issue: Emails are not reaching.
EmailErrorHandler processed NORMAL priority issue: In Email, CC field is disabled always.
FaxErrorHandler processed HIGH priority issue :Fax is not reaching destinations.
问答环节
-
在示例中,消息优先级的目的是什么?
接得好。实际上,您可以忽略它们,因为为了处理程序的简单,您只需搜索单词 email 或 fax 。添加这些优先级是为了美化代码。但是,您可以创建一个不同类型的链,根据优先级来处理消息,而不是对电子邮件和传真使用单独的处理程序。在这种情况下,可以更有效地利用这些优先级。
-
使用责任链设计模式的优势是什么?
-
您可以有多个对象来处理一个请求。(请注意,如果一个处理程序不能处理整个请求,它可能会将责任转发给链中的下一个处理程序)。
-
链的节点可以动态添加或删除。另外,你可以打乱顺序。例如,如果您注意到大多数问题都与电子邮件处理有关,那么您可以将 EmailErrorHandler 作为链中的第一个处理程序,以节省应用程序的平均处理时间。
-
处理程序不需要知道链中的下一个处理程序将如何处理请求。它只关注自己的处理机制。
-
在这种模式中,您提倡松耦合,因为它将(请求的)发送者与接收者分离。
-
-
使用责任链设计模式的相关挑战是什么?
-
不能保证请求会被处理(全部或部分),因为您可能会到达链的末端;但是有可能您还没有找到任何显式的接收者来处理请求。
-
对于这种设计,调试可能会变得棘手。
-
-
你如何处理已经到达链末端,但是请求根本没有被处理的情况?
一个简单的解决办法就是使用
try/catch(或者try/finally或者try/catch/finally)积木。您可以将处理程序放在这些结构中。您可能会注意到一个try模块也可以与多个catch模块相关联。最后,如果没有人能够处理这个请求,您可以用适当的消息引发一个异常,并在您想要的
catch块中捕获这个异常以引起您的注意(或者用一些不同的方式处理它)。GoF 在类似的背景下谈到了 Smalltalk 的自动转发机制
doesNotUnderstand。如果消息找不到合适的处理程序,它会在doesNotUnderstand实现中被捕获,该实现可以被覆盖以在对象的后继中转发消息,将其记录在文件中,并将其存储在队列中以供以后处理,或者您可以简单地执行任何其他预期的操作。但是您必须注意,在默认情况下,该方法会引发一个需要以适当方式处理的异常。 -
简而言之,如果一个处理程序不能完全处理请求,它将把它传递给下一个处理程序。这是正确的吗?
是的。
-
观察者模式和责任链模式之间似乎有相似之处。这是正确的吗?
在观察者模式中,所有注册用户并行获得通知;但是在责任链模式中,链中的对象以连续的方式被一个接一个地通知。这个过程一直持续到一个对象完全处理完通知为止(或者到达链的末尾)。我在第十四章的“问答环节”中用图表展示了这些对比。
二十三、解释器模式
本章涵盖了解释器模式。
GoF 定义
给定一种语言,为它的语法定义一个表示,以及一个使用该表示来解释该语言中的句子的解释器。
概念
为了理解这个模式,你需要熟悉一些关键术语,比如句子、语法、语言等等。所以,如果你对自动机中的形式语言不熟悉,你可能需要访问它们。
通常,这种模式处理的是如何评估语言中的句子。所以,你首先需要定义一个语法来表示这种语言。然后解释器处理语法。如果语法简单,这种模式是最好的。
这个模式中的每个类可能代表该语言中的一个规则,并且它应该有一个解释表达式的方法。因此,为了处理更多的规则,您需要创建更多的类。这就是解释器模式不应该用于处理复杂语法的原因。
让我们考虑计算器程序中不同的算术表达式。虽然这些表达式是不同的,但它们都是使用一些基本规则构造的,这些规则是在语言的语法中定义的(这些算术表达式)。因此,如果您能够解释这些规则的一般组合,而不是将每个规则组合视为单独的情况,这是最好的。在这种情况下,可以使用解释器模式。
这种模式的典型结构通常用类似于图 23-1 的图表来描述。
图 23-1
典型解释器模式的结构
术语描述如下。
-
抽象表达式(abstract expression):通常是一个带有解释方法的接口。您需要向该方法传递一个上下文对象。
-
终端表达式:用于终端表达式。终结表达式不需要其他表达式来解释。这些基本上是数据结构中的叶节点(即,它们没有子节点)。
-
非终结符:用于非终结符表达式。也称为交替表达式、重复表达式或顺序表达式。这就像可以包含终结和非终结表达式的组合。当你在这上面调用
interpret()方法时,你基本上是在它的所有子节点上调用它。 -
Context :保存解释器需要的全局信息。
-
客户端:调用
interpret()方法。它可以根据语言的规则有选择地建立一个语法树。
注意
解释器用于处理具有简单规则或语法的语言。理想情况下,开发人员不想创建他们自己的语言。这就是他们很少使用这种模式的原因。
真实世界的例子
-
翻译外语的翻译者。
-
把音符看作语法,音乐家在其中扮演解释者的角色。
计算机世界的例子
-
Java 编译器将 Java 源代码解释成 JVM 可以理解的字节码。
-
在 C# 中,源代码被转换成由 CLR 解释的 MSIL 代码。在执行时,这个 MSIL(中间代码)被 JIT 编译器转换成本机代码(二进制可执行代码)。
注意
在 Java 中,您可能还会注意到充当解释器的 java.util.regex.Pattern 类。您可以通过调用 compile()方法创建这个类的一个实例,然后您可以使用 Matcher 实例根据语法来评估一个句子。
说明
这些是实现这种模式的一些重要步骤。
-
第一步。定义您想要为其构建解释器的语言的规则。
-
第二步。定义抽象类或接口来表示表达式。它应该包含一个解释表达式的方法。
-
迈步 2A。识别终结和非终结表达式。例如,在接下来的示例中,IndividualEmployee 类是一个终端表达式类。
-
步骤 2B。创建非终结符表达式类。他们每个人都在他们的孩子身上调用解释方法。例如,在接下来的示例中,OrExpression 和 AndExpression 类是非终结表达式类。
-
-
第三步。使用这些类构建抽象语法树。您可以在客户端代码中完成这项工作,也可以创建一个单独的类来完成任务。
-
第四步。一个客户现在用这个树来解释一个句子。
-
第五步。将上下文传递给解释器。它通常有需要解释的句子。解释器可以使用这个上下文执行额外的任务。
在接下来的程序中,我使用解释器模式作为规则验证器。我用不同员工的“经验年数”和当前等级来举例说明他们。请注意下面几行。
Employee emp1 = new IndividualEmployee(5,"G1");
Employee emp2 = new IndividualEmployee(10,"G2");
Employee emp3 = new IndividualEmployee(15,"G3");
Employee emp4 = new IndividualEmployee(20,"G4");
为简单起见,这里考虑了四个不同级别的四名员工——G1、G2、G3 和 G4。
还要注意上下文,如下所示。
//Minimum Criteria for promoton is:
//The year of experience is minimum 10 yrs. and
//Employee grade should be either G2 or G3
Context context=new Context(10,"G2","G3");
因此,您可以假设我想要根据上下文验证一些条件,这基本上是告诉您,要获得晋升,员工应该至少有 10 年的工作经验,并且他/她应该来自 G2 级或 G3 级。解释完这些表达式后,您会看到布尔值形式的输出。
需要注意的重要一点是,这种设计模式并没有指导你如何构建语法树或者如何解析句子。它给了你继续前进的自由。因此,为了给出一个简单的场景,我使用了一个 EmployeeBuilder 类和一个名为buildExpression()的方法来完成我的任务。
类图
图 23-2 为类图。
图 23-2
类图
包资源管理器视图
图 23-3 显示了程序的高层结构。
图 23-3
包资源管理器视图
履行
下面是实现。
package jdp2e.interpreter.demo;
import java.util.ArrayList;
import java.util.List;
interface Employee
{
public boolean interpret(Context context);
}
class IndividualEmployee implements Employee
{
private int yearOfExperience;
private String currentGrade;
public IndividualEmployee(int experience, String grade){
this.yearOfExperience=experience;
this.currentGrade=grade;
}
@Override
public boolean interpret(Context context)
{
if(this.yearOfExperience>=context.getYearofExperience() && context.getPermissibleGrades().contains(this.currentGrade))
{
return true;
}
return false;
}
}
class OrExpression implements Employee
{
private Employee emp1;
private Employee emp2;
public OrExpression(Employee emp1, Employee emp2)
{
this.emp1 = emp1;
this.emp2 = emp2;
}
@Override
public boolean interpret(Context context)
{
return emp1.interpret(context) || emp2.interpret(context);
}
}
class AndExpression implements Employee
{
private Employee emp1;
private Employee emp2;
public AndExpression(Employee emp1, Employee emp2)
{
this.emp1 = emp1;
this.emp2 = emp2;
}
@Override
public boolean interpret(Context context)
{
return emp1.interpret(context) && emp2.interpret(context);
}
}
class NotExpression implements Employee
{
private Employee emp;
public NotExpression(Employee expr)
{
this.emp = expr;
}
@Override
public boolean interpret(Context context)
{
return !emp.interpret(context);
}
}
class Context
{
private int yearofExperience;
private List<String> permissibleGrades;
public Context(int experience,String... allowedGrades)
{
this.yearofExperience=experience;
this.permissibleGrades=new ArrayList<>();
for( String grade:allowedGrades)
{
permissibleGrades.add(grade);
}
}
public int getYearofExperience()
{
return yearofExperience;
}
public List<String> getPermissibleGrades()
{
return permissibleGrades;
}
}
class EmployeeBuilder
{
public Employee buildExpression(Employee emp1, String operator, Employee emp2)
{
//Whatever the input,converting it to lowarcase
switch(operator.toLowerCase())
{
case "or":
return new OrExpression(emp1,emp2);
case "and":
return new AndExpression(emp1,emp2);
case "not":
return new NotExpression(emp1);
default:
System.out.println("Only AND,OR and NOT operators are allowed at present");
return null;
}
}
}
public class InterpreterPatternExample {
public static void main(String[] args) {
System.out.println("***Interpreter Pattern Demo***\n");
//Minimum Criteria for promoton is:
//The year of experience is minimum 10 yrs. and
//Employee grade should be either G2 or G3
Context context=new Context(10,"G2","G3");
//Different employees with grades
Employee emp1 = new IndividualEmployee(5,"G1");
Employee emp2 = new IndividualEmployee(10,"G2");
Employee emp3 = new IndividualEmployee(15,"G3");
Employee emp4 = new IndividualEmployee(20,"G4");
EmployeeBuilder builder=new EmployeeBuilder();
System.out.println("emp1 is eligible for promotion. " + emp1.interpret(context));
System.out.println("emp2 is eligible for promotion. " + emp2.interpret(context));
System.out.println("emp3 is eligible for promotion. " + emp3.interpret(context));
System.out.println("emp4 is eligible for promotion. " + emp4.interpret(context));
System.out.println("Is either emp1 or emp3 is eligible for promotion?" +builder.buildExpression(emp1,"Or",emp3).interpret(context));
System.out.println("Is both emp2 and emp4 are eligible for promotion? ?" + builder.buildExpression(emp2,"And",emp4).interpret(context));
System.out.println("The statement 'emp3 is NOT eligible for promotion' is true? " + builder.buildExpression(emp3, "Not",null).interpret(context));
//Invalid input expression
//System.out.println("Is either emp1 or emp3 is eligible for promotion?" +builder.buildExpression(emp1,"Wrong",emp3).interpret(context));
}
}
输出
这是输出。
***Interpreter Pattern Demo***
emp1 is eligible for promotion. false
emp2 is eligible for promotion. true
emp3 is eligible for promotion. true
emp4 is eligible for promotion. false
Is either emp1 or emp3 is eligible for promotion?true
Is both emp2 and emp4 are eligible for promotion? ?false
The statement 'emp3 is NOT eligible for promotion' is true? false
分析
您可以看到每个复合表达式都在调用其所有子表达式的interpret()方法。
修改后的插图
您已经看到了解释器模式的一个简单例子。从这个实现来看,您似乎已经处理了一些简单明了的表达式。因此,让我们在修改后的实现中处理一些复杂的规则或表达式。
修改的类图
在修改后的实现中,主要更改仅在 EmployeeBuilder 类中进行。所以,让我们快速浏览一下这个类的类图(见图 23-4 )。
图 23-4
修改了 EmployeeBuilder 类的类图
已修改的包资源管理器视图
在修改后的实现中,主要更改仅反映在 EmployeeBuilder 类中。因此,在本节中,我只扩展了这个类。图 23-5 显示了修改后的包浏览器视图。
图 23-5
已修改的包资源管理器视图
修改的实现
下面是修改后的实现。关键变化以粗体显示。
package jdp2e.interpreter.modified.demo;
import java.util.ArrayList;
import java.util.List;
interface Employee
{
public boolean interpret(Context context);
}
class IndividualEmployee implements Employee
{
private int yearOfExperience;
private String currentGrade;
public IndividualEmployee(int experience, String grade){
this.yearOfExperience=experience;
this.currentGrade=grade;
}
@Override
public boolean interpret(Context context)
{
if(this.yearOfExperience>=context.getYearofExperience() && context.getPermissibleGrades().contains(this.currentGrade))
{
return true;
}
return false;
}
}
class OrExpression implements Employee
{
private Employee emp1;
private Employee emp2;
public OrExpression(Employee emp1, Employee emp2)
{
this.emp1 = emp1;
this.emp2 = emp2;
}
@Override
public boolean interpret(Context context)
{
return emp1.interpret(context) || emp2.interpret(context);
}
}
class AndExpression implements Employee
{
private Employee emp1;
private Employee emp2;
public AndExpression(Employee emp1, Employee emp2)
{
this.emp1 = emp1;
this.emp2 = emp2;
}
@Override
public boolean interpret(Context context)
{
return emp1.interpret(context) && emp2.interpret(context);
}
}
class NotExpression implements Employee
{
private Employee emp;
public NotExpression(Employee expr)
{
this.emp = expr;
}
@Override
public boolean interpret(Context context)
{
return !emp.interpret(context);
}
}
class Context
{
private int yearofExperience;
private List<String> permissibleGrades;
public Context(int experience,String... allowedGrades)
{
this.yearofExperience=experience;
this.permissibleGrades=new ArrayList<>();
for( String grade:allowedGrades)
{
permissibleGrades.add(grade);
}
}
public int getYearofExperience()
{
return yearofExperience;
}
public List<String> getPermissibleGrades()
{
return permissibleGrades;
}
}
class EmployeeBuilder
{
// Building the tree
//Complex Rule-1: emp1 and (emp2 or (emp3 or emp4))
public Employee buildTree(Employee emp1, Employee emp2,Employee emp3,Employee emp4)
{
//emp3 or emp4
Employee firstPhase=new OrExpression(emp3,emp4);
//emp2 or (emp3 or emp4)
Employee secondPhase=new OrExpression(emp2,firstPhase);
//emp1 and (emp2 or (emp3 or emp4))
Employee finalPhase=new AndExpression(emp1,secondPhase);
return finalPhase;
}
//Complex Rule-2: emp1 or (emp2 and (not emp3 ))
public Employee buildTreeBasedOnRule2(Employee emp1, Employee emp2,Employee emp3)
{
//Not emp3
Employee firstPhase=new NotExpression(emp3);
//emp2 or (not emp3)
Employee secondPhase=new AndExpression(emp2,firstPhase);
//emp1 and (emp2 or (not emp3 ))
Employee finalPhase=new OrExpression(emp1,secondPhase);
return finalPhase;
}
}
public class ModifiedInterpreterPatternExample {
public static void main(String[] args) {
System.out.println("***Modified Interpreter Pattern Demo***\n");
//Minimum Criteria for promoton is:
//The year of experience is minimum 10 yrs. and
//Employee grade should be either G2 or G3
Context context=new Context(10,"G2","G3");
//Different Employees with grades
Employee emp1 = new IndividualEmployee(5,"G1");
Employee emp2 = new IndividualEmployee(10,"G2");
Employee emp3 = new IndividualEmployee(15,"G3");
Employee emp4 = new IndividualEmployee(20,"G4");
EmployeeBuilder builder=new EmployeeBuilder();
//Validating the 1st complex rule
System.out.println("Is emp1 and any of emp2,emp3, emp4 is eligible for promotion?" +builder.buildTree(emp1,emp2, emp3,emp4).interpret(context));
System.out.println("Is emp2 and any of emp1,emp3, emp4 is eligible for promotion?" +builder.buildTree(emp2,emp1, emp3,emp4).interpret(context));
System.out.println("Is emp3 and any of emp1,emp2, emp3 is eligible for promotion?" +builder.buildTree(emp3,emp1, emp2,emp4).interpret(context));
System.out.println("Is emp4 and any of emp1,emp2, emp3 is eligible for promotion?" +builder.buildTree(emp4,emp1, emp2,emp3).interpret(context));
System.out.println("");
//Validating the 2nd complex rule
System.out.println("Is emp1 or (emp2 but not emp3) is eligible for promotion?" +builder.buildTreeBasedOnRule2(emp1, emp2, emp3).interpret(context));
System.out.println("Is emp2 or (emp3 but not emp4) is eligible for promotion?" +builder.buildTreeBasedOnRule2(emp2, emp3, emp4).interpret(context));
}
}
修改输出
下面是修改后的输出。
***Modified Interpreter Pattern Demo***
Is emp1 and any of emp2,emp3, emp4 is eligible for promotion?false
Is emp2 and any of emp1,emp3, emp4 is eligible for promotion?true
Is emp3 and any of emp1,emp2, emp4 is eligible for promotion?true
Is emp4 and any of emp1,emp2, emp3 is eligible for promotion?false
Is emp1 or (emp2 but not emp3) is eligible for promotion?false
Is emp2 or (emp3 but not emp4) is eligible for promotion?true
分析
现在,您已经了解了如何使用解释器模式来处理遵循所示方法的复杂规则。
问答环节
-
什么时候应该使用这种模式?
在日常编程中,不是很需要。尽管在一些罕见的情况下,您可能需要使用自己的编程语言来定义特定的协议。在这种情况下,这种模式可能会变得很方便。但是在你继续之前,你必须问问你自己关于投资回报(ROI)的问题。
-
使用解释器设计模式有什么好处?
-
你很大程度上参与了如何为你的语言定义语法,以及如何表达和解释这些句子的过程。你也可以改变和扩展你的语法。
-
你有充分的自由去解释这些表达。
-
-
使用解释器设计模式的相关挑战是什么?
我相信工作量是最大的问题。维护复杂的语法也变得棘手,因为您可能需要创建(和维护)单独的类来处理不同的规则。
二十四、简单工厂模式
本章介绍简单工厂模式。
目的
在不向客户端公开实例化逻辑的情况下创建对象。
概念
在面向对象编程中,工厂是一种可以创建其他对象的特殊对象。可以通过多种方式调用工厂,但最常见的是,它使用一种可以返回具有不同原型的对象的方法。任何可以帮助创建这些新对象的子程序都被认为是一个工厂。使用工厂方法的最终目的是从应用程序的消费者那里抽象出对象创建机制(或过程)。
真实世界的例子
考虑一家制造不同型号汽车的汽车制造公司。他们必须有一个不同生产单位的工厂。其中一些单位可以生产所有型号通用的零件,而其他单位则专用于生产特定型号的零件。当他们制造最终产品时,他们将特定型号的零件与通用零件组装在一起。从客户的角度来看,汽车是由汽车厂制造的;客户不知道汽车是如何制造的。但是如果你进一步调查,你会发现基于汽车的模型,工厂的一个生产单元改变零件。例如,一个特定的汽车型号可以只支持手动变速箱,而另一个型号可以同时支持自动和手动变速箱。因此,基于汽车的模型,汽车工厂为汽车制造特殊的变速箱。
考虑一个更简单的例子。当孩子向他/她的父母要玩具时,孩子不知道父母将如何满足他/她的需求。在这种情况下,父母被认为是他们小孩的工厂。现在站在家长的角度思考。父母可以自己制作玩具或从商店购买玩具来逗孩子开心。
计算机世界的例子
简单工厂模式在软件应用程序中非常常见,但是在我们进一步讨论之前,您必须记住以下几点。
-
在 GoF 的著名著作中,简单工厂并不被视为标准设计模式,但是这种方法对于您编写的任何应用程序来说都是常见的,您需要将变化很大的代码与没有变化的代码部分分开。假设您在编写的任何应用程序中都尝试遵循这种方法。
-
简单工厂被认为是工厂方法模式(或抽象工厂模式)的最简单形式。因此,您可以假设任何遵循工厂方法模式或抽象工厂模式的应用程序也支持简单工厂模式的设计目标的概念。
注意
java.text.NumberFormat 类的静态 getInstance()方法就是这种类型的一个例子。
让我们按照我在一个常见用例中讨论的这个模式的实现来看。
说明
以下是以下实现的重要特征。
-
在这个例子中,有两种动物:狗和老虎。对象创建过程取决于用户的输入。
-
我假设他们每个人都会说话,他们更喜欢做一些动作。
-
SimpleFactory 是工厂类,simpleFactory(注意“s”不是大写的)是该类的对象。在客户端代码(SimpleFactoryPatternExample 类)中,您会看到下面一行。
preferredType = simpleFactory.createAnimal();
这意味着要获得 preferredType 对象,需要调用 simpleFactory 对象的createAnimal()方法。因此,使用这种方法,您不需要在客户端代码中直接使用“new”操作符来获取对象。
- 我将变化的代码与最不可能变化的代码分开。这种方法有助于消除系统中的紧密耦合。(如何?遵循“问答环节”部分。)
注意
在某些应用程序中,您可能会注意到,在建议使用参数化构造函数的地方,这种模式有一点变化。因此,在那些应用程序中,要获得 preferredType 对象,您可能需要使用类似于这行的代码:preferred type = simple factory . create animal(" Tiger ")。
类图
图 24-1 显示了简单工厂模式的类图。
图 24-1
类图
包资源管理器视图
图 24-2 显示了程序的高层结构。
图 24-2
包资源管理器视图
履行
下面是实现。
package jdp2e.simplefactory.demo;
import java.util.Scanner;//Available Java5 onwards
interface Animal
{
void speak();
void preferredAction();
}
class Dog implements Animal
{
public void speak()
{
System.out.println("Dog says: Bow-Wow.");
}
public void preferredAction()
{
System.out.println ("Dogs prefer barking...");
}
}
class Tiger implements Animal
{
public void speak()
{
System.out.println("Tiger says: Halum.");
}
public void preferredAction()
{
System.out.println("Tigers prefer hunting...");
}
}
class SimpleFactory
{
public Animal createAnimal()
{
Animal intendedAnimal=null;
System.out.println("Enter your choice( 0 for Dog, 1 for Tiger)");
/* To suppress the warning message:Resource leak:'input' is never closed. So,the following line is optional in this case*/
@SuppressWarnings("resource")
Scanner input=new Scanner(System.in);
int choice=Integer.parseInt(input.nextLine());
System.out.println("You have entered :"+ choice);
switch (choice)
{
case 0:
intendedAnimal = new Dog();
break;
case 1:
intendedAnimal = new Tiger();
break;
default:
System.out.println("You must enter either 0 or 1");
//We'll throw a runtime exception for any other choices.
throw new IllegalArgumentException(" Your choice tries to create an unknown Animal");
}
return intendedAnimal;
}
}
//A client is interested to get an animal who can speak and perform an
//action.
class SimpleFactoryPatternExample
{
public static void main(String[] args) {
System.out.println("*** Simple Factory Pattern Demo***\n");
Animal preferredType=null;
SimpleFactory simpleFactory = new SimpleFactory();
// The code that will vary based on users preference.
preferredType = simpleFactory.createAnimal();
//The codes that do not change frequently.
//These animals can speak and prefer to do some specific actions.
preferredType.speak();
preferredType.preferredAction();
}
}
输出
这是输出。
案例 1。用户输入:0
*** Simple Factory Pattern Demo***
Enter your choice( 0 for Dog, 1 for Tiger)
0
You have entered :0
Dog says: Bow-Wow.
Dogs prefer barking...
案例 2。用户输入:1
*** Simple Factory Pattern Demo***
Enter your choice( 0 for Dog, 1 for Tiger)
1
You have entered :1
Tiger says: Halum.
Tigers prefer hunting...
案例 3。不需要的用户输入:2
*** Simple Factory Pattern Demo***
Enter your choice( 0 for Dog, 1 for Tiger)
2
You have entered :2
You must enter either 0 or 1Exception in thread "main"
java.lang.IllegalArgumentException: Your choice tries to create an unknown Animal
at jdp2e.simplefactory.demo.SimpleFactory.createAnimal(SimpleFactoryPatternExample.java:54)
at jdp2e.simplefactory.demo.SimpleFactoryPatternExample.main(SimpleFactoryPatternExample.java:68)
问答环节
-
在这个例子中,客户通过简单工厂委托对象的创建。但是相反,他们可以用“new”操作符直接创建对象。这是正确的吗?
No. These are the key reasons behind the preceding design.
-
一个重要的面向对象设计原则是将代码中最有可能发生变化的部分与其余部分分开。
-
在这种情况下,只有“对象创建部分”不同。我假设这些动物必须说话和执行动作,并且我不需要改变客户端内部的那部分代码。因此,在将来,如果您需要修改创建过程,您只需要更改 SimpleFactory 类的
createAnimal()方法。这些修改不会影响客户端代码。 -
“你是如何创造物体的?”隐藏在客户端代码中。这种抽象提高了安全性。
-
这种方法可以帮助您避免在客户端代码中使用大量的
if/else块(或 switch 语句),因为它们会让您的代码看起来很笨拙。
-
-
与此模式相关的挑战是什么?
-
随着时间的推移,决定实例化哪个对象变得复杂。在这些情况下,您应该更喜欢工厂方法模式。
-
如果你想添加一个新的动物或者删除一个已有的动物,你需要修改工厂类的
createAnimal()方法。这种方法显然违反了坚实原则的开闭原则(基本上是说你的代码应该对扩展开放,但对修改关闭)。
-
注意
罗伯特·c·马丁提倡坚实的原则。你可以在 https://en.wikipedia.org/wiki/SOLID 了解他们。
-
I learned that programming with an abstract class or interface is always a better practice. So, to make a better implementation, you could write something like this:
abstract class ISimpleFactory { public abstract IAnimal createAnimal() throws IOException; } class SimpleFactory extends ISimpleFactory {//rest of the code }这是正确的吗?
是的。用抽象类或接口编程总是更好的做法。这种方法可以防止您将来进行更改,因为任何新添加的类都可以简单地实现接口,并通过多态在体系结构中安顿下来。但是如果你仅仅依赖于具体的类,当你想在架构中集成一个新的类时,你需要改变你的代码,在这种情况下,你违反了规则,即你的代码应该被关闭以进行修改。
所以,你的理解是正确的。您可以使用这样的结构使它成为一个更好的程序。但最终,你会学到工厂方法模式(见第四章第四章),在这里你需要将实例化过程推迟到子类。因此,在这种情况下,建议你用抽象类或接口来编写程序。
-
能否让工厂类(SimpleFactory)成为静态的?
不可以。在 Java 中,不允许用顶级类来标记单词 static 。换句话说,按照设计,编译器总是抱怨 Java 中的顶级静态类。
二十五、空对象模式
维基百科上说,“在面向对象的计算机编程中,空对象是没有引用值或定义了中性(空)行为的对象。空对象设计模式描述了此类对象的用途及其行为(或缺乏行为)。它最初发表在程序设计的模式语言系列丛书中。Hillside Group 发起了程序模式语言(PLoP)年会。
该模式可以实现“什么都不做”关系,或者当应用程序遇到空对象而不是真实对象时,它可以提供默认行为。简而言之,核心目标是通过if块避免“空对象检查”或“空协作检查”,从而制定一个更好的解决方案。使用这种模式,您试图通过提供一个默认的什么都不做的行为来封装对象的缺失。
概念
这种模式的显著特点是,当您在空对象上调用操作时,您不需要做任何事情(或者不存储任何东西)。考虑下面的程序和相应的输出。让我们试着理解与下面的程序段相关的问题,分析可能的解决方案,在本章的结尾,你会看到一个使用这种设计模式的更好的实现。
在下面的实现中,让我们假设您有两种类型的交通工具:公共汽车和火车。客户可以通过不同的输入选择公共汽车或火车对象,如“a”或“b”。让我们进一步假设应用程序认为这两个只是有效的输入。
错误的程序
这是一个错误的程序。
package jdp2e.nullobject.context.demo;
import java.util.Scanner;
interface Vehicle
{
void travel();
}
class Bus implements Vehicle
{
public static int busCount = 0;
public Bus()
{
busCount++;
}
@Override
public void travel()
{
System.out.println("Let us travel with a bus");
}
}
class Train implements Vehicle
{
public static int trainCount = 0;
public Train()
{
trainCount++;
}
@Override
public void travel()
{
System.out.println("Let us travel with a train");
}
}
public class NeedForNullObjectPattern {
public static void main(String[] args) {
System.out.println("***Need for Null Object Pattern Demo***\n");
String input = null;
int totalObjects = 0;
while (true)
{
System.out.println("Enter your choice( Type 'a' for Bus, 'b' for Train ) ");
Scanner scanner=new Scanner(System.in);
input = scanner.nextLine();
Vehicle vehicle = null;
switch (input.toLowerCase())
{
case "a":
vehicle = new Bus();
break;
case "b":
vehicle = new Train();
break;
}
totalObjects = Bus.busCount + Train.trainCount;
vehicle.travel();
System.out.println("Total number of objects created in the system is : "+ totalObjects);
}
}
}
具有有效输入的输出
***Need for Null Object Pattern Demo***
Enter your choice( Type 'a' for Bus, 'b' for Train )
a
Let us travel with a bus
Total number of objects created in the system is : 1
Enter your choice( Type 'a' for Bus, 'b' for Train )
b
Let us travel with a train
Total number of objects created in the system is : 2
Enter your choice( Type 'a' for Bus, 'b' for Train )
b
Let us travel with a train
Total number of objects created in the system is : 3
Enter your choice( Type 'a' for Bus, 'b' for Train )
不需要输入的分析
假设用户错误地提供了一个不同的字符“d ”,如下所示:
***Need for Null Object Pattern Demo***
Enter your choice( Type 'a' for Bus, 'b' for Train )
a
Let us travel with a bus
Total number of objects created in the system is : 1
Enter your choice( Type 'a' for Bus, 'b' for Train )
b
Let us travel with a train
Total number of objects created in the system is : 2
Enter your choice( Type 'a' for Bus, 'b' for Train )
b
Let us travel with a train
Total number of objects created in the system is : 3
Enter your choice( Type 'a' for Bus, 'b' for Train )
d
遇到异常
这一次,您会收到System.NullPointerException运行时异常。
Enter your choice( Type 'a' for Bus, 'b' for Train )
d
Exception in thread "main"
java.lang.NullPointerException
at jdp2e.nullobject.context.demo.NeedForNullObjectPattern.main(NeedForNullObjectPattern.java:61)
即时补救措施
您可能想到的直接补救方法是在调用操作之前进行空检查,如下所示:
//A immediate remedy
if(vehicle !=null)
{
vehicle.travel();
}
分析
先前的解决方案在这种情况下有效。但是请考虑一个企业应用程序。如果您需要对每个可能的场景进行空检查,那么您可能需要在每次执行时评估大量的if条件,这种方法会使您的代码变脏。与此同时,你可能会注意到难以维护的副作用。空对象模式的概念在类似的情况下很有用。
真实世界的例子
让我们考虑一个现实生活中的洗衣机场景。如果门是关着的,并且供水顺畅,没有任何内部泄漏,洗衣机就可以正常洗涤。但是假设,有一次,你忘记关门或者中途停止供水。在这些情况下,洗衣机不应损坏自身。它可以发出一些警报来引起你的注意,并指示目前没有水或门仍然开着。
计算机世界的例子
假设在客户机服务器体系结构中,服务器根据客户机输入进行某种处理。服务器应该足够智能,以便它不会启动任何不必要的计算。在处理输入之前,它可能希望进行交叉验证,以确保是否需要启动该流程,或者是否应该忽略无效的输入。在这种情况下,您可能会注意到命令模式和空对象模式的使用。
基本上,在企业应用程序中,使用这种设计模式可以避免大量的空检查和if/else块。下面的实现可以让您很好地了解这种模式。
注意
在 Java 中,你可能见过 java.awt.event 包中各种适配器类的使用。可以认为这些类更接近于空对象模式。例如,考虑 MouseMotionAdapter 类。它是一个抽象类,但包含具有空体的方法,如 mouseDragged(MouseEvent e){ },mouseMoved(MouseEvent e){ }。但是由于适配器类是用 abstract 关键字标记的,所以不能直接创建该类的对象。
说明
和以前一样,在下面的实现中,让我们假设您有两种类型的交通工具:公共汽车和火车。客户可以通过不同的输入选择公共汽车或火车:“a”或“b”。如果用户错误地提供了任何无效数据(即,在这种情况下除了“a”或“b”之外的任何输入),他根本不能旅行。应用程序通过使用 NullVehicle 对象 什么都不做来忽略无效输入。在下面的例子中,我不会重复创建这些 NullVehicle 对象。一旦它被创建,我将简单地重用该对象。**
类图
图 25-1 为类图。(这个概念是用单例模式实现的,因此,您可以避免不必要的对象创建)。
图 25-1
类图
包资源管理器视图
图 25-2 显示了程序的高层结构。
图 25-2
包资源管理器视图
履行
下面是实现。
package jdp2e.nullobject.demo;
import java.util.Scanner;
interface Vehicle
{
void travel();
}
class Bus implements Vehicle
{
public static int busCount = 0;
public Bus()
{
busCount++;
}
@Override
public void travel()
{
System.out.println("Let us travel with a bus");
}
}
class Train implements Vehicle
{
public static int trainCount = 0;
public Train()
{
trainCount++;
}
@Override
public void travel()
{
System.out.println("Let us travel with a train");
}
}
class NullVehicle implements Vehicle
{
//Early initialization
private static NullVehicle instance = new NullVehicle();
public static int nullVehicleCount;
//Making constructor private to prevent the use of "new"
private NullVehicle()
{
nullVehicleCount++;
System.out.println(" A null vehicle object created.Currently null vehicle count is : "+nullVehicleCount);
}
// Global point of access.
public static NullVehicle getInstance()
{
//System.out.println("We already have an instance now. Use it.");
return instance;
}
@Override
public void travel()
{
//Do Nothing
}
}
public class NullObjectPatternExample {
public static void main(String[] args) {
System.out.println("***Null Object Pattern Demo***\n");
String input = "dummyInput";
int totalObjects = 0;
Scanner scanner;
while(!input.toLowerCase().contains("exit"))
{
System.out.println("Enter your choice( Type 'a' for Bus, 'b' for Train.Type 'exit' to close the application. ) ");
scanner=new Scanner(System.in);
if(scanner.hasNextLine())
{
input = scanner.nextLine();
}
Vehicle vehicle = null;
switch (input.toLowerCase())
{
case "a":
vehicle = new Bus();
break;
case "b":
vehicle = new Train();
break;
case "exit":
System.out.println("Closing the application");
vehicle = NullVehicle.getInstance();
break;
default:
System.out.println("Invalid input");
vehicle = NullVehicle.getInstance();
}
totalObjects = Bus.busCount + Train.trainCount+NullVehicle.nullVehicleCount;
//A immediate remedy
//if(vehicle !=null)
//{
vehicle.travel();
//}
System.out.println("Total number of objects created in the system is : "+ totalObjects);
}
}
}
输出
这是输出。
***Null Object Pattern Demo***
Enter your choice( Type 'a' for Bus, 'b' for Train.Type 'exit' to close the application. )
a
A null vehicle object created.Currently null vehicle count is : 1
Let us travel with a bus
Total number of objects created in the system is : 2
Enter your choice( Type 'a' for Bus, 'b' for Train.Type 'exit' to close the application. )
b
Let us travel with a train
Total number of objects created in the system is : 3
Enter your choice( Type 'a' for Bus, 'b' for Train.Type 'exit' to close the application. )
c
Invalid input
Total number of objects created in the system is : 3
Enter your choice( Type 'a' for Bus, 'b' for Train.Type 'exit' to close the application. )
dfh
Invalid input
Total number of objects created in the system is : 3
Enter your choice( Type 'a' for Bus, 'b' for Train.Type 'exit' to close the application. )
exit
Closing the application
Total number of objects created in the system is : 3
分析
-
无效输入及其影响以粗体显示。
-
除了最初的情况,请注意,对象计数并没有因为空的车辆对象或无效的输入而增加。
-
这次我没有执行任何空检查(注意下面代码段中的注释行)。
//A immediate remedy //if(vehicle !=null) //{ vehicle.travel(); //} -
这次程序执行不会因为无效的用户输入而中断。
问答环节
-
开始时,我看到创建了一个额外的对象。是故意的吗?
为了节省内存,我遵循了一种单例设计模式机制,该机制支持 NullVehicle 类结构中的早期初始化。我不想为每个无效输入创建一个 NullVehicle 对象。应用程序很可能需要处理大量的无效输入。如果您不注意这种情况,大量的 NullVehicle 对象驻留在系统中(基本上是无用的),这些对象会占用更多的内存。因此,您可能会注意到一些典型的副作用(例如,系统变慢等。).
-
为了实现一个简单的空对象模式,我可以忽略不同的对象计数器(在前一个例子中使用的)并减少大量代码。这是正确的吗?
Yes. Ideally, consider the following code segment.
//Another context List<Vehicle> vehicleList=new ArrayList<Vehicle>(); vehicleList.add(new Bus()); vehicleList.add(new Train()); vehicleList.add(null); for( Vehicle vehicle : vehicleList) { vehicle.travel(); }您不能循环通过这段代码,因为您遇到了
java.lang.NullPointerException。Note a class like the following.
class NullVehicle implements Vehicle { @Override public void travel() { //Do nothing } }And you code like this:
//Another context discussed in Q&A session List<Vehicle> vehicleList=new ArrayList<Vehicle>(); vehicleList.add(new Bus()); vehicleList.add(new Train()); //vehicleList.add(null); vehicleList.add(new NullVehicle()); for( Vehicle vehicle : vehicleList) { vehicle.travel(); }This time you can loop through smoothly. So, remember that the following structure prior to implementing a null object pattern (see Figure 25-3).
图 25-3
空对象模式的基本结构
-
什么时候应该使用这种模式?
-
如果您不想在某些典型场景中遇到 Java 中的 NullPointerException,那么该模式非常有用。(例如,如果您错误地试图调用一个空对象的方法。)
-
您可以忽略代码中的大量“空检查”。
-
没有这些空检查会使您的代码更整洁,更易于维护。
-
-
与空对象模式相关的挑战是什么?
-
在某些情况下,您可能希望找到失败的根本原因。因此,如果抛出对您更有意义的 NullPointerException,您总是可以在
try/catch或try/catch/finally块中处理这些异常,并相应地更新日志信息。 -
空对象模式基本上帮助我们实现一个默认行为,当你无意识地想要处理一个根本不存在的对象时。但是这种方法可能不适合系统中每个可能的对象。
-
空对象模式的不正确实现会抑制在程序执行中正常出现的真包。
-
在所有可能的场景中创建一个合适的空对象可能并不容易。在某些类中,这可能会导致影响类方法的变化。
-
-
空对象像代理一样工作。这是正确的吗?
不。一般来说,代理在某个时间点作用于真实对象,它们也可以提供一些行为。但是空对象不应该做这样的事情。
-
空对象模式总是与 NullPointerException 相关联。这是正确的吗?
概念是相同的,但是异常名可以不同或者是特定于语言的。例如,在 Java 中,您使用它来保护 java.lang.NullPointerException,但是在像 C# 这样的语言中,您可以使用此模式来保护 System.NullReferenceException。
二十六、MVC 模式
模型-视图-控制器(MVC)是一种架构模式。
这种模式的使用在 web 应用程序中或我们开发强大的用户界面时很常见。但值得注意的是,Trygve Reenskaug 在 1979 年的一篇题为“Smalltalk-80TM 中的应用程序编程:如何使用模型-视图-控制器”的论文中首次描述了 MVC,这是在万维网时代之前。那时候还没有 web 应用的概念。但是现代的应用程序可以被看作是最初概念的一种适应。值得注意的是,一些开发人员认为这不是真正的设计模式,相反,他们更愿意称之为“MVC 架构”
在这里,您将用户界面逻辑从业务逻辑中分离出来,并以一种可以有效重用的方式分离主要组件。这种方法也促进了并行开发。MVC 最好的标题之一是“我们需要智能模型、瘦控制器和愚蠢的视图。”( http://wiki.c2.com/?ModelViewController
概念
从这个介绍中,很明显模式由三个主要组件组成:模型、视图和控制器。控制器放置在视图和模型之间,模型和视图只能通过控制器相互通信。在这里,您将数据显示的机制与数据操作的机制分开。图 26-1 显示了一个典型的 MVC 架构。
图 26-1
典型的 MVC 架构
需要记住的要点
以下是对该模式中关键组件的简要描述。
-
视图表示输出。它是表示层。把它想象成一个用户界面/图形用户界面。你可以用各种技术来设计它。例如,在. NET 应用程序中,您可以使用 HTML、CSS、WPF 等等,而对于 Java 应用程序,您可以使用 AWT、Swing、JSF、JavaFX 等等。
-
模型是应用程序的大脑。它管理数据和业务逻辑。它知道如何存储和管理(或操作)数据,以及如何处理来自控制器的请求。但是这个组件与视图组件是分离的。一个典型的例子是数据库、文件系统或类似的存储。它可以用 JavaBeans、Oracle、SQL Server、DB2、Hadoop、MySQL 等等来设计。
-
控制器是接受来自视图组件的用户输入并将请求传递给模型的中介。当它从模型得到响应时,它将数据传递给视图。可以用 C# 设计。NET、ASP.NET、VB.NET、核心 Java、JSP、servlets、PHP、Ruby、Python 等等。
这种架构在不同的应用中有不同的实现方式。其中一些如下:
-
您可以有多个视图。
-
视图可以将运行时值(例如,使用 JavaScript)传递给控制器。
-
您的控制器可以验证用户的输入。
-
您的控制器可以通过多种方式接收输入。例如,它可以通过 URL 从 web 请求中获取输入,或者您可以通过按下表单上的 Submit 按钮来传递输入。
-
在某些应用程序中,模型组件可以更新视图组件。
基本上,你需要使用这个模式来支持你自己的需求。图 26-2 、图 26-3 和图 26-4 展示了 MVC 架构的一些已知变体。
变体 1
图 26-2
典型的 MVC 框架
变体 2
图 26-3
一个多视图的 MVC 框架
变体 3
图 26-4
用观察者模式/基于事件的机制实现的 MVC 模式
我最喜欢的关于 MVC 的描述来自 Connelly Barnes,他说:“理解 MVC 的一个简单方法是:模型是数据,视图是屏幕上的窗口,控制器是两者之间的粘合剂。”( http://wiki.c2.com/?ModelViewController
真实世界的例子
让我们重温一下模板方法模式的真实例子。但是这一次你有不同的解释。我说过,在餐馆里,根据顾客的输入,厨师可以改变口味,做出最终的产品。顾客不直接向厨师点餐。顾客看到菜单卡(视图),可能会咨询服务员,并下订单。服务员将订单交给厨师,厨师从餐厅的厨房收集所需的材料(类似于仓库/计算机数据库)。准备好后,服务员会把盘子端到顾客的桌子上。所以,你可以考虑把服务员/女招待的角色作为控制者,把厨师和他们的厨房作为模型(把准备食物的材料作为数据)。
计算机世界的例子
许多 web 编程框架使用 MVC 框架的概念。一些典型的例子包括 Django、Ruby on Rails、ASP.NET 等等。例如,一个典型的 ASP.NET MVC 项目的结构如图 26-5 所示。
图 26-5
ASP.NET 项目中典型的 MVC 结构
但是应该注意的是,不同的技术可以遵循不同的结构,因此,没有必要像这样获得具有严格命名约定的文件夹结构。在 Java 世界中,在 MVC 架构中,您可能会注意到使用 Java servlets 作为控制器,使用 JavaBeans 作为模型,而 JSP 创建不同的视图。
说明
大多数情况下,您希望将 MVC 的概念与能够为您提供内置支持并为您做大量基础工作的技术结合使用。在这种情况下,你可能需要学习新的术语。在 Java 应用程序中,您可能希望使用 Swing 或 JavaFX 等来获得更好的 GUI。
在本书中,我使用了一个控制台窗口来显示不同设计模式实现的输出。因此,让我们在即将到来的实现中继续使用控制台窗口作为视图,因为这里的重点是 MVC 结构,而不是新技术。
为了简单和符合我们的理论,我将即将到来的实现分成三个基本部分:模型、视图和控制器。当您查看 Package Explorer 视图时,您会看到创建了单独的包来完成这项任务。以下是一些要点。
-
在这个应用中,要求非常简单。有些员工需要在应用程序/系统中注册自己。最初,应用程序从三个不同的注册员工开始:Amit、Jon 和 Sam。在任何时候,您都应该能够在系统中看到注册的员工。
-
您可以在注册员工列表中添加新员工或删除员工。
-
在 Employee 类中添加了一个简单的检查,以确保不会在应用程序中重复添加雇员。
-
要从注册列表中删除一个雇员,您需要在客户机代码中传递雇员 ID,但是如果在注册列表中没有找到雇员 ID,应用程序将什么也不做。
现在检查一下实现,并考虑一下您的即时参考意见。
类图
图 26-6 为类图。为了强调核心架构,我省略了客户端代码依赖性。
图 26-6
类图
包资源管理器视图
图 26-7 显示了程序的高层结构。
图 26-7
包资源管理器视图
履行
下面是实现。
//Employee.java
package jdp2e.mvc.model;
//The key "data" in this application
public class Employee
{
private String empName;
private String empId;
public String getEmpName() {
return empName;
}
public String getEmpId() {
return empId;
}
public Employee(String empName, String empId)
{
this.empName=empName;
this.empId=empId;
}
@Override
public String toString()
{
return empName + "'s employee id is: "+ empId ;
}
@Override
//To check uniqueness.
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Employee)) return false;
Employee empObject = (Employee) o;
if (!empName.equals(empObject.empName)) return false;
//cannot use the following for an int
if (!empId.equals(empObject.empId)) return false;
return true;
}
}
//Model.java
package jdp2e.mvc.model;
import java.util.List;
//Model interface
public interface Model
{
List<Employee> getEnrolledEmployeeDetailsFromModel();
void addEmployeeToModel(Employee employeee);
void removeEmployeeFromModel(String employeeId);
}
//EmployeeModel.java
package jdp2e.mvc.model;
import java.util.ArrayList;
import java.util.List;
import java.util.ListIterator;
//EmployeeModel class
public class EmployeeModel implements Model
{
List<Employee> enrolledEmployees;
public EmployeeModel()
{
//Adding 3 employees at the beginning.
enrolledEmployees = new ArrayList<Employee>();
enrolledEmployees.add(new Employee("Amit","E1"));
enrolledEmployees.add(new Employee("John","E2"));
enrolledEmployees.add(new Employee("Sam","E3"));
}
public List<Employee> getEnrolledEmployeeDetailsFromModel()
{
return enrolledEmployees;
}
//Adding an employee to the model(student list)
@Override
public void addEmployeeToModel(Employee employee)
{
System.out.println("\nTrying to add an employee to the registered list.");
if( !enrolledEmployees.contains(employee))
{
enrolledEmployees.add(employee);
System.out.println(employee+" [added recently.]");
}
else
{
System.out.println(employee+" is already added in the system.");
}
}
//Removing an employee from model(student list)
@Override
public void removeEmployeeFromModel(String employeeId)
{
boolean flag=false;
ListIterator<Employee> employeeIterator=enrolledEmployees.listIterator();
System.out.println("\nTrying to remove an employee from the registered list.");
while(employeeIterator.hasNext())
{
Employee removableEmployee=((Employee)employeeIterator.next());
if(removableEmployee.getEmpId().equals(employeeId))
{
//To avoid ConcurrentModificationException,try to
//remember to invoke remove() on the iterator but not on
//the list.
employeeIterator.remove();
System.out.println("Employee " + removableEmployee.getEmpName()+ " with id "+ employeeId+" is removed now.");
flag=true;
}
}
if(flag==false)
{
System.out.println("###Employee Id " + employeeId +" Not found.###");
}
}
}
//View.java
package jdp2e.mvc.view;
import java.util.List;
import jdp2e.mvc.model.Employee;
public interface View
{
void showEnrolledEmployees(List<Employee> enrolledEmployees);
}
//ConsoleView.java
package jdp2e.mvc.view;
import java.util.List;
import jdp2e.mvc.model.Employee;
//ConsoleView class
public class ConsoleView implements View
{
@Override
public void showEnrolledEmployees(List<Employee> enrolledEmployees)
{
System.out.println("\n ***This is a console view of currently enrolled employees.*** ");
for( Employee employee : enrolledEmployees)
{
System.out.println(employee);
}
System.out.println("---------------------");
}
}
//Controller.java
package jdp2e.mvc.controller;
import jdp2e.mvc.model.Employee;
//Controller
public interface Controller
{
void displayEnrolledEmployees();
void addEmployee(Employee employee);
void removeEmployee(String employeeId);
}
//EmployeeController.java
package jdp2e.mvc.controller;
import java.util.List;
import jdp2e.mvc.model.*;
import jdp2e.mvc.view.*;
public class EmployeeController implements Controller
{
private Model model;
private View view;
public EmployeeController(Model model, View view)
{
this.model = model;
this.view = view;
}
@Override
public void displayEnrolledEmployees()
{
//Get data from Model
List<Employee> enrolledEmployees = model.getEnrolledEmployeeDetailsFromModel();
//Connect to View
view.showEnrolledEmployees(enrolledEmployees);
}
//Sending a request to model to add an employee to the list.
@Override
public void addEmployee(Employee employee)
{
model.addEmployeeToModel(employee);
}
//Sending a request to model to remove an employee from the list.
@Override
public void removeEmployee(String employeeId)
{
model.removeEmployeeFromModel(employeeId);
}
}
//客户端代码
//mvcarchitectureexample . Java
package jdp2e.mvc.demo;
import jdp2e.mvc.model.*;
import jdp2e.mvc.view.*;
import jdp2e.mvc.controller.*;
public class MVCArchitectureExample {
public static void main(String[] args) {
System.out.println("***MVC architecture Demo***\n");
//Model
Model model = new EmployeeModel();
//View
View view = new ConsoleView();
//Controller
Controller controller = new EmployeeController(model, view);
controller.displayEnrolledEmployees();
//Add an employee
controller.addEmployee(new Employee("Kevin","E4"));
controller.displayEnrolledEmployees();
//Remove an existing employee using the employee id.
controller.removeEmployee("E2");
controller.displayEnrolledEmployees();
//Cannot remove an employee who does not belong to the list.
controller.removeEmployee("E5");
controller.displayEnrolledEmployees();
//Avoiding duplicate entry
controller.addEmployee(new Employee("Kevin","E4"));
}
}
输出
这是输出。
***MVC architecture Demo***
***This is a console
view of currently enrolled employees.***
Amit's employee id is: E1
John's employee id is: E2
Sam's employee id is: E3
---------------------
Trying to add an employee to the registered list.
Kevin's employee id is: E4 [added recently.]
***This is a console view of currently enrolled employees.***
Amit's employee id is: E1
John's employee id is: E2
Sam's employee id is: E3
Kevin's employee id is: E4
---------------------
Trying to remove an employee from the registered list.
Employee John with id E2 is removed now.
***This is a console view of currently enrolled employees.***
Amit's employee id is: E1
Sam's employee id is: E3
Kevin's employee id is: E4
---------------------
Trying to remove an employee from the registered list.
###Employee Id E5 Not found.###
***This is a console view of currently enrolled employees.***
Amit's employee id is: E1
Sam's employee id is: E3
Kevin's employee id is: E4
---------------------
Trying to add an employee to the registered list.
Kevin's employee id is: E4 is already added in the system.
问答环节
-
假设你有一名程序员、一名数据库管理员和一名图形设计师。你能猜出他们在 MVC 架构中的角色吗?
图形设计师设计视图层。数据库管理员制作模型,程序员制作智能控制器。
-
使用 MVC 设计模式的关键 优势 是什么?
-
“高内聚、低耦合”是 MVC 的口号。在这种模式中,模型和视图之间的紧密耦合很容易消除。因此,它可以很容易地扩展和重用。
-
它支持并行开发。
-
您还可以提供多个运行时视图。
-
-
与 MVC 模式相关的 挑战 有哪些?
-
需要高技能人员。
-
它可能不适合微小的应用。
-
开发者需要熟悉多种语言/平台/技术。
-
多部分一致性是一个大问题,因为您将整个项目分成三个不同的部分。
-
-
在这个实现中,你能提供多个视图吗?
Sure. Let’s add a new view called “Mobile view” in the application. Let’s add this class inside the jdp2e.mvc.view package as follows.
package jdp2e.mvc.view; import java.util.List; import jdp2e.mvc.model.Employee; //This class is added to discuss a question in "Q&A Session" //MobileView class public class MobileView implements View { @Override public void showEnrolledEmployees(List<Employee> enrolledEmployees) { System.out.println("\n ***This is a mobile view of currently enrolled employees.*** "); System.out.println("Employee Id"+ "\t"+ " Employee Name"); System.out.println("______________________"); for( Employee employee : enrolledEmployees) { System.out.println(employee.getEmpId() + "\t"+ employee.getEmpName()); } System.out.println("---------------------"); } }
修改后的包浏览器视图类似于图 26-8 。
图 26-8
已修改的包资源管理器视图
将以下代码段添加到客户端代码的末尾。
//This segment is addeed to discuss a question in "Q&A Session"
view = new MobileView();
controller = new EmployeeController(model, view);
controller.displayEnrolledEmployees();
现在,如果您运行应用程序,您会看到修改后的输出。
修改输出
下面是修改后的输出。输出的最后一部分显示了新更改的效果。这些变化用粗体显示。
***MVC architecture Demo***
***This is a console view of currently enrolled employees.***
Amit's employee id is: E1
John's employee id is: E2
Sam's employee id is: E3
---------------------
Trying to add an employee to the registered list.
Kevin's employee id is: E4 [added recently.]
***This is a console view of currently enrolled employees.***
Amit's employee id is: E1
John's employee id is: E2
Sam's employee id is: E3
Kevin's employee id is: E4
---------------------
Trying to remove an employee from the registered list.
Employee John with id E2 is removed now.
***This is a console view of currently enrolled employees.***
Amit's employee id is: E1
Sam's employee id is: E3
Kevin's employee id is: E4
---------------------
Trying to remove an employee from the registered list.
###Employee Id E5 Not found.###
***This is a console view of currently enrolled employees.***
Amit's employee id is: E1
Sam's employee id is: E3
Kevin's employee id is: E4
---------------------
Trying to add an employee to the registered list.
Kevin's employee id is: E4 is already added in the system.
***This is a mobile view of currently enrolled employees.***
Employee Id Employee Name
______________________
E1 Amit
E3 Sam
E4 Kevin
---------------------