本文介绍了在 OO 模型中添加双向关系的最佳实践的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

这里是一段 Java 代码,尽管问题在很大程度上与语言无关:

class 客户 {私有集合订单 = 新的 HashSet();公共无效地点订单(订单o){订单.添加(o);o.setCustomer(this);}}类订单{私人客户客户;public void setCustomer (Customer c) {客户 = c;}}

让我烦恼的是,鉴于模型有人可以轻松调用:

o.setCustomer(c);

而不是正确的

c.placeOrder(o);

形成单向链接而不是双向链接.

仍在学习 OOP,任何人都可以帮助解决这个问题的惯用和实用方法,而无需求助于反射"或花哨的框架(无论如何都依赖于反射).

附言有一个类似的问题:在我的 java 模型中管理双向关联,但是我觉得它没有回应我的请求.

P.S.S.非常感谢任何指向在 db4o 之上实现业务模型的实际项目的源代码的链接!

解决方案

这是一个非常有趣的问题,对 OOP 的理论和实践有着深远的影响.首先,我将告诉您(几乎)完成您所要求的快速而肮脏的方法.一般来说,我不推荐这个解决方案,但是因为没有人提到它并且(如果我没有记错的话)它在 Martin Fowler(UML Distilled)的一本书中提到,它可能是值得的谈论;您可以从以下位置更改 setCustomer 方法的定义:

public void setCustomer (Customer c) {客户 = c;}

到:

void setCustomer (Customer c) {客户 = c;}

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

在 Java 的常见编程实践中,包可见性在很大程度上是可以容忍的;我觉得在 C++ 社区中,friend 修饰符不像 Java 中的包可见性那样被容忍,尽管它具有类似的目的.我真的不明白为什么,因为 friend 更具选择性:基本上,您可以为每个类指定其他朋友类和函数,它们将能够访问第一个类的私有成员.>

然而,毫无疑问,Java 的包可见性和 C++ 的朋友都不能很好地代表 OOP 的含义,更不能代表 Object-Based Programming 的含义(OOP 基本上是 OBP 加继承和多态性;从现在开始我将使用术语 OOP).OOP 的核心方面是存在称为对象 的实体,它们通过相互发送消息进行通信.对象有一个内部状态,但这个状态只能由对象本身改变.状态通常是结构化,即它基本上是字段的集合,例如nameageorders.在大多数语言中,消息是同步的,它们不会被错误地丢弃,例如邮件或 UDP 数据包.当你写c.placeOrder(o)时,它意味着sender,也就是this,正在向c发送消息强>.此消息的内容是 placeOrdero.

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

public void placeOrder(Order o) {订单.添加(o);o.setCustomer(this);}

方法的主体是您编写指令的地方,这些指令将在必要时改变对象c的状态.在本例中,orders 字段被修改.

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

大多数现代语言都完全遵循此方案,但前提是您将自己限制在私有字段和公共/受保护方法.不过,也有一些问题.例如,在 Customer 类的方法中,您可以访问 another Customer 的私有字段,例如 orders> 对象.

您链接的页面上的两个答案实际上非常好,我都赞成.但是,我认为,就 OOP 而言,拥有真正的双向关联是完全合理的,正如您所描述的.原因是要向某人发送消息,您必须有对他的引用.这就是为什么我将尝试概述问题所在,以及为什么我们 OOP 程序员有时会为此而苦苦挣扎.长话短说,真正的 OOP 有时很乏味,而且非常类似于复杂的形式化方法.但它生成的代码更易于阅读、修改和扩展,并且通常可以使您免于许多麻烦.我一直想把这个写下来,我认为你的问题是一个很好的借口.

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

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

Real OOP 要求对象必须交换消息才能完成此任务.业务逻辑在多个对象的职责之间进行分割.CRC 卡 是研究 OOP 中业务逻辑的非正式工具.

要从 John 失业的 valid 状态,到他在研发部门担任项目经理的另一个有效状态,需要经过多个无效状态, 最后一个.所以有一个初始状态,一个无效状态和一个最终状态,一个人和一个部门之间至少交换了两条消息.您还可以确保部门必须收到一条消息,以便有机会改变其内部状态,出于同样的原因,该人员必须收到另一条消息.中间状态是无效的,因为它在现实世界中并不真正存在,或者可能存在但并不重要.但是,您的应用程序中的逻辑模型必须以某种方式对其进行跟踪.

