ดีกว่าที่จะมี 2 วิธีด้วยความหมายที่ชัดเจนหรือเพียง 1 วิธีใช้คู่?


30

หากต้องการทำให้อินเทอร์เฟซง่ายขึ้นจะดีกว่าgetBalance()ไหมที่ไม่มีวิธี? การส่งผ่าน0ไปยังcharge(float c);จะให้ผลลัพธ์เดียวกัน:

public class Client {
    private float bal;
    float getBalance() { return bal; }
    float charge(float c) {
        bal -= c;
        return bal;
    }
}

อาจจดบันทึกในjavadoc? หรือเพียงแค่ปล่อยให้ผู้ใช้ระดับชั้นเรียนรู้วิธีการรับยอดคงเหลือ


26
ฉันสมมุติว่านี่เป็นตัวอย่างรหัสตัวอย่างและคุณไม่ได้ใช้เลขทศนิยมในการจัดเก็บค่าเงิน มิฉะนั้นฉันจะต้องได้รับการสั่งสอนทั้งหมด :-)
corsiKa

1
@corsiKa nah นั่นเป็นเพียงตัวอย่าง แต่ใช่ฉันจะใช้ลอยเพื่อเป็นตัวแทนของเงินในชั้นเรียนจริง ... ขอบคุณที่เตือนฉันเกี่ยวกับอันตรายของตัวเลขจุดลอยตัว
david

10
บางภาษาแยกความแตกต่างระหว่าง mutators และ accessors เพื่อผลที่ดี มันจะน่ารำคาญอย่างยิ่งที่จะไม่สามารถรับความสมดุลของอินสแตนซ์ที่เข้าถึงได้เป็นค่าคงที่เท่านั้น!
JDługosz

คำตอบ:


90

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

นอกจากนี้การเรียกcharge(0)ละเมิดหลักการที่น่าประหลาดใจน้อยที่สุดหรือที่รู้จักกันในชื่อWTFs ต่อนาที (จาก Clean Code, ภาพด้านล่าง) ทำให้ยากสำหรับสมาชิกใหม่ของทีม พวกเขาเข้าใจว่าการโทรนั้นใช้เพื่อให้เกิดความสมดุล คิดว่าผู้อ่านคนอื่นจะตอบสนองอย่างไร:

ป้อนคำอธิบายรูปภาพที่นี่

นอกจากนี้ลายเซ็นของchargeวิธีการนั้นไม่สอดคล้องกับแนวทางในการแยกสิ่งเดียวและสิ่งเดียวและการแยกแบบสอบถามคำสั่งเนื่องจากทำให้วัตถุเปลี่ยนสถานะในขณะที่ส่งคืนค่าใหม่

โดยสรุปฉันเชื่อว่าอินเทอร์เฟซที่ง่ายที่สุดในกรณีนี้คือ:

public class Client {
  private float bal;
  float getBalance() { return bal; }
  void charge(float c) { bal -= c; }
}

25
จุดเกี่ยวกับการแยกแบบสอบถามคำสั่งมีความสำคัญอย่างยิ่ง IMO ลองนึกภาพคุณเป็นนักพัฒนาใหม่ในโครงการนี้ เมื่อมองดูรหัสมันจะชัดเจนทันทีว่าgetBalance()คืนค่าบางอย่างและไม่แก้ไขอะไรเลย ในทางกลับกันcharge(0)ดูเหมือนว่ามันอาจจะปรับเปลี่ยนบางสิ่งบางอย่าง ... มันอาจส่งเหตุการณ์ PropertyChanged หรือไม่ แล้วมันจะคืนอะไรค่าก่อนหน้าหรือค่าใหม่? ตอนนี้คุณต้องค้นหามันในเอกสารและสมองเสียซึ่งสามารถหลีกเลี่ยงได้โดยใช้วิธีการที่ชัดเจนยิ่งขึ้น
Mage Xy

