写给自己看的小设计4 - 对象设计通用原则之扩展原则

  除了前面学习的那些核心原则,还有一些衍生的原则,掌握它们,你将更好的面向对象。不妨称它们为"扩展原则"吧。
迪米特法则:尽量不与无关的类发生关系。
  迪米特法则全称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关系,也就是说如果某个类只是作为另一个类的从属关系存在的时候,就可以使用组合了。
  注意这句话中的"宽松",组合使用起来就是可以这么"任性"。
  对于很多的功能,其实纯用继承也是可以实现的,但是总是不完美,要么有冗余成员,要么复用程度不够,这个时候基本就说明单纯的继承是不够的,可以尝试使用"组合+继承"的方式。
 
  好了,大原则中能上台面的也就是这么多了。
posted @ 2015-03-06 18:19  沙场秋点兵  阅读(987)  评论(0编辑  收藏  举报