สัญญาความหมายของอินเทอร์เฟซ (OOP) ให้ข้อมูลมากกว่าลายเซ็นฟังก์ชัน (FP) หรือไม่


16

บางคนบอกว่าถ้าคุณนำหลักการของ SOLID มาใช้กับสุดขั้วคุณก็จะจบลงด้วยการเขียนโปรแกรมที่ใช้งานได้ ฉันเห็นด้วยกับบทความนี้ แต่ฉันคิดว่าซีแมนทิกส์บางอย่างหายไปจากการเปลี่ยนจากส่วนต่อประสาน / วัตถุไปเป็นฟังก์ชัน / ปิดและฉันต้องการทราบว่า Function Programming สามารถลดความสูญเสียได้อย่างไร

จากบทความ:

นอกจากนี้หากคุณใช้หลักการแยกส่วนต่อประสาน (ISP) อย่างจริงจังคุณจะเข้าใจว่าคุณควรใช้การเชื่อมต่อแบบสวมบทบาทแทนการเชื่อมต่อส่วนหัว

หากคุณผลักดันการออกแบบของคุณไปสู่อินเทอร์เฟซที่เล็กลงเรื่อย ๆ ในที่สุดคุณจะมาถึงสุดยอด Role Interface: อินเทอร์เฟซด้วยวิธีการเดียว เรื่องนี้เกิดขึ้นกับฉันมาก นี่คือตัวอย่าง:

public interface IMessageQuery
{
    string Read(int id);
}

ถ้าฉันใช้การพึ่งพาIMessageQueryส่วนหนึ่งของสัญญาโดยนัยคือการโทรRead(id)จะค้นหาและส่งคืนข้อความด้วย ID ที่กำหนด

เปรียบเทียบสิ่งนี้กับการพึ่งพาของลายเซ็นการทำงานที่เทียบเท่า, int -> string. โดยไม่ต้องชี้นำใด ๆ ToString()เพิ่มเติมฟังก์ชั่นนี้อาจจะง่าย หากคุณดำเนินการIMessageQuery.Read(int id)ด้วยToString()ฉันอาจกล่าวหาว่าคุณถูกโค่นล้มโดยเจตนา!

ดังนั้นโปรแกรมเมอร์ฟังก์ชันสามารถทำอะไรได้เพื่อรักษาซีแมนทิกส์ของอินเตอร์เฟสที่มีชื่อดี ยกตัวอย่างเช่นมันเป็นเรื่องธรรมดาที่จะสร้างประเภทเรคคอร์ดโดยมีสมาชิกคนเดียวหรือไม่?

type MessageQuery = {
    Read: int -> string
}

5
อินเทอร์เฟซ OOP นั้นเหมือนกับ FP typeclassไม่ใช่ฟังก์ชันเดียว
9000

2
คุณสามารถมีสิ่งที่ดีมากเกินไปการใช้ ISP 'อย่างจริงจัง' เพื่อจบลงด้วย 1 วิธีต่อหนึ่งอินเตอร์เฟสเป็นไปไกลเกินไป
gbjbaanb

3
@gbjbaanb ส่วนใหญ่ของอินเทอร์เฟซของฉันมีเพียงวิธีเดียวที่มีการใช้งานหลายอย่าง ยิ่งคุณใช้หลักการ SOLID มากเท่าไรคุณก็ยิ่งเห็นประโยชน์มากขึ้นเท่านั้น แต่นั่นไม่ใช่หัวข้อสำหรับคำถามนี้
AlexFoxGill

1
@jk: ใน Haskell มันเป็นประเภทเรียนใน OCaml คุณใช้โมดูลหรือ functor ใน Clojure คุณใช้โปรโตคอล ไม่ว่าในกรณีใดคุณมักจะไม่ จำกัด อินเทอร์เฟซของคุณคล้ายคลึงกับฟังก์ชันเดียว
9000

2
Without any additional clues... บางทีมันอาจจะเป็นเหตุผลที่เอกสารเป็นส่วนหนึ่งของสัญญา ?
SJuan76

คำตอบ:


8

ตามที่ Telastyn กล่าวว่าการเปรียบเทียบนิยามแบบคงที่ของฟังก์ชัน:

