65.9K
CodeProject 正在变化。 阅读更多。
Home

避免做什么:反模式及解决方案

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.84/5 (58投票s)

2011年4月26日

CPOL

21分钟阅读

viewsIcon

122496

downloadIcon

1098

了解什么不该做。

引言

不久前,有人发了一个链接,指向这个关于所谓“反模式”的页面[^]。反模式与设计模式[^]相反,它们是重复的编程实践,会制造问题而不是解决问题。浏览那个页面时,我发现了很多我在刚开始编程时用过的模式(真羞耻!)。幸运的是,从那时起我学到了很多,并且我认为我不再使用任何反模式了。然而,我确实看到网上很多人仍然在使用这些适得其反的技术,无论是知情还是不知情。CP 和其他网络来源的许多文章讨论了各种设计模式。但反模式通常没有被讨论。
所以设计模式解决了问题,但是如果你没有真正意识到任何问题,为什么要解决问题呢?好吧,你可能实际上已经有一个问题了。我相信了解问题本身已经是解决方案的一半。但是要承认一个问题,你首先必须知道存在问题。因此,了解什么该做可能和了解什么做一样重要。这正是我写这篇文章的原因。我想向(初级)程序员指出什么不该做,以及应该怎么做。我从这个页面[^]中选取了一些反模式,我将讨论它们,并在一个示例项目中使用它们(也进行了修正)。

“优秀的程序员使用他们的大脑,但好的指导方针让我们不必考虑每个案例。”—— (Francis Glassborow)

背景

了解为什么不使用这些反模式,或者它们最初为什么是反模式,需要一些关于面向对象编程[^]的知识。虽然面向对象编程的基础,即所谓的SOLID原则[^],并不难理解(假设有一些编程知识),但它们很难付诸实践(而且你可能永远无法完全正确地做到)。因此,我不会详细介绍这些SOLID原则。目前,重要的是要知道,使用这些原则可以使代码和(Class结构)更有条理,这使你的应用程序更稳定、更易于维护。作为额外的好处,将设计模式应用到你的软件中变得更容易,从而更容易解决常见的编程问题。所以,正如你可能预料到的,反模式与正确的SOLID原则和设计模式相反,它们使你的代码难以阅读,稳定性差,并且可能使代码维护成为一场噩梦。

“构建软件设计有两种方法。一种是让它如此简单,以至于明显没有缺陷。另一种是让它如此复杂,以至于没有明显的缺陷。”—— (C.A.R. Hoare)

使用代码

Anti_Patterns_and_the_Solutions.jpg

为了这篇文章,我创建了两个示例项目。一个用 Visual Basic,一个用 C#。为什么?因为我已经使用 VB 好几个月了,对我来说它仍然比 C# 更熟悉。但因为这里很多人可能更熟悉 C#,所以我选择也将项目翻译成 C#。这对我来说也是一个很好的语法练习。我最终决定将 C# 代码放在文章中,并将 VB 项目作为附加内容。
那么看看这个项目,我们看到了什么?我们看到了一个 Windows Form,左侧有几个标签页,右侧有许多按钮和一个文本框。虽然许多按钮做同样的事情,但它们处理方式不同。一个按钮可能使用反模式执行操作 A,而另一个按钮执行完全相同的事情,但(部分)解决了反模式。调试代码是理解这里发生的事情的关键!代码注释很详细,所以没有借口不进入调试器 :) 

右侧的 TextBox 用作日志机制,除了其他功能外,还会在出现问题时向用户显示信息。

我必须提到,我已努力使项目简单易懂,适合所有人。因此,我没有使用适当的面向对象设计,这对于我在这里所做的事情来说是必不可少的。相反,请将此项目视为绝对初学者迈向面向对象编程的第一步。

在项目中,我模拟了一个电子邮件客户端。我没有发送电子邮件,而是进行了一些字符串拼接,并通过 MessageBoxes 向用户显示代表这封电子邮件的信息(除了 Windows.Forms.Form Class,你绝不应该在你的 Classes 中使用 MessageBoxes)。然而,事情可能会出错(我就是这样编程的)。使用反模式,这可能会出现问题(正如右侧的 TextBox 所示)。当我们解决了反模式后,事情仍然可能出错,但在这种情况下,这带来的问题较少(请记住,每个应用程序都在某个时候出现问题)。 

