標籤:

[技術] 談談編程思想

這段時間又攢了很多答應了,但還未動手的文章。大概一兩周前,有個讀者留言:「程序君,能發篇文章有關編程思想的嗎?我是編程初學者,對編程思想沒啥概念,求傳授點經驗!」

今天就講講編程思想。編程思想是個宏大的主題,我不敢保證我能在短短的一兩個小時里講得全面而深入。推薦給大家一本好書『冒號課堂』,是國內為數不多的講編程思想的經典之作。無奈這本書已經不再出版,只能在圖書館裡一睹芳容(我幾年前在國圖和它偶遇)。

各種軟體思想雖然層出不窮,但其本質是降低系統複雜度,減少重複,減少代碼的變更。掌握了這個大方向,理解各種編程思想就容易多了。

(下文所涉及的代碼大多是剪短清晰的python代碼)

以程序君不太準確的分類,編程思想可以分為以下幾個大類:

  • 原則(Principles)
  • 範式(Paradigms)
  • 方法論(Methodologies)
  • 模式(Patterns)

我們一點點展開,說到哪算哪。

原則(Principles)

我認識(或者說現在想得起來的)的原則主要有以下幾種:

  • DRY (Dont Repeat Yourself)
  • OCP (Open Close Principle)
  • SoC (Separation of Concerns)
  • IoC (Inversion of Control)
  • CoC (Configuration over Convention)
  • Indirection (Layering)

"Dont repeat yourself"很好理解。當你第二次寫同樣結構,變化不大的代碼時,腦袋裡就要閃現一個大大的問號:我是不是在repeat myself?如果是,就要重構,或封裝,或抽象,或函數化,總之一個目的,消除重複。以筆者的經驗,DRY原則看似基本,實則很多大型軟體公司都未能做好,copy & paste到處可見。我們寫代碼,從一開始就要把握好這個原則,否則在「破窗理論」的指引下,代碼的質量會快速劃向萬劫不復的深淵。

OCP原則是說「軟體要對擴展開放,對修改封閉」。比如你寫一個message dispatching的代碼,如果你只用一個主函數去處理所有消息,那麼,每加一個message type,你就需要修改這個函數使之能處理新的消息。正確的,使用了OCP原則的代碼是每個消息都有自己的callback,主函數僅僅根據消息的類型找到對應callback,然後執行。這樣,新加的任何消息都無需改動主處理函數。這就是「對擴展開放,對修改封閉」的一個最淺顯的例子。軟體開發中的很多手段,如繼承,如Observer pattern(觀察者模式)目的就是實現OCP原則。

以上兩個原則是最基礎最基礎的原則,之後的原則都是在此基礎上衍生出來的。

SoC聽起來高大上,其實就是解耦,將複雜系統分治成不同的子系統,儘可能將變化控制在子系統中。如果你十多年前做過互聯網,就知道那時的html混雜著語義和樣式,牽一髮而動全身;現在的網站html/css基本分離,上帝的歸上帝,凱撒的歸凱撒,各司其職。這就是SoC。另一個SoC的經典應用場景就是MVC design pattern —— 整個系統的邏輯被分成 Model,View,Controller三層,(理想狀態下)其中一層的改動不會影響到另一層。

IoC原則的思想是"Dont call me, Ill call you"。這一原則促使軟體產業從lib時代發展到framework時代。想想libc,裡面有各種各樣的函數供你驅使,整個控制權在你;再看看django這樣的framework,你並沒有整個系統的控制權,你只能被動地按照規範寫出一個個函數或類,在必要的時候由framework調用。使用IoC原則的好處是高級的細節和邏輯被隱藏,開發者只需要關注business logic。比如說使用ChicagoBoss(erlang的一個 web framework)來寫web app,你寫的代碼基本上是順序的,並發(concurrency)無關的,但整個系統的執行是非同步的,大量並發的。

CoC原則出自Rails(或者至少Rails將其發揚光大),它的意思是:為了簡單起見,我們寫代碼按照一定的約定寫(代碼放在什麼目錄,用什麼文件名,用什麼類名等),這樣省去了很多不必要的麻煩(但也不失flexibility,因為約定可以通過配置修改),比如說在ember里component的定義在controller里要CamelCase,在template里要用"-"。在django里,只要你在"app/management/commands"里寫一個文件,繼承BaseCommand類,就可以通過"./manage.py xxx"運行你的命令。

