【面向对象】宽接口、窄接口和访问方法(上)

时间:2023-02-02 17:22:19

封装

封装、继承和多态是面向对象“三大金刚”。这其中封装可谓三大金刚之首。封装(或称信息隐藏)亦即不对使用者公开类型的内部实现手段,只对外提供一些接口,使用者只能通过这些公开的接口与类型进行交谈。

封装不好实际上继承和多态也是无稽之谈,即使不无稽也会风雨飘摇,使用者可能绕过你精心构造的对象层次,直接访问对象的数据,因为直接访问一切看起来那么自然而然,很简单,很直观也很容易,不需要经过大脑。

面向对象

面向对象是一种将数据和行为绑定在一起的编程方法,虽然在面向过程的时代,也可以使用模块化设计将数据以及使用这些数据的行为绑定在一起,但是毕竟那是靠程序员的个人自律。使用者还是可以轻松的无视这些约定,这样就导致很难发现这块数据有多少地方使用了,如何使用,带来一个问题就是我如果修改这块数据将会带来多大的影响也将是未可知的。面向对象第一次使用强制的手段将数据和行为绑定在一起,但这一切是建立在封装的基础之上的。如果你随意的公开你的数据,那么使用者也就可以随意的使用你的数据,没有人会觉得心里愧疚。因为那毕竟是最直接的手段。这也就是为什么很多人在使用着面向对象的语言干着面向过程的事情的原因。

访问方法

还有一点需要指出的是封装并不是叫你将所有的内部数据都通过getter和setter的方法来访问,套一个简简单单,全裸的方法,就说你是在封装,你说你没有让使用者直接访问数据,你骗谁呢。但是,一些著名的规范或者框架却直接无视三大金刚之首,比如Java Bean,比如像Hibernate之类的ORM。将setter和getter作为规范或标准来执行。不过,没有办法,人家毕竟要通过一种手段来访问你的数据,但是我觉得这种“随意”的要求你将内部敞开的做法不是什么好主意,即使你要访问内部数据,你也要将门槛设高点。还有一点是,大部分时候我们需要在界面上显示数据,收集用户填充的数据,如是我们还是需要一堆的getter和setter。看来getter和setter还是避免不了,但观察上面的问题我们发现,需要公开所有getter和setter的地方是在一些特定的上下文内,并不是所有地方我们都应该热情地敞开胸怀。这样我们就可以根据不同的上下文公开不同的接口来获得更好的封装性。

比如在界面上需要显示或收集数据时,在ORM需要这种getter和setter方法时,我们提供一种宽接口,而在业务逻辑部分我们采用窄接口,因为我不想在业务逻辑计算的时候别的类窥探我的隐私。因为,一旦我能很容易窥探到你的隐私,就总是有这么一种诱惑:根据你的隐私我做出一些决策,而这些决策本应该是你自己做出的,因为毕竟这是你的隐私,你对它最熟悉。比如经常看到如下的代码:

   1: //if user loged in
   2: if(String.IsNullOrEmpty(user.Username) && String.IsNullOrEmpty(user.Password))
   3: {
   4:     //do something
   5: }

写出这样的代码的原因是我访问User对象的内部数据太容易了,轻而易举,如是我就帮User一个忙,我自己检查一下它的用户名和密码是不是为空,这样就能知道这个User是不是已经登录了。可是用户名和密码都应该是用户的私有数据,本不应该暴露出来,而且验证用户是否登录的方法是否真的是如此呢?即使今天是这样明天也不一定是这样啊。如果User类没有暴露出它的用户名和密码,那么User类的使用者也就无法使用上面的代码判断用户是否登录了,那么他要么自己去给User类添加一个IsLogedIn的方法,要么祈求User类的开发人员添加一个。这样我们能获得什么样的好处呢?

1、我们用方法名(IsLogedIn)就能描述我们要干的事儿,代码的可读性也就更佳了,所以上面代码的第一行的注释可以问心无愧的删除。

2、如果有一天验证用户是否登录的逻辑改变了,我们只需要修改User类里面的逻辑就够了,其他地方都无需更改。

宽接口、窄接口

其实造成上面那段代码的原因责任并不在于编写那段代码的人,责任应该归咎于编写User类的人,你太随意了。

