单一职责原则
单一职责原则的英文是 Single Responsibility Principle,缩写为 SRP。这个原则的英文描述是这样的:A class or module should have a single reponsibility。一个类或者模块只负责完成一个功能。
从定义可知,在开发中不要设计大而全的类,要设计粒度小、功能单一的类。如果一个类包含了两个或者两个以上业务不相干的功能,那我们就说它职责不够单一,应该将它拆分成多个功能更加单一、粒度更细的类。
单一职责原则的优点:
- 类的复杂性降低,实现什么职责都有明确的定义;
- 逻辑变得简单,类的可读性提高了,而且,因为逻辑简单,代码的可维护性也提高了;
- 变更的风险降低,因为只会在单一的类中的修改。
举个例子:
我们在做一个项目需要管理用户信息,我们有一个用户信息的类:
public class User{
//修改用户信息
public void Update(Userinfo userinfo){}
//禁用某个用户
public void DisableByUID(int uid){}
}
如果这时需要增加一个给客户发送邮件通知的功能,可能会写成下面这样
//错误的例子
public class User{
//修改用户信息
public void Update(Userinfo userinfo){}
//禁用某个用户
public void DisableByUID(int uid){}
//发送邮件通知
public void SendEmail(){}
}
这种写法就给User类增加了过多的功能,也不利于后期维护和复用,比方说管理员也需要增加发送邮件通知,比方说需要再增加发送短信通知,所以符合单一职责的写法就是将通知功能独立到一个类中
//用户类
pubilc class User{
//修改用户信息
public void Update(Userinfo userinfo){}
//禁用某个用户
public void DisableByUID(int uid){}
}
//通知类
public class Notification{
//发送消息
//MessageType 1:短信,2:邮件
public void SendMessage(int MessageType, Message message){}
}
//消息体
public class Message{
//发送人
public string SendFrom {get;set;}
//接受人
public string SendTo{get;set;}
//消息内容
public string Content{get;set;}
//发送时间
public datetime SentTime{get;set;}
}
开闭原则
一个软件实体如类、模块和函数应该对扩展开放,对修改关闭
一个软件实体应该通过扩展来实现变化,而不是通过修改已有的代码实现变化。之所以不建议修改已有代码,是因为一旦对原有的代码进行修改,就有可能影响到原有的模块,引起bug,修改完还需要对项目中所有涉及到该模块的功能进行测试,徒增工作量。让对象对扩展开放,这要求我们要对需求的变更有一定的前瞻性和预见性,在实现层面,让代码依赖于抽象而非细节。比方说我们做的项目是使用SqlServer数据库,但我们知道该项目后期也可能更换成mysql数据库,就可以向下面这样实现
interface IDatabase{
void Insert();
void Delete();
void Update();
void Select();
}
public class SqlServerDB : IDatabase{
public void Insert();
public void Delete();
public void Update();
public void Select();
}
public class MySqlDB : IDatabase{
public void Insert();
public void Delete();
public void Update();
public void Select();
}
//业务层在调用时,依赖抽象,也可以通过配置参数来实现实例化不同的数据库操作类
IDatabase db = new SqlServerDB();
db.Delete();
里氏替换原则
如果对每一个类型为T1的对象o1,都有类型为T2的对象o2,使得以T1定义的所有程序P在所有对象o1都替换成o2的时候,程序P的行为都没有发生变化,那么类型T2是类型T1的子类型。
说人话就是:
只要父类能出现的地方子类就可以出现。
这个原则定义了4层含义:
- 子类可以实现父类的抽象方法,但是不能覆盖父类的非抽象方法。
- 子类可以有自己的个性,可以有自己的属性和方法。
- 子类覆盖或重载父类的方法时输入参数可以被放大。
- 子类覆盖或重载父类的方法时输出结果可以被缩小,也就是说返回值要小于或等于父类的方法返回值。
在软件开发过程中,子类替换父类后,程序的行为应该是一样的。只有当子类替换掉父类后,此时软件的功能不受影响,父类才能真正地被复用,子类才能在父类的基础上添加新的行为。下面是一个错误的示范:
public class C {
public int func(int a, int b){
return a+b;
}
}
public override class C1 : C{
public int func(int a, int b) {
return a-b;
}
}
public class Client{
public static void main(String[] args) {
C c = new C1();
Console.WriteLine("2+1=" + c.func(2, 1));
}
}
运行结果:2+1=1
子类重写了父类C的func方法,导致引用父类的地方并不能透明的使用子类的对象,违背里氏替换原则。正确的做法是在子类中重新写一个心的方法:
public class C {
public int func(int a, int b){
return a+b;
}
}
public class C1 : C{
public int func2(int a, int b) {
return a-b;
}
}
public class Client{
public static void main(String[] args) {
C1 c = new C1();
Console.WriteLine("2-1=" + c.func2(2, 1));
}
}
依赖倒置原则
高层模块不应该依赖底层模块,两者都应该依赖其抽象;
抽象不应该依赖细节;
细节应该依赖抽象;
在C#中点抽象指的是接口和抽象类,两者都是不能直接被实例化的;细节就是实现类,实现接口或继承抽象类而产生的类就是细节,特点就是可以直接被实例化。所以该原则可以这么理解:
模块间的依赖通过抽象发生,实现类之间不发生直接的依赖关系,其依赖关系是通过接口或抽象类产生的
接口或抽象类不依赖于实现类
实现类应该依赖接口或抽象类
依赖倒置原则基于这样一个事实:相对于细节的多变性,抽象的东西要稳定的多。以抽象为基础搭建起来的架构比以细节为基础搭建起来的架构要稳定的多。依赖倒置原则的核心思想是面向接口编程,如果理解了面向接口编程的概念也就自然掌握依赖倒置原则,网上有个读书读报纸的例子很形象的解释了该原则:
class Book{
public String getContent(){
return "从前有座山,山里有个庙……";
}
}
class Mother{
public void narrate(Book book){
Console.WriteLine("妈妈开始讲故事");
Console.WriteLine(book.getContent());
}
}
public class Client{
public static void main(String[] args){
Mother mother = new Mother();
mother.narrate(new Book());
}
}
如果现在要增加读报纸的功能
class Newspaper{
public String getContent(){
return "今日热点新闻";
}
}
这时不得不修改Mother类才能读报纸,这样不科学,所以我们引入接口的概念来重构这段代码
interface IReader{
public String getContent();
}
定义个读的接口,然后让读书和读报纸都继承该接口,此时Mother就能读各种内容了
class Newspaper implements IReader {
public String getContent(){
return "今日热点新闻";
}
}
class Book implements IReader{
public String getContent(){
return "从前有座山,山里有个庙……";
}
}
class Mother{
public void narrate(IReader reader){
Console.WriteLine("妈妈开始讲故事");
Console.WriteLine(reader.getContent());
}
}
public class Client{
public static void main(String[] args){
Mother mother = new Mother();
mother.narrate(new Book());
mother.narrate(new Newspaper());
}
}
接口隔离原则
客户端不应该依赖它不需要的接口,类间的依赖关系应该建立在最小的接口之上
接口隔离原则将非常庞大、臃肿的接口拆分成更小更具体的接口。一个接口如果定义的过于臃肿,则代表它的每一个实现类都要考虑所有的实现逻辑,无形增加了维护的成本。
举例来说,所有云服务供应商都与阿里云一样提供相同种类的功能。但当你着手为其他供应商提供支持时, 程序库中绝大部分的 接口会显得过于宽泛。其他云服务供应商没有提供部分方法 所描述的功能。
那腾讯云中没有接口定义的3个方法,这种情况就是违反了接口隔离原则,正确的做法是将接口拆分成更小的粒度,如下:
迪米特法则
一个对象应该对其他对象有最小的了解。如果两个类不必彼此直接通信,那么这两个类就应当发生直接的相互作用。如果其中一个类需要调用另一个类的某一个方法的话,可以通过第三者转发这个调用。
迪米特法则强调的前提是在类的结构设计上,每一个类都应当尽量降低成员的访问权限,也就是说,一个类包装好自己的private状态,不需要让别的类知道的字段或行为就不要公开,其实就是在封装一个类的时候尽量明确这个类的属性和操作属性的方法。迪米特法则的根本思想,是强调类自己的松耦合。类之间的松耦合越弱,越有利于复用。
来看个错误的例子:
public class LODErrorTest {
public static void main(String[] args) {
Phone phone = new Phone();
phone.readBook();
}
}
/**
* 错误的示范
*/
public class Phone {
App app = new App();
//关键是下面这行代码
Book book = new Book("设计模式");
public void readBook() {
app.read(book);
}
}
public class App {
public void read(Book book) {
Console.WriteLine(book.getTitle());
}
}
public class Book {
private String title;
public Book(String title) {
this.title = title;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
}
在手机类中,手机和书籍是没有关系的,书籍应该是在阅读软件里面,所以应该将手机与书籍隔离开,正确的写法是这样子的:
public class LODRightTest {
public static void main(String[] args) {
Phone phone = new Phone();
phone.readBook();
}
}
/**
* 正确的示范
*/
public class Phone {
private App app = new App();
public void readBook() {
app.read();
}
}
public class App {
private Book book = new Book("设计模式");
public void read() {
Console.WriteLine(book.getTitle());
}
}
public class Book {
private String title;
public Book(String title) {
this.title = title;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
}