除了前面学习的那些核心原则,还有一些衍生的原则,掌握它们,你将更好的面向对象。不妨称它们为"扩展原则"吧。
迪米特法则:尽量不与无关的类发生关系。
迪米特法则全称Law of Demeter,简称LoD,也称为最少知识原则(Least Knowledge Principle,LKP)。这个原则没什么固定的定义,大体上有这么几种说法:
1. 只与你的朋友说话 2. 不和陌生人说话 3. 对象应该只与必须交互的对象通信
通俗地讲,一个类应该对自己需要调用的类知道得最少,你调用的类的内部是如何实现的,都和我没关系,那是你的事情,我就知道你提供的接口方法,我就调用这么多,其他的一概不关心。
也可以说,不要让类也染上人们之间的那种神秘的暧昧关系。对象之间联系越是简单,则越是容易管理。
具体的从技术上来说,就是要求对象只与下列必须交互的各类对象通信:
(1) 当前对象本身(this); (2) 以参数形式传入到当前对象方法中的对象; (3) 当前对象的成员对象; (4) 如果当前对象的成员对象是一个集合,那么集合中的元素也都是可以交互的; (5) 当前对象所创建的对象。
此外,还需要适当设置对象的访问权限。
正面教材太多了,就看一些反面教材吧:
// 方法链式调用,此种方式在Web页面开发中倒是常用,但是静态语言中似乎不推荐 public void Do() { m_accessor.GetUser().Rename(); } // 无谓的公开方法 class UI { public void Do() { WorkHelper(); } public void WorkHelper() { } }
好莱坞法则:不要调用我,让我调用你。
在前面我们分析对象之间交互的时候,直接调用指的就是直接调用对象的方法,间接调用中很重要的一种就是回调,特别是在异步编程中,好莱坞法则从某种程度上来说,就是等同于在合适的时候,多使用回调函数。
下面是C#版本的事件实现:
public class Program { static void Main(string[] args) { User user = new User(); View ui = new View(user); user.Name = "Hello"; } } delegate void OnNameChange(string name); class View { public View(User user) { user.onNameChanged += user_onNameChanged; } void user_onNameChanged(string name) { Console.WriteLine(name); } } class User { private string m_name; public string Name { get { return m_name; } set { m_name = value; onNameChanged(m_name); } } public event OnNameChange onNameChanged; }
这也是简单的MVC模式中的MV之间的交互方式,View作为事件的接收者,只需要提供好回调函数,当Model部分发生变化时,View自动接收到变化去更新UI(此处只是打印了出来)。
如果这里不使用事件(观察者模式)来实现,那么Model必然要保存View的引用(实际上内部当然还是保存了相关引用的,但是观察者合理采用各种抽象手段安排好了引用管理,比如这个例子中delegate的使用,作为C#中相当弱的耦合关系,它远比直接使用继承,实现接口的耦合性要弱的多),当Model的数据发生变化时,直接调用View的相关方法去更新UI,这种强烈的互相依赖关系对程序来说并不是什么好的做法。而且一旦多个未确定的类似于View的角色对Model的改变感兴趣的时候,直接应用常常难于处理。
电影中常说,单线联系最安全,如此是也。
优先使用组合原则:多使用组合,少使用继承
复用的手段除了继承这种强约束手段,组合这种弱耦合的关系更加灵活。
看一个小例子:
class User { public virtual void PrintType() { } } class Admin : User { public override void PrintType() { Console.WriteLine("Employer"); } } class Programmer : User { public override void PrintType() { Console.WriteLine("Employer"); } } class Manager : User { public override void PrintType() { Console.WriteLine("Employer"); } } class Contractor : User { public override void PrintType() { Console.WriteLine("Temp"); } }
公司的系统中除了Contractor外几乎全是正式员工,打印类型的时候只需要打印Employer即可,而只有Contractor需要打印Temp。
针对这个功能,如果我们设计一个类层次像上面这样,工作是完全正常的。而且当有新的正式员工类型的话,也只需要复制一遍Admin的PrintType方法即可,这里没有违背任何的我们前面介绍的基本或者核心原则。但是我们还是发现了不爽的地方,那就是打印正式员工的代码复制的到处都是,咋办?还是老套路,抽象加封装,再传进来。
public class Program { static void Main(string[] args) { User admin = new Admin(new EmployerPrintor()); admin.PrintType(); User contractor = new Contractor(new TempPrintor()); contractor.PrintType(); } } class User { Printor m_printor; public User(Printor printor) { m_printor = printor; } public virtual void PrintType() { m_printor.PrintType(); } } class Admin : User { public Admin(Printor printor) :base(printor) { } } class Contractor : User { public Contractor(Printor printor) : base(printor) { } } class Printor { public virtual void PrintType() { } } class EmployerPrintor : Printor { public override void PrintType() { Console.WriteLine("Employer"); } } class TempPrintor : Printor { public override void PrintType() { Console.WriteLine("Temp"); } }
对于这个原则,其实我自己宁愿描述为:合理使用继承与组合。作为复用和描述对象关系的两种最基本的手段,我想说的是适合继承的使用场景时候还是得用继承,适合使用组合的时候就使用组合。
个人认为:
继承的使用场景:满足严格的IS-A关系,也就是说当基类是真正的作为子类的强约束存在时,也即子类完全复用基类的所有信息的时候,继承是必须的。
注意这句话中的"严格"和"强约束",继承作为一种最为沉重的复用关系,使用继承时要多加考虑,因为现代语言大多数都是单继承(只能继承一个类)、多实现(可以实现多个接口)的使用方式,一旦从类的继承关系被使用了以后,扩展性其实是被限制在了基类的范围内了。但是一旦确定需要它,就放下顾虑,直接使用。其实在前面的所有例子中,我们几乎每个例子中都离不开继承。
组合的使用场景:满足宽松的HAS-A关系,也就是说如果某个类只是作为另一个类的从属关系存在的时候,就可以使用组合了。
注意这句话中的"宽松",组合使用起来就是可以这么"任性"。
对于很多的功能,其实纯用继承也是可以实现的,但是总是不完美,要么有冗余成员,要么复用程度不够,这个时候基本就说明单纯的继承是不够的,可以尝试使用"组合+继承"的方式。
好了,大原则中能上台面的也就是这么多了。