基本上的想法是,当人力资源人员填写新员工"JFrame 并单击雇用"JButton 时,从JButton 中检索所选部门strong>JComboBox,这反过来可能是从数据库中填充的,并且根据各种 JComponents 中的信息创建了一个新的 Person.也许创建了一份至少包含职位名称和薪水的工作合同.最后,有适当的业务逻辑将所有对象连接在一起并触发所有状态的更新.该业务逻辑由 Department 类中定义的 hire 方法触发,该方法将 Person合同 作为参数强>.所有这些都可能发生在 JButtonActionListener 中.

Department 部门 = (Department)cbDepartment.getSelectedItem();Person person = new Person(tfFirstName.getText(), tfLastName.getText());Contract contract = new Contract(tfPositionName.getText(), Integer.parseInt(tfSalary.getText()));部门.雇用(人,合同);

我想强调一下第 4 行发生的事情,在 OOP 方面;这个(在我们的例子中是ActionListener,正在向部门发送消息,说他们必须雇用人员契约下.让我们来看看这三个类的合理实现.

Contract 是一个非常简单的类.

package com.example.payroll.domain;公共类合同{私人字符串 mPositionName;private int mSalary;公共合同(字符串位置名称,整数工资){mPositionName = 位置名称;mSalary = 工资;}公共字符串 getPositionName() {返回 mPositionName;}公共 int getSalary() {返回 mSalary;}/*这里没有太多的业务逻辑.你可以认为关于合约作为一种非常简单的、不可变的类型,谁的状态不会改变,那真的不能回复任何消息,就像一张纸.*/}

人物更有趣.

package com.example.payroll.domain;公共类人{私人字符串 mFirstName;私人字符串mLastName;私人部门m部门;私人布尔 mResigning;公共人(字符串名字,字符串姓氏){mFirstName = 名字;mLastName = 姓氏;mDepartment = null;mResigning = false;}公共字符串 getFirstName() {返回 mFirstName;}公共字符串 getLastName() {返回mLastName;}公共部门 getDepartment() {返回 m 部门;}公共布尔 isResigning() {返回 mResigning;}//========== 业务逻辑 ==========public void youAreHired(部门部门){断言(部门!= null);断言(m部门!=部门);断言(部门.isBeingHired(这个));if (mDepartment != null)辞职();mDepartment = 部门;}公共无效youAreFired(){断言(mDepartment != null);断言(mDepartment.isBeingFired(this));mDepartment = null;}公共无效辞职(){断言(mDepartment != null);mResigning = true;mDepartment.iResign(this);mDepartment = null;mResigning = false;}}

部门很酷.

package com.example.payroll.domain;导入 java.util.Collection;导入 java.util.HashMap;导入 java.util.Map;公开课部{私人字符串 mName;私人地图<人,合同>员工;私人 mBeingHired;私人 mBeingFired;公共部门(字符串名称){mName = 姓名;mEmployees = new HashMap();mBeingHired = null;mBeingFired = null;}公共字符串 getName() {返回名称;}公共收藏获取员工(){返回 mEmployees.keySet();}公共合同getContract(员工员工){返回 mEmployees.get(employee);}//========== 业务逻辑 ==========public boolean isBeingHired(Person person) {返回 mBeingHired == 人;}public boolean isBeingFired(Person person) {返回 mBeingFired == 人;}公共无效租用(人,合同合同){断言(!mEmployees.containsKey(人));断言(!mEmployees.containsValue(合同));mBeingHired = 人;mBeingHired.youAreHired(this);mEmployees.put(mBeingHired, 合同);mBeingHired = null;}公共无效火(人){断言(mEmployees.containsKey(人));mBeingFired = 人;mBeingFired.youAreFired();mEmployees.remove(mBeingFired);mBeingFired = null;}public void iResign(员工员工){断言(mEmployees.containsKey(员工));断言(employee.isResigning());mEmployees.remove(员工);}}

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

部门可以收到以下消息:

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

个人可以收到以下消息:

  • youAreHired:部门发送此消息以通知此人他已被录用.
  • youAreFired:部门发送此消息以通知员工他被解雇了.
  • 辞职:发件人希望此人辞去目前的职位.请注意,被其他部门聘用的员工可以向自己发送 resign 消息以辞去旧工作.

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

还要注意没有set方法;这与使用 JavaBeans 的常见做法形成对比.JavaBean 本质上与 C 结构非常相似,因为它们倾向于为每个私有属性设置一个 set/get(或 set/is 表示布尔值)对.但是,它们确实允许对 set 进行验证,例如,您可以检查传递给 set 方法的 String 是否为非 null 和非空,并最终引发异常.

我在不到一个小时的时间内编写了这个小库.然后我编写了一个驱动程序,它在第一次运行时与 JVM -ea 开关(启用断言)一起正常工作.

package com.example.payroll;导入 com.example.payroll.domain.*;公共类应用{私有静态部resAndDev;私人静态部门制作;私人静态部门[]部门;静止的 {resAndDev = new Department("研究与开发");生产=新部门(生产");部门=新部门[] {resAndDev,生产};}公共静态无效主(字符串 [] args){Person person = new Person("John", "Smith");打印员工();resAndDev.hire(人,新合同(项目经理",3270));打印员工();生产.雇用(人,新合同(质量控制分析师",3680));打印员工();生产.火(人);打印员工();}私有静态无效printEmployees(){for(部门部门:部门){System.out.println(String.format("部门:%s",部门.getName()));对于(员工员工:department.getEmployees()){合同合同=部门.getContract(员工);System.out.println(String.format(" %s.%s, %s. Salary: EUR %d", contract.getPositionName(), employee.getFirstName(), employee.getLastName(), contract.getSalary()));}}System.out.println();}}

虽然它有效,但并不是一件很酷的事情;很酷的一点是,只有招聘或解雇部门有权向被聘用或解雇的人发送youAreHiredyouAreFired 消息;以类似的方式,只有辞职员工才能向其部门发送iResign消息,并且只能发送给该部门;从 ma​​in 发送的任何其他非法消息都会触发断言.在实际程序中,您将使用异常而不是断言.

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

请注意,通过使用静态分析、合同设计和自动定理证明器,您将能够验证您的程序对于任何可能的输入是否正确,无需运行它.OOP 是使您能够以这种方式思考的抽象框架.它不一定比过程式编程更紧凑,也不会自动导致代码重用.但我坚持认为它更易于阅读、修改和扩展;我们来看看这个方法:

 public void youAreHired(部门部门){断言(部门!= null);断言(m部门!=部门);断言(部门.isBeingHired(这个));if (mDepartment != null)辞职();mDepartment = 部门;}

与用例相关的业务逻辑是最后的赋值;if 语句是一个扩展,一种特殊情况,仅在此人已经是另一个部门的员工时才会发生.前三个断言描述了禁止特殊情况.如果有一天我们想禁止上一个部门的这种自动辞职,我们只需要修改这个方法:

 public void youAreHired(部门部门){断言(部门!= null);断言(mDepartment == null);断言(部门.isBeingHired(这个));mDepartment = 部门;}

我们还可以通过使 youAreHired 成为一个布尔函数来扩展应用程序,只有当旧部门对新招聘没有问题时才返回 true.显然,我们可能需要更改其他内容,在我的情况下,我将 Person.resign 设为一个布尔函数,而这又可能要求 Department.iResign 成为一个布尔函数:

 public boolean youAreHired(部门部门){断言(部门!= null);断言(m部门!=部门);断言(部门.isBeingHired(这个));if (mDepartment != null)如果(!辞职())返回假;mDepartment = 部门;返回真;}

现在,当前员工在决定是否可以将员工转移到另一个部门方面拥有最终决定权.当前部门可以将确定这一点的责任委托给一个战略,后者可能会考虑到员工参与的项目、截止日期和各种合同限制.

本质上,向客户添加订单确实是业务逻辑的一部分.如果需要双向关联,而反射不是一种选择,并且对此提出的解决方案和链接问题都不能令人满意,我认为唯一的解决方案是这样.

I'm struggling to come up with a good way of adding a bidirectional relation in OO model. Let's say there is a Customer who can place many Orders, that is to say there is a one-to-many association between Customer and Order classes that need to be traversable in both directions: for a particular customer it should be possible to tell all orders they have placed, for an order it should be possible to tell the customer.

Here is a snippet of Java code, although the question is largely language-agnostic:

class Customer {
 private Set orders = new HashSet<Order> ();

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

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

What buggers me is that given the model someone could easily call:

o.setCustomer(c);

instead of correct

c.placeOrder(o);

forming unidirectional link instead of bidirectional one.

Still learning OOP, could anyone please help with what would be an idiomatic and practical way of solving this problem without resorting to "reflection" or fancy frameworks (that would anyway rely on reflection).

P.S. There is a similar question: Managing bidirectional associations in my java model, however I don't feel it answers my plea.

P.S.S. Any links to source code of real-life projects implementing business model on top of db4o are greatly appreciated!

解决方案

This is a very interesting question, which has profound implications on the theory and practice of OOP. First I will tell you the quick and dirty way to (almost) accomplish what you requested. In general I don't recommend this solution, but since nobody mentioned it and (if memory doesn't fail me) it is mentioned in a book from Martin Fowler (UML Distilled), it is probably worth talking about; you can change the definition of the setCustomer method from:

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

to:

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

and make sure Customer and Order are in the same package. If you don't specify an access modifier, setCustomer defaults to package visibility, which means it will be only accessible from classes within the same package. Obviously this does not protect you from illegitimate access from classes other than Customer within the same package. Also, your code will break if you decide to move Customer and Order in two different packages.

Package visibility is largely tolerated in common programming practice in Java; I feel like within the C++ community the friend modifier is not as tolerated as package visibility in Java, despite the fact that it serves a similar purpose. I can't really understand why, because friend is much more selective: basically for each class you can specify other friend classes and functions which will be able to access the private members of the first class.

However, there are no doubts that neither Java's package visibility nor C++'s friend are good representatives of what OOP means, and not even of what Object-Based Programming means (OOP is basically OBP plus inheritance and polymorphism; I'll use the term OOP from now on). The core aspect of OOP is that there are entities called objects, and they communicate by sending messages to each other. Objects have an internal state, but this state can only be altered by the object itself. State is typically structured i.e. it is basically a collection of fields such as name, age and orders. In most languages messages are synchronous and they can't be dropped by mistake, like a mail or a UDP packet. When you write c.placeOrder(o) it means that sender, which is this, is sending a message to c. The contents of this message are placeOrder and o.

When an object receives a message it must handle it. Java, C++, C# and a lot of other languages assume that an object can handle a message only if its class defines a method with an appropriate name and list of formal parameters. The set of the methods of a class is called its interface, and languages such as Java and C# also have an appropriate construct, namely interface to model the concept of a set of methods. The handler for the message c.placeOrder(o) is the method:

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

The body of the method is where you write the instructions that will alter the state of object c, if necessary. In this example the orders field is modified.

This is, in essence, what OOP means. OOP was developed in the context of simulations, in which you basically have a lot of black boxes that communicate with each other, and each box is responsible for its own internal state.

Most modern languages adhere perfectly to this scheme, but only if you restrict yourself to private fields and public/protected methods. There are a few gotchas, though. For instance, within a method of class Customer you could access the private fields, such as orders, of another Customer object.

The two answers on the page you linked are actually very good, and I upvoted both. However, I think, it is completely reasonable with respect to OOP, to have a real bidirectional association, as you described. The reason is that to send a message to someone, you must have a reference to him. That is why I'll try to outline what the problem is, and why we OOP programmers sometimes struggle with this. Long story short, real OOP is sometimes tedious, and very akin to a complex formal method. But it produces code that is easier to read, modify and extend, and in general saves you from a lot of headaches. I've been wanting to write this down for a while, and I think your question is a good excuse to do it.

The main problem with OOP techniques arises whenever a group of object must alter the internal state simultaneously, as a result of an external request, dictated by business logic. For instance, when a person is hired, lots of stuff happen. 1) The employee must be configured to point to his department; 2) he must be added to the list of hired employees in the department; 3) something else must be added somewhere else, like a copy of the contract (maybe even a scan of it), insurance information and so on. The first two actions that I cited are exactly an example of establishing (and maintaining, when the employee is fired or transferred) a bidirectional association, like the one you described between customers and orders.

In procedural programming Person, Department and Contract would be structures, and a global procedure like hirePersonInDepartmentWithContract associated to the click of a button in an user interface would manipulate 3 instances of these structures by the means of three pointers. The entire business logic is inside this function, and it must take into consideration every possible special case while updating the state of these three objects. For instance, there is the possibility that when you click the button to hire someone, he is already employed in another department, or even worse in the same. And computer scientists know that special cases are bad. Hiring a person is basically a very complex use case, with lots of extensions which don't happen very often, but that must be considered.

Real OOP mandates instead that objects must exchange messages to accomplish this task. The business logic is split among the responsibilities of several objects. CRC cards are an informal tool to study business logic in OOP.

To get from the valid state where John is unemployed, to the other valid state where he is a project manager at the R&D department, it is necessary to go through a number of invalid states, at least one. So there is an initial state, an invalid state and a final state, and at least two messages exchanged between a person and a department. You can also be sure that one message must be received by the department, to give it a chance of altering its internal state, and another one must be received by the person, for the same reason. The middle state is invalid in the sense that it doesn't really exist in the real world, or maybe exists but is of no importance. However, the logical model in your application must in a way keep track of it.

Basically the idea is that when the human resource guy fills the "New Employee" JFrame and clicks the "Hire" JButton, the selected department is retrieved from a JComboBox, which in turn may have been populated from a database, and a new Person is created based on the information inside the various JComponents. Maybe a job contract is created containing at least the name of the position and the salary. Finally there is appropriate business logic that wires all the objects together and triggers updates for all the states. This business logic is triggered by a method called hire defined in class Department, which takes as arguments a Person and a Contract. All of this may happen in the ActionListener 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);

I would like to stress what's going on at line 4, in OOP terms; this (which in our case is the ActionListener, is sending a message to department, saying they must hire person under contract. Let's have a look at a plausible implementation of these three classes.

Contract is a very simple class.

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 is way more interesting.

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;
    }
}

Department is quite cool.

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);
    }
}

