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

SOLID 和 DRY 第二部分

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.68/5 (18投票s)

2009年5月25日

CPOL

5分钟阅读

viewsIcon

35077

关于软件开发的 SOLID 和 DRY 首字母缩略词的 2 部分文章的第二部分。本部分涵盖接口隔离和依赖注入。

接口隔离原则

如果我不在乎,就不要让我实现它!

接口隔离原则只是规定您在设计接口时要考虑其他原则。与其创建臃肿的接口,不如将接口隔离成功能内聚的单元。这样,接口的实现就可以专注于对该类重要的内容。

为了更好地说明这一点,请考虑一个数据访问类。一种常见的方法可能是接口化该类,并声明用于加载、保存、更新、删除、搜索、执行列表、安全检查以及其他任何未知操作的方法。然后,您可以创建一个实现了该接口的具体类。结果是一个“胖接口”,它完成了类所需的所有操作,但不一定能实现适当的扩展。

另一个例子是使用 NHibernate 等 ORM。也许您的公司认为这对于基本的 CRUD 操作(创建、读取、更新、删除)很有用,但将使用 LINQ to SQL 或其他一些框架来进行复杂的搜索和排序。通过一个大的“IWidgetDataAccess”接口,您将不得不实现所有方法签名,即使您实际上只对 CRUD 部分执行了具体操作。一种常见的做法是这样做

void MethodNotUsed() 
{
   throw new NotImplementedException();
}

如果您发现自己抛出此异常,这可能是一个很好的迹象,表明您没有遵循接口隔离原则。更好的方法是拆分成两个不同的接口。一个接口是标准的 CRUD 接口,称之为“ICrud”,另一个是类的特定方法集(“IWidget”)。然后,您可以使用具体的 NHibernate 实例来实现 ICrud 部分,并将 IWidget 保存到另一个具体类中。

这实际上回到了 Liskov 替换原则。我不想用管理员的方法来污染我的 IUser 接口,否则每个人都必须实现方法签名(即使只是为了抛出 NotImplementedException)。通过接口隔离,我可以拥有 IUserIAdminUser,然后我的 AdminUser 将实现两者。

public class AdminUser : BaseUser, IUser, IAdminUser

总而言之,接口应该是细粒度的,并专注于将实现接口的客户端类,而不是臃肿的、涵盖所有可能性的接口。

依赖注入或控制反转

依赖于抽象,而不是具体实现。

最后一个原则非常流行但理解不深。我看到很多关于依赖注入和控制反转的讨论,但似乎都集中在实现它的框架(如 StructureMap、Unity、Spring.NET 等)上,而不是原则本身。像其他 SOLID 和 DRY 原则一样,这个原则并不是孤立的,而是与其他原则相互关联的。

这个想法可以追溯到我们的“单一职责”。再次,让我们举个例子。我正在编写我的数据访问类,为了进行故障排除,我想记录问题。我决定使用 log4net,所以我配置了我需要的一切,然后开始在我的类中编写日志信息。

这时,我违反了“单一职责”,因为我的类不是只做一件事(数据访问),而是负责两件事(数据访问和日志记录)。如果我继续沿着这条路走下去,我将最终拥有数十甚至数百个类,其中嵌入了 log4net 日志记录。然后我从当权者那里收到通知,说我们的项目不能使用任何类型的开源。我们决定转移到 Enterprise Library。现在我有很多工作要做,因为我非常依赖具体的日志实现,以至于我必须修改每个类。

通过专注于抽象,我可以改为允许一个 ILogger 接口并对其进行编码。我不再让我的类负责日志记录,而是它只接受日志记录器接口(ILogger)的抽象,并依赖其他东西使其具体化。这就是“依赖注入”的由来:具体实现的依赖由其他东西注入。这也解释了“控制反转”是什么——我的类不再控制日志记录,而是我反转了控制权,让链条上更高层的东西来决定。

有关依赖注入的更详细解释,请阅读 简化使用依赖注入进行模拟

正如我在那篇文章中提到的,DI 不需要框架。以日志记录为例,我可以在开始时轻松做到这一点,当我想“我们将使用 log4net,但让我先遵循一些牢固的面向对象设计原则并对其进行抽象,以防万一。”

public class Widget
{
    private ILogger _logger;

    public Widget() 
    {
       _logger = LoggerFactory.GetLogger();
    }
 
    public Widget(ILogger logger) 
    {
       _logger = logger;
    }
    ...
}

public static class LoggerFactory 
{
    public static ILogger GetLogger()
    {
       return new Log4NetLogger();
    }
}

正如您所见,不需要框架。我的类在没有人知道要注入什么的情况下也能正常工作,但它们仍然提供构造函数,以便如果我决定迁移到配置或框架,我就可以这样做。我使用工厂模式来获取日志记录器。同样,我们现在是硬编码的。但现在我有可能性。例如,如果我切换到 Enterprise Library,我只需要去一个地方更改我的日志记录器的具体实例。此外,我可能希望在测试期间将日志 Debug.Print,在这种情况下,我创建自定义的 DebugLogger,它使用 Debug.Print 并将其注入用于测试。

摘要

这些概念绝不是“编程法则”,而且有很多变体。我也无意对此进行全面的讨论,但希望触及了表面,并激起了足够多的兴趣和好奇心,促使那些不熟悉的人开始进行更多研究,并将这些方法应用于他们自己的软件。在我看来,您可以给某些概念赋予复杂的名称,或者发明新的“架构”或“设计模式”,但最终,它就是回到基础,并继续编写简单、模块化、易于阅读和理解、可维护的代码,这些代码可以作为构建更强大应用程序的基石。

Jeremy Likness

© . All rights reserved.