这是一个非常有趣的问题,它对面向对象编程的理论和实践有着深远的影响。首先我会告诉你如何快速而简单地(几乎)实现您所请求的内容。一般来说,我不建议使用这种解决方案,但由于没有人提到它,并且(如果我的记忆没有出错)马丁·福勒(Martin Fowler)的书籍《UML精简》中确实提到了它,因此值得讨论;您可以更改
setCustomer方法的定义:
public void setCustomer (Customer c) {
customer = c;
}
到:
void setCustomer (Customer c) {
customer = c;
}
确保
Customer和
Order在同一个包中。如果不指定访问修饰符,
setCustomer默认为
package可见性,这意味着只能从同一包内的类中访问它。显然,这并不能保护您免受来自同一包内除
Customer外其他类的非法访问。此外,如果您决定将
Customer和
Order移动到两个不同的包中,则代码将无法正常运行。
在Java中,通常会容忍包可见性;我觉得在C++社区中,尽管其目的类似,但friend修饰符并不像Java中的包可见性那样被容忍。我无法理解其中的原因,因为friend更加有选择性:基本上,对于每个类,您都可以指定其他友元类和函数,它们将能够访问第一个类的私有成员。
然而,毫无疑问,Java的包可见性和C++的友元都不是面向对象编程(OOP)的良好代表,甚至不是面向对象编程(OOP)的良好代表(OOP基本上是OBP加上继承和多态性; 从现在开始我将使用术语OOP)。 OOP的核心方面是存在被称为“对象”的实体,并且它们通过向彼此发送消息来进行通信。 对象具有内部状态,但是该状态只能由对象本身更改。 状态通常是结构化的,即基本上是一组字段,例如名称,年龄和订单。 在大多数语言中,消息是同步的,它们不能像邮件或UDP数据包一样被意外丢弃。 当您编写c.placeOrder(o)时,这意味着“sender”(即this)正在向c发送消息。 此消息的内容是placeOrder和o。
当一个对象接收到一条消息时,它必须处理它。Java、C++、C#以及许多其他语言都假设一个对象只有在其类定义了一个适当名称和形式参数列表的
方法时才能处理消息。一个类的方法集称为其
接口,而像Java和C#这样的语言也有一个适当的构造,即
接口来模拟一组方法的概念。对于消息
c.placeOrder(o)的处理程序是该方法:
public void placeOrder(Order o) {
orders.add(o);
o.setCustomer(this);
}
方法的主体是您编写指令以更改对象c的状态(如果必要)的地方。在此示例中,修改了orders字段。
这本质上就是面向对象编程的含义。面向对象编程是在模拟环境中开发的,在该环境中,您基本上有很多相互通信的黑盒子,每个盒子都负责自己的内部状态。
大多数现代语言完全遵循此方案,但仅限于私有字段和公共/受保护方法。不过还有一些需要注意的地方。例如,在Customer类的方法中,您可以访问另一个Customer对象的私有字段,如orders。
您提供的链接页面上的两个答案实际上都非常好,我已经点赞了。然而,我认为,就面向对象编程而言,像您所描述的具有真正双向关联是完全合理的。原因是,要向某人发送消息,您必须拥有对他的引用。这就是为什么我将尝试概述问题以及我们面向对象编程人员有时会遇到的困难。长话短说,
真正的面向对象编程有时很繁琐,非常类似于复杂的形式化方法。但它能产生更易于阅读、修改和扩展的代码,并且通常可以使您免除许多头痛。我一直想写下这些内容,我认为您的问题是一个好的借口。
主要的问题在于面向对象编程技术会在一组对象必须同时改变内部状态时出现,这是由“业务逻辑”所决定的外部请求引起的。例如,当一个人被聘用时,会发生很多事情。1)必须将员工配置为指向他的部门;2)他必须添加到部门中已雇用员工的列表中;3)其他一些东西必须添加到其他地方,比如合同的副本(甚至是扫描件)、保险信息等等。我提到的前两个动作恰好是建立(和维护,当员工被解雇或调动时)双向关联的例子,就像你描述客户和订单之间的关联一样。
在过程式编程中,
Person、
Department和
Contract将成为结构体,一个全局过程,例如
hirePersonInDepartmentWithContract与用户界面中的按钮关联,将通过三个指针操作这些结构的3个实例。整个业务逻辑都在此函数内部,并且在更新这三个对象的状态时必须考虑到
每一个可能的特殊情况。例如,当您单击雇用某人的按钮时,有可能他已经在另一个部门工作,甚至更糟的是在同一个部门工作。计算机科学家知道
特殊情况很糟糕。雇用一个人基本上是一个非常复杂的用例,有许多
扩展,不经常发生,但必须考虑。
真正的面向对象编程要求对象必须通过交换消息来完成任务。业务逻辑分散在几个对象的责任之间。CRC cards是一种在面向对象编程中研究业务逻辑的非正式工具。
要从John失业的有效状态到达他是研发部门的项目经理的另一个有效状态,需要经过至少一个无效状态。因此,存在一个初始状态、一个无效状态和一个最终状态,以及至少两个消息在人员和部门之间交换。您还可以确定,部门必须接收一个消息,以便有机会改变其内部状态,而另一个消息必须被人员接收,出于同样的原因。中间状态在实际世界中不存在,或者可能存在但不重要。然而,您应用程序中的逻辑模型必须以某种方式跟踪它。
基本上的想法是,当人力资源负责人填写“新员工”
JFrame并点击“雇用”
JButton时,所选部门将从
JComboBox中检索出来,而该
JComboBox可能已经从数据库中填充,并且根据各种
JComponents内部的信息创建一个新的
Person。也许会创建一个包含职位名称和薪水的工作合同。最后,适当的业务逻辑将所有对象连接在一起,并触发所有状态的更新。这个业务逻辑由类
Department中定义的名为
hire的方法触发,该方法接受一个
Person和一个
Contract作为参数。所有这些可能发生在
JButton的
ActionListener中。
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);
我想强调的是,在面向对象编程术语中,第4行发生了什么。在我们的情况下,
this(即
ActionListener)向
department发送一条消息,告诉他们必须雇用
contract下的
person。让我们来看看这三个类的可能实现。
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;
}
}
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;
}
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);
}
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);
}
}
我定义的信息,至少有非常生动的名称; 在实际应用中,您可能不想使用这些名称,但在本例的上下文中,它们有助于以有意义和直观的方式建模对象之间的交互。
部门可以接收以下信息:
- isBeingHired: 发送者想知道特定人员是否正在被该部门雇用。
- isBeingFired: 发送者想知道特定人员是否正在被该部门解雇。
- hire: 发送者希望该部门雇用具有指定合同的人员。
- fire: 发送者希望该部门解雇一个员工。
- iResign: 发送者可能是一名员工,并告诉部门他辞职了。
个人可以接收以下信息:
- youAreHired:部门发送此消息以通知人员已被录用。
- youAreFired:部门发送此消息以通知员工已被解雇。
- resign:发送者希望该人从当前职位辞职。请注意,被另一个部门录用的员工可以向自己发送 resign 消息以退出旧工作。
字段Person.mResigning、Department.isBeingHired和Department.isBeingFired是我用来编码上述无效状态的方式:当它们中的任何一个为“非零”时,应用程序处于无效状态,但正在走向有效状态。
还要注意,这里没有
设置方法;这与处理
JavaBeans的常见做法形成了对比。实质上,JavaBeans非常类似于C结构,因为它们往往针对每个私有属性具有一对set/get(或针对布尔值的set/is)方法。但是它们确实允许设置验证,例如您可以检查传递给set方法的
字符串不是空值并且不为空,并最终引发异常。
我在不到一个小时的时间里编写了这个小库。然后我编写了一个驱动程序,并在第一次运行时使用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();
}
}
事实上,它能工作并不是很酷,而酷的是只有招聘或解雇部门被授权向被雇用或解雇的人发送
youAreHired和
youAreFired消息;同样地,只有辞职的员工才能向其部门发送
iResign消息,并且仅限于该部门;从
main发送的任何其他非法消息都会触发断言。在真正的程序中,您应该使用异常而不是断言。
这是否有点过度?这个例子确实有些极端。但我觉得这就是OOP的本质。对象必须相互合作以实现特定目标,即根据预定的业务逻辑(在这种情况下是招聘、解雇和辞职)改变应用程序的全局状态。一些程序员认为业务问题不适合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;
}
我们还可以通过将
youAreHired 设为布尔函数来扩展应用程序,仅在旧部门同意新员工加入时返回
true。显然,我们可能需要改变其他一些东西,在我的情况下,我将
Person.resign 设为布尔函数,这又可能需要将
Department.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;
}
现在,当前雇主有最终决定权来确定员工是否可以被调到另一个部门。当前部门可以将这一责任委托给
策略,该策略可能考虑员工参与的项目、截止日期和各种合同限制。
实质上,向客户添加订单确实是业务逻辑的一部分。如果需要双向关联,而反射不是选项,并且此问题和链接问题中提出的解决方案都不令人满意,我认为唯一的解决方案是类似于这样的东西。