public string Read(int id) { /*...*/ }

ถึง

let read (id:int) = //...

คุณไม่ได้สูญเสียอะไรเลยตั้งแต่ OOP ไปจนถึง FP

อย่างไรก็ตามนี่เป็นเพียงส่วนหนึ่งของเรื่องราวเนื่องจากฟังก์ชั่นและอินเทอร์เฟซไม่ได้อ้างถึงในคำจำกัดความคงที่เท่านั้น พวกเขายังผ่านรอบ ดังนั้นสมมติว่าเราถูกอ่านโดยอีกชิ้นหนึ่งของรหัสที่เป็นMessageQuery MessageProcessorจากนั้นเรามี:

public void ProcessMessage(int messageId, IMessageQuery messageReader) { /*...*/ }

ตอนนี้เราไม่สามารถเห็นชื่อเมธอดIMessageQuery.Readหรือพารามิเตอร์ได้int idโดยตรง แต่เราสามารถเข้าถึงได้ง่ายผ่าน IDE ของเรา โดยทั่วไปแล้วความจริงที่ว่าเราส่งผ่านIMessageQueryอินเทอร์เฟซใด ๆ ด้วยวิธีการที่ฟังก์ชั่นจาก int ถึงสตริงหมายความว่าเรากำลังเก็บidเมตาดาต้าชื่อพารามิเตอร์ที่เชื่อมโยงกับฟังก์ชั่นนี้

ในทางกลับกันสำหรับเวอร์ชั่นการทำงานของเราเรามี:

let read (id:int) (messageReader : int -> string) = // ...

ดังนั้นสิ่งที่เราได้เก็บและสูญเสีย? เรายังมีชื่อพารามิเตอร์messageReaderซึ่งอาจทำให้ชื่อประเภท (เทียบเท่าIMessageQuery) ไม่จำเป็น แต่ตอนนี้เราสูญเสียชื่อพารามิเตอร์idในฟังก์ชั่นของเรา


มีสองวิธีหลักรอบนี้:

  • ประการแรกจากการอ่านลายเซ็นนั้นคุณสามารถคาดเดาได้ดีว่าจะเกิดอะไรขึ้น ด้วยการทำให้ฟังก์ชั่นสั้นง่ายและเหนียวแน่นและใช้การตั้งชื่อที่ดีทำให้คุณง่ายขึ้นมากในการหยั่งรู้หรือค้นหาข้อมูลนี้ เมื่อเราอ่านฟังก์ชั่นจริง ๆ แล้วมันจะง่ายขึ้นกว่าเดิม

  • ประการที่สองเป็นการออกแบบที่ใช้สำนวนในภาษาที่ใช้งานได้หลายประเภทเพื่อสร้างสิ่งเล็ก ๆ ในกรณีนี้สิ่งที่ตรงกันข้ามเกิดขึ้นแทนการแทนที่ชื่อประเภทด้วยชื่อพารามิเตอร์ ( IMessageQueryเป็นmessageReader) เราสามารถแทนที่ชื่อพารามิเตอร์ด้วยชื่อประเภท ตัวอย่างเช่นintสามารถห่อในประเภทที่เรียกว่าId:

    type Id = Id of int
    

    ตอนนี้readลายเซ็นของเรากลายเป็น:

    let read (id:int) (messageReader : Id -> string) = // ...
    

    ซึ่งเป็นเพียงข้อมูลเท่าที่เราเคยมีมาก่อน

    ในฐานะที่เป็นบันทึกด้านข้างนี้ยังให้การป้องกันคอมไพเลอร์ที่เรามีใน OOP ด้วย ในขณะที่รุ่น OOP มั่นใจเราเอาเฉพาะIMessageQueryมากกว่าแค่เก่า ๆint -> stringฟังก์ชั่นที่นี่เรามีที่คล้ายกัน ( แต่ที่แตกต่างกัน) การป้องกันที่เรากำลังสละมากกว่าแค่การเก่าId -> stringint -> string