“控制复杂性是计算机编程的本质。”—— (Brian Kernigan)

“魔法按钮”和“复制/粘贴编程”反模式

我想讨论的第一个反模式是魔法按钮反模式[^]。启动示例项目并浏览到“Abstractions”选项卡。为什么这被称为“Abstractions”选项卡?几分钟后你就会知道。首先,点击“Magic Pushbutton”分组框中的“Send Test Email”按钮。你得到了什么?一封发给 fubar@customer.com 的电子邮件,消息是“This is a test email!”,主题是“Test”,所有内容都整齐地格式化在一个消息框中。还要注意,文本框(代表日志)的第一个条目是“-- Email was sent.”。哇,这真是太棒了!让我们看看该按钮下方的代码。

private void btnSendTestEmail_Click(System.Object sender, System.EventArgs e)
{
    try
    {
        string mail = "To: fubar@customer.com{0}{0}This is a test email!";

        // Show a MessageBox representing the email.
        MessageBox.Show(string.Format(mail, Environment.NewLine), 
          "Test", MessageBoxButtons.OK, MessageBoxIcon.Information);
        // Log to the UI. Not best practice, but it will do for the example.
        this.txtLog.Text += "-- Email was sent." + Environment.NewLine;

    }
    catch (Exception ex)
    {
        // Show the user that something went wrong.
        MessageBox.Show(ex.Message, this.Text, MessageBoxButtons.OK,
                        MessageBoxIcon.Error);
        // Then log the Message. Imagine someone forgetting
        // the -- or NewLine. Your log would be a mess!
        this.txtLog.Text += "-- " + ex.Message + Environment.NewLine;
    }
}
首先我们看到,邮件接收者是 fubar@customer.com,主题是“Test”(写在 MessageBox 的参数中),正文是“This is a test email!”(不完全是,只是想象一下)。接下来,txtLog 获得了一个新条目“-- Email was sent.”。这看起来很不错,对吧?代码做了它应该做的事情。但事实并非如此。

代码直接实现在 ButtonEvent Handler 中,如果 Button 不存在,代码就毫无用处。这被称为“魔法按钮模式”。你按下 Button,然后发生了一些魔法。这有几个缺点,没有任何优点!第一个缺点是代码可能会变得难以管理。你的客户可能会要求随电子邮件发送附件,并且他们可能希望将副本发送到自己的电子邮件地址,并且他们希望在电子邮件发送后喝杯咖啡。你的 Event Handler 将增长到几十行代码......第二,如果你想用另一个 ButtonControl 或在另一个 Form 甚至在另一个应用程序中做完全相同的事情怎么办?如果我们想更改电子邮件的接收者、主题或正文怎么办?这根本不可能。这个 Button Event Handler 不能用不同的值调用。代码只能用于这个特定的 Button

那么如果我们仍然想发送另一封电子邮件怎么办?我们将为此使用另一个反模式,称为复制/粘贴编程反模式[^]。但首先,点击第二个 Button。它位于 GroupBox “Copy / Paste”中,标题是“Send Test Email with Attachment”。这个 Button 显示一个 MessageBox (电子邮件),它与第一个大致相同。略有不同的是。主题和正文已更改,并且电子邮件中添加了一个附件。让我们再看看代码

