SOLID:依赖反转原则
依赖反转原则是SOLID原则的最后一个原则。它有助于实现松耦合。
依赖反转原则指出:
现在的问题是,什么是高层模块和低层模块,什么是抽象?
高层模块是一个使用其他模块(类)来执行任务的模块(类)。低层模块包含某个特定任务的详细实现,可供其他模块使用。高层模块通常是应用程序的核心业务逻辑,而低层模块是输入/输出、数据库、文件系统、Web API或其他与用户、硬件或其他系统交互的外部模块。
抽象是非具体的事物。抽象不应该依赖于细节,而细节应该依赖于抽象。例如,抽象类或接口包含需要在具体类中实现的方法声明。这些具体类依赖于抽象类或接口,反之则不然。
现在,我们如何知道一个类依赖于另一个类?
如果一个类创建了另一个类的对象,你就可以识别出它依赖于另一个类。你可能需要添加命名空间的引用才能编译或运行代码。
让我们使用以下示例来理解DIP
public class Student
{
public int StudentId { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public DateTime DoB { get; set; }
//tight coupling
private StudentRepository _stdRepo = new StudentRepository();
public Student()
{
}
public void Save()
{
_stdRepo.AddStudent(this);
}
}
public class StudentRepository
{
public void AddStudent(Student std)
{
//EF code removed for clarity
}
public void DeleteStudent(Student std)
{
//EF code removed for clarity
}
public void EditStudent(Student std)
{
//EF code removed for clarity
}
public IList<Student> GetAllStudents()
{
//EF code removed for clarity
}
}
上面的Student类创建了StudentRepository
类的对象,用于对数据库执行CRUD操作。因此,Student
类依赖于StudentRepository
类进行CRUD操作。Student
类是高层模块,StudentRepository
类是低层模块。
这里的问题是,Student
类使用new
关键字创建了具体StudentRepository
类的对象,导致两者紧密耦合。这会导致以下问题:
- 在所有地方都使用
new
关键字创建对象是重复的代码。对象创建不在一个地方。这违反了“不要重复你自己”(DRY)原则。如果StudentRepository
类的构造函数发生变化,那么我们需要在所有地方进行更改。如果对象创建在一个地方,那么维护代码将很容易。 - 使用
new
创建对象也使得单元测试不可能。我们无法单独对Student
类进行单元测试。 StudentRepository
类是一个具体类,因此该类中的任何更改都需要同时更改Student
类。
DIP指出高层模块不应该依赖于低层模块。两者都应该依赖于抽象。这里,抽象意味着使用接口或抽象类。
以下是将DIP原则应用于上述示例的结果。
public class Student
{
public int StudentId { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public DateTime DoB { get; set; }
private IStudentRepository _stdRepo;
public Student(IStudentRepository stdRepo)
{
_stdRepo = stdRepo;
}
public void Save()
{
_stdRepo.AddStudent(this);
}
}
public interface IStudentRepository
{
void AddStudent(Student std);
void EditStudent(Student std);
void DeleteStudent(Student std);
IList<Student> GetAllStudents();
}
public class StudentRepository : IStudentRepository
{
public void AddStudent(Student std)
{
//code removed for clarity
}
public void DeleteStudent(Student std)
{
//code removed for clarity
}
public void EditStudent(Student std)
{
//code removed for clarity
}
public IList<Student> GetAllStudents()
{
//code removed for clarity
}
}
上面的StudentRepository
类实现了IStudentRepository
接口。这里,IStudentRepository
是学生相关数据的CRUD操作的抽象。StudentRepository
类提供了这些方法的实现,因此它依赖于IStudentRepository
接口的方法。
Student类不使用new
关键字创建StudentRepository
类的对象。构造函数需要一个IStudentRepository
类的参数,该参数将从调用代码传入。因此,它也依赖于抽象(接口),而不是低层具体类(StudentRepository
)。
这将创建松耦合,并使每个类都可以进行单元测试。Student
类的调用者可以传入实现IStudentRepository
接口的任何类的对象,因此不会与特定的具体类绑定。
public static void Main(string[] args)
{
//for production
Student std1 = new Student(new StudentRepository);
//for unit test
Student std2 = new Student(new TestStudentRepository);
}
}
你可以使用工厂类来创建对象,而不是手动创建,这样所有对象创建都将集中在一个地方。
public class RepositoryFactory
{
public static IStudentRepository GetStudentRepository()
{
return new StudentRepository();
}
public static IStudentRepository GetTestStudentRepository()
{
return new TestStudentRepository();
}
}
public class Program
{
public static void Main(string[] args)
{
//for production
Student std1 = new Student(RepositoryFactory.GetStudentRepository());
//for unit test
Student std2 = new Student(RepositoryFactory.TestGetStudentRepository());
}
}
建议使用依赖注入和IoC容器来创建低层类的对象并将其传递给高层类。