Indirection/Layering原則也是為了解耦,就是把系統分成不同的層次,嚴格規定層次間的調用關係。layering最著名的例子是ISO/OSI七層模型;indirection最著名的例子是hypervisor。軟體領域最著名的一句話是:"All problems in computer science can be solved by another level of indirection."

範式(Paradigms)

講完了原則,講講範式。我能想到的兩個範式是:

  • GP: Generic Programming
  • MP: Meta Programming

很多人一看到GP(泛型編程)就想到C++中的template,想到STL。此GP非彼GP也。這裡的泛型編程是從抽象度的角度來看問題 —— 即對演算法和演算法操作的數據進行解耦。舉個例子,我們要計算一個字元串表達式的值:"3* 20 * 7 * 48"。這用python不難實現:

s = "3* 20 * 7 * 48"ndef calc(s, sep):n r = 1n for t in s.split(sep):n if t != "":n r *= int(t)n return rncalc(s, "*")n20160n

如果s是個加法的表達式呢?更好的方式是:

s = "3* 20 * 7 * 48"ndef calc(s, sep):n op = {*: operator.mul, +: operator.add, ...}n return reduce(op[sep], map(int, filter(bool, s.split(sep))))ncalc(s, "*")n20160n

在這個實現里,演算法被抽象出來與數據無關。

再比如下面這個函數,對給定的iterator裡面的任何一個元素執行一個測試,如果測試通過,則執行action,返回執行結果的list。

def process(l, test, action):n def f(x):n return action(x) if test(x) else Nonenn return filter(None, map(f, l))n

這個函數可以應用於很多場景,比如說:「從公司的directory里找到所有女性工程師,將她們的工資統一漲10%」,「給我自己的微博里所有在北京的粉絲髮一條消息」這樣兩個看似完全無關的場景。最重要的是,process函數一旦寫完,就基本不需要任何改動而應用於這兩個(甚至更多的)場景。從這裡也可以看出,GP的一個作用就是實現OCP原則。

以上所述原則和範式都與具體的語言無關,是可以放之四海而皆準的基本思想。但Metaprogramming不然。它跟語言的能力很有關係。

狹義的metaprogramming指代碼能夠將代碼當作數據操作,廣義講就是在接近語言級的層面寫的讓代碼更具動態性的代碼。先舉一個後者的例子:

class dotdict(dict):n def __getattr__(self, attr):n return self.get(attr, None)nn __setattr__ = dict.__setitem__n __delattr__ = dict.__delitem__ndd = dotdict({a: 1, b: 2})ndd.an1n

在python里,訪問字典需要使用"[]",但我們可以使用語言自身的魔法函數(magic functions)將"."操作與"[]"映射起來,達到使用"."來訪問字典的效果(就像javascript一樣)。字典里的key是無限延伸的,你無法對每個key生成一個方法,但求助於語言層的能力,就可以做到這一點。同理,如果讓你寫一個微博的api的sdk,你不必為每一個api寫一個方法,一個__getattr__就可以將所有api抽象統一。這就是廣義的metaprogramming,讓代碼更具動態性。

狹義的metaprogramming用django的ORM來說明最好:

class TagItem(models.Model):n class Meta:n app_label = caymann verbose_name = verbose_name_plural = _(關聯的實體)nn tag = models.ForeignKey(Tag, verbose_name=_(分類), null=True)n content_type = models.ForeignKey(ContentType, default=None, blank=True, null=True)n object_id = models.PositiveIntegerField(blank=True, null=True)n item = generic.GenericForeignKey(content_type, object_id)n

複雜的Object relational mapping以這樣一種declarative的方式解決了(你甚至可以將它看成一種DSL,Domain Specific Language),如果沒有metaprogramming的支持,想想你如何以如此優雅地方式實現這種mapping?

當然,就metaprogramming的能力而言,把代碼完全看做數據(列表展開與求值)的lisp族語言更甚一籌。這是我為何說metaprogramming的能力和語言相關。我沒有真正寫過lisp代碼(clojure僅僅寫了幾個hello world級的函數),但據說lisp程序員寫一個系統時,會先寫一個針對該系統的DSL,然後再用這個DSL寫代碼。聽說而已,我沒有親見。

方法論

主流的方法論不外乎三種:

  • OOP(Object Oriented Programming)
  • AOP(Aspect Oriented Programming)
  • FP(Functional Programming)