private void btnSendTestEmailWithAttachment_Click(System.Object sender, System.EventArgs e)
{
    try
    {
        string mail = "To: fubar@customer.com{0}{0}Attachment: ";

        // Add the attachment here.
        string attachment = @"C:\\Reports\TestAttachment.txt";
        mail += attachment;
        this.txtLog.Text += string.Format("--The attachment {0} " + 
             "has been added to the email.{1}", 
             attachment, Environment.NewLine);

        // Add the body to the mail.
        mail += "{0}{0}This is a test email with Attachment!";

        // Show a MessageBox representing the email.
        MessageBox.Show(string.Format(mail, Environment.NewLine), 
          "Test with Attachment", MessageBoxButtons.OK,
          MessageBoxIcon.Information);
        // Log to the UI. Not best practice, but it will do for the example.
        this.txtLog.Text += "--Email was sent." + Environment.NewLine;

    }
    catch (Exception ex)
    {
        // Show the user that something went wrong.
        MessageBox.Show(ex.Message, this.Text, MessageBoxButtons.OK, 
                        MessageBoxIcon.Error);
        // Then log the Message. Imagine someone forgetting
        // the -- or NewLine. Your log would be a mess!
        this.txtLog.Text += "--" + ex.Message + Environment.NewLine;
    }
}
注意两件事。第二个 ButtonEvent Handler 中呈现的代码有一半与第一个 ButtonEvent Handler 中的代码相同。为什么?因为我复制/粘贴了它!不同的部分是我包含附件的地方(这需要一些额外的 string 拼接)。这样做似乎没问题,因为电子邮件仍然正确发送(或者在我们的例子中在 MessageBox 中正确格式化)。但请注意日志与第一个 Button 的日志不一致。老实说,我在创建这两个 Button Event Handler 时,在每个日志前面添加了“-- ”。这意味着我必须纠正两个 Event Handler 才能正确格式化日志消息,但我忘记了一个 Handler。这真的不是我太笨,而是要找到每个这样的日志操作真的非常困难,所以这种方法非常容易出错(祈祷它只涉及日志消息中的一个空格,我曾经因为复制/粘贴而使整个 Form 无法使用……)。所以这个 Button 具备第一个 Button 的所有缺点,而且也没有完全正确地粘贴。如何解决这些问题(现在你就会知道第一个选项卡为什么叫“Abstraction”)?我们需要将这些操作从 Button Event Handler 中抽象出来。

让我们先看看我是如何将日志从 Handler 中抽象出来的

private void LogMessage(string message)
{
    // Writes a message to the log window.
    this.txtLog.Text += "-- " + message + Environment.NewLine;
}
我创建了一个返回 void(VB 中为 Sub)的方法,它接受一个 string 作为参数。它将此 string(和“-- ”)添加到日志中。看到这怎么可能不出错吗?唯一可能出错的是程序员传递了一个难以理解的消息作为参数。然而,这种情况发生的可能性很小。

那么那些电子邮件函数呢?我们如何使它们更抽象,以便它们也能被我们 Form 中的其他 Button 使用?

private void SendSimpleMail(string subject, string message, string email)
{
    string mail = "To: {1}{0}{0}{2}";

    // Show a MessageBox representing the email.
    MessageBox.Show(string.Format(mail, Environment.NewLine, email, message), 
                    subject, MessageBoxButtons.OK, MessageBoxIcon.Information);
    // Log to the UI. Not best practice, but it will do for the example.
    LogMessage("Email was sent.");
}

private void SendMailWithAttachment(string subject, string message, 
                         string email, string attachment)
{
    string mail = "To: {1}{0}{0}Attachment: ";

    // Add the attachment here.
    mail += attachment;
    LogMessage(string.Format("The attachment {0} has " + 
               "been added to the email.", attachment));

    // Add the body to the mail.
     mail += "{0}{0}{2}";

    // Show a MessageBox representing the email.
    MessageBox.Show(string.Format(mail, Environment.NewLine, email, message), 
                    subject, MessageBoxButtons.OK, MessageBoxIcon.Information);
    // Log to the UI. Not best practice, but it will do for the example.
    LogMessage("Email was sent.");
}
我创建了两个返回 voidMethod(VB 中为 Sub),它们执行与前两个 ButtonEvent Handler 完全相同的操作。然而,这些 Method 期望将 subjectmessageemail 地址作为 parameters 传递。这意味着我可以在我的 Form 中从每个 Button(或任何其他 Control)使用这些 Method,并且它们将始终执行相同的操作。还要注意,新创建的 LogMessage Method 被调用。因此日志将始终保持统一。现在按下“Abstractions”选项卡中剩余的两个 Button。它们与前两个 Button 执行完全相同的操作,但日志现在是正确的,并且 ButtonEvent Handlers 看起来非常不同。
private void btnSendTestEmail2_Click(System.Object sender, System.EventArgs e)
{
    try
    {
        SendSimpleMail("Test", "This is a test email!", 
                       "fubar@customer.com");
    }
    catch (Exception ex)
    {
        // Show the user that something went wrong.
        MessageBox.Show(ex.Message, this.Text, MessageBoxButtons.OK, 
                        MessageBoxIcon.Error);
        // Then log the Message.
        LogMessage(ex.Message);
    }
}

