实际项目中面向对象的最佳实践
1.让"行为"驱动设计
进行设计的第一步,我会在纸上记录下要需求分析中要实现的几个功能,然后,试着在超级记事本里面用伪c#代码实现他,此时,一般可以确定出需要的类,以及类之间的关联。
实践:在敏捷软件开发里面,这一般叫做测试驱动开发。只不过,在实际项目中,可能由于种种原因,不能先写单元测试,或者根本就不能写单元测试,此时最好的办法就是在记事本里用伪c#代码来代替实际的单元测试。
2.解耦:一个用于解耦的通用方案
对于某些易于变化的业务,需要对于进行抽象以便隔离这种变化,一个简单而通用的办法是定义一个抽象类或者接口来把这种业务抽象出来。
实践1)需求分析中那些“可以这样做,也可以这样做,如果....就这样,否则,就那样”,这样的需求一般都是易于变化的,当然,也有可能不变化,但是,最好还是对齐进行一个抽象,否则日后需求真的发生了改变,在改变架构设计就异常痛苦了。
实践2)在C#中,一旦定义了一个抽象类或者接口,肯定不会将这个抽象类和接口放在一个程序集中,因为抽象类和接口是不易于变化的,而派生类是易于变化的,我们可能会增加,删除派生类,这意味着如果将抽象类和他的派生类放到一个程序集中,修改派生类就会导致整个程序集的重编译,发布。
实践3)循环依赖,实际架构中,或多或少都会遇到循环依赖的问题。考虑如下场景:
上图中,接口B单独放到了一个程序集InterfaceAssembly中,他的派生实现放在了另一个单独的程序集DerivedAssembly中,这个程序集中的B1恰好要使用ClientAssembly程序集中定义的A类。现在如果程序集ClientAssembly要使用B1类,那么他就必须引用DerivedAssembly程序集和InterfaceAssembly程序集,循环依赖出现了。这在.net中是不允许的。
解决这种问题没有一个固定的方案,具体做法要依赖于具体的需求,来决定将哪些类移动到哪个程序集中,或者在定义一个间接的抽象层。
3.考虑使用简单工厂方法来代替new。
这是我非常喜爱的一个实践。在我写的代码里,永远不会在客户端里出现new关键字。而是定义一系列简单工厂方法,如下所示:
public void TestOpCodeNew(){ OpCode Opcode=Opcode.CreateOpcode(); } public class OpCode { //派生类可能会调用这个默认构造函数 protected OpCode() { } public OpCode CreateOpCode() { return new OpCode(); } public OpCode CreateOpCode(byte[] value, string name) { return new OpCode(value,name); } protected OpCode(byte[] value, string name) { _value = value; _name = name; } byte[] _value; string _name; public byte[] Value { get; set; } public string Name { get; set; } }
这种使用方式,不会增加任何复杂性,他带来的好处是相当客观的。譬如,你可以很轻易的就将Opcode升级到一个工厂模式或者抽象工厂模式,而不用修改任何引用OpCode的代码。如下图所示:
现在,将OpCode升级为一个抽象类时,所有使用OpCode的代码都不需要修改。我们只需要修改CreateOpCode的内部实现即可,从而他返回某个具体的派生类。
4.我对C#对象初始化器的看法
为了更方便的初始化对象,C#定义了对象初始化器。如下:
OpCode opcode=new OpCode(){Value=0x0,Name="nop"};
在实际项目中,这种初始化方式随处可见,实际上,对象初始化器本质上是为.net3.5中的匿名对象设计的。
与使用构造函数初始化对象相比,这种对象初始化器的好处如下:
1)一眼就能看出要初始化的是那些属性,譬如0x0是赋值给Value,"nop"是赋值给Name属性。
2)太方便了,简直就不用构造函数了,而起,vs的只能提示也相当给力!!!
但是,他的缺点也暴露无疑:
1)属性必须是可读写的,换句话说,必须带有set属性器。
2)由于某些属性可能不会进行修改,所以,对其公开一个set属性会带来很大的危害,因为有些人很有可能在创建好一个对象之后由修改他。举个例子,订单号一旦生成就不可更改,但是如果你为订单号设置了set属性,那么就很有可能有人在初始化一个订单之后在修改订单号。
实践:
1)除非使用匿名对象,否则永远不要使用对象初始化器。
2)如果想要使用对象初始化器,考虑哪些属性是可变的,哪些属性是不变的,只对可变的属性使用对象初始化器,不可变的属性使用构造函数或者简单工厂方法。如下:
//这里假设订单号是不变的,所以使用构造函数来初始化,而联系人姓名是可变的(下订单后可以修改联系人姓名),所以使用对象初始化器来对其进行初始化。 Order newOrder=new Order("123455"){ContactName="francis"}; public class Order{ public Order(int id){ } //由于订单ID不可修改,所以不设置set属性器 public string OrderID{ get{return _orderID;} } public string ContactName{ get{return _contactName;} set{_contactName=value;} } string _orderID; string _contactName; }
3)使用某些代码生成器生成的代码中,很有可能会对所有属性都提供get和set,此时最好最好开发一个适用于自己的简单的代码生成器。
5.不要在派生类中使用基类的字段。
尽量不要在派生类中直接访问基类中定义的字段,而是通过属性来访问,如下:
public Base{ private int a; protected Int A{get;set;} } publi Derived:Base{ public void SomeMethod{ //base.a;//尽量不要直接访问基类中的字段 base.A; } }
如果在派生类中直接访问字段,那么如果修改基类中的字段,就会影响到派生类,当然了vs提供了强大重构功能,可能简单的重构一下就OK了。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
· 单线程的Redis速度为什么快?
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 展开说说关于C#中ORM框架的用法!
· SQL Server 2025 AI相关能力初探
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库