ฉันลังเลที่จะพูดด้วยความมั่นใจ 100% ว่าเทคนิคเหล่านี้จะดีและให้ข้อมูลเสมอว่ามีข้อมูลที่สมบูรณ์บนอินเทอร์เฟซ แต่ฉันคิดว่าจากตัวอย่างข้างต้นคุณสามารถพูดได้ว่าส่วนใหญ่แล้วเรา อาจทำผลงานได้ดี


1
นี่คือคำตอบที่ฉันโปรดปราน - FP กระตุ้นให้ใช้ประเภทความหมาย
AlexFoxGill

10

เมื่อทำ FP ฉันมักจะใช้ประเภทความหมายที่เฉพาะเจาะจงมากขึ้น

ตัวอย่างเช่นวิธีการของคุณสำหรับฉันจะกลายเป็นเหมือน:

read: MessageId -> Message

นี้ค่อนข้างสื่อสารมากขึ้นกว่า OO (การ / java) สไตล์ThingDoer.doThing()สไตล์


2
+1 นอกจากนี้คุณสามารถเข้ารหัสคุณสมบัติได้มากขึ้นในระบบประเภทในภาษา FP ที่พิมพ์เช่น Haskell หากเป็นประเภทฮาเซลฉันจะรู้ว่ามันไม่ได้ทำ IO เลย (และอาจอ้างอิงการแมปในหน่วยความจำของรหัสไอดีกับข้อความที่อื่น) ในภาษา OOP ฉันไม่มีข้อมูลนั้น - ฟังก์ชั่นนี้สามารถส่งเสียงเรียกเข้าฐานข้อมูลเป็นส่วนหนึ่งของการโทรได้
แจ็ค

1
ฉันไม่ชัดเจนเกี่ยวกับเหตุผลนี้จะสื่อสารมากขึ้นกว่ารูปแบบ Java อะไรread: MessageId -> Messageบอกคุณที่string MessageReader.GetMessage(int messageId)ไม่ได้?
Ben Aaronson

@BenAaronson อัตราส่วนสัญญาณต่อเสียงรบกวนนั้นดีกว่า ถ้ามันเป็นวิธีอินสแตนซ์ของ OO ที่ฉันไม่รู้ว่ากำลังทำอะไรอยู่ใต้ฝากระโปรงมันอาจมีการพึ่งพาใด ๆ ที่ไม่ได้แสดงออกมา แจ็คพูดถึงเมื่อคุณมีประเภทที่แข็งแกร่งขึ้นคุณมีการรับรองมากขึ้น
Daenyth

9

ดังนั้นโปรแกรมเมอร์ฟังก์ชันสามารถทำอะไรได้เพื่อรักษาซีแมนทิกส์ของอินเตอร์เฟสที่มีชื่อดี

ใช้ฟังก์ชั่นที่มีชื่อดี

IMessageQuery::Read: int -> stringเพียงแค่กลายเป็นReadMessageQuery: int -> stringหรือสิ่งที่คล้ายกัน

สิ่งสำคัญที่ควรทราบคือชื่อเป็นเพียงสัญญาในความหมายของคำ มันจะทำงานได้ก็ต่อเมื่อคุณและโปรแกรมเมอร์คนอื่นอนุมานความหมายเดียวกันของชื่อและเชื่อฟังพวกเขา ด้วยเหตุนี้คุณจึงสามารถใช้ชื่อใดก็ได้ที่สื่อสารพฤติกรรมนั้นโดยนัย การเขียนโปรแกรม OO และการทำงานมีชื่อของพวกเขาในสถานที่ที่แตกต่างกันเล็กน้อยและในรูปร่างที่แตกต่างกันเล็กน้อย แต่ฟังก์ชั่นของพวกเขาจะเหมือนกัน

สัญญาความหมายของอินเทอร์เฟซ (OOP) ให้ข้อมูลมากกว่าลายเซ็นฟังก์ชัน (FP) หรือไม่

ไม่ได้อยู่ในตัวอย่างนี้ ดังที่ฉันได้อธิบายไว้ข้างต้นคลาส / อินเทอร์เฟซเดียวที่มีฟังก์ชั่นเดียวนั้นไม่มีความหมายมากกว่าข้อมูลมากกว่าฟังก์ชั่นสแตนด์อโลนที่มีชื่อคล้ายกัน