private void btnSendTestEmailWithAttachment2_Click(System.Object sender, 
             System.EventArgs e)
{
    try
    {
        SendMailWithAttachment("Test", "This is a test email!", 
           "fubar@customer.com", @"C:\\Reports\TestAttachment.txt");
    }
    catch (Exception ex)
    {
        // Show the user that something went wrong.
        MessageBox.Show(ex.Message, this.Text, MessageBoxButtons.OK, 
                        MessageBoxIcon.Error);
        // Then log the Message.
        LogMessage(ex.Message);
    }
}
实际上,现在 Handler 中的大部分代码都在向用户显示并记录可能的 Exception。发送电子邮件(无论带不带附件)现在已从 ButtonEvent Handler 中处理掉。代码现在变得更具可重用性。这是解决“魔法按钮”和“复制/粘贴编程”反模式的第一步!像这样抽象 Method 是一个好习惯。然而,在我们当前的代码中,Method 仍然在我们的 Form 中。因此,如果我们在另一个 Form 或应用程序中想要使用这些 Method,我们仍然会陷入困境。最重要的是,一个 Method 仍然是另一个 Method 的大部分副本。我们当前的解决方案仍然不够充分,我们必须将 Method 移到一个单独的 Class 中,并将创建、发送和添加附件到电子邮件的操作拆分成不同的代码片段。

对象狂欢反模式

在这部分中,我将电子邮件 Method 移到了一个单独的 Class 中。我不会详细介绍,但请看看我是如何做到这一点的。当然,我以一种糟糕的方式完成了这项工作,使用了对象狂欢反模式[^]。对象狂欢意味着一个 Object 没有充分封装[^],这意味着 Object 可以以你不希望的方式或在你不希望的位置被访问。封装[^]是面向对象编程的三个主要特性之一(与继承和多态性一起),我建议你好好学习它。你将在下一个示例中看到封装的重要性。
但首先,让我们看看我是如何将前两个 Method 实现到一个名为“BadEmailClass”的新 Class 中的(此处为可读性而减少了注释)
public static List<string> Attachments { get; set; }

public static void AddAttachment(string filePath, LogMessage logger)
{
    // Make sure the List<string> is not null.
    if (Attachments == null)
    {
        Attachments = new List<string>();
    }

    // Add the file.
    Attachments.Add(filePath);
    logger(string.Format("The attachment {0} has been " + 
                         "added to the email.", filePath));
}

public static void SendMail(string Subject, string message, 
              string email, LogMessage logger)
{
    string mail = "To: " + email + "{0}";

    if (Attachments != null && Attachments.Count > 0)
    {
        mail += "{0}Attachments:{0}";
        foreach (string attachment in Attachments)
        {
            mail += attachment + "{0}";
        }
    }

    mail += "{0}" + message;

    try
    {
        // An Exception MAY occur. This Class Catches it,
        // Handles it, and you will never notice!
        Random rand = new Random();
        if (rand.Next(1, 5) == 1)
        {
            // Throw an Exception with some meaningless Message.
            throw new Exception("An Error has occurred");
        }

        // Calling a MessageBox from a Class... Not done in real worls apps!
        MessageBox.Show(string.Format(mail, Environment.NewLine), Subject,
                        MessageBoxButtons.OK, MessageBoxIcon.Information);
        logger("Email was sent.");

    }
    catch (Exception ex)
    {
        logger(ex.Message);
    }
}

