设计目标
设计可维护性高,可复用性强的软件。可维护性指的是软件能够被理解、修改、适用及扩展的难易程度,而可复用性指的是软件能够被重复使用的难易程度
设计原则分类
单一职责原则
定义
Single Responsibility Principle(SPR):一个类最好只负责一项事务,只有一个引起它改变的原因。单一职责原则是实现高内聚、低耦合的方式,其关键在于依据需求控制类的粒度大小。
案例
设计一个满足用户需求的智能手表,设计厂商设计了如下的智能手表接口
public interface IWatch { public string Cpu { get; set; } ////// 内存 /// public string Ram { get; set; } /// /// 显示屏尺寸 /// public int Size { get; set; } /// /// 计步 /// /// int StepCount(); /// /// 心率 /// /// int HeartRate(); /// /// 获取时间 /// /// TimeSpan GetTime(); }
生产厂商完成了某款手表的设计
public class HuaWeiWatch : IWatch { public string Cpu { get; set; } public string Ram { get; set; } public int Size { get; set; } public int StepCount() { throw new System.NotImplementedException(); } public int HeartRate() { throw new System.NotImplementedException(); } public TimeSpan GetTime() { throw new NotImplementedException(); } }
以手表为最小粒度去设计在可以保证功能与硬件设施不改变下也是可行的,但是一般情况智能手表的功能各不相同,硬件也略有差距,所以以手表本身为单一职责的目标进行设计接口不能完全满足需求的变更。所以我们将手表的功能与需求分别定义接口规范
public interface IWatchFunction { ////// 计步 /// /// int StepCount(); /// /// 心率 /// /// int HeartRate(); /// /// 获取时间 /// /// TimeSpan GetTime(); } public class WatchFunction : IWatchFunction { public int StepCount() { throw new NotImplementedException(); } public int HeartRate() { throw new NotImplementedException(); } public TimeSpan GetTime() { throw new NotImplementedException(); } }
public interface IWatchHardwareFacility { public string Cpu { get; set; } ////// 内存 /// public string Ram { get; set; } /// /// 显示屏尺寸 /// public int Size { get; set; } } public class WatchHardwareFacility : IWatchHardwareFacility { public string Cpu { get; set; } public string Ram { get; set; } public int Size { get; set; } }
手表实现类如下
public class HuaWeiWatch { public IWatchHardwareFacility WatchHardwareFacility { get; set; } public IWatchFunction WatchFunction { get; set; } public HuaWeiWatch(IWatchHardwareFacility watchHardwareFacility, IWatchFunction watchFunction) { WatchHardwareFacility = watchHardwareFacility; WatchFunction = watchFunction; } }
现在手表实现类如果增加新功能只需要修改IWatchFunction和其实现,修改IWatchHardwareFacility和它的实现。貌似可以解决所有问题了,但是我们的同款智能手表可是会有基础款与pro的区别的,pro会在基础款的基础上多一些扩展功能,依据这个需求又可以将功能分为基本功能与扩展功能,将硬件设施分为基础硬件与扩展硬件。所以有了下面的实现
public interface IWatchBaseFunc { ////// 计步 /// /// int StepCount(); /// /// 心率 /// /// int HeartRate(); /// /// 获取时间 /// /// TimeSpan GetTime(); } public class WatchBaseFunc : IWatchBaseFunc { public int StepCount() { throw new NotImplementedException(); } public int HeartRate() { throw new NotImplementedException(); } public TimeSpan GetTime() { throw new NotImplementedException(); } }
public interface IWatchExtensionFunc { ////// 移动支付 /// void MobilePayment(); } public class WatchExtensionFunc : IWatchExtensionFunc { public void MobilePayment() { throw new System.NotImplementedException(); } }
public interface IWatchBaseHardware { public string Cpu { get; set; } ////// 内存 /// public string Ram { get; set; } /// /// 显示屏尺寸 /// public int Size { get; set; } } public class WatchBaseHardware : IWatchBaseHardware { public string Cpu { get; set; } public string Ram { get; set; } public int Size { get; set; } }
public interface IWatchExtensionHardware { ////// 气压传感器 /// public string Baroceptor { get; set; } } public class WatchExtensionHardware : IWatchExtensionHardware { public string Baroceptor { get; set; } }
上面实现了手表基础款与pro款,如果还有其他更加细致的需求,粒度还可以再减小。所以单一职责的粒度控制最终由需求决定
依赖倒置原则
定义
Dependency Inversion Principle(DIP):高层模块不应该依赖低层模块,它们都应该依赖抽象。抽象不应该依赖于细节,细节应该依赖于抽象
案例
某用户数据系统有Excel数据与Txt文本数据需要转换存储到数据库中
namespace DependencyInversionPrinciple { public class Customer { } }
namespace DependencyInversionPrinciple { public class DatabaseOperation { public static void AddCustomers(IConvertor convertor) { var customers = convertor.DataToCustomers(); Add(customers); } private static void Add(IEnumerablecustomers) { Console.WriteLine("向数据库中添加用户数据"); } } }
namespace DependencyInversionPrinciple { public interface IConvertor { IEnumerableDataToCustomers(); } }
namespace DependencyInversionPrinciple { public class ExcelDataConvertor : IConvertor { public IEnumerableDataToCustomers() { Console.WriteLine("将excel转换成Customers集合"); return new List (); } } }
namespace DependencyInversionPrinciple { public class TxtDataConvertor : IConvertor { public IEnumerableDataToCustomers() { Console.WriteLine("将txt转换成Customers集合"); return new List (); } } }
现在如果我们想要增加一个xml数据只需要添加一个XmlDataConvertor类实现IConvertor接口即可,这样我们的上层数据库操作类就只依赖于IConvertor的抽象对象
namespace DependencyInversionPrinciple { class Program { static void Main(string[] args) { IConvertor convertor = new ExcelDataConvertor(); DatabaseOperation.AddCustomers(convertor); Console.ReadKey(); } } }
当然我们也可以使用Unity等依赖注入容器在配置文件中决定使用哪种数据转换类
开闭原则
定义
Open-Close Principle(OCP):软件实体应该对扩展开放,对修改关闭
案例
在上述依赖倒置原则的例子中,如果我们需要增加新的xml用户数据进行数据库操作
namespace DependencyInversionPrinciple { public class XmlDataConvertor : IConvertor { public IEnumerableDataToCustomers() { Console.WriteLine("将xml转换成Customers集合"); return new List (); } } }
namespace DependencyInversionPrinciple { class Program { static void Main(string[] args) { IConvertor convertor = new XmlDataConvertor(); DatabaseOperation.AddCustomers(convertor); Console.ReadKey(); } } }
在需求变更后,我们只需要扩展一个新的类,其他方法不需要改动下即可完成功能
里氏代换原则
Liskov Substitution Principle(LSP):所有引用基类的地方都应该可以使用子类对象
结合依赖倒置原则,如果细节是依赖于抽象的话,那么子类实现都是基于接口的细节定义。在这种情况下如果没有扩展功能子类是可以在任何时候被其他子类替换掉的。
接口隔离原则
定义
Interface Segregation Principle(ISP):客户端不应该依赖于那些不需要的接口
结合单一职责原则我们可以理解为按照需求我们应该对接口功能过于臃肿的接口进行功能分类,比方说有一个IOrder接口,但是应为业务增长出现生产与销售两个部门,那么IOrder接口理应变成IProductOrder和ISaleOrder
合成复用原则
定义
Composite Reuse Principle(CRP):优先使用对象组合而不是直接继承达到复用目的
使用类间关联关系,即一个类中包含另一个对象。如果类内包含的该对象需要改变,可以对该对象做新的子类实现,子类实现符合里氏代换原则,这样系统仍然符合开闭原则。
迪米特法则
定义
Law of Demeter(LoD):软件单元之间应该减少不必要的联系,类与类之间应该尽量减少耦合
在一个群聊系统中即存在一对一聊天,一对多聊天,也存在多对一的复杂关系。直接让每个Person对象直接和其他相关联加大了系统的耦合度,此时我们可以使用一个中间通讯员类进行消息一对一,一对多,多对一的转发从而降低系统耦合度。