เมื่อคุณได้รับมากกว่าหนึ่งฟังก์ชัน / เขต / คุณสมบัติในคลาสคุณสามารถอนุมานข้อมูลเพิ่มเติมเกี่ยวกับพวกเขาได้เพราะคุณสามารถเห็นความสัมพันธ์ของพวกเขา สามารถพิสูจน์ได้ว่าเป็นข้อมูลที่มากกว่าฟังก์ชั่นแบบสแตนด์อโลนที่ใช้พารามิเตอร์เดียวกัน / คล้ายกันหรือฟังก์ชั่นแบบสแตนด์อโลนซึ่งจัดโดยเนมสเปซหรือโมดูล

โดยส่วนตัวแล้วฉันไม่คิดว่า OO จะให้ความรู้มากกว่าเดิมแม้จะเป็นตัวอย่างที่ซับซ้อนกว่าก็ตาม


2
แล้วชื่อพารามิเตอร์ล่ะ?
Ben Aaronson

@BenAaronson - แล้วพวกเขาล่ะ? ทั้ง OO และภาษาที่ใช้งานได้ให้คุณระบุชื่อพารามิเตอร์เพื่อให้เป็นแบบพุช ฉันแค่ใช้ลายเซ็นประเภทย่อที่นี่เพื่อให้สอดคล้องกับคำถาม ในภาษาจริงมันจะมีลักษณะเหมือนReadMessageQuery id = <code to go fetch based on id>
Telastyn

1
ถ้าฟังก์ชั่นใช้อินเทอร์เฟซเป็นพารามิเตอร์ดังนั้นการเรียกเมธอดบนอินเตอร์เฟสนั้นชื่อพารามิเตอร์สำหรับเมธอดอินเตอร์เฟสจะพร้อมใช้งาน หากฟังก์ชั่นใช้ฟังก์ชั่นอื่นเป็นพารามิเตอร์มันไม่ใช่เรื่องง่ายที่จะกำหนดชื่อพารามิเตอร์สำหรับฟังก์ชันที่ส่งผ่าน
Ben Aaronson

1
ใช่ @BenAaronson นี่เป็นส่วนหนึ่งของข้อมูลที่สูญหายไปในลายเซ็นฟังก์ชั่น ยกตัวอย่างเช่นในlet consumer (messageQuery : int -> string) = messageQuery 5การพารามิเตอร์เป็นเพียงid intฉันเดาอาร์กิวเมนต์หนึ่งคือการที่คุณควรจะได้รับการส่งผ่านไม่ใช่Id intในความเป็นจริงที่จะทำให้คำตอบที่ดีในตัวเอง
AlexFoxGill

@AlexFoxGill ที่จริงแล้วฉันเพิ่งเขียนขั้นตอนบางอย่างตามบรรทัดเหล่านั้น!
Ben Aaronson

0

ฉันไม่เห็นด้วยว่าฟังก์ชั่นเดียวไม่สามารถมี 'สัญญาความหมาย' พิจารณากฎหมายเหล่านี้เพื่อfoldr:

foldr f z nil = z
foldr f z (singleton x) = f x z
foldr f z (xn <> ys) = foldr f (foldr f z ys) xn

ในแง่ใดที่ไม่ใช่ความหมายหรือไม่ใช่สัญญา คุณไม่จำเป็นต้องกำหนดประเภทสำหรับ 'foldrer' โดยเฉพาะเนื่องจากfoldrมีการกำหนดโดยกฎหมายเหล่านั้นโดยเฉพาะ คุณรู้ว่าสิ่งที่จะทำ

หากคุณต้องการฟังก์ชั่นบางประเภทคุณสามารถทำสิ่งเดียวกันได้:

-- The first argument `f` must satisfy for all x, y, z
-- > f x x = true
-- > f x y = true and f y x = true implies x = y
-- > f x y = true and f y z = true implies f x z = true
sort :: forall 'a. (a -> a -> bool.t) -> list.t a -> list.t a;

คุณจะต้องตั้งชื่อและจับภาพประเภทนั้นหากคุณต้องการสัญญาเดียวกันหลายครั้ง:

