在OO模型中添加双向关系的最佳实践

我正在努力想出一个在OO模型中添加双向关系的好方法。 假设有一个客户可以发出许多订单,也就是说客户和订单类之间存在一对多关联,需要在两个方向上遍历:对于特定客户,应该可以告诉所有订单他们已下订单,对于订单,应该可以告诉客户。

这是一段Java代码,虽然这个问题主要与语言无关:

class Customer { private Set orders = new HashSet (); public void placeOrder (Order o) { orders.add(o); o.setCustomer(this); } } class Order { private Customer customer; public void setCustomer (Customer c) { customer = c; } } 

让我感到困惑的是,鉴于模特有人可以轻易打电话:

 o.setCustomer(c); 

而不是正确的

 c.placeOrder(o); 

形成单向链路而不是双向链路。

仍在学习OOP,任何人都可以请求帮助解决这个问题的惯用和实用方法,而不诉诸“反思”或花哨的框架(无论如何都依赖于反思)。

PS有一个类似的问题: 在我的java模型中管理双向关联 ,但我觉得它不能回答我的请求。

PSS任何链接到在db4o之上实现业务模型的现实项目的源代码都非常感谢!

首先,除非您计划在客户之间移动订单,我认为您不应该提供setCustomer()方法,客户应该是构造函数的参数并保持不变。

那么,构造函数不应该是用户可访问的,只能使用Owner的工厂方法。

这是一个非常有趣的问题,它对OOP的理论和实践产生了深远的影响。 首先,我会告诉你快速而肮脏的方式(几乎)完成你的要求。 一般来说,我不推荐这个解决方案,但是由于没有人提及它(如果记忆没有让我失望),在Martin Fowler(UML Distilled)的一本书中提到过,它可能值得一提; 您可以从以下位置更改setCustomer方法的定义:

 public void setCustomer (Customer c) { customer = c; } 

至:

 void setCustomer (Customer c) { customer = c; } 

并确保客户订单在同一个包中。 如果未指定访问修饰符,则setCustomer默认为可见性,这意味着只能从同一包中的类访问它。 显然,这并不能保护您免受同一程序包中除Customer以外的类的非法访问。 此外,如果您决定将客户订单移至两个不同的包中,您的代码将会中断。

在Java中的常见编程实践中,封装可见性基本上是可以容忍的 我觉得在C ++社区中,尽管它具有类似的用途,但友情修饰符并不像Java中的包可见性那样容忍。 我无法理解为什么,因为朋友更有选择性:基本上每个class级你都可以指定其他朋友类和函数,这些类和函数将能够访问第一类的私有成员。

但是,毫无疑问,Java的包可见性和C ++的朋友都不是OOP意味着什么的好代表,甚至不是基于对象的编程意味着什么(OOP基本上是OBP加上inheritance和多态;我将使用术语OOP来自从今起)。 OOP的核心方面是有些实体称为对象 ,它们通过相互发送消息进行通信。 对象具有内部状态,但此状态只能由对象本身更改。 State通常是结构化的,即它基本上是名称年龄订单字段的集合。 在大多数语言中,消息是同步的,它们不能被错误地删除,如邮件或UDP数据包。 当你编写c.placeOrder(o)时,这意味着发送者就是这样 ,正在向c发送消息。 此消息的内容是placeOrdero

当一个对象收到一条消息时,它必须处理它。 Java,C ++,C#和许多其他语言都假设一个对象只有在其类定义了具有适当名称和forms参数列表的方法时才能处理消息。 类的方法集称为其接口 ,Java和C#等语言也具有适当的构造,即用于对一组方法的概念进行建模的接口 。 消息c.placeOrder(o)的处理程序是方法:

 public void placeOrder(Order o) { orders.add(o); o.setCustomer(this); } 

如果需要,该方法的主体是您编写将更改对象c的状态的指令的位置。 在此示例中, 订单字段已修改。

实质上,这就是OOP的含义。 OOP是在模拟环境中开发的,其中你基本上有很多黑框相互通信,每个框都负责自己的内部状态。

大多数现代语言完全符合此方案,但前提是您将自己局限于私有字段和公共/受保护方法。 但是有一些陷阱。 例如,在Customer类的方法中,您可以访问另一个 Customer对象的私有字段,例如orders

您链接的页面上的两个答案实际上非常好,我同时赞成。 但是,我认为,正如你所描述的那样,对于OOP来说,实现真正的双向关联是完全合理的。 原因是要向某人发送消息,您必须提及他。 这就是为什么我会尝试概述问题是什么,以及为什么我们OOP程序员有时会对此感到困惑。 长话短说, 真正的 OOP有时是乏味的,非常类似于复杂的forms方法。 但它产生的代码更易于阅读,修改和扩展,并且通常可以避免许多麻烦。 我一直想把它写下来一段时间,我认为你的问题是一个很好的借口。

每当一组对象必须同时改变内部状态时,就会出现OOP技术的主要问题,这是由业务逻辑决定的外部请求的结果。 例如,当一个人被雇用时,会发生很多事情。 1)员工必须配置为指向他的部门; 2)必须将他添加到部门的雇员名单中; 3)必须在其他地方添加其他东西,比如合同的副本(甚至是扫描它),保险信息等等。 我引用的前两个操作正是建立(并维护,当员工被解雇或转移时)双向关联的一个例子,就像您在客户和订单之间描述的那样。

在过程编程中, PersonDepartmentContract将是结构,并且诸如hirePersonInDepartmentWithContract的全局过程与用户界面中的按钮的点击相关联将通过三个指针操纵这些结构的3个实例。 整个业务逻辑都在这个函数中,并且在更新这三个对象的状态时必须考虑每个可能的特殊情况。 例如,有可能当你点击按钮雇用某人时,他已经在另一个部门就业,或者甚至更糟。 计算机科学家知道特殊情况很糟糕 。 雇用一个人基本上是一个非常复杂的用例,有很多扩展并不经常发生,但必须加以考虑。