首先请注意,不再有“SendMailWithAttachment”。相反,这个 Class 包含一个 List<string>(VB 中为 List(Of String)),可以通过“AddAttachmentMethod 添加附件。添加所有附件后,你可以调用“SendMail”,邮件将自动发送,包括所有添加的附件。其次,还要注意“SendMail” Method 可能会产生 Exception。最后,我将一个 Delegate 传递给每个 Method,负责我的 Form 上的日志记录。不必担心 Delegate,它对于示例和我想表达的观点并不重要。如果你想了解更多关于 Delegate 的信息,请参阅此页面[^]获取信息。还应该(再次)说明,除了 System.Windows.Forms.Form 之外,在 Classes 中使用 MessageBoxes 是不对的。这样做你的 Class 只能在 WinForm 项目中使用,并且使用你的 Class 的程序员必须强制应用程序的用户点击另一个 MessageBox。我将 Methods 和 List<string> 都声明为 static(VB 中为 Shared),所以任何想使用这个 Class 的人都可以直接使用它,而无需创建 Class 的实例(我第一次发现它时觉得它非常方便!)。在你按下任何 Button 之前,我想让你看看使用这个 Class 的代码(同样,已删除注释)。

private void btnSendInvoiceBad_Click(System.Object sender, System.EventArgs e)
{
    try
    {
        string mailBody = "Dear Customer,{0}Please, " + 
                          "PAY UP!!!{0}Regards,{0}Your Supplier";
        BadEmailClass.AddAttachment(@"C:\\Reports\Invoice.rpt", LogMessage);
        BadEmailClass.SendMail("Invoice", 
          string.Format(mailBody, Environment.NewLine), 
          "fubar@customer.com", LogMessage);
    }
    catch (Exception ex)
    {
        // Show the user that something went wrong.
        MessageBox.Show(ex.Message, this.Text, 
               MessageBoxButtons.OK, MessageBoxIcon.Error);
        LogMessage(ex.Message);
    }
}

private void btnSendReminderBad_Click(System.Object sender, System.EventArgs e)
{
    try
    {
        // The mofo better pay up!
        string mailBody = "Dear Customer,{0}You still have not " + 
           "shown us the money!{0}Regards,{0}Your Supplier";
        BadEmailClass.Attachments.Add(@"C:\\Reports\Reminder.rpt");
        BadEmailClass.SendMail("Reminder", 
          string.Format(mailBody, Environment.NewLine), 
          "fubar@customer.com", LogMessage);
    }
    catch (Exception ex)
    {
        // Show the user that something went wrong.
        MessageBox.Show(ex.Message, this.Text, MessageBoxButtons.OK, MessageBoxIcon.Error);
        LogMessage(ex.Message);
    }
}
所以现在假设你按下第一个 Button,附件被添加到电子邮件中,然后电子邮件被发送。然后假设你按下第二个 Button。同样,附件被添加,然后电子邮件被发送。任何 Exceptions 都将被捕获,显示给用户并记录。代码量很小,一切似乎都很完美!现在去“Object Orgy”选项卡,在“Bad Email Class” GroupBox 中多按几次 Buttons......

你会注意到几件事:日志有时会显示错误消息,但你没有收到通知(你知道发生了错误,因为你没有看到 MessageBox)。当你更频繁地按下 Button 时,附件的数量会不断增加。在一个 Button 中添加的附件会出现在另一个 Button 的电子邮件中,反之亦然。当发送提醒给客户时,添加附件的操作没有被记录。简而言之,一团糟! 

那么这里出了什么问题呢?这个 Class 中的所有内容都被声明为 static(VB 中为 Shared),这使得其他 Class(如我们的 Form)可以无限制地访问我们的 BadEmailClass 的内部值(在这个例子中是 List)。List 在邮件发送后没有被清空或 Disposed。实际上,因为它是一个 static 变量,所以它永远不会被 Garbage Collector Disposed,因为任何其他 Class 都可能在任何给定时间访问它并使用其项。那么我们可以让 SendMail Method 清空 List 吗?不行,因为如果我们想将邮件副本发送到另一个电子邮件地址怎么办?附件就会消失。那么我们应该强制我们的 Class 的用户在使用 Class 之前清空列表吗?不行,因为这会导致另一个反模式,即顺序耦合[^],这意味着程序员必须按照特定的顺序调用 Class 中的任何 Method,否则 Class 将无法工作或表现出意外行为。我们这里遇到的就是对象狂欢。这个 ClassMethodField 被不正确地封装了。我已经在 GoodEmailClass 中解决了这个问题。看看 MethodList 是如何在这个 Class 中声明的这个类中的声明方式

private List<string> _attachments;

public GoodEmailClass()
{
    _attachments = new List<string>();
}