-- Type of functions `f` satisfying, for all x, y, z
-- > f x x = true
-- > f x y = true and f y x = true implies x = y
-- > f x y = true and f y z = true implies f x z = true
type comparison.t 'a = a -> a -> bool.t;

ตัวตรวจสอบชนิดจะไม่บังคับใช้ความหมายใด ๆ ที่คุณกำหนดให้กับประเภทดังนั้นการสร้างประเภทใหม่สำหรับทุกสัญญาเป็นเพียงแค่ต้นแบบ


1
ฉันไม่คิดว่าจุดแรกของคุณตอบคำถาม - มันเป็นนิยามฟังก์ชั่นไม่ใช่ลายเซ็น แน่นอนโดยดูที่การดำเนินงานของชั้นเรียนหรือฟังก์ชั่นที่คุณสามารถบอกสิ่งที่มันไม่ - คำถามที่ถามว่าคุณยังคงสามารถรักษาความหมายในระดับที่เป็นนามธรรม (อินเตอร์เฟซหรือฟังก์ชั่นลายเซ็น)
AlexFoxGill

@AlexFoxGill - มันไม่ได้foldrนิยามฟังก์ชันแม้ว่ามันจะไม่ซ้ำกันตรวจสอบ คำแนะนำ: คำจำกัดความของfoldrมีสองสมการ (ทำไม?) ในขณะที่ข้อกำหนดที่ระบุข้างต้นมีสาม
Jonathan Cast

สิ่งที่ฉันหมายถึงคือ foldr เป็นฟังก์ชั่นที่ไม่ได้เป็นลายเซ็นของฟังก์ชั่น กฎหมายหรือคำจำกัดความไม่ได้เป็นส่วนหนึ่งของลายเซ็น
AlexFoxGill

-1

ภาษาที่มีการพิมพ์แบบสแตติกส่วนใหญ่มีวิธีที่จะใช้นามแฝงประเภทพื้นฐานในแบบที่คุณต้องประกาศเจตนาทางความหมายของคุณอย่างชัดเจน คำตอบอื่น ๆ บางตัวอย่างเป็นตัวอย่าง ในทางปฏิบัติโปรแกรมเมอร์ที่มีประสบการณ์ทำงานต้องการเหตุผลที่ดีมากในการใช้กระดาษห่อประเภทเหล่านี้

ตัวอย่างเช่นสมมติว่าลูกค้าต้องการให้มีการใช้งานคิวรีข้อความที่ได้รับการสนับสนุนโดยรายการ ใน Haskell การติดตั้งอาจทำได้ง่ายเช่นนี้

messages = ["Message 0", "Message 1", "Message 2"]
messageQuery = (messages !!)

การใช้newtype Message = Message Stringสิ่งนี้จะตรงไปตรงมาน้อยกว่ามากและปล่อยให้การติดตั้งนี้ดูเหมือน:

messages = map Message ["Message 0", "Message 1", "Message 2"]
messageQuery (Id index) = messages !! index

นั่นอาจดูเหมือนไม่ใช่เรื่องใหญ่ แต่คุณต้องทำการแปลงชนิดนั้นไปมาทุกหนทุกแห่งหรือตั้งค่าเลเยอร์ขอบเขตในโค้ดของคุณซึ่งทุกอย่างที่กล่าวInt -> Stringมาแล้วนั้นคุณต้องแปลงId -> Messageให้เป็นเลเยอร์ด้านล่าง ว่าฉันต้องการเพิ่มความเป็นสากลหรือจัดรูปแบบในตัวพิมพ์ใหญ่ทั้งหมดหรือเพิ่มบริบทการบันทึกหรืออะไรก็ตาม การดำเนินการเหล่านี้คือทั้งหมดที่เรียบง่ายที่ตายแล้วจะแต่งกับและน่ารำคาญกับInt -> String Id -> Messageไม่ใช่ว่าไม่เคยมีกรณีใดที่ข้อ จำกัด ประเภทที่เพิ่มขึ้นเป็นที่พึงปรารถนา แต่ความน่ารำคาญก็น่าจะคุ้มค่ากับการแลกเปลี่ยน