OOP就不在這裡討論了,這是一個已經被說爛了的名詞。注意OOP是一種思想,和語言是否支持無關。不支持OOP的C一樣可以寫出OOP的代碼(請參考linux kernel的device),支持OOP的python也有很多人寫出來過程化的代碼。

AOP是指把輔助的關注點從主關注點中分離,有點SoC的意味。在django里,我們會寫很多view,這些view有各自不同的邏輯,但它們都需要考慮一件事:用戶登錄(獲得授權)後才能訪問這些view。這個關注點和每個view的主關注點是無關的,我們不該為此分心,於是(為了簡便起見,以下我使用了django里已經逐漸廢棄的function based view):

@login_requiredndef user_view(request, *args, **kwargs):n user = request.usern profile = user.get_profile()n ...n

這裡,login_required這個decorator就是AOP思想的一個很好的例子。

很多時候AOP是OOP的一個用於解耦的補充。

OOP發展了這麼多年,慢慢地觸及了它固有的天花板 —— 為了容納更多的業務,不斷抽象,不斷分層,最終超過了人腦所能理解的極限。儘管有些design patterns努力幫我們把縱向的金字塔結構往橫向發展(如composite pattern,decorator pattern等),但依然改變不了OOP樹狀的,金字塔型的結構。

如果說OOP幫助你構建層級式的系統,那麼FP(函數式編程)則反其道而行之:在FP的世界裡,一切是平的。你要構建的是一個個儘可能抽象的函數,然後將其組織起來形成系統。

比如說要你做一個系統,實現對list的各種合併,如果你是個OOP的好手,你可能這麼做:

class Base(object):n def __init__(self, l):n self.l = ln def reduce(self):n raise NotImplementednclass Adder(Base):n def reduce(self):n n = 0n for item in self.l:n n += itemn return nnclass Multipler(Base):n ...n

但對於FP,你大概會這麼做:

def list_op(f):n def apply(l):n return reduce(f, l)n return applynadder = list_op(operator.add)nmultipler = list_op(operator.mul)n

函數式編程通過變化,組合各種基本的函數能夠實現複雜的功能,且實現地非常優雅。如我們前面舉的例子:

return reduce(op[sep], map(int, filter(bool, s.split(sep))))n

這種兼具可讀性和優雅性的代碼代表了代碼撰寫的未來。我們再看一個例子(haskell):

boomBangs xs = [if x < 10 then "BOOM!" else "BANG!" | x <- xs, odd x]n

即使你沒學過haskell,你也能立即領會這段代碼的意思。

函數式編程有部分或全部如下特點(取決於語言的能力):

  • Immutable data
  • First-class functions
  • Higher-order functions
  • Pure functions (no side effects)
  • Recursion & tail recursion
  • Iterators, sequences
  • Lazy evaluation
  • curry
  • Pattern matching
  • Monads....

其中不少思想和目前的多核多線程場景下進行高並發開發的思想契合。所以你會看到erlang,haskell這樣的語言越來越受到重視,並被用到各種生產環境。

模式(Patterns)

模式是在系統開發的過程中反覆被用到的一些解決具體問題的思想。設計模式(Design patterns)首先由GoF(Gang of Four)總結,然後在Java中發揚光大。其實隨著語言的進化,不少模式已經被整合在語言當中,比如iterator,有些已經固化到你寫代碼的方式當中,比如bridge,decorator,有些在framework里在不斷使用而你不知道,如經典的MVC,如django的middleware(chain of responsibility),command(command pattern)等等。時間關係,就不一一道來。

最後,寫代碼是為了解決問題,而不是秀肌肉。腦袋裡有了大原則,那麼範式,方法論,模式這些實現手段哪個順手,哪個更好地能解決問題就用哪個。代碼寫出來首先要為功能服務,其次為可讀性服務,不是為某個思想服務的,也就是說,不要為了OO而OO,不要為了MP而MP,那樣沒有意義。

如果你對本文感興趣,歡迎訂閱公眾號『程序人生』(搜索微信號 programmer_life)。每天一篇原汁原味的文章,早8點與您相會。

推薦閱讀:

AWS 及 IAM 答疑
Apple TV 4:三屏合一的終章?
當勤勉追上時間的腳步
創新不是運動,而是文化
且寫且珍惜

TAG:迷思 |