实现接口时聚合中为何不能用基接口具体实现?违反哪条OOP原则?
咱们先把问题里的核心代码摆出来,方便理清场景:
public interface IValetKeyResponse { IStorageEntitySas Sas { get; set; } string UploadUrl { get; set; } } public class ValetKeyResponse : IValetKeyResponse { // 这里就是问题所在:把接口规定的抽象类型换成了具体实现 public StorageEntitySas Sas { get; set; } }
为什么聚合关系里不能这么做?
当你实现一个接口时,接口就相当于和调用方签了一份契约:我承诺提供这些属性/方法,它们的类型和行为都严格按照接口定义来。
在这个场景里,IValetKeyResponse规定Sas属性是IStorageEntitySas类型——这意味着任何依赖这个接口的代码,都期望能给Sas赋值任何实现了IStorageEntitySas的类(比如以后可能新增的AzureStorageEntitySas或者LocalStorageEntitySas),同时也能通过Sas调用IStorageEntitySas定义的所有方法。
但你在ValetKeyResponse里把Sas改成具体的StorageEntitySas后,这个契约就被打破了:调用方再也不能给Sas赋值其他IStorageEntitySas的实现,只能用StorageEntitySas,直接限制了代码的灵活性和扩展性。
这违反了哪条OOP原则?
最直接违反的是里氏替换原则(Liskov Substitution Principle, LSP)。这条原则的核心逻辑很简单:子类/接口实现类必须能完全替换掉父类/接口,而不会让程序出问题。
具体到这个例子:
- 假设某段代码依赖
IValetKeyResponse,它做了这样的操作:response.Sas = new AnotherStorageSas()(AnotherStorageSas实现了IStorageEntitySas) - 如果把
response换成ValetKeyResponse,这段代码会直接编译报错,因为ValetKeyResponse的Sas只接受StorageEntitySas类型 - 这就说明
ValetKeyResponse不能安全地替换IValetKeyResponse,完全违反了里氏替换原则。
另外,这也间接违背了依赖倒置原则(Dependency Inversion Principle, DIP):依赖倒置要求我们依赖抽象而不是具体实现,而你的实现把接口的抽象依赖硬改成了具体类依赖,让所有使用ValetKeyResponse的代码都被迫和StorageEntitySas耦合,以后要替换存储实现会变得非常麻烦。
内容的提问来源于stack exchange,提问作者Abhijeet