不过现在带来另外一个问题,刚才我们刚刚大谈特谈不应该随意的使用setter和getter方法将类型内部的数据暴露出去,但是我们现在需要做一个用户登录页面,需要用户输入账号密码,然后验证,或者我们在后台管理页面需要显示本系统所有用户列表。看来我们还是躲不过setter和getter的魔咒。这里的用户界面部分以及上面的那段代码也就是系统的不同上下文。我们可以对界面上下文公开宽接口,而对业务逻辑等部分公开窄接口。给不同的上下文看不同的接口有很多种方法,不同的语言里也有不同的实践:

1、在C++里我们有友元(friend),如果我们有一个LoginView类表示登录窗口,User表示用户类,我们可以将LoginView作为User的友元,这样LoginView就可以访问User的私有数据。不过使用我个人觉得使用friend是一种非常不好的实践。首先,friend关系是不能被继承的,这在构建一些对象层次时是会出现问题的。再次,这样在一个领域类里引入一个界面类实在是一件非常奇怪的事情,说出去都有点不好意思见人。

2、.NET里的友元程序集。.NET虽然没有友元类这个概念,但却是有友元程序集的,我们可以将LoginView所属的程序集设为User所属程序集的友元,然后将setter和getter方法设为internal的。不过,还是一样,领域对象所在的程序集居然要知道一个界面所在的程序集,这很荒谬。

3、我们创建一个IUser接口,然后User实现该接口。IUser是一个窄接口,在业务逻辑部分使用,而User就是宽接口,会通过setter和getter暴露内部数据。

那么我们还是来看一个案例吧。

案例

我们要开发一个选课系统,这里有这样三个对象:科目[Course](像数学啊,物理啊等,要学这个科目还必须学完该科目的预修科目,所以有个预修科目列表)、课程[CourseOffering](课程里面包括这是哪个科目的课程,讲师是谁,最多可以有多少个学生,现在有多少个学生等信息),还有一个对象就是学生[Student]了(学生知道自己已经修了哪些科目了)。

现在有个问题,要选课的话,实际上就是往课程的学生列表里添加学生,那么我们该怎么做呢?

代码1:

   1: public class CourseService
   2: {
   3:     public void Choose(Student student,CourseOffering courseOffering)
   4:     {
   5:         if(student.Courses.Contains(courseOffering.Course.PreRequiredCourses) && courseOffering.LimitStudents > courseOffering.Students.Size)
   6:         {
   7:             courseOffering.Students.Add(student);
   8:         }
   9:     }
  10: }

大部分人看了上面这部分代码都会摇头,这完全就是披着class的外衣,写着过程式的代码。我们写了一个服务,里面有个Choose方法,传个学生,传个课程,然后看看学生是不是修完了该课程对应科目的预修课程,而且看看这个课程的学生是不是已经满了,如果条件符合的话我们就将这个学生收了。经过这么一解释,嘿嘿,这逻辑貌似很自然啊。面向过程就是这样,完全不饶弯弯,很直白的将逻辑表现出来(但这往往是表象,因为代码一多,逻辑一复杂,面向过程的代码就会像面条一样纠缠不清,而且因为抽象层次低,需求一改变什么都玩了)。
其实我们可以思考一下为什么会写出上面的代码。实际上我想的是写Student、CourseOffering和Course这三个类的人太随意了,将所有的数据都公开出来,因此我在这里很容易访问,也就很容易写出这种方法了。

实际上,经过思考我们觉得这个Choose方法更应该放在CourseOffering类里,这样我们就可以不暴露Students了:

代码2:

   1: public class CourseOffering
   2: {
   3:     private readonly Course course;
   4:     
   5:     private IList<Student> students = new List<Student>();
   6:  
   7:     private readonly int limitStudents;
   8:     
   9:     public CourseOffering(int limitStudents,Course course)
  10:     {
  11:         this.limitStudents = limitStudents;
  12:         this.course = course;
  13:     }
  14:  
  15:     public void AddStudent(Student student)
  16:     {
  17:         if(student.Courses.Contains(course.PreRequiredCourses) && limitStudents > students.Count)
  18:         {
  19:             students.Add(student);
  20:         }
  21:     }
  22: }

那么选课服务也许就像下面这样了:

代码3:

   1: public class CourseService
   2: {
   3:     public void Choose(Student student,CourseOffering courseOffering)
   4:     {
   5:         courseOffering.AddStudent(student);
   6:     }
   7: }

