การแก้ไขวัตถุที่ส่งผ่านโดยการอ้างอิงเป็นการปฏิบัติที่ไม่ดี


12

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

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

วิธีเก่า:

public void InsertUser(User user) {
    user.Username = GenerateUsername(user);
    user.Password = GeneratePassword(user);

    context.Users.Add(user);
}

วิธีการใหม่:

public void InsertUser(User user) {
    SetUsername(user);
    SetPassword(user);

    context.Users.Add(user);
}

private void SetUsername(User user) {
    var username = "random business logic";

    user.Username = username;
}

private void SetPassword(User user) {
    var password = "more business logic";

    user.Password = password;
}

โดยทั่วไปแล้วการตั้งค่าของคุณสมบัติจากวิธีอื่นเป็นการปฏิบัติที่ไม่ดีหรือไม่?


6
เพียงแค่ว่ากันว่า ... คุณยังไม่ได้ผ่านการอ้างอิงใด ๆ ที่นี่ การส่งผ่านการอ้างอิงโดยค่าไม่ใช่สิ่งเดียวกัน
cHao

4
@cHao: นั่นคือความแตกต่างโดยไม่มีความแตกต่าง พฤติกรรมของรหัสจะเหมือนกันโดยไม่คำนึงถึง
Robert Harvey

2
@JDDavis: พื้นฐานความแตกต่างที่แท้จริงเพียงอย่างเดียวระหว่างสองตัวอย่างของคุณคือคุณได้ให้ชื่อที่มีความหมายกับชื่อผู้ใช้ที่กำหนดและการดำเนินการตั้งรหัสผ่าน
Robert Harvey

1
การโทรทั้งหมดของคุณอ้างอิงจากที่นี่ คุณต้องใช้ประเภทค่าใน C # เพื่อผ่านค่า
Frank Hileman

2
@FrankHileman: การโทรทั้งหมดเป็นไปตามค่าที่นี่ นั่นคือค่าเริ่มต้นใน C # การผ่านการอ้างอิงและการผ่านโดยการอ้างอิงนั้นเป็นสัตว์ที่แตกต่างกันและความแตกต่างก็มีความสำคัญ หากถูกส่งผ่านโดยอ้างอิงรหัสจะงัดมันออกมาจากมือของผู้โทรและแทนที่โดยเพียงแค่บอกว่าการพูดuser user = null;
cHao

คำตอบ:


10

ปัญหาที่นี่คือที่Userจริงสามารถมีสองสิ่งที่แตกต่าง:

  1. เอนทิตีผู้ใช้ที่สมบูรณ์ซึ่งสามารถส่งผ่านไปยังที่เก็บข้อมูลของคุณ

  2. ชุดขององค์ประกอบข้อมูลที่ต้องการจากผู้เรียกเพื่อเริ่มกระบวนการสร้างเอนทิตีผู้ใช้ ระบบจะต้องเพิ่มชื่อผู้ใช้และรหัสผ่านก่อนที่จะเป็นผู้ใช้ที่ถูกต้องเช่นใน # 1 ข้างต้น

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

ฉันขอแนะนำให้คุณต้องการเอนทิตี้สองอย่างเช่นUserคลาสและEnrollRequestคลาส หลังสามารถมีทุกสิ่งที่คุณจำเป็นต้องรู้เพื่อสร้างผู้ใช้ ดูเหมือนว่าคลาสผู้ใช้ของคุณ แต่ไม่มีชื่อผู้ใช้และรหัสผ่าน จากนั้นคุณสามารถทำสิ่งนี้:

public User InsertUser(EnrollRequest request) {
    var userName = GenerateUserName();
    var password = GeneratePassword();

    //You might want to replace this with a factory call, but "new" works here as an example
    var newUser = new User
    (
        request.Name, 
        request.Email, 
        userName, 
        password
    );
    context.Users.Add(user);
    return newUser;
}

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


2
วิธีการที่มีชื่อคล้ายInsertUserมีแนวโน้มว่าจะมีผลข้างเคียงของการแทรก ฉันไม่แน่ใจว่าคำว่า "icky" หมายถึงอะไรในบริบทนี้ สำหรับการใช้ "ผู้ใช้" ที่ยังไม่ได้เพิ่มคุณดูเหมือนจะพลาดจุดนี้ไป หากยังไม่ได้เพิ่มมันไม่ใช่ผู้ใช้เพียงแค่ขอให้สร้างผู้ใช้ คุณมีอิสระที่จะทำงานกับคำขอได้แน่นอน
John Wu

@candiedorange เหล่านั้นไม่ใช่ผลข้างเคียงพวกเขาต้องการผลของการเรียกวิธีการ หากคุณไม่ต้องการเพิ่มทันทีคุณสร้างวิธีอื่นที่ตรงกับกรณีการใช้งาน แต่นั่นไม่ใช่กรณีการใช้งานที่ส่งผ่านไปในคำถามดังนั้นจึงกล่าวได้ว่าความกังวลนั้นไม่สำคัญ
Andy