The messages I defined have, at the very least, very pittoresque names; in a real application you might not want to use names like these, but in the context of this example they help to model the interactions between objects in a meaningful and intuitive way.

Department can receive the following messages:

  • isBeingHired: the sender wants to know whether a particular person is in the process of being hired by the department.
  • isBeingFired: the sender wants to know whether a particular person is in the process of being fired by the department.
  • hire: the sender wants the department to hire a person with a specified contract.
  • fire: the sender wants the department to fire an employee.
  • iResign: the sender is likely an employee, and is telling the department that he is resigning.

Person can receive the following messages:

  • youAreHired: the department sends this message to inform the person that he is hired.
  • youAreFired: the department sends this message to inform the employee that he is fired.
  • resign: the sender wants the person to resign from his current position. Note that an employee who was hired by another department can send the resign message to himself in order to quit the old job.

The fields Person.mResigning, Department.isBeingHired, Department.isBeingFired are what I use to encode the aforementioned invalid states: when either one of them is "non-zero", the application is in an invalid state, but is on its way to a valid one.

Also note that there are no set methods; this contrasts with the common practice of working with JavaBeans. JavaBeans are in essence very similar to C structures, because they tend to have a set/get (or set/is for boolean) pair for every private property. However they do allow for validation of set, for instance you can check that a String being passed to a set method is not-null and not empty and eventually raise an exception.

