我在C#/ WPF中编写了一个相当大的应用程序。我使用Builder模式创建始终处于一致状态的对象,并且对象是不可变的。
我在这个设计中遇到了一个问题,我不知道如何修复。
考虑以下两个类:
public class Employee {
public string Name { get; }
public double Salary { get; }
public IReadOnlyList<EmployeeBonus> Bonuses { get; } // read-only list of bonuses
public Employee(string name, double salary, IEnumerable<EmployeeBonus> bonuses) {
Name = name;
Salary = salary;
Bonuses = new List<EmployeeBonus>(bonuses); // list of bonuses initialized in constructor
}
}
public class EmployeeBonus {
public Employee Employee { get; } // bonus has reference to Employee
public string Description { get; }
public double Amount { get; }
public EmployeeBonus(Employee employee, string description, double amount) {
Employee = employee; // employee must be initialized in constructor
Description = description;
Amount = amount;
}
}
所以 - 我有一个员工类,其中包括每个员工收到的奖金列表。 EmployeeBonus类包含一个返回Employee的引用。 由于这两个类都是不可变的,因此每个类必须通过引用另一个来初始化。但这当然是不可能的,因为我无法创建一个对象,而该对象引用了另一个不存在的对象。
我想到的解决方案:
一个。我想在EmployeeBonus中没有对Employee的引用,因此EmployeeBonus只需要构造Description和Amount。但这打破了我的存储库模式:我的Repository<EmployeeBonus>
有一个Add方法,它只接受一个EmployeeBonus。为了正确保存这个对象,我需要知道哪个员工拥有它,并且因为该方法只接受EmployeeBonus - 该对象必须包含Employee。
B中。我考虑过向Employee添加一个AddBonus(字符串描述,双倍金额)方法,这样奖金列表不会在构造函数中初始化,而是稍后会添加每个奖金,而Employee类会将自己附加到每个奖励 - 但这样做使员工不再是不可变的。
℃。我可以打破我的通用存储库并创建另一个方法Add(EmployeBonus bonus, Employee employee)
,然后从EmployeeBonus中删除Employee - 但我的EmployeeBonusRepository不会继承自Repository<EmployeeBonus>
。
d。我能想到的最正确的解决方案(正确但非常浪费)是:
public class Employee
{
public string Name { get; }
public double Salary { get; }
public IReadOnlyList<EmployeeBonus> Bonuses { get; }
public Employee(string name, double salary, IEnumerable<EmployeeBonus> bonuses) {
Name = name;
Salary = salary;
Bonuses = new List<EmployeeBonus>(bonuses);
}
}
public class EmployeeBonus
{
public string Description { get; }
public double Amount { get; }
public EmployeeBonus(string description, double amount) {
Description = description;
Amount = amount;
}
}
public class EmployeeBonusWithEmployee
{
public Employee Employee { get; }
public EmployeeBonus Bonus { get; }
public EmployeeBonusWithEmployee(Employee employee, EmployeeBonus bonus)
{
Employee = employee;
Bonus = bonus;
}
}
public class EmployeeBonusWithEmployeeRepository : Repository<EmployeeBonusWithEmployee>
{
public void Add(EmployeeBonusWithEmployee bonus)
{
// save complete employee bonus
}
}
public class EmployeeRepository : Repository<Employee>
{
//...
public void Add(Employee employee)
{
// saves employee first,
// then creates an EmployeeBonusWithEmployee object for each EmployeeBonus in the list
// and saves it using an EmployeeBonusWithEmployeeRepository
}
}
这个解决方案(D)有意义吗?有更优雅的方式吗?
答案 0 :(得分:1)
要在C#中创建相互引用的对象,需要延迟相互引用的代码执行 - 直到两个对象都已创建。
public class B
{
private Lazy<A> _a;
public A GetA
{
get { return _a.Value; }
}
public B(Lazy<A> forLater)
{
_a = forLater;
}
}
和A班一样。 然后创建相互引用的对象:
A a = null;
B b = null;
a = new A(new Lazy<B>(() => b));
b = new B(new Lazy<A>(() => a));
Lazy<T>延迟执行代码直到稍后,允许两个构造函数完成。
这是一个可怕的解决方案 - 其他语言使这更容易 - 所以我建议一个更实际的方法。
答案 1 :(得分:1)
解决方案一:
为每位员工提供一个独特的钥匙;让我们假设它是为了争论的指导。
创建一个不可变的全局查找:
static ImmutableDictionary<Guid, Employee> employees = ... ;
然后当你创建一个新员工时:
Guid key = Guid.NewGuid(); // NOT new Guid() !
EmployeeBonus[] bs = new [] { new EmployeeBonus(key, description, amount) };
Employee e = new Employee(key, name, salary, bs);
employees = employees.Add(key, e);
现在你已经准备好了。除了变量“员工”之外,一切都是不可改变的,从奖金转到员工只需要从奖金中获取钥匙并在字典中查找。
解决方案二:
创建一个不可变的定向标记图类型。开发一个不可变的定向标记图是一个练习。这很有趣!
static Graph g = Graph.Empty;
创建新员工时,请向图表添加节点。图是不可变的,因此这会生成一个新图。
Employee e = new Employee(name, salary);
Bonus b = new Bonus(amount);
g = g.AddNode(e);
g = g.AddNode(b);
g = g.AddEdge(e, b, "Bonus");
g = g.AddEdge(b, e, "Employee");
所以现在你有了一名员工,你想知道他们有什么奖金,你可以参考图表:
// What nodes of the graph are connected to e by an edge "Bonus"?
IEnumerable<Bonuses> bs = g.GetNeighbors(e, "Bonus").OfType<Bonus>();
如果你有奖金并且想要雇员,那就相同。
Employee e = g.GetNeighbors(b, "Employee").OfType<Employee>().Single();
解决方案三:
而不是图表,维护一个不可变的行动分类帐。分类帐是一个只能从最后扩展的事务列表,从不在中间编辑。开发这种类型只是一种练习。
static Ledger ledger = Ledger.Empty;
...
Employee e = new Employee(name, salary);
Bonus b = new Bonus(amount);
ledger = ledger.AddHire(e);
ledger = ledger.AddBonus(e, b);
...
现在,当您想知道哪些奖金与员工相关联,或者员工与奖金相关联时,您过滤分类帐以获得一系列匹配添加奖励事件。
请注意,所有这些解决方案基本相同。每个对象都是不可变的,但总有一个可变变量包含全局真实的来源,并且该变量随着世界的变化而变化。
这些解决方案中的每一个都具有不同的性能特征,因此请考虑您可能执行的操作。另请注意,字典和图形解决方案会丢失有关发生的订单事件的信息;能够及时运行涉及事件序列的查询对您来说可能很重要。
另请注意,这些解决方案都具有不同的垃圾收集性能特征。字典和分类帐解决方案让员工永远活着。