@candiedorange หากการเชื่อมต่อทำให้รหัสลูกค้าง่ายขึ้นฉันจะบอกว่าไม่เป็นไร สิ่งที่ devs หรือ pos อาจคิดว่าในอนาคตไม่เกี่ยวข้อง หากเวลานั้นมาถึงคุณเปลี่ยนรหัส ไม่มีอะไรสิ้นเปลืองไปกว่าการสร้างรหัสที่สามารถรองรับกรณีการใช้งานในอนาคต
Andy

5
@CandiedOrange ฉันขอแนะนำให้คุณพยายามที่จะไม่จับคู่ระบบการพิมพ์ที่กำหนดไว้อย่างแม่นยำของการใช้งานกับองค์กรที่มีแนวคิดเป็นภาษาอังกฤษธรรมดาของธุรกิจ แนวคิดทางธุรกิจของ "ผู้ใช้" สามารถนำไปใช้ได้อย่างแน่นอนในสองคลาสหากไม่มากกว่านั้นเพื่อเป็นตัวแทนของตัวแปรที่มีประโยชน์ ผู้ใช้ที่ไม่ยืนยันไม่รับรองชื่อผู้ใช้ที่ไม่ซ้ำไม่มีคีย์หลักที่สามารถผูกธุรกรรมและไม่สามารถลงชื่อเข้าใช้ได้ดังนั้นฉันจึงบอกว่ามันประกอบด้วยตัวแปรที่สำคัญ สำหรับคนธรรมดาแน่นอนว่า EnrollRequest และผู้ใช้นั้นเป็น "ผู้ใช้" ในภาษาที่คลุมเครือ
John Wu

5

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

 repo.InsertUser(user);

เพื่อเปลี่ยนสถานะที่เก็บข้อมูลภายในไม่ใช่สถานะของวัตถุผู้ใช้ ปัญหานี้มีอยู่ในการติดตั้งใช้งานของคุณทั้งสองวิธีInsertUserสิ่งนี้เป็นการภายในที่ไม่เกี่ยวข้องอย่างสมบูรณ์

เพื่อแก้ปัญหานี้คุณสามารถทำได้

  • การกำหนดค่าเริ่มต้นแยกจากการแทรก (ดังนั้นผู้เรียกใช้InsertUserต้องจัดเตรียมUserวัตถุที่เริ่มต้นอย่างสมบูรณ์หรือ

  • สร้างการเริ่มต้นในกระบวนการก่อสร้างของวัตถุผู้ใช้ (ตามคำแนะนำของคำตอบอื่น ๆ ) หรือ

  • เพียงลองค้นหาชื่อที่ดีขึ้นสำหรับวิธีการที่แสดงออกอย่างชัดเจนว่ามันทำอะไร

ดังนั้นเลือกชื่อวิธีการเช่นPrepareAndInsertUser, OrchestrateUserInsertion, InsertUserWithNewNameAndPasswordหรือสิ่งที่คุณชอบที่จะทำให้มีผลข้างเคียงมากขึ้นชัดเจน

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


3

ฉันกำลังดูสองตัวเลือกที่คุณเลือกและฉันต้องบอกว่าฉันชอบวิธีเก่ามากกว่าวิธีใหม่ที่เสนอ มีเหตุผลสองสามข้อแม้ว่ามันจะทำสิ่งเดียวกันโดยพื้นฐาน

ในทั้งสองกรณีที่คุณกำลังตั้งค่าและuser.UserName user.Passwordฉันมีการจองเกี่ยวกับรายการรหัสผ่าน แต่การจองเหล่านั้นไม่ได้ใกล้เคียงกับหัวข้อในมือ

ผลกระทบของการแก้ไขวัตถุอ้างอิง

  • มันทำให้การเขียนโปรแกรมพร้อมกันยากขึ้น - แต่ไม่ใช่ทุกแอพพลิเคชั่นที่มีหลายเธรด
  • การปรับเปลี่ยนเหล่านั้นอาจเป็นเรื่องที่น่าประหลาดใจโดยเฉพาะอย่างยิ่งหากไม่มีอะไรเกี่ยวกับวิธีการที่แนะนำว่ามันจะเกิดขึ้น
  • ความประหลาดใจเหล่านั้นสามารถทำให้การบำรุงรักษายากขึ้น

วิธีเก่ากับวิธีใหม่

วิธีการแบบเก่าทำให้การทดสอบง่ายขึ้น:

  • GenerateUserName()สามารถทดสอบได้อย่างอิสระ คุณสามารถเขียนการทดสอบกับวิธีการนั้นและตรวจสอบให้แน่ใจว่าชื่อถูกสร้าง
  • หากชื่อต้องการข้อมูลจากวัตถุผู้ใช้คุณสามารถเปลี่ยนลายเซ็นเป็นGenerateUserName(User user)และรักษาความสามารถในการทดสอบนั้น

วิธีการใหม่ซ่อนการกลายพันธุ์:

  • คุณไม่รู้ว่าUserวัตถุกำลังเปลี่ยนแปลงจนกว่าคุณจะลงลึก 2 ชั้น
  • การเปลี่ยนแปลงของUserวัตถุนั้นน่าแปลกใจมากในกรณีนี้
  • SetUserName()ทำมากกว่าตั้งชื่อผู้ใช้ นั่นไม่ใช่ความจริงในการโฆษณาซึ่งทำให้นักพัฒนาซอฟต์แวร์รายใหม่ค้นพบวิธีการทำงานของแอปพลิเคชันของคุณได้ยากขึ้น

1

ฉันเห็นด้วยอย่างยิ่งกับคำตอบของ John Wu คำแนะนำของเขาเป็นสิ่งที่ดี แต่มันก็หายไปเล็กน้อยคำถามตรงของคุณ

โดยทั่วไปแล้วการตั้งค่าของคุณสมบัติจากวิธีอื่นเป็นการปฏิบัติที่ไม่ดีหรือไม่?

ไม่โดยธรรมชาติ

คุณไม่สามารถทำสิ่งนี้ได้ไกลเกินไปเนื่องจากคุณมีพฤติกรรมที่ไม่คาดคิด เช่นPrintName(myPerson)ไม่ควรเปลี่ยนวัตถุบุคคลเนื่องจากวิธีการแสดงถึงความสนใจในการอ่านค่าที่มีอยู่เท่านั้น แต่นั่นเป็นข้อโต้แย้งที่แตกต่างไปจากกรณีของคุณเนื่องจากSetUsername(user)มันบอกเป็นนัยว่ามันจะตั้งค่า


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

ตัวอย่างเช่น:

var myContract = CreateEmptyContract();

ArrrangeContractDeletedStatus(myContract);

ฉันคาดหวังอย่างชัดเจนว่าArrrangeContractDeletedStatusวิธีการเปลี่ยนสถานะของmyContractวัตถุ

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

ถ้าฉันรวมCreateEmptyContractและArrrangeContractDeletedStatusเป็นวิธีเดียว; ฉันจะต้องสร้างวิธีการนี้ที่หลากหลายสำหรับสัญญาที่แตกต่างกันทุกครั้งที่ฉันต้องการทดสอบในสถานะที่ถูกลบ

และในขณะที่ฉันสามารถทำสิ่งที่ชอบ:

myContract = ArrrangeContractDeletedStatus(myContract);

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

การเปลี่ยนวัตถุเป็นวิธีที่ง่ายที่สุดในการได้รับสิ่งที่ฉันต้องการโดยไม่ต้องทำงานมากขึ้นเพียงเพื่อหลีกเลี่ยงการไม่เปลี่ยนวัตถุตามหลักการ


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


0

ฉันพบว่าทั้งเก่าและใหม่เป็นปัญหา

วิธีเก่า:

1) InsertUser(User user)

