四、 接口隔离原则(Interface Segregation Principe,ISP)
类的依赖关系应建立在最小接口上,不要都塞在一起。即客户端不应该依赖它不需要的接口。
根据上面的定义可以看出,对接口的建立要最小化,而不是依赖所有功能都塞在一起的大而全的接口。换种说法就是,方法尽量要细化,要少。当然,也不要拆分成一个一个的,而是要把一些功能紧密绑的方法封装起来,不要暴露太多细节。哇,这是不单一职责吗?不对,它们的审视角度是不同的,一个是接口的依赖,要求接口的方法尽量要少;一个是职责的分离,类和接口的职责要单一。接口隔离原则就是要求只提供尽可能小的接口,需要高内聚,不需要的行为要隐藏起来
。当然,拆分接口时,也需要满足单一职责原则。
来看一个手机的例子吧,手机在例子的世界还是很火爆的,现在是至少人手一部了。看图4.1
定义了一个手机的接口,手机可以打电话,发短信,上网,玩游戏等,然后使用了一个场景类People来使用手机。看起来是不是很完美,但仔细想想,ICellphone 这个接口有没有最优设计,是不是功能有点太多了,当然,单从单一职责上考滤是没有问题的,加上接口隔离原则的话,就不同了。时间回到手机刚诞生的年代,那时候的手机是不是只能打个电话,发个短信。手机在迭代的过程中,才出现了各种功能,上网,玩游戏,在线支付等,当今平板的出现,除了上网等高级功能外,基础的打电话,发短信功能反而没有了。这样功能都封装在一起,是不是就过度了,而且都暴露到了使用场景中,不就有问题了吗。那我们根据接口隔离原则重新修改一下类图,如图4.2所示
这样,不管以后使用什么手机,都可保持接口的稳定。需要什么,就继承什么,如果是平板,就不需要电话和短信,保持了接口的最小化。来看一下代码实现:
///
/// 基础手机功能
/// 打电话、发短信
///
public interface IBaseCellphone
{
void Call();
void Text();
}
///
/// 上网玩游戏的功能
///
public interface IOnlineGameCellphone
{
void Online();
void PlayGame();
}
///
/// 现代手机,即可打电话,短信,还可以上网玩游戏
///
public class Cellphone : IBaseCellphone, IOnlineGameCellphone
{
public void Call()
{
Console.WriteLine("打电话");
}
public void Text()
{
Console.WriteLine("发短信");
}
public void Online()
{
Console.WriteLine("手机已连网");
}
public void PlayGame()
{
Console.WriteLine("开始玩游戏");
}
}
场景中使用
///
/// 现实生活中的场景,使用手机
///
public class People
{
public int Id { get; set; }
public string Name { get; set; }
///
/// 一些人只使用手机的基本功能
///
///
public void UsePhone(IBaseCellphone cellphone)
{
Console.WriteLine("我是 {0},我只用基础的功能", this.Name);
cellphone.Call();
cellphone.Text();
}
///
/// 只想上网玩游戏
///
///
public void PlayOnlineGame(IOnlineGameCellphone cellphone)
{
Console.WriteLine("我是 {0},我只想上网玩游戏", this.Name);
cellphone.Online();
cellphone.PlayGame();
}
}
五、迪米特法则(Law of Demeter,LOD)
一个对象应尽可能少的了解其它对象
迪米特法则也称最少知识原则(Least Knowledge Principle,LKP),名字虽不同,规则确是同样的。说的都是类与类之间的关系,一个类在使用其它类时,不需要知道其细节,知道的越少越好,你内部如何复杂多变,都是你自己的事情,我不关心,我只关心调用方法,取得结果,其它的一概不关心。迪米特法则的指导思想就是使类与类之间保持松耦合的关系
。主要包括如下:
- 不跟非直接的朋友说话
- 只是内部服务的属性,要封闭到内部
- C#中
[Serializable]
特性,尽量少用,牵涉到序列化的问题
还是用实例来说话吧,有一个关于学校的日常故事,老师让班长多关注下同学们的学习情况,有没有好好听课,认真写作业。让我们来用程序实现一下,先看类图,如图5.1所示:
分别定义了老师,班长,学生三个角色,Teacher通过Command方法让Monitor检查学生的学习情况,Monitor通过CheckLearn方法检查学生,来看下代码实现:
///
/// 学生
///
public class Student
{
public int Id { get; set; }
public string Name { get; set; }
}
///
/// 班长
///
public class Monitor
{
public void CheckLearn(List students)
{
students.ForEach(item =>
{
//检查有没有好好学习
var learn = new Random(Guid.NewGuid().GetHashCode()).Next(0, 2) == 1 ? "有" : "没有";
Console.WriteLine($"{item.Name},{learn}好好学习");
});
}
}
///
/// 老师
///
public class Teacher
{
public void Command(Monitor monitor)
{
//初始化学生
var list = new List
{
new Student {Id = 1, Name = "张三"},
new Student {Id = 2, Name = "李四"},
new Student {Id = 3, Name = "王五"},
new Student {Id = 4, Name = "赵六"},
new Student {Id = 5, Name = "陈七"}
};
//班长检查学习情况
monitor.CheckLearn(list);
}
}
场景中调用:
class Program
{
static void Main(string[] args)
{
try
{
var teacher = new Teacher();
teacher.Command(new Monitor());
}
catch (Exception e)
{
Console.WriteLine(e.Message);
}
Console.Read();
}
}
运行结果:
张三,没有好好学习
李四,有好好学习
王五,有好好学习
赵六,有好好学习
陈七,没有好好学习
整个过程都实现了,貌似没有什么问题,但回过头来想想,老师有几个直接的朋友类,这里面其实就只有一个,那就是班长,直接告诉的班长。那老师也依赖了学生啊,不也是朋友类吗?迪米特法则告诉我们,像这种方法体内的类不属于朋友类,一个类只和朋友说话,班长才直接和学生有交流。Command中出现List
断开了非直接的朋友,学生的学习情况也内聚到自身,具体看一下代码实现:
///
/// 学生
///
public class Student
{
public int Id { get; set; }
public string Name { get; set; }
private readonly Random _random = new Random(Guid.NewGuid().GetHashCode());
private int _score; //只是内部服务的属性,要封闭到内部
private const int GoodScore = 180;
public void Learn()
{
this.Lesson();
this.Homework();
var learnString = this._score > GoodScore ? "有" : "没有";
Console.WriteLine($"{this.Name},{learnString}好好学习");
}
///
/// 听课
/// 内部方法,尽量减少公开的方法和属性
///
private void Lesson()
{
this._score += _random.Next(150);
}
///
/// 写作业
///
private void Homework()
{
this._score += _random.Next(150);
}
}
///
/// 班长
///
public class Monitor
{
public List StudentList { get; set; }
public void CheckLearn()
{
StudentList.ForEach(item =>
{
//只关心调用方法,不需要知道细节
item.Learn();
});
}
}
///
/// 老师
///
public class Teacher
{
public void Command(Monitor monitor)
{
//断开了对Student的依赖,减少依赖,不跟非直接的朋友说话
//班长检查学习情况
monitor.CheckLearn();
}
}
场景中调用:
class Program
{
static void Main(string[] args)
{
try
{
//初始化学生
var studentList = new List
{
new Student {Id = 1, Name = "张三"},
new Student {Id = 2, Name = "李四"},
new Student {Id = 3, Name = "王五"},
new Student {Id = 4, Name = "赵六"},
new Student {Id = 5, Name = "陈七"}
};
var teacher = new Teacher();
teacher.Command(new Monitor {StudentList = studentList});
}
catch (Exception e)
{
Console.WriteLine(e.Message);
}
Console.Read();
}
}
六、开闭原则(Open Closed Principle,OCP)
一个软件实体应当对扩展开放,对修改封闭
一个软件或系统在开发的过程中,以及上线生产后,随着时间的推移,都会产生变化,这是铁定的事实。那我们在设计时就要尽量适应这些变化,来提高系统的稳定性和灵活性,开闭原则就是说一个软件实体应该通过扩展来实现变化,而不是通过修改已有的代码来实现变化。开闭原则指导我们如何建立一个稳定的、灵活的系统
。
开闭原则是最基础的原则,也是最重要的面向对象设计原则,在面向对象的开发时,都会提到的原则。开闭原则就是一个目标,其它五大原则都是实现手段,它就像一个口号,你们都要奔着这个口号来。要满足开闭原则,就需要对系统进行抽象化设计,抽象化是开闭原则的关键,换句说法就是对系统进行抽象约束。
每个人心中,都有一个武侠梦,梦想到有朝一日,能成为一个侠客。如今,各种武侠类游戏也层出不穷,好多人都喜欢玩,毕竟有个圆梦的地方了。游戏中,有各种门派,如少林,武当,玩家选择各自喜欢的门派,玩的美滋滋的。我们来实现下这个过程,类图如6.1所示
上图中,Player可以玩游戏了,武当、少林都能玩,可是游戏是会迭代的,有一天上了一个新版本,加入了一个新的门派,不但要修改Player类,还要更改场景中的处理,违反了开闭原则。重构一下,如类图6.2所示
增加了一个门派接口,各门派需要实现接口。Player只依赖接口,通过场景类来确定玩家玩什么门派,来看下代码实现:
public interface IFaction
{
void Characteristic();
}
public class WuDang : IFaction
{
public void Characteristic()
{
Console.WriteLine("武当派是攻击系的,内功远程群攻,很6。");
}
}
public class ShaoLin : IFaction
{
public void Characteristic()
{
Console.WriteLine("少林派是防御系的,内功防御,你们都打不疼我。");
}
}
public class Player
{
public void PlayFaction(IFaction faction)
{
Console.WriteLine($"玩家开始玩游戏,选择的是{faction.GetType().Name}");
faction.Characteristic();
}
}
场景中调用:
class Program
{
static void Main(string[] args)
{
try
{
var player = new Player();
//玩家玩武当
player.PlayFaction(new WuDang());
//玩家玩少森
player.PlayFaction(new ShaoLin());
}
catch (Exception e)
{
Console.WriteLine(e.Message);
}
Console.Read();
}
}
很简单,运行结果就不展示了。玩什么,就调用什么,如果新增一个门派峨眉,代码如下所示:
public class EMei : IFaction
{
public void Characteristic()
{
Console.WriteLine("峨眉派是治愈系的,医术很高,有起死回生之力。");
}
}
场景中调用:
class Program
{
static void Main(string[] args)
{
try
{
var player = new Player();
//新门派上线了,我要试试
player.PlayFaction(new EMei());
}
catch (Exception e)
{
Console.WriteLine(e.Message);
}
Console.Read();
}
}
看到了吧,是不是很容易,加一个门派,只在场景中很少的改动,就新增了一个门派,都是通过扩展来实现的,原有的类都未进行改动过。这就是开闭原则,对扩展开放,对修改封闭。
至此,六大原则就说完了,写出来还真是挺不容易的,比想象中的要困难的多,如有什么错误或问题讨论,多多指正和交流。后面的23种设计模式才是真枪实弹,坚持!