先决条件:Don';不要重复自己或单一责任原则
本文关键字:自己 单一 原则 责任 Don 先决条件 | 更新日期: 2023-09-27 17:58:27
在升级一些旧代码时,我发现这两个OO原则似乎相互冲突。
考虑以下伪代码(这是我遇到的简化版本):
int numberOfNewRecords;
int numberOfOldRecords;
int numberOfUndefinedRecords;
void ColourAndCount()
{
foreach(Row row in dataGridView1.Rows)
{
switch(row.Cells["Status"])
{
case: "Old"
row.BackColor = Color.Red;
numberOfOldRecords++;
break;
case: "New"
row.BackColor = Color.Blue;
numberOfNewRecords++;
break;
default:
row.BackColor = Color.White;
numberOfUndefinedRecords++;
break;
}
}
}
这段代码做了两件事:它根据记录的状态统计记录的数量,还根据它们的状态为每一行着色。这很混乱,但由于这两个操作(到目前为止)总是同时调用,所以它没有造成任何问题,并使添加额外状态等维护要求变得容易。
尽管如此,单一责任原则告诉我应该把它分成两种不同的方法:
(编辑)小提示:我刚刚意识到我可能在这里滥用了"单一责任原则"这个词,据我所知,它指的是阶级。"每种方法一个操作"设计模式的术语是什么?
int numberOfNewRecords;
int numberOfOldRecords;
int numberOfUndefinedRecords;
void Count()
{
foreach(Row row in dataGridView1.Rows)
{
switch(row.Cells["Status"])
{
case: "Old"
numberOfOldRecords++;
break;
case: "New"
numberOfNewRecords++;
break;
default:
numberOfUndefinedRecords++;
break;
}
}
}
void Colour()
{
foreach(Row row in dataGridView1.Rows)
{
switch(row.Cells["Status"])
{
case: "Old"
row.BackColor = Color.Red;
break;
case: "New"
row.BackColor = Color.Blue;
break;
default:
row.BackColor = Color.White;
break;
}
}
}
但这违反了Don't Repeat Yourself:循环和switch语句在两个方法中都是重复的,而且由于此代码最有可能的升级路径是添加其他Statues,这会使未来的升级更加困难,而不是更少。
我很难找到最优雅的重构方法,所以我觉得最好向社区咨询,以防我明显遗漏了什么。你将如何应对这种情况?
(编辑)
我想出了一个可能的解决方案,但在我看来,这就像是一个过度设计简单问题的例子(它并没有真正解决最初的单一责任问题)。
struct Status
{
public string Name,
public int Count,
public Color Colour,
}
Dictionary<string, Status> StatiiDictionary = new Dictionary<string, int>();
void Initialise()
{
StatiiDictionary.Add(new Status("New", 0, Color.Red));
StatiiDictionary.Add(new Status("Old", 0, Color.Blue));
StatiiDictionary.Add(new Status("Undefined", 0, Color.White));
}
void ColourAndCountAllRows()
{
foreach(Row row in dataGridView1.Rows)
{
CountRow(row, StatiiDictionary);
ColourRow(row, StatiiDictionary);
}
}
void CountRow(Row row, Dictionary<string, Status> StatiiDictionary)
{
StatiiDictionary[row.Cells["Status"]].Count++;
}
void ColourRow(Row row, Dictionary<string, Status> StatiiDictionary)
{
row.BackColour = StatiiDictionary[row.Cells["Status"]].Colour;
}
出于实用的原因,有很多编程规则需要不时违反。在你的情况下,我同意DRY和SRP似乎在竞争,所以我建议两个标准来决定获胜者:
- 对代码来说,遵守每一个规则意味着什么
- 对于应用程序来说,遵守每一个规则意味着什么
在这种情况下,对我来说,两次枚举网格行的效率低下似乎是压倒一切的因素,在这种特殊情况下,DRY将胜过。在其他情况下,情况可能会相反。
值得添加一条注释来解释您所做的决定以及原因,这样以后查看代码的人都会清楚。这正是注释应该用来做的事情,即为什么代码正在做它正在做的事情而不仅仅是它正在做什么。
设计模式为决策提供信息。他们不应该被虔诚地追随。它们可以帮助您将属性添加到代码中,从而更易于维护。遵循每一种设计模式都会导致严重的过度设计,这比忽视某个原则要糟糕得多。
从表面上看,将多个操作合并为一个迭代器似乎是合理的。这比迭代两次更有效,但它限制了您同时执行这两个操作,而不是执行其中一个或另一个。因此,您可以使用可行实现的结果属性来判断哪一个是最好的。如果你认为单独应用这些操作很重要,你会觉得重复自己也没问题。
有一个解决方案可以同时满足这两种需求,但问题是它的设计过于复杂。您希望您的代码必须同时尊重DRY和SRP,并具有第一个解决方案的效率优势的属性是:
- 允许您单独应用操作-SRP
- 在行上不重复迭代两次-DRY/效率
- 不要重复相同的切换语句两次-DRY/efficiency
在类似的伪代码中,使用Java风格的方法而不是函数方法,您可以通过以下解决方案满足这些标准:
public abstract class RowOperation {
public void apply(string status, Row row) {
switch(status)
{
case: "Old"
this.OldCase(row);
break;
case: "New"
this.NewCase(row);
break;
default:
this.OtherCase(row);
break;
}
}
abstract void OldCase(Row);
abstract void NewCase(Row);
abstract void OtherCase(Row);
}
public class ColorRow implements RowOperation {
private static final ColorCells OP = new ColorCells();
private ColorCells(){}
// This operation isn't stateful so we use a singleton :D
public static RowOperation getInstance() {
return this.OP
}
public void OldCase(row) {
row.BackColor = Color.Red;
}
public void NewCase(row) {
row.BackColor = Color.Blue;
}
public void OtherCase(row) {
row.BackColor = Color.White;
}
}
public class CountRow implements CellOperation {
public int oldRows = 0;
public int newRows = 0;
public int otherRows= 0;
// This operation is stateful so we use the contructor
public CountRow() {}
public void OldCase(Row row) {
oldRows++;
}
public void NewCase(Row row) {
newRows++;
}
public void OtherCase(Row row) {
otherRows++;
}
}
// For each row in the grid we will call each operation
// function with the row status and the row
void UpdateRows(Grid grid, RowOperation[] operations)
{
foreach(Row row in grid.Rows)
{
string status = row.Cells["Status"]
foreach(RowOperation op in operations)
{
op.apply(status, row)
}
}
}
然后,您可以在一次迭代中将多个操作应用于行,根据需要添加新操作
RowOperations[] ops = {
ColorRow.getInstance(),
new CountRows()
};
UpdateRows(dataGrid1, ops);
但正如您所注意到的,实现您所读到的每一种设计模式都会导致这种过度设计的解决方案。我甚至跳过了一个级别的类层次结构,它仍然很糟糕。代码具有此处所尊重的设计模式的所有优点,但问题是,在应用程序的上下文中,您真的需要所有这些属性吗?答案可能是否定的。