เมื่ออ่านชื่อวิธีการนี้โผล่ขึ้นมาใน IDE ของฉันสิ่งแรกที่อยู่ในใจของฉันคือ

ผู้ใช้แทรกอยู่ที่ไหน

AddUserToContextชื่อวิธีการควรอ่าน อย่างน้อยนี่คือสิ่งที่วิธีการทำในท้ายที่สุด

2) InsertUser(User user)

สิ่งนี้ละเมิดหลักการของความประหลาดใจน้อยที่สุดอย่างชัดเจน บอกเด็ก ๆ ว่าฉันทำงานของฉันจนได้และสร้างตัวอย่างใหม่ของ a Userและมอบให้เขาnameและตั้งค่าpasswordฉันจะประหลาดใจด้วย:

ก) สิ่งนี้ไม่เพียง แต่แทรกผู้ใช้ลงในบางสิ่ง

b) มันยังทำลายความตั้งใจของฉันที่จะแทรกผู้ใช้ตามที่เป็นอยู่; ชื่อและรหัสผ่านถูกแทนที่

c) สิ่งนี้บ่งชี้ว่าเป็นการละเมิดหลักการความรับผิดชอบเดียว

วิธีการใหม่:

1) InsertUser(User user)

ยังคงละเมิด SRP และหลักการของความประหลาดใจน้อยที่สุด

2) SetUsername(User user)

คำถาม

ตั้งชื่อผู้ใช้? คืออะไร?

ดีกว่า: SetRandomNameหรือสิ่งที่สะท้อนถึงความตั้งใจ

3) SetPassword(User user)

คำถาม

ตั้งรหัสผ่าน? คืออะไร?

ดีกว่า: SetRandomPasswordหรือสิ่งที่สะท้อนถึงความตั้งใจ


คำแนะนำ:

สิ่งที่ฉันต้องการอ่านจะเป็นสิ่งที่ชอบ:

public User GenerateRandomUser(UserFactory Uf){
    User u = Uf.GetUser();
    u.Name = GenerateRandomUserName();
    u.Password = GenerateRandomUserPassword();
    return u;
}

...

public void AddUserToContext(User u){
    this.context.Users.Add(u);
}

เกี่ยวกับคำถามเริ่มต้นของคุณ:

การแก้ไขวัตถุที่ส่งผ่านโดยการอ้างอิงเป็นการปฏิบัติที่ไม่ดี

ไม่มันเป็นเรื่องของรสนิยมมากกว่า

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.