I wrote this little library in less than a hour. Then I wrote a driver program and it worked correctly with the JVM -ea switch (enable assertions) at the very first run.

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();
    }
}

The fact that it worked is not the cool thing though; the cool thing is that only the hiring or firing department is authorized to send youAreHired and youAreFired messages to the person that is being hired or fired; in a similar way, only a resigning employee can send the iResign message to its department, and only to that department; any other illegitimate message sent from main would trigger an assertion. In a real program you would use exceptions instead of assertions.

Is all of this overkill? This example is admittedly a little extreme. But I feel like this is the essence of OOP. Objects must cooperate to achieve a certain goal i.e. changing the global state of the application according to predetermined pieces of business logic, in this case hiring, firing and resign. Some programmers think that business problems are not suited for OOP, but I disagree; business problems are basically workflows, and they are very simple tasks by themselves, but they involve a lot of actors (i.e. objects), which communicate through messages. Inheritance, polymorphism, and all the patterns are welcome extensions, but they are not the base of the object-oriented process. In particular, reference-based associations are often preferred to implementation inheritance.

Note that by using static analysis, design-by-contract and automatic theorem provers, you would be able to verify that your program is correct, for any possible input, without running it. OOP is the abstraction framework that enables you to think this way. It is not necessarily more compact than procedural programming, and it does not automatically lead to code reuse. But I insist that it is easier to read, modify and extend; let's have a look at this method:

    public void youAreHired(Department department) {
        assert(department != null);
        assert(mDepartment != department);
        assert(department.isBeingHired(this));

        if (mDepartment != null)
            resign();

        mDepartment = department;
    }

The business logic relevant to the use case is the assignment at the end; the if statement is an extension, a special case that only occurs when the person is already an employee in another department. The first three assertions describe forbidden special cases. If one day we want to forbid this automatic resign from the previous department we only need to modify this method:

    public void youAreHired(Department department) {
        assert(department != null);
        assert(mDepartment == null);
        assert(department.isBeingHired(this));

        mDepartment = department;
    }

We can also extend the application by making youAreHired a boolean function, which returns true only if the old department is ok with the new hiring. Obviously we may need to change something else, in my case I made Person.resign a boolean function, which in turn may require Department.iResign to be a boolean function:

    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;
    }

Now the current employeer has the final word in determining whether an employee can be transferred to another department. The current department could delegate the responsibility of determining this to a Strategy which may in turn take into consideration the projects in which the employee is involved, their deadlines and various contractual constraints.

In essence, adding an order to a customer really is part of business logic. If a bidirectional association is required, and reflection is not an option, and none of the solutions proposed on this and the linked question are satisfactory, I think the only solution is something like this.

这篇关于在 OO 模型中添加双向关系的最佳实践的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持!

08-23 15:48