先决条件: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;
}

先决条件:Don';不要重复自己或单一责任原则

出于实用的原因,有很多编程规则需要不时违反。在你的情况下,我同意DRY和SRP似乎在竞争,所以我建议两个标准来决定获胜者:

  1. 对代码来说,遵守每一个规则意味着什么
  2. 对于应用程序来说,遵守每一个规则意味着什么

在这种情况下,对我来说,两次枚举网格行的效率低下似乎是压倒一切的因素,在这种特殊情况下,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);

但正如您所注意到的,实现您所读到的每一种设计模式都会导致这种过度设计的解决方案。我甚至跳过了一个级别的类层次结构,它仍然很糟糕。代码具有此处所尊重的设计模式的所有优点,但问题是,在应用程序的上下文中,您真的需要所有这些属性吗?答案可能是否定的。