哪个优先:不要重复自己或单一责任原则?

时间:2014-04-16 01:07:31

标签: c# oop dry single-responsibility-principle

虽然升级了一些旧代码,但我发现这两种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;
        }
    }
}

但这违反了不要重复自己:循环和切换语句在两种方法中都是重复的,并且由于此代码最可能的升级路径是添加其他状态,因此它使未来的升级更加困难而不是更少。

我无法找到最优雅的方式来重构这一点,所以我觉得最好问一下社区,以防有什么东西显而易见我错过了。你会如何解决这种情况?

(编辑)

我提出了一个可能的解决方案,但它看起来像是一个过度设计一个简单问题的例子,(它并没有真正解决原来的单一责任问题)。

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

2 个答案:

答案 0 :(得分:5)

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

  1. 代码遵守每一个代码意味着什么。
  2. 申请遵守每一项申请意味着什么。
  3. 在这种情况下,两次计算网格行的效率低下似乎是对我来说最重要的因素,DRY会在这种特殊情况下胜出 。在另一些情况下,它可能是相反的方式。

    值得添加注释来解释您做出的决定以及为什么以后对任何查看代码的人都很清楚。这正是应该使用注释的那种东西,即为什么代码正在做它正在做的事情,而不仅仅是它正在做的事情。

答案 1 :(得分:4)

设计模式为决策提供信息。他们不应该被虔诚地追随。它们可以帮助您为代码添加属性,使其更易于维护。遵循每一种设计模式都会导致严重过度工程,这远比忽略某种原则更糟糕。

从表面上看,将多个操作连接到一个迭代器似乎是合理的。这比迭代两次更有效,但它限制了您执行这两个操作,而不是一个或另一个。因此,您可以使用可行实现的结果属性来判断哪个是最佳的。如果您认为单独应用这些操作很重要,那么您可以重复自己。

有一种解决方案可以满足两者,但问题是它会过度设计。您希望代码必须尊重DRY和SRP的属性以及第一个解决方案的效率优势:

  • 允许您单独应用操作 - SRP
  • 不重复行两次迭代 - DRY /效率
  • 两次不重复相同的开关语句 - DRY /效率

在类似的伪代码中,使用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);

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