19
นอกจากนี้การใช้charge(0)เป็นวิธีการรับยอดเงินเพราะมันไม่มีผลกระทบใด ๆ ในขณะนี้จะนำคุณไปสู่รายละเอียดการใช้งาน ฉันสามารถจินตนาการถึงความต้องการในอนาคตที่การติดตามจำนวนการเรียกเก็บเงินมีความเกี่ยวข้อง ณ จุดที่รหัสฐานของคุณเกลื่อนไปด้วยค่าใช้จ่ายที่ไม่ได้คิดค่าใช้จ่ายจริง ๆ จะกลายเป็นจุดเจ็บปวด คุณควรจัดเตรียมองค์ประกอบของอินเทอร์เฟซซึ่งหมายถึงสิ่งที่ลูกค้าของคุณต้องทำไม่ใช่สิ่งที่เกิดขึ้นเพียงเพื่อทำสิ่งที่ลูกค้าของคุณต้องทำ
เบ็น

12
การตักเตือน CQS นั้นไม่เหมาะสม สำหรับcharge()วิธีการหากวิธีนั้นเป็นอะตอมในระบบที่เกิดขึ้นพร้อมกันก็อาจจะเหมาะสมที่จะส่งกลับค่าใหม่หรือเก่า
Dietrich Epp

3
ฉันพบการอ้างอิงทั้งสองของคุณเพื่อ CQRS ที่ไม่น่าเชื่อถืออย่างยิ่ง ตัวอย่างเช่นฉันมักชอบความคิดของเมเยอร์ แต่ดูที่ประเภทการคืนของฟังก์ชั่นเพื่อบอกว่ามันกลายพันธุ์บางสิ่งนั้นเป็นสิ่งที่ไม่แน่นอนหรือไม่ โครงสร้างข้อมูลที่เกิดขึ้นพร้อมกันหรือไม่เปลี่ยนแปลงจำนวนมากต้องการการกลายพันธุ์ + ผลตอบแทน คุณจะต่อท้ายสตริงโดยไม่ละเมิด CQRS อย่างไร คุณมีการอ้างอิงที่ดีกว่าไหม
949300

6
เกือบจะไม่มีรหัสที่สอดคล้องกับการแยกคำสั่ง มันไม่ได้เป็นสำนวนในภาษาเชิงวัตถุใด ๆ ที่เป็นที่นิยมและถูกละเมิดโดย API ในตัวของทุกภาษาที่ฉันเคยใช้ เพื่ออธิบายว่ามันเป็น "วิธีปฏิบัติที่ดีที่สุด" จึงดูเหมือนแปลกประหลาด; คุณสามารถตั้งชื่อโครงการซอฟต์แวร์เดียวได้หรือไม่
Mark Amery

28

IMO การแทนที่getBalance()ด้วยcharge(0)แอปพลิเคชันของคุณไม่ใช่การทำให้เป็นเรื่องง่าย ใช่มันเป็นเส้นที่น้อยกว่า แต่มันทำให้เสียความหมายของcharge()วิธีการซึ่งอาจทำให้ปวดหัวลงเมื่อคุณหรือคนอื่นจำเป็นต้องทบทวนรหัสนี้อีกครั้ง

แม้ว่าพวกเขาอาจให้ผลลัพธ์เดียวกันการรับยอดคงเหลือของบัญชีไม่เหมือนกับการคิดค่าศูนย์ดังนั้นมันอาจเป็นการแยกความกังวลของคุณได้ดีที่สุด ตัวอย่างเช่นหากคุณจำเป็นต้องแก้ไขcharge()เพื่อเข้าสู่ระบบเมื่อใดก็ตามที่มีการทำธุรกรรมบัญชีตอนนี้คุณมีปัญหาและจะต้องแยกการทำงานต่อไป


13

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

float charge(float c) {
    lockDownUserAccountUntilChargeClears();
    bal -= c;
    Unlock();
    return bal;
}

ทันทีที่ใช้charge()เพื่อให้ได้สมดุลนั้นดูไม่ดีนัก


ใช่ จุดดีที่chargeหนักกว่ามาก
Deduplicator

2

