封裝
封裝、繼承和多態是面向對象“三大金剛”。這其中封裝可謂三大金剛之首。封裝(或稱信息隱藏)亦即不對使用者公開類型的內部實現手段,只對外提供一些接口,使用者只能通過這些公開的接口與類型進行交談。
封裝不好實際上繼承和多態也是無稽之談,即使不無稽也會風雨飄搖,使用者可能繞過你精心構造的對象層次,直接訪問對象的數據,因爲直接訪問一切看起來那麼自然而然,很簡單,很直觀也很容易,不需要經過大腦。
面向對象
面向對象是一種將數據和行爲綁定在一起的編程方法,雖然在面向過程的時代,也可以使用模塊化設計將數據以及使用這些數據的行爲綁定在一起,但是畢竟那是靠程序員的個人自律。使用者還是可以輕鬆的無視這些約定,這樣就導致很難發現這塊數據有多少地方使用了,如何使用,帶來一個問題就是我如果修改這塊數據將會帶來多大的影響也將是未可知的。面向對象第一次使用強制的手段將數據和行爲綁定在一起,但這一切是建立在封裝的基礎之上的。如果你隨意的公開你的數據,那麼使用者也就可以隨意的使用你的數據,沒有人會覺得心裏愧疚。因爲那畢竟是最直接的手段。這也就是爲什麼很多人在使用着面向對象的語言幹着面向過程的事情的原因。
訪問方法
還有一點需要指出的是封裝並不是叫你將所有的內部數據都通過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: }
至此,我們的三個領域類都不瞭解對方內部到底藏有什麼花花腸子,我們可以任意更改我們每個類的內部實現,只需要我們的公開接口不變就行了,我們每個類都有清晰的職責,我們還通過具有描述性的名稱來提升了概念的抽象層次。
但是我們的問題依然沒有解決,如果這些內部的數據都不公開,我們要做一個界面顯示這些對象的信息該怎麼辦?