常用的面向对象设计原则(C#版)
对于面向对象设计来说,在支持代码可维护性的同时,需要提高代码可复用性。复用性可以提高开发效率,提高开发质量,节约开发成本,恰当的复用还可以提升可维护性。
设计原则名称 | 定义 | 重要程度 |
---|---|---|
单一职责原则(Single Responsibility Principle, SRP) | 一个对象应该只包含单一的职责,并且该职责被完整地封装在一个类中 | ※※※※ |
开闭原则(Open-Closed Principle, OCP) | 软件实体应当对扩展开放,对修改关闭 | ※※※※※ |
迪米特法则(Law of Demeter, LoD) | 每一个软件单位对其他的单位都只有最少的知识,而且局限于那些与本单位密切相关的软件单位 | ※※※ |
依赖倒置原则(Dependency Inversion Principle, DIP) | 高层模块不应该依赖低层模块,它们都应该依赖抽象。抽象不应该依赖于细节,细节应该依赖于抽象 | ※※※※※ |
接口隔离原则(Interface Segregation Principle, ISP) | 客户端不应该依赖那些它不需要的接口 | ※※ |
合成复用原则(Composite Reuse Principle, CRP) | 优先使用对象组合,而不是继承来达到复用的目的 | ※※※※ |
里氏代换原则(Liskov Substitution Principle, LSP) | 所有引用基类的地方必须能透明地使用其子类的对象 | ※※※※ |
关注1个类如何设计(使类具有高内聚)
1. 单一职责原则
概括:一个类只负责一个功能领域中的相应职责
优点:类的复杂度降低,提高了复用性
缺点:无法精确地度量每个类的具体职责
违反原则例子
class Study
{
//交学费不在学习类的范围当中,违反单一职责原则
public void PayTuition()
{
Console.WriteLine("交学费");
}
public void ReadBook()
{
Console.WriteLine("读书");
}
public void WriteCode()
{
Console.WriteLine("写字");
}
}
符合原则例子
class Study
{
public void ReadBook()
{
Console.WriteLine("读书");
}
public void WriteCode()
{
Console.WriteLine("写字");
}
}
//增加支付类专门用来支付各种费用
class PayMoney
{
public void PayTuition()
{
Console.WriteLine("交学费");
}
public void PayHourseRent()
{
Console.WriteLine("交房租");
}
}
2. 开闭原则
概括:类、模块、函数等等应该可以扩展,但是不可修改。
优点:更容易扩展,提高了扩展性
缺点:容易导致类、模块、函数等等过多
违反原则例子
class Book
{
//如增加一种类型的数据,就要修改这个对象方法
public void GetBookType(int id)
{
if(id == 0)
{
Console.WriteLine("编程语言");
}
else if(id == 1)
{
Console.WriteLine("游戏开发");
}
}
}
符合原则例子
abstract class Book
{
public abstract void GetBookType();
}
class ProgrammeBook : Book
{
public override void GetBookType()
{
Console.WriteLine("编程语言");
}
}
class GameDevelopmentBook : Book
{
public override void GetBookType()
{
Console.WriteLine("游戏开发");
}
}
//若有新类型书籍,无需修改原对象代码,只需要增加类
class MultiMediaBook : Book
{
public override void GetBookType()
{
Console.WriteLine("多媒体");
}
}
关注类之间关系如何设计(使类之间具有低耦合)
1. 迪米特法则-两个类尽量不要发生关系
概括:要求类应当尽可能少的与其他类发生相互作用
优点:减小类之间的依赖,降低类之间的耦合性
缺点:出现大量方法用来间接调用,造成模块间通信效率降低,不容易协调。
违反原则例子
class Student
{
//类中包含其他类
private Dad myDad = new Dad();
public void RequestPay()
{
myDad.PayTuition();
}
}
class Dad
{
public void PayTuition()
{
Console.WriteLine("交学费");
}
}
class School
{
//类中包含其他类
private List<Student> stuList;
public void WarnPay()
{
foreach(Student stu in stuList)
{
stu.RequestPay();
}
}
}
符合原则例子
class Student
{
public void RequestPay(Dad myDad)
{
myDad.PayTuition();
}
}
class Dad
{
public void PayTuition()
{
Console.WriteLine("交学费");
}
}
class School
{
//客户端可声明后调用,不再需要对学生集合进行赋值
public void WarnPay(List<Student> stuList)
{
foreach(Student stu in stuList)
{
stu.RequestPay(new Dad());
}
}
}
2. 依赖倒置原则-非要有关系,语法要依赖抽象
概括:要求抽象不应该依赖于细节,细节应该依赖于抽象;要针对接口编程,不要针对接实现编程
优点:提高稳定性,降低代码修改带来的风险
缺点:抽象要以项目为度,一般抽象难度较大
违反原则例子
class Cat
{
public void Move()
{
Console.WriteLine("猫会动");
}
}
class Dog
{
public void Move()
{
Console.WriteLine("狗会动");
}
}
class Program
{
static void Main(string[] args)
{
//若是Dog类,要重新声明一个对象
Cat cat1 = new Cat();
cat1.Move();
Console.Read();
}
}
符合原则例子
abstract class Animal
{
public abstract void Move();
}
class Cat:Animal
{
public override void Move()
{
Console.WriteLine("猫会动");
}
}
class Dog:Animal
{
public override void Move()
{
Console.WriteLine("狗会动");
}
}
class Program
{
static void Main(string[] args)
{
//若是Dog类只需重新赋值Dog对象
Animal an1 = new Cat();
an1.Move();
Console.Read();
}
}
3. 接口隔离原则-若有关系,语义上要仅依赖自己需要的服务
概括:要求客户端不应该依赖那些它不需要的接口,即将一些大的接口细化成一些小的接口供客户端使用
优点:分解成多个小接口,提高代码复用性
缺点:定义过小的接口,造成接口数目过多,使设计复杂化
违反原则例子
class Person
{
public void LetAnimalInRefrigerator(string animalName)
{
//函数内容极容易复用
Console.WriteLine("打开冰箱");
Console.WriteLine("将" + animalName + "放进冰箱");
Console.WriteLine("关闭冰箱");
}
}
符合原则例子
class Person
{
public void LetAnimalInRefrigerator(string animalName)
{
OpenRefrigerator();
Console.WriteLine("将" + animalName + "放进冰箱");
CloseRefrigerator();
}
public void OpenRefrigerator()
{
Console.WriteLine("打开冰箱");
}
public void CloseRefrigerator()
{
Console.WriteLine("关闭冰箱");
}
//新增的方法就可以复用上面两个方法
public void TakeSomethingInRefrigerator(string thingName)
{
OpenRefrigerator();
Console.WriteLine("将" + thingName + "拿出");
CloseRefrigerator();
}
}
4. 合成复用原则-若要复用,优先使用关联关系
概括:要求复用时尽量使用对象组合,而不使用继承
优点:降低类之间的耦合度,类之间的影响较小
缺点:通过组合/聚合的对象,容易产生过多的对象
违反原则例子
class Person
{
public void Move()
{
Console.WriteLine("走...");
}
}
class Student : Person
{
public void GoSchool()
{
Move();
Console.Write("到学校");
}
}
class Laborer : Person
{
//由于继承导致多出Move方法
public void GoCompany()
{
Console.Write("跑...到公司");
}
}
符合原则例子
interface IMoveable
{
void Move();
}
class Student : IMoveable
{
public void Move()
{
Console.WriteLine("走...到学校");
}
}
class Laborer : IMoveable
{
public void Move()
{
Console.WriteLine("跑...到公司");
}
}
class Tyler
{
public void Move(IMoveable iMoveable)
{
iMoveable.Move();
}
}
class Program
{
static void Main(string[] args)
{
//通过接口,使类更加灵活
IMoveable iMoveable = new Laborer();
Tyler tyler = new Tyler();
tyler.Move(iMoveable);
Console.Read();
}
}
5. 里氏替换原则-非要使用继承,需满足该原则
概括:可以通俗表述为如果能够使用基类对象,那么一定能够使用其子类对象
优点:子类可以形似基类,但又与基类不同
缺点:子类必须拥有基类对象的全部属性和方法,包括不需要的
违反原则例子
class Animal
{
protected string name;
public virtual void SetName(string name)
{
this.name = name;
}
public string GetName()
{
return name;
}
}
class Bird:Animal
{
//子类重写了方法,违背了里氏替换原则,使得调用基类方法与子类方法不一样
public override void SetName(string name)
{
this.name = name + "鸟";
}
}
class Program
{
static void Main(string[] args)
{
Animal bird1 = new Animal();
bird1.SetName("布谷");
Console.WriteLine(bird1.GetName());//布谷
Animal bird2 = new Bird();
bird2.SetName("布谷");
Console.WriteLine(bird2.GetName());//布谷鸟
Console.Read();
}
}
符合原则例子
class Animal
{
protected string name;
public void SetName(string name)
{
this.name = name;
}
public string GetName()
{
return name;
}
}
class Bird:Animal
{
//与基类方法不同,且拥有基类的方法
public void SetBirdName(string name)
{
this.name = name + "鸟";
}
}
class Program
{
static void Main(string[] args)
{
Animal bird1 = new Animal();
bird1.SetName("咕咕");
Console.WriteLine(bird1.GetName());//咕咕
Bird bird2 = new Bird();
bird2.SetBirdName("布谷");
Console.WriteLine(bird2.GetName());//布谷鸟
Console.Read();
}
}
因为作者精力有限,文章中难免出现一些错漏,敬请广大专家和网友批评、指正。