การใช้charge(0);เพื่อให้เกิดความสมดุลเป็นความคิดที่ไม่ดี: วันหนึ่งบางคนอาจเพิ่มรหัสบางส่วนที่นั่นเพื่อบันทึกการเรียกเก็บเงินที่เกิดขึ้นโดยไม่ทราบว่ามีการใช้งานฟังก์ชั่นอื่น ๆ แล้วทุกครั้งที่มีคนได้รับยอดคงเหลือ (มีหลายวิธีรอบตัวเช่นคำสั่งแบบมีเงื่อนไขที่ระบุสิ่งที่ชอบ:

if (c > 0) {
    // make and log charge
}
return bal;

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

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


0

ฉันรู้ว่ามีคำตอบมากมาย แต่อีกเหตุผลหนึ่งcharge(0)คือการพิมพ์ผิดง่าย ๆcharge(9)จะทำให้ยอดเงินของลูกค้าลดลงทุกครั้งที่คุณต้องการรับยอดเงินของพวกเขา หากคุณมีการทดสอบหน่วยที่ดีคุณอาจสามารถลดความเสี่ยงดังกล่าวได้ แต่ถ้าคุณล้มเหลวในการขยันในทุกการโทรchargeคุณอาจได้รับอุบัติเหตุนี้


-2

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

ในกรณีดังกล่าวการทำให้งานการเขียนแต่ละครั้งง่ายขึ้นมีค่ามากกว่าความชัดเจนของการใช้งานเพราะในอดีตหลีกเลี่ยงข้อผิดพลาดการละเมิดสัญญา กู้คืนโดยฟังก์ชั่นผู้ช่วยหรือวิธี superclass กำหนดในแง่ของgetBalancecharge

(นี่คือรูปแบบการออกแบบซึ่งฉันจำไม่ได้ว่าชื่อเฉพาะสำหรับ: การกำหนดอินเทอร์เฟซที่เป็นมิตรกับผู้โทรเข้าที่ซับซ้อนในแง่ของการเป็นมิตรกับผู้ใช้งานขั้นต่ำใน Classic Mac OS อินเทอร์เฟซน้อยที่สุด แต่ดูเหมือนจะไม่เป็นคำที่นิยม)

หากไม่ใช่กรณีนี้ (มีการใช้งานไม่กี่อย่างหรืออย่างใดอย่างหนึ่ง) จากนั้นแยกวิธีการเพื่อความชัดเจนและเพื่อให้สามารถเพิ่มพฤติกรรมที่เกี่ยวข้องกับการเรียกเก็บเงินที่ไม่ใช่ศูนย์charge()ได้อย่างง่ายดาย


1
อันที่จริงฉันมีกรณีที่เลือกอินเทอร์เฟซน้อยที่สุดเพื่อให้การทดสอบครอบคลุมสมบูรณ์ง่ายขึ้น แต่สิ่งนี้ไม่จำเป็นต้องเปิดเผยต่อผู้ใช้ของชั้นเรียน เช่นแทรก , ผนวกและลบทั้งหมดจะสามารถห่อที่เรียบง่ายของทั่วไปแทนที่ซึ่งมีทุกเส้นทางการควบคุมที่ดีการศึกษาและทดสอบ
JDługosz

ฉันเห็น API ดังกล่าวแล้ว ตัวจัดสรร Lua 5.1+ ใช้มัน คุณจัดเตรียมหนึ่งฟังก์ชันและขึ้นอยู่กับพารามิเตอร์ที่ส่งผ่านคุณตัดสินใจว่าจะพยายามจัดสรรหน่วยความจำใหม่ยกเลิกการจัดสรรหน่วยความจำหรือจัดสรรหน่วยความจำใหม่จากหน่วยความจำเก่า มันเจ็บปวดที่จะเขียนฟังก์ชั่นดังกล่าว ฉันจะค่อนข้างดีที่ Lua มอบหน้าที่สองอย่างให้คุณหนึ่งฟังก์ชันสำหรับการจัดสรรและอีกหนึ่งสำหรับการจัดสรรคืน
Nicol Bolas

@NicolBolas: และไม่มีการจัดสรรใหม่ใช่ไหม อย่างไรก็ตามสิ่งนั้นมี "ประโยชน์" ที่เพิ่มเข้ามาเกือบจะเหมือนกับreallocในบางระบบ
Deduplicator

@Dupuplicator: การจัดสรรกับการจัดสรรใหม่คือ ... ตกลง มันไม่ได้ยอดเยี่ยมที่จะใส่พวกเขาในฟังก์ชั่นเดียวกัน แต่ก็ไม่ได้แย่ไปกว่าการจัดสรรคืนในที่เดียวกัน นอกจากนี้ยังเป็นความแตกต่างที่ง่ายต่อการทำ
Nicol Bolas

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