真正的 OOP强制要求对象必须交换消息才能完成此任务。 业务逻辑分为几个对象的职责 。 CRC卡是研究OOP中业务逻辑的非正式工具。

为了从John失业的有效状态,到他在研发部门的项目经理的另一个有效状态,有必要经历许多无效状态,至少一个。 因此,存在初始状态,无效状态和最终状态,以及在个人和部门之间交换的至少两个消息。 您也可以确定部门必须收到一条消息,以便有机会改变其内部状态,并且出于同样的原因,该人必须接收另一条消息。 中间状态是无效的,因为它在现实世界中并不存在,或者可能存在但并不重要。 但是,应用程序中的逻辑模型必须以某种方式跟踪它。

基本上这个想法是当人力资源人员填写“新员工” JFrame并点击“雇用” JButton时 ,从JComboBox中检索所选部门,而JComboBox又可能已从数据库中填充,而新人员则是基于各种JComponents内部的信息创建。 也许创建的工作合同至少包含职位名称和工资。 最后,有适当的业务逻辑将所有对象连接在一起并触发所有状态的更新。 此业务逻辑由类Department中定义的名为hire的方法触发,该方法将PersonContract视为参数。 所有这些都可能发生在JButtonActionListener中。

 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行发生的事情; 这个 (在我们的例子中是ActionListener ,正在向部门发送消息,说他们必须根据合同雇用人员 。让我们来看看这三个类的合理实现。

合同是一个非常简单的课程。

 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. */ } 

更有趣。

 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 mEmployees; private Person mBeingHired; private Person mBeingFired; public Department(String name) { mName = name; mEmployees = new HashMap(); mBeingHired = null; mBeingFired = null; } public String getName() { return mName; } public Collection 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); } } 

我定义的消息至少有很多pittoresque的名字; 在实际应用程序中,您可能不希望使用这些名称,但在此示例的上下文中,它们有助于以有意义且直观的方式对对象之间的交互进行建模。

部门可以收到以下消息:

  • isBeingHired :发件人想知道某个人是否正在被部门聘用。
  • isBeingFired :发件人想要知道某个人是否正在被部门解雇。
  • 雇用 :发送人希望部门雇用具有指定合同的人。
  • :发件人希望部门解雇员工。
  • iResign :发件人可能是一名员工,并告诉部门他正在辞职。

可以收到以下消息:

  • youAreHired :部门发送此消息以通知该人他已被雇用。
  • youAreFired :部门发送此消息以通知员工他被解雇了。
  • 辞职 :发件人希望此人辞职。 请注意,由其他部门雇用的员工可以将辞职消息发送给自己以退出旧作业。

Person.mResigningDepartment.isBeingHiredDepartment.isBeingFired字段是我用来编码上述无效状态的字段:当其中任何一个为“非零”时,应用程序处于无效状态,但正在前往一个有效的。

另请注意,没有设定方法; 这与使用JavaBeans的常见做法形成对比。 JavaBeans本质上与C结构非常相似,因为它们倾向于为每个私有属性设置set / get(或set / is for boolean)对。 但是它们允许validationset,例如,您可以检查传递给set方法的String是否为null而不是空并最终引发exception。

我在不到一个小时的时间里写了这个小图书馆。 然后我编写了一个驱动程序,它在第一次运行时与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(); } } 

它工作的事实并不是很酷的事情; 很酷的事情是,只有招聘或解雇部门才有权向雇用或被解雇的人发送已经雇用你的消息; 以类似的方式,只有辞职的员工才能将iResign消息发送给其部门,并且只发送给该部门; 从main发送的任何其他非法消息都会触发断言。 在实际程序中,您将使用exception而不是断言。

所有这些都是矫枉过正吗? 这个例子无疑是有点极端。 但我觉得这是O​​OP的本质。 对象必须合作以实现某个目标,即根据预定的业务逻辑来改变应用程序的全局状态 ,在这种情况下招聘解雇辞职 。 一些程序员认为业务问题不适合OOP,但我不同意; 业务问题基本上是工作流 ,它们本身就是非常简单的任务,但它们涉及许多通过消息进行通信的参与者(即对象 )。 inheritance,多态和所有模式都是受欢迎的扩展,但它们不是面向对象过程的基础。 特别是,基于引用的关联通常优先于实现inheritance

请注意,通过使用静态分析,按合同设计和自动定理certificate,您可以在运行任何可能的输入的情况下validation程序是否正确。 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; } 

现在,当前的雇主在决定是否可以将雇员转移到另一个部门时有最终决定权。 目前的部门可以将确定这一点的责任委托给一项战略 ,该战略可以反过来考虑到员工参与的项目,截止日期和各种合同限制。

实质上,向客户添加订单确实是业务逻辑的一部分。 如果需要双向关联,并且reflection不是一个选项,并且在此和链接问题上提出的解决方案都不令人满意,我认为唯一的解决方案是这样的。

没有一个答案。 这真的取决于所涉及的课程。 在你的情况下,你显然不想让人们选择做无效的事情,所以我会摆脱Order.SetCustomer。

但情况并非总是如此。 就像我说的那样,这取决于所涉及的课程。

如果要在Customer.placeOrder(Order)中维护双向关系,为什么不在Order.setCustomer(Customer)执行相同的Order.setCustomer(Customer)

 class Order { private Customer customer; public void setCustomer (Customer c) { customer = c; c.getOrders().add(this); // ... or Customer.placeOrder(this) } } 

它似乎是复制代码,但它解决了问题。 但更简单的做法是尽可能避免双向关系。