public void AddAttachment(string filePath, LogMessage logger)
{ //... }

public void SendMail(string Subject, string message, string email, LogMessage logger)
{ //... }
 

我已经移除了 static(VB 中的 Shared)关键字,并将 List 设置为 private并将这两个 Method 设为 Public。这样做将解决我们几乎所有的问题。让我们看看它现在如何在我们的 Form 中使用

private void btnSendInvoiceGood_Click(System.Object sender, System.EventArgs e)
{
    try
    {
        string message = string.Format("Dear Customer,{0}Please, " + 
          "PAY UP!!!{0}Regards,{0}Your Supplier", Environment.NewLine);
        SendMail("Invoice", message, "fubar@customer.com", 
                 @"C:\\Reports\Invoice.rpt");
    }
    catch (Exception ex)
    {
        MessageBox.Show(ex.Message, this.Text, 
                        MessageBoxButtons.OK, MessageBoxIcon.Error);
        LogMessage(ex.Message);
    }
}

private void btnSendReminderGood_Click(System.Object sender, System.EventArgs e)
{
    try
    {
        string message = "Dear Customer,{0}You still have not " + 
               "shown us the money!{0}Regards,{0}Your Supplier";
        SendMail("Reminder", message, "fubar@customer.com", 
                 @"C:\\Reports\Reminder.rpt");
    }
    catch (Exception ex)
    {
    MessageBox.Show(ex.Message, this.Text, MessageBoxButtons.OK, MessageBoxIcon.Error);
    LogMessage(ex.Message);
    }
}

private void btnSendFunnyJokes_Click(System.Object sender, System.EventArgs e)
{
    try
    {
        SendMail("Funny!", "Some funny stuff!!!", 
          "friend@fubar.com", "funnypic1.jpg", 
          "funnypic2.jpg", "funnyvid.mov");
    }
    catch (Exception ex)
    {
        MessageBox.Show(ex.Message, this.Text, 
             MessageBoxButtons.OK, MessageBoxIcon.Error);
        LogMessage(ex.Message);
    }
}

// Overloaded Method, in case you do not want to send any attachments.
private void SendMail(string subject, string message, string emailAddress)
{
    SendMail(subject, message, emailAddress, null);
}

private void SendMail(string subject, string message, 
        string emailAddress, params string[] attachments)
{
    // We have to instantiate a new Object. This will
    // start from scratch (no unwanted surprises).
    GoodEmailClass email = new GoodEmailClass();

    if (attachments != null)
    {
        foreach (string attachment in attachments)
        {
            email.AddAttachment(attachment, LogMessage);
        }
    }

    email.SendMail(subject, message, emailAddress, LogMessage);
}
正如你所看到的,新的实现强制我们创建 Class 的新实例,这也确保了正在创建一个新的 List。现在还可以根据需要添加任意数量的附件,只要你拥有对 GoodEmailClass 的引用即可。这更容易调试,因为现在很清楚你的 GoodEmailClass 实例在哪里使用。你甚至可以通过右键单击 email 变量并在 Visual Studio 中单击“查找所有引用”来查找所有引用。现在单击“Good Email Class” GroupBox 中的 Button,看看所有内容如何被记录,你如何收到任何 Exception 的通知,以及你发送的每封电子邮件如何只包含你期望的附件! 

那么既然我们已经修复了这个 Class 的封装问题,那些 Exception 又怎么回事呢?为什么我们在 GoodEmailClass 中发生 Exception 时会看到,而在 BadEmailClass 中却看不到呢?那是因为我们的 BadEmailClass 实现了另一个反模式,即错误隐藏反模式[^]。这意味着我们的 BadEmailClass 产生了 Exception,但从未将 Exception 传递给 Class 的用户(我们的 Form)。相反,它捕获并处理了它(这很好,但要确保“顶层”也始终获得 Exception)。最重要的是,即使它确实将 Exception 向上 Throw 到我们的 Form,我们也会得到一个无用的 Exception 消息:“发生了一个错误。”。相反,GoodEmailClass 根本不捕获 Exception,并且它给了我们一个详细的错误描述。有关正确 Exception Handling 的更多信息,请阅读这篇文章[^]。

上帝对象 

最后,但同样重要的是,本文讨论的反模式是上帝对象反模式[^]。有了 GoodEmailClass,我们的应用程序实际上相当整洁。我们把它卖给了一个满意的客户,现在这个客户要求我们增加一些额外的功能!如果这还不是好消息,那什么才是呢!?这个客户希望我们的应用程序能够向电话发送短信,并能够为每个员工设置电子邮件和短信权限。所以我们开始编程,在不知不觉中,我们已经将以下内容添加到了我们的 GoodEmailClass 中(我将 GoodEmailClass 复制到 GodClass 中,并添加了以下内容)

public static void SendSms(string phoneNumber, 
              string message, LogMessage logger)
{
    // Check if the telephone number is of a correct format.
    if (!Regex.IsMatch(phoneNumber, @"^\d+$") || phoneNumber.Length != 10)
    {
        throw new System.Exception(phoneNumber + 
          " is not recognized as a valid telephone number.");
    }

    MessageBox.Show(message, phoneNumber, 
                    MessageBoxButtons.OK, MessageBoxIcon.Information);
    logger("SMS was sent.");
}

// This is a user setting.
// Probably stored somewhere in a DB or config file.
protected static bool _canSendEmail = true;
public static bool CanSendEmail
{
    get { return _canSendEmail; }
}

// This is a user setting. Probably stored somewhere in a DB or config file.
protected static bool _canSendSms = true;
public static bool CanSendSms
{
    get { return _canSendSms; }
}
我们 Form 中发送电子邮件的实现基本保持不变(我使用 GodClass 重新实现了它,现在检查 CanSendEmail 是否为 true)。SMS 功能的实现如下:
private void btnSendGodSms_Click(System.Object sender, System.EventArgs e)
{
    try
    {
        SendGodSms("0612345678", "This is a text message!");
    }
    catch (Exception ex)
    {
        MessageBox.Show(ex.Message, this.Text, 
                   MessageBoxButtons.OK, MessageBoxIcon.Error);
        LogMessage(ex.Message);
    }
}
        
private void SendGodSms(string phoneNumber, string message)
{
    // First check if the user is allowed to send an SMS.
    if (GodClass.CanSendSms)
    {
        // The SMS is send and all is fine.
        GodClass.SendSms(phoneNumber, message, LogMessage);
    }
    else
    {
        // If the user is not allowed we prompt him.
        // Usually the user would not be able
        // to press any 'Send SMS' button at all.
        MessageBox.Show("You are not allowed to send an sms.", 
                        this.Text, MessageBoxButtons.OK, 
                        MessageBoxIcon.Hand);
        LogMessage("SMS was denied.");
    }
}
我们首先需要检查用户是否被允许发送短信。在大多数情况下,我认为如果当前用户不被允许发送短信,最好是 hidedisable SMS Button,但目前我们先这样做。就这样。我们刚刚创建了我们的第一个上帝 Object!一个上帝 Object 是一个无所不知的 Object,或者一个知道或做太多事情的 Object。在这个例子中,我们有一个 Class,它发送电子邮件和短信,还存储用户设置。尽管这很诱人,但这是一个糟糕的实践。是的,电子邮件和短信都是沟通形式,所以它们应该(可以)放在一起,是的,用户设置与电子邮件和短信密切相关。但请尝试维护这段代码。每当需要新功能时,你都会忍不住也把它放入这个 GodClass 中(反正它已经有很多功能了)。此外,当你修改短信功能时,你的电子邮件功能很可能会崩溃。即使你认为它永远不会发生,它在某种程度上发生。修改一个 Class 如果不小心,可能会导致整个 Class 的行为发生变化。因此,最好让一个 Class 只负责一件事(单一职责原则,SOLID 原则中的 S!)此外,团队中的新程序员绝不会期望在一个名为“Email”的 Class 中找到短信功能。将你的 Class 命名为“Communications”也不会使事情变得更清晰。几个月甚至几年后,你的 Object 的大部分将不再使用(遗留代码),并且未使用的部分无法与已使用的部分区分开来。维护上帝 Object 可能会令人厌烦和困惑,因为不同的功能会相互交织。

想象一下一个潜在客户打电话过来
客户:“你好,我是潜在客户。”
你:“你好,我们是潜在供应商!”
客户:“你能为我制作一个可以发送电子邮件的应用程序吗?”
你:“当然,你想要短信功能吗?”
客户:“不……”
你:“好吧,反正你也会得到它!”
*客户挂断电话*  

你可不想那样。
那么解决办法是什么呢?就是大量的 Class(可能包括 BaseClassInterface,但那是面向对象设计,我就不谈了)。将相关的 Class 放在一起,既要通过使用文件夹在硬盘上物理地放在一起,也要通过使用 Namespace(我在示例项目中没有使用)在代码中放在一起。正如你在示例项目中看到的那样,我将 GodClass 分为了 SmsClassUserSettings Class。我们已经有了一个 GoodEmailClass,它将继续被使用。我们的 Form 中短信功能的实现现在看起来像这样:

private void btnSendMultipleClassesSms_Click(System.Object sender, System.EventArgs e)
{
    try
    {
        SendSms("0612345678", "This is a final text message!");
    }
    catch (Exception ex)
    {
        MessageBox.Show(ex.Message, this.Text, 
                   MessageBoxButtons.OK, MessageBoxIcon.Error);
        LogMessage(ex.Message);
    }
}
        
private void SendSms(string phoneNumber, string message)
{
    // First check if the user is allowed to send an SMS.
    if (UserSettings.CanSendSms)
    {
        SmsClass.SendSms(phoneNumber, message, LogMessage);
    }
    else
    {
        MessageBox.Show("You are not allowed to send an sms.", 
              this.Text, MessageBoxButtons.OK, MessageBoxIcon.Hand);
        LogMessage("SMS was denied.");
    }
}

项目中也提供了提供错误电话号码的示例。UserSettings 类可能有点令人困惑。确实如此。Propertiesstatic(VB 中为 Shared),而它包含的成员是 Protected。每次你想检查用户是否有权限执行某个操作时都简单地创建一个 UserSettings Class 的新实例是行不通的,因为这会重置所有设置。你可以在每次实例化 Object 时再次设置权限,但在这种情况下,我希望用户在权限更改时重新启动应用程序。所以我只想在应用程序启动时设置权限。对于这样的问题(在任何时候都必须只存在一个 Class 实例),我们可能会使用单例设计模式[^](注意,既然我们不再拥有上帝 Object,我们现在就可以使用这种模式了!)。但我答应过你,这将是一篇关于反模式而不是设计模式的文章。所以我会让你自己去解决这个问题。所以我们看到代码现在更具可读性,并且 Class 具有逻辑划分的功能。更改 SMS 功能永远不会损害我们的电子邮件功能,因为我们在物理上不同的文件和 Class 中工作(没有依赖)。现在去“上帝对象选项卡”上按一些 Button 吧 :)

结论 

虽然本文中讨论的许多反模式可能看起来像是可以轻易解决的简单错误,但它们是常见的错误,可能会损害任何项目(甚至导致失败)。事实是,这些“简单的小错误”会累积到无法再“轻易修复”的地步。这些错误可能很微妙,任何人都可以犯,例如将 Field 声明为 Public 而不是 Private,或者忘记 Throw 一个 Exception。刚开始学习这门语言的人可能无法看到或理解这些错误的微妙之处,而拥有多年经验的程序员可能只是偶尔走神,不小心犯了这些错误。通过指出这些错误,并通过展示这些错误(甚至是模式)的危险,我希望人们在下次声明 Field Public 时能更加警惕,因为有人可能想更改值,或者捕获 Exception 而不重新抛出它,因为谁会想知道呢。

兴趣点 

我提供的示例项目离应有的水平还很远。恰当的面向对象设计很困难,我不想让读者为此烦恼。然而,面向对象思维的第一步已经建立。我建议任何尚未完全熟悉封装原则的人先练习它,然后再深入研究面向对象设计。还要学习如何使用继承和组合[^],我在本文中甚至没有讨论过。使用和理解接口[^]和多态性[^]至关重要。如果所有提及的术语对你来说都像是胡言乱语,那么请先学习这些。如果你想了解更多关于面向对象设计和设计模式的信息,除了文章中已引用的文献外,我推荐以下文章:

祝您好运!
© . All rights reserved.