因为CourseOffering不再公开students属性了,所以我们写这个选课服务的时候我们没办法了,我们只有求助CourseOffering。

但是在CourseOffering类的内部,还是有信息的泄露,Student将它已修的课程透露出来了(其实我是个差生,经常逃课,我真的不想将我的已修课程透露出去)。再思考一下这里的逻辑,你不觉得检查自己是不是可以修某个科目不应该是学生自己的职责么,因为学生知道他自己修了哪些课程了。那么我们可以进一步封装:

   1: public class Student
   2: {
   3:     private IList<Course> alreadyCourses = new List<Course>();
   4:     
   5:     public bool CanAttend(Course course)
   6:     {
   7:         return alreadyCourses.Contains(course.PreRequiredCourses);
   8:     }
   9: }
  10: public class CourseOffering
  11: {
  12:     private readonly Course course;
  13:     
  14:     private IList<Student> students = new List<Student>();
  15:  
  16:     private readonly int limitStudents;
  17:     
  18:     public CourseOffering(int limitStudents,Course course)
  19:     {
  20:         this.limitStudents = limitStudents;
  21:         this.course = course;
  22:     }
  23:  
  24:     public void AddStudent(Student student)
  25:     {
  26:         if(studnet.CanAttend(this.course) && limitStudents > students.Count)
  27:         {
  28:             students.Add(student);
  29:         }
  30:     }
  31: }

这里不仅将Student应该有的职责分离出去了,还提升了student.Courses.Contains(course.PreRequiredCourses)这条语句的抽象层次(其实面向对象的成功之一就是能不断的提高抽象层次,抽象出领域的各种概念,促进团队对整个系统的认识)。

不过在Student里还是存在对比的对象内部数据的知悉:Student知道了课程的预修课程。嘿嘿,其实我这门课程啊,虽然预修课程有108门,但实际上你只要修了那么五门也就可以了,但是这个事情可不能透露给那些学生哦,如果他们听到了那其余103门的补考费我找谁收去啊,呵呵。所以检查这个学生能不能修还是我自己操刀吧,而且我还想内部的动态改变这个是不是能修的策略呢(当然,这是笑谈,不过这也透露了一点,用户的需求经常是变化的,怎么应对这种变化?):

   1: public class Course
   2: {
   3:     private IList<Course> preRequireds = new List<Course>();
   4:  
   5:     public bool Acceptable(IList<Course> courses)
   6:     {
   7:         return courses.Contains(preRequireds);
   8:     }
   9: }
  10: public class Student
  11: {
  12:     private IList<Course> alreadyCourses = new List<Course>();
  13:     
  14:     public bool CanAttend(Course course)
  15:     {
  16:         return !IsAlreadyAttend(course) && course.Acceptable(alreadyCourses);
  17:     }
  18:     
  19:     private bool IsAlreadyAtteded(Course course)
  20:    {
  21:         return alreadyCourses.Contains(course);
  22:    }
  23: }
  24: public class CourseOffering
  25: {
  26:     private readonly Course course;
  27:     
  28:     private IList<Student> students = new List<Student>();
  29:  
  30:     private readonly int limitStudents;
  31:     
  32:     public CourseOffering(int limitStudents,Course course)
  33:     {
  34:         this.limitStudents = limitStudents;
  35:         this.course = course;
  36:     }
  37:  
  38:     public void AddStudent(Student student)
  39:     {
  40:         if(studnet.CanAttend(this.course) && limitStudents > students.Count)
  41:         {
  42:             students.Add(student);
  43:         }
  44:     }
  45: }

至此,我们的三个领域类都不了解对方内部到底藏有什么花花肠子,我们可以任意更改我们每个类的内部实现,只需要我们的公开接口不变就行了,我们每个类都有清晰的职责,我们还通过具有描述性的名称来提升了概念的抽象层次。

但是我们的问题依然没有解决,如果这些内部的数据都不公开,我们要做一个界面显示这些对象的信息该怎么办?

 

 

【注】:如果你觉得本文的案例部分的例子有点熟悉,那么恭喜你,你的感觉是对的。本文的案例示例采用了《OOD沉思录》里的一个讲述,不过本文只采用了原文的“创意”部分(不过这年头,最缺的就是创意)。