คุณสามารถใช้คำพ้องความหมายประเภทแทน wrapper (ใน Haskell typeแทนที่จะเป็นnewtype) ซึ่งเป็นเรื่องธรรมดามากและไม่ต้องการการแปลงทั่วสถานที่ แต่มันไม่ได้ให้การรับรองประเภทคงที่ไม่ว่าจะเป็นรุ่น OOP ของคุณ แค่นิดหน่อย เครื่องห่อประเภทส่วนใหญ่จะใช้ในกรณีที่ลูกค้าไม่ได้คาดหวังที่จะจัดการกับค่าทั้งหมดเพียงแค่เก็บมันและส่งกลับ ตัวอย่างเช่นการจัดการไฟล์

ไม่มีอะไรสามารถป้องกันไม่ให้ลูกค้า "ถูกโค่นล้ม" คุณแค่สร้างห่วงเพื่อกระโดดข้ามสำหรับลูกค้าทุกคน การลอกเลียนแบบง่าย ๆ สำหรับการทดสอบหน่วยทั่วไปมักต้องการพฤติกรรมแปลก ๆ ที่ไม่สมเหตุสมผลในการผลิต อินเทอร์เฟซของคุณควรเขียนเพื่อไม่ให้สนใจถ้าเป็นไปได้


1
สิ่งนี้ให้ความรู้สึกเหมือนมีความคิดเห็นเพิ่มเติมเกี่ยวกับคำตอบของ Ben / Daenyth - คุณสามารถใช้ semantic types แต่คุณทำไม่ได้เพราะความไม่สะดวก? ฉันไม่ได้ลงคะแนนให้คุณ แต่มันไม่ใช่คำตอบสำหรับคำถามนี้
AlexFoxGill

-1 มันจะไม่ทำร้ายความสามารถในการเรียงความหรือการนำกลับมาใช้เลย หากIdและMessageเป็น wrappers ง่าย ๆIntและStringมันก็เป็นเรื่องเล็กน้อยที่จะแปลงระหว่างพวกเขา
Michael Shaw

ใช่มันเป็นเรื่องเล็กน้อย แต่ก็ยังน่ารำคาญที่จะทำทั่วสถานที่ ฉันได้เพิ่มตัวอย่างและอธิบายความแตกต่างระหว่างการใช้งานคำพ้องความหมายชนิดและคำเสริม
Karl Bielefeldt

ดูเหมือนว่าจะเป็นเรื่องขนาด ในโครงการขนาดเล็กประเภทของ wrapper เหล่านี้อาจไม่มีประโยชน์เลย ในโครงการขนาดใหญ่เป็นเรื่องธรรมดาที่ขอบเขตจะเป็นสัดส่วนเล็กน้อยของรหัสโดยรวมดังนั้นการแปลงประเภทนี้ที่ขอบเขตแล้วส่งผ่านประเภทที่ล้อมรอบทุกที่อื่นไม่ยากเกินไป เช่นเดียวกับอเล็กซ์พูดฉันไม่เห็นว่านี่เป็นคำตอบของคำถาม
Ben Aaronson

ฉันอธิบายวิธีการทำแล้วทำไมโปรแกรมเมอร์ที่ใช้งานไม่ได้ทำ นั่นคือคำตอบแม้ว่าจะไม่ใช่คนที่ต้องการ มันไม่มีอะไรเกี่ยวข้องกับขนาด ความคิดที่ว่ามันเป็นสิ่งที่ดีที่จะทำให้มันยากที่จะนำรหัสของคุณกลับมาใช้ใหม่จะเป็นแนวคิดของ OOP การสร้างประเภทผลิตภัณฑ์ที่เพิ่มมูลค่าบางอย่าง? ตลอดเวลา. การสร้างประเภทที่เหมือนกับประเภทที่ใช้กันอย่างแพร่หลายยกเว้นคุณสามารถใช้ในไลบรารีเดียวได้หรือไม่ แทบจะไม่เคยได้ยินยกเว้นในรหัส FP ที่โต้ตอบอย่างหนักกับรหัส OOP หรือเขียนโดยคนที่คุ้นเคยกับสำนวน OOP เป็นส่วนใหญ่
Karl Bielefeldt
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.