这是一个very有趣的问题,对 OOP 的理论和实践具有深远的影响。首先,我将告诉您(几乎)完成您所要求的任务的快速而肮脏的方法。一般来说,我不推荐这个解决方案,但是因为没有人提到它并且(如果我没记错的话)它isMartin Fowler (UML Distilled) 的书中提到过,这可能值得一谈;你可以改变的定义设置客户方法来自:
public void setCustomer (Customer c) {
customer = c;
}
to:
void setCustomer (Customer c) {
customer = c;
}
并确保Customer and Order位于同一个包中。如果不指定访问修饰符,设置客户默认为package可见性,这意味着它只能从同一包中的类访问。显然,这并不能保护您免受除Customer在同一个包内。另外,如果您决定移动,您的代码将会损坏Customer and Order在两个不同的包中。
在 Java 的常见编程实践中,包可见性在很大程度上是可以容忍的;我觉得在 C++ 社区中friend尽管修饰符具有类似的用途,但其容忍度不如 Java 中的包可见性。我实在无法理解为什么,因为friend更具选择性:基本上对于每个类,您都可以指定其他友元类和函数,这些类和函数将能够访问第一个类的私有成员。
然而,毫无疑问,无论是 Java 的包可见性还是 C++ 的包可见性,friend很好地代表了 OOP 的含义,甚至不能代表基于对象的编程的含义(OOP 基本上是 OBP 加上继承和多态性;从现在开始我将使用术语 OOP)。 OOP 的核心是存在称为objects,并且它们通过相互发送消息进行通信。对象具有内部状态,但该状态只能由对象本身改变。状态通常是结构化的即它基本上是一个集合fields例如name, age and orders。在大多数语言中,消息都是同步的,并且不会被错误地丢弃,就像邮件或 UDP 数据包一样。当你写的时候c.placeOrder(o)代表着sender,即this, 正在发送消息至c。这条消息的内容是下订单 and o.
当对象接收到消息时,它必须处理该消息。 Java、C++、C# 和许多其他语言假定对象只有在其类定义了消息时才能处理消息method具有适当的名称和形式参数列表。一个类的方法的集合称为它的界面,Java 和 C# 等语言也有适当的构造,即界面对一组方法的概念进行建模。消息的处理程序c.placeOrder(o)方法是:
public void placeOrder(Order o) {
orders.add(o);
o.setCustomer(this);
}
The body方法的位置是您编写将改变对象状态的指令的地方c,如果需要的话。在这个例子中orders字段被修改。
从本质上讲,这就是 OOP 的含义。 OOP 是在模拟的背景下开发的,其中基本上有很多相互通信的黑匣子,每个黑匣子负责自己的内部状态。
大多数现代语言都完美地遵循这个方案,但前提是你限制自己private领域和公共/受保护方法。不过,也有一些问题。例如,在类的方法中Customer您可以访问私有字段,例如orders, of another Customer object.
您链接的页面上的两个答案实际上非常好,我都赞成。然而,我认为,对于 OOP 来说,拥有真正的双向关联是完全合理的,正如您所描述的。原因是要向某人发送消息,您必须有对他的引用。这就是为什么我会尝试概述问题所在,以及为什么我们 OOP 程序员有时会遇到这个问题。长话短说,realOOP 有时很乏味,并且非常类似于复杂的形式方法。但它生成的代码更容易阅读、修改和扩展,并且通常可以让您免去很多麻烦。我想把这个写下来已经有一段时间了,我认为你的问题是一个很好的借口。
当一组对象由于外部请求而必须同时改变内部状态时,OOP 技术的主要问题就出现了,商业逻辑。例如,当一个人被雇用时,会发生很多事情。 1)员工必须配置为指向其部门; 2)必须将其列入本部门聘用人员名单; 3)必须在其他地方添加其他内容,例如合同副本(甚至可能是scan)、保险信息等。我引用的前两个操作正是建立(并在员工被解雇或调动时维护)双向关联的示例,就像您在客户和订单之间描述的那样。
在过程编程中Person, 部门 and Contract将是结构,以及像这样的全局程序雇佣部门内人员并签订合同与点击用户界面中的按钮相关联的操作将通过三个指针来操纵这些结构的 3 个实例。整个业务逻辑都在这个函数内部,必须考虑到every更新这三个对象的状态时可能出现的特殊情况。例如,当您单击雇用某人的按钮时,他可能已经在另一个部门工作,甚至更糟的是在同一部门工作。计算机科学家知道特殊情况不好 http://luigigobbi.com/jokesaboutmathematicians.htm。雇用一个人基本上是一个非常复杂的用例,有很多扩展 http://blog.casecomplete.com/post/Writing-Use-Case-Extensions.aspx这种情况并不经常发生,但必须考虑到这一点。
RealOOP 要求对象必须交换消息才能完成此任务。业务逻辑分为责任的几个对象。CRC卡 http://en.wikipedia.org/wiki/Class-responsibility-collaboration_card是研究 OOP 中业务逻辑的非正式工具。
要从valid从约翰失业的状态到他担任研发部门项目经理的另一个有效状态,需要经过多个无效状态,至少一个。所以有一个初始状态,一个无效状态和一个最终状态,一个人和一个部门之间至少有两条消息交换。您还可以确保该部门必须收到一条消息,以便有机会改变其内部状态,出于同样的原因,该人员也必须收到另一条消息。中间状态是无效的,因为它并不真正存在于现实世界中,或者可能存在但并不重要。但是,应用程序中的逻辑模型必须以某种方式跟踪它。
基本上这个想法是,当人力资源人员填补“新员工”时JFrame并点击“雇用”JButton,所选部门是从J组合框,这又可能是从数据库中填充的,以及一个新的Person是根据各种内部信息创建的J组件。也许创建的工作合同至少包含职位名称和工资。最后,有适当的业务逻辑将所有对象连接在一起并触发所有状态的更新。该业务逻辑由一个名为的方法触发hire类中定义的部门,它以参数 aPerson and a Contract。这一切都可能发生在动作监听器 of the JButton.
Department department = (Department)cbDepartment.getSelectedItem();
Person person = new Person(tfFirstName.getText(), tfLastName.getText());
Contract contract = new Contract(tfPositionName.getText(), Integer.parseInt(tfSalary.getText()));
department.hire(person, contract);
我想用 OOP 的术语强调一下第 4 行发生了什么;this(在我们的例子中是动作监听器, 正在发送消息至部门,说他们必须雇用person under contract。让我们看一下这三个类的合理实现。
Contract是一个非常简单的类。
package com.example.payroll.domain;
public class Contract {
private String mPositionName;
private int mSalary;
public Contract(String positionName, int salary) {
mPositionName = positionName;
mSalary = salary;
}
public String getPositionName() {
return mPositionName;
}
public int getSalary() {
return mSalary;
}
/*
Not much business logic here. You can think
about a contract as a very simple, immutable type,
whose state doesn't change and that can't really
answer to any message, like a piece of paper.
*/
}
Person更有趣。
package com.example.payroll.domain;
public class Person {
private String mFirstName;
private String mLastName;
private Department mDepartment;
private boolean mResigning;
public Person(String firstName, String lastName) {
mFirstName = firstName;
mLastName = lastName;
mDepartment = null;
mResigning = false;
}
public String getFirstName() {
return mFirstName;
}
public String getLastName() {
return mLastName;
}
public Department getDepartment() {
return mDepartment;
}
public boolean isResigning() {
return mResigning;
}
// ========== Business logic ==========
public void youAreHired(Department department) {
assert(department != null);
assert(mDepartment != department);
assert(department.isBeingHired(this));
if (mDepartment != null)
resign();
mDepartment = department;
}
public void youAreFired() {
assert(mDepartment != null);
assert(mDepartment.isBeingFired(this));
mDepartment = null;
}
public void resign() {
assert(mDepartment != null);
mResigning = true;
mDepartment.iResign(this);
mDepartment = null;
mResigning = false;
}
}
部门很酷。
package com.example.payroll.domain;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
public class Department {
private String mName;
private Map<Person, Contract> mEmployees;
private Person mBeingHired;
private Person mBeingFired;
public Department(String name) {
mName = name;
mEmployees = new HashMap<Person, Contract>();
mBeingHired = null;
mBeingFired = null;
}
public String getName() {
return mName;
}
public Collection<Person> getEmployees() {
return mEmployees.keySet();
}
public Contract getContract(Person employee) {
return mEmployees.get(employee);
}
// ========== Business logic ==========
public boolean isBeingHired(Person person) {
return mBeingHired == person;
}
public boolean isBeingFired(Person person) {
return mBeingFired == person;
}
public void hire(Person person, Contract contract) {
assert(!mEmployees.containsKey(person));
assert(!mEmployees.containsValue(contract));
mBeingHired = person;
mBeingHired.youAreHired(this);
mEmployees.put(mBeingHired, contract);
mBeingHired = null;
}
public void fire(Person person) {
assert(mEmployees.containsKey(person));
mBeingFired = person;
mBeingFired.youAreFired();
mEmployees.remove(mBeingFired);
mBeingFired = null;
}
public void iResign(Person employee) {
assert(mEmployees.containsKey(employee));
assert(employee.isResigning());
mEmployees.remove(employee);
}
}
我定义的消息至少非常皮托式的姓名;在真实的应用程序中,您可能不想使用这样的名称,但在本示例的上下文中,它们有助于以有意义且直观的方式对对象之间的交互进行建模。
部门可以收到以下消息:
-
正在招聘:发件人想知道某个特定人员是否正在被该部门聘用。
-
正在被解雇:发件人想知道某个人是否正在被部门解雇。
-
hire:发件人希望部门雇用具有指定合同的人员。
-
fire:发件人希望部门解雇一名员工。
-
iResign:发件人可能是一名员工,并告诉部门他要辞职。
Person可以收到以下消息:
-
你被录取了:部门发送此消息是为了通知该人他已被录用。
-
你被开除了:部门发送此消息是为了通知该员工他被解雇了。
-
resign:发件人希望此人辞去目前的职位。请注意,被其他部门雇用的员工可以发送resign给自己发一条信息,以辞掉原来的工作。
田野Person.mResigning, 部门正在招聘, 部门正在被解雇是我用来编码上述内容的invalid状态:当其中任何一个为“非零”时,应用程序处于invalid状态,但正在走向有效状态。
另请注意,没有set方法;这与合作的常见做法形成鲜明对比JavaBeans http://en.wikipedia.org/wiki/JavaBeans。 JavaBean 本质上与 C 结构非常相似,因为它们往往为每个私有属性都有一个 set/get(或 boolean 的 set/is)对。然而,它们确实允许验证集合,例如您可以检查String传递给 set 方法时不为 null 且不为空,并最终引发异常。
我在不到一个小时的时间内写了这个小库。然后我编写了一个驱动程序,它在第一次运行时与 JVM -ea 开关(启用断言)一起正常工作。
package com.example.payroll;
import com.example.payroll.domain.*;
public class App {
private static Department resAndDev;
private static Department production;
private static Department[] departments;
static {
resAndDev = new Department("Research & Development");
production = new Department("Production");
departments = new Department[] {resAndDev, production};
}
public static void main(String[] args) {
Person person = new Person("John", "Smith");
printEmployees();
resAndDev.hire(person, new Contract("Project Manager", 3270));
printEmployees();
production.hire(person, new Contract("Quality Control Analyst", 3680));
printEmployees();
production.fire(person);
printEmployees();
}
private static void printEmployees() {
for (Department department : departments) {
System.out.println(String.format("Department: %s", department.getName()));
for (Person employee : department.getEmployees()) {
Contract contract = department.getContract(employee);
System.out.println(String.format(" %s. %s, %s. Salary: EUR %d", contract.getPositionName(), employee.getFirstName(), employee.getLastName(), contract.getSalary()));
}
}
System.out.println();
}
}
但事实上它有效并不是一件很酷的事情。最酷的是,只有招聘或解雇部门有权发送你被录取了 and 你被开除了给被雇用或解雇的人的消息;同样,只有辞职员工才能发送iResign向其部门发送消息,并且仅向该部门发送消息;发送自的任何其他非法消息main会触发一个断言。在真实的程序中,您将使用异常而不是断言。
这一切是不是太过分了?诚然,这个例子有点极端。但我觉得这就是OOP的精髓。对象必须合作实现某个目标,即改变全局状态根据预定的业务逻辑来控制应用程序,在本例中hiring, firing and resign。有些程序员认为业务问题不适合 OOP,但我不同意;业务问题基本上是工作流程,它们本身是非常简单的任务,但涉及很多参与者(即objects),通过消息进行通信。继承、多态性和所有模式都是受欢迎的扩展,但它们不是面向对象过程的基础。特别是,基于参考的关联通常优于实现继承.
请注意,通过使用静态分析、按合同设计和自动定理证明器,您将能够验证您的程序对于任何可能的输入是否正确,without运行它。 OOP 是使您能够以这种方式思考的抽象框架。它不一定比过程式编程更紧凑,也不会自动导致代码重用。但我坚持认为它更容易阅读、修改和扩展;我们来看看这个方法:
public void youAreHired(Department department) {
assert(department != null);
assert(mDepartment != department);
assert(department.isBeingHired(this));
if (mDepartment != null)
resign();
mDepartment = department;
}
与用例相关的业务逻辑是最后的赋值;这if声明是一种扩展,一种特殊情况,仅当该人已经是另一个部门的员工时才会发生。前三个断言描述了禁止的特别案例。如果有一天我们想禁止这种自动从上一部门离职的情况,只需要修改这个方法:
public void youAreHired(Department department) {
assert(department != null);
assert(mDepartment == null);
assert(department.isBeingHired(this));
mDepartment = department;
}
我们还可以通过以下方式扩展应用程序你被录取了一个布尔函数,返回true仅当旧部门同意新招聘时。显然我们可能需要改变其他东西,就我而言,我做了人.辞职一个布尔函数,这又可能需要部门.iResign是一个布尔函数:
public boolean youAreHired(Department department) {
assert(department != null);
assert(mDepartment != department);
assert(department.isBeingHired(this));
if (mDepartment != null)
if (!resign())
return false;
mDepartment = department;
return true;
}
现在,现任员工对是否可以调动拥有最终决定权。当前部门可以将确定这一点的责任委托给一个Strategy http://en.wikipedia.org/wiki/Strategy_pattern反过来,这可能会考虑员工参与的项目、项目的截止日期和各种合同限制。
从本质上讲,向客户添加订单确实是业务逻辑的一部分。如果需要双向关联,并且反射不是一种选择,并且对此提出的解决方案和链接的问题都不是令人满意的,我认为唯一的解决方案是这样的。