位元詩人 [Nim] 語言程式教學:組合 (Composition) 和繼承 (Inheritance)

Facebook Twitter LinkedIn LINE Skype EverNote GMail Yahoo Email

組合和繼承是兩種不同思維的重用程式碼的方式,本文介紹在 Nim 裡面如何使用這兩種模式撰寫程式。

繼承

透過繼承,類別之間可以共用程式碼,兩個類別分別是父類別 (parent class) 和子類別 (child class),子類別可重覆利用父類別的程式碼,但父類別則無法使用子類別的程式碼。在 Nim 語言中,使用 ref object of 在宣告繼承,如下例:

import random

# Parent class
type
  Employee* = ref object of RootObj
    s: float

proc salary*(e: Employee): float =
  e.s

proc `salary=`*(e: Employee, salary: float) =
  assert(salary >= 0.0)
  e.s = salary

proc newEmployee*(salary: float): Employee =
  new(result)
  result.s = salary

# Child class
type
  Programmer* = ref object of Employee
    plns: seq[string]
    pes: seq[string]

proc langs*(p: Programmer): seq[string] =
  p.plns

proc `langs=`*(p: Programmer, langs: seq[string]) =
  p.plns = langs

proc editors*(p: Programmer): seq[string] =
  p.pes

proc `editors=`*(p: Programmer, editors: seq[string]) =
  p.pes = editors

proc solve*(p: Programmer, problem: string) =
  randomize()

  let ln = p.langs[random(p.langs.low..p.langs.len)]
  let e = p.editors[random(p.editors.low..p.editors.len)]

  echo "The programmer solved " & problem & " in " & ln & " with " & e

proc newProgrammer*(langs: seq[string], editors: seq[string], salary: float): Programmer =
  new(result)
  result.plns = langs
  result.pes = editors
  result.s = salary

# Main program
when isMainModule:
  let pr: Programmer = newProgrammer(
    langs = @["Go", "Rust", "D", "Nim"],
    editors = @["Atom", "Sublime Text", "Visual Studio Code"],
    salary = 1000)

  # Use the method from child class.
  pr.solve("Linked List")
  pr.solve("Tower of Hanoi")

  # Use the method from parent class.
  assert(pr.salary == 1000)

目前 Nim 的問題在於僅有單一繼承,卻沒有官方的介面 (interface) 或 mixin 等替代的方案,無法利用介面來實作一些設計模式。目前一些可行的替代方法:

  • 多用組合,少用繼承:用類似 C 或 Go 語言的思維來寫物件,見下文
  • 使用帶有方法宣告的 tuple:某種程度可模擬介面,見後續關於多型的說明
  • 使用模板 (template):跳脫型別的限制,詳見後文

組合

組合 (composition) 的想法在於直接重用類別,但類別之間沒有繼承的關係,從外部來看,兩個類別是各自獨立的。我們修改先前的例子,建立兩個類別,在這兩個類別中,Programmer 類別直接重用 Employee 類別:

import random

type
  Employee* = ref object
    s: float

# Declare procedures as above.

type
  Programmer* = ref object
    plns: seq[string]
    pes: seq[string]
    pee: Employee

proc salary*(p: Programmer): float =
  p.pee.salary

proc `salary=`*(p: Programmer, salary: float) =
  assert(salary >= 0.0)
  p.pee.salary = salary

# Declare procedures as above.

proc newProgrammer*(langs: seq[string], editors: seq[string], salary: float): Programmer =
  new(result)
  result.plns = langs
  result.pes = editors
  result.pee = newEmployee(salary = salary)

when isMainModule:
  let pr: Programmer = newProgrammer(
    langs = @["Go", "Rust", "D", "Nim"],
    editors = @["Atom", "Sublime Text", "Visual Studio Code"],
    salary = 100)

  pr.solve("Linked List")
  pr.solve("Tower of Hanoi")

  assert(pr.salary == 100)

如果單獨使用這兩個類別,不會有什麼問題,但如果需要一些多型的特性,這種方式則無法滿足我們的需求。我們將於後文說明如何處理。

關於作者

身為資訊領域碩士,位元詩人 (ByteBard) 認為開發應用程式的目的是為社會帶來價值。如果在這個過程中該軟體能成為永續經營的項目,那就是開發者和使用者雙贏的局面。

位元詩人喜歡用開源技術來解決各式各樣的問題,但必要時對專有技術也不排斥。閒暇之餘,位元詩人將所學寫成文章,放在這個網站上和大家分享。