Ste*_*row 12 java language-agnostic oop db4o model
我正在努力想出一个在OO模型中添加双向关系的好方法.假设有一个客户可以下多个订单,也就是说客户和订单类之间存在一对多关联,需要在两个方向上遍历:对于特定客户,应该可以告诉所有订单他们已下订单,对于订单,应该可以告诉客户.
这是一段Java代码,虽然这个问题主要与语言无关:
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;
}
}
Run Code Online (Sandbox Code Playgroud)
让我感到困惑的是,鉴于模特有人可以轻易打电话:
o.setCustomer(c);
Run Code Online (Sandbox Code Playgroud)
而不是正确的
c.placeOrder(o);
Run Code Online (Sandbox Code Playgroud)
形成单向链路而不是双向链路.
仍在学习OOP,任何人都可以请求帮助解决这个问题的惯用和实用方法,而不诉诸"反思"或花哨的框架(无论如何都依赖于反思).
PS有一个类似的问题:在我的java模型中管理双向关联,但我觉得它不能回答我的请求.
PSS任何链接到在db4o之上实现业务模型的现实项目的源代码都非常感谢!
首先,除非您计划在客户之间移动订单,我认为您不应该提供setCustomer()方法,客户应该是构造函数的参数并保持不变.
那么,构造函数不应该是用户可访问的,只能使用工厂方法Owner.
这是一个非常有趣的问题,对OOP的理论和实践具有深远的影响。首先,我将告诉您(几乎)完成您所要求的快速而肮脏的方法。总的来说,我不推荐这种解决方案,但是由于没有人提到它,并且(如果记忆不会让我失望),在马丁·福勒(Martin Fowler,UML Distilled)的一本书中也提到了它,所以可能值得一提;您可以从以下位置更改setCustomer方法的定义:
public void setCustomer (Customer c) {
customer = c;
}
Run Code Online (Sandbox Code Playgroud)
至:
void setCustomer (Customer c) {
customer = c;
}
Run Code Online (Sandbox Code Playgroud)
并确保客户和订单位于同一包装中。如果未指定访问修饰符,则setCustomer默认为程序包可见性,这意味着只能从同一程序包中的类进行访问。显然,这不能保护您免受同一软件包中Customer类之外的其他类的非法访问。此外,如果您决定将客户和订单转移到两个不同的包中,则代码将中断。
在Java的通用编程实践中,包可见性在很大程度上是可以容忍的。我觉得像C ++社区内的朋友修饰符没有容忍在Java包的知名度,尽管它的作用也类似的事实。我不能真正理解为什么,因为friend的选择要多得多:基本上,对于每个类,您都可以指定其他Friend类和函数,这些类和函数将能够访问第一类的私有成员。
但是,毫无疑问,Java的程序包可见性和C ++的朋友都不能很好地代表OOP的含义,甚至不能代表基于对象的编程的含义(OOP基本上是OBP加上继承和多态性;我将使用术语OOP源自从今起)。OOP的核心方面是存在称为对象的实体,它们通过相互发送消息进行通信。对象具有内部状态,但是此状态只能由对象本身更改。状态通常是结构化的,即它基本上是名称,年龄和顺序等字段的集合。在大多数语言中,消息是同步的,并且不能像邮件或UDP数据包一样被错误地丢弃。当您编写c.placeOrder(o)时,这意味着发送方(即此方)正在向c发送消息。该消息的内容是placeOrder和o。
当一个对象收到一条消息时,它必须处理它。Java,C ++,C#和许多其他语言都假定对象只有在其类定义了具有适当名称和形式参数列表的方法时,才能处理消息。类的方法集称为其接口,并且Java和C#等语言也具有适当的构造,即用于对方法集概念进行建模的接口。消息c.placeOrder(o)的处理程序是方法:
public void placeOrder(Order o) {
orders.add(o);
o.setCustomer(this);
}
Run Code Online (Sandbox Code Playgroud)
方法的主体是您编写指令的地方,如有必要,这些指令将更改对象c的状态。在此示例中,订单字段被修改。
从本质上讲,这就是OOP的意思。OOP是在模拟的环境中开发的,在该模拟中,您基本上有很多相互通信的黑匣子,每个黑匣子负责其内部状态。
大多数现代语言都完全遵循此方案,但前提是您仅限于使用私有字段和公共/受保护的方法。但是,有一些陷阱。例如,类的方法中的客户,你可以访问私有字段,如订单的,另一个 客户对象。
您链接的页面上的两个答案实际上非常好,我都赞成。但是,我认为,正如您所描述的,对于OOP而言,具有真正的双向关联是完全合理的。原因是要向某人发送消息,您必须参考他。这就是为什么我将尝试概述问题所在的原因,以及为什么我们的OOP程序员有时会为此感到困惑。长话短说,真正的 OOP有时很乏味,并且非常类似于复杂的正式方法。但是它产生的代码更易于阅读,修改和扩展,并且通常使您免于烦恼。我一直想写下来一段时间,我认为您的问题是这样做的一个很好的借口。
OOP技术的主要问题出现在每组对象必须根据业务逻辑指示的外部请求而同时更改内部状态时。例如,当一个人被雇用时,会发生很多事情。1)必须配置员工指向其部门;2)必须将其添加到部门的雇用员工列表中;3)一定是别的东西在其他地方补充,如合同复印件(甚至是一个扫描的话),保险信息等。我引用的前两个动作恰好是建立(和维持,在解雇或转移员工时)双向关联的示例,例如您在客户和订单之间描述的双向关联。
在过程编程中,Person,Department和Contract将是结构,并且与hirePersonInDepartmentWithContract之类的全局过程相关联的用户界面中的按钮单击将通过三个指针来操纵这些结构的3个实例。整个业务逻辑都在此功能内部,并且在更新这三个对象的状态时必须考虑所有可能的特殊情况。例如,当您单击该按钮以雇用某个人时,该人可能已经在另一部门工作,或者甚至在同一部门任职的情况更糟。而且计算机科学家知道特殊情况是不好的。雇用一个人基本上是一个非常复杂的用例,其中有很多扩展并不经常发生,但这必须加以考虑。
实际的 OOP要求对象必须交换消息才能完成此任务。业务逻辑被划分为多个对象的职责。CRC卡是研究OOP中业务逻辑的非正式工具。
为了从约翰失业的有效状态过渡到他是研发部门的项目经理的另一有效状态,必须经历许多无效状态,至少一个无效状态。因此,存在一个初始状态,一个无效状态和一个最终状态,以及人与部门之间交换的至少两个消息。您也可以确保部门必须收到一条消息,以便有机会更改其内部状态,出于相同的原因,该人员也必须收到另一条消息。中间状态是无效的,因为它在现实世界中实际上并不存在,或者可能存在但并不重要。但是,应用程序中的逻辑模型必须以某种方式对其进行跟踪。
基本上,这种想法是,当人力资源人员填写“新员工” JFrame并单击“雇用” JButton时,从JComboBox检索选定的部门,而该JComboBox可能又是从数据库中填充的,而新的Person是根据各种JComponents中的信息创建的。可能会创建一份至少包含职位名称和薪水的工作合同。最后,有适当的业务逻辑将所有对象连接在一起并触发所有状态的更新。此业务逻辑由类Department中定义的称为hire的方法触发,该方法将a 和a 作为参数。 Person合同。所有这些都可能在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);
Run Code Online (Sandbox Code Playgroud)
我想强调一下第四行发生的事情,以面向对象操作的方式;这(在我们的例子是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.
*/
}
Run Code Online (Sandbox Code Playgroud)
人更有趣。
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;
}
}
Run Code Online (Sandbox Code Playgroud)
部门很酷。
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);
}
}
Run Code Online (Sandbox Code Playgroud)
我定义的消息至少具有非常笨拙的名称。在实际的应用程序中,您可能不想使用这样的名称,但是在本示例的上下文中,它们有助于以有意义且直观的方式对对象之间的交互进行建模。
部门可以收到以下消息:
人员可以收到以下消息:
我用来编码上述无效状态的字段是Person.mResigning,Department.isBeingHired,Department.isBeingFired:当其中一个为“非零”时,应用程序处于无效状态,但正在途中有效的。
还要注意,没有设置方法。这与使用JavaBeans的常规做法形成对比。JavaBean本质上与C结构非常相似,因为它们倾向于为每个私有属性都有一个set / get(或set / is为布尔值)对。但是,它们确实允许对set进行验证,例如,您可以检查传递给set方法的String是否为非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();
}
}
Run Code Online (Sandbox Code Playgroud)
它起作用的事实并不是一件很酷的事情。很酷的事情是,只有雇用或解雇部门才有权向正在雇用或被解雇的人发送youAreHired和youAreFired消息;以类似的方式,只有辞职员工可以将iResign消息发送到其部门,并且只能发送到该部门;从main发送的任何其他非法消息都将触发一个断言。在实际程序中,您将使用异常而不是断言。
所有这些都是过度杀伤力吗?当然,这个例子有些极端。但是我觉得这是OOP的本质。对象必须协作以实现某个目标,即根据预定的业务逻辑(在这种情况下,雇用,解雇和辞职)更改应用程序的全局状态。一些程序员认为业务问题不适合OOP,但我不同意。业务问题基本上是工作流,它们本身就是非常简单的任务,但是它们涉及许多参与者(即对象)),它们通过消息进行通信。继承,多态和所有模式都是受欢迎的扩展,但它们不是面向对象过程的基础。特别地,基于引用的关联通常比实现继承更可取。
请注意,通过使用静态分析,按合同设计和自动定理证明,对于任何可能的输入,您都可以验证您的程序正确,而无需运行它。OOP是使您能够以这种方式思考的抽象框架。它不一定比过程编程更紧凑,并且不会自动导致代码重用。但是我坚持认为,它更易于阅读,修改和扩展。让我们看看这个方法:
public void youAreHired(Department department) {
assert(department != null);
assert(mDepartment != department);
assert(department.isBeingHired(this));
if (mDepartment != null)
resign();
mDepartment = department;
}
Run Code Online (Sandbox Code Playgroud)
与用例相关的业务逻辑是最后的分配。在如果语句的扩展,只有当人已经在其他部门雇员出现的特殊情况。前三个断言描述了禁止的特殊情况。如果有一天我们想禁止从上一个部门自动辞职,我们只需要修改此方法:
public void youAreHired(Department department) {
assert(department != null);
assert(mDepartment == null);
assert(department.isBeingHired(this));
mDepartment = department;
}
Run Code Online (Sandbox Code Playgroud)
我们还可以通过将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;
}
Run Code Online (Sandbox Code Playgroud)
现在,当前雇员拥有决定是否可以将雇员调任到另一个部门的最终决定权。现任部门可以将确定此问题的责任委托给一项战略,该战略可以反过来考虑员工所涉及的项目,其截止日期和各种合同约束。
本质上,向客户添加订单确实是业务逻辑的一部分。如果需要双向关联,并且不能选择反射,并且对此和链接的问题提出的解决方案都不令人满意,那么我认为唯一的解决方案就是这样。
| 归档时间: |
|
| 查看次数: |
2777 次 |
| 最近记录: |