การส่งวัตถุเป็นวิธีที่เปลี่ยนวัตถุมันเป็นรูปแบบทั่วไป (ต่อต้าน -) หรือไม่?


17

ฉันอ่านเกี่ยวกับรหัสที่พบบ่อยมีกลิ่นในหนังสือ Refactoring Martin Fowler ของ ในบริบทนั้นฉันสงสัยเกี่ยวกับรูปแบบที่ฉันเห็นในฐานรหัสและไม่มีใครสามารถคิดว่ามันเป็นรูปแบบการต่อต้าน

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

var something = new Thing();
// ...
Foo(something);
int result = Bar(something, 42);
Baz(something);

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

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

var something1 =  new Thing();
// ...

// Let's return a new instance of Thing
var something2 = Foo(something1);

// Let's use out param to 'return' other info about the operation
int result;
var something3 = Bar(something2, out result);

// If necessary, let's capture and make explicit complex changes
var changes = Baz(something3)
something3.Apply(changes);

สำหรับฉันดูเหมือนว่ารูปแบบแรกจะถูกเลือกบนสมมติฐาน

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

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

และหากมีอะไรผิดปกติกับทางเลือกอื่นของฉัน



1
@ DaveHillier ขอบคุณฉันคุ้นเคยกับคำศัพท์ แต่ไม่ได้ทำการเชื่อมต่อ
Michiel van Oosterhout

คำตอบ:


9

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

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

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


1
ตัวอย่างเช่นหากฉันมีวัตถุ "ไฟล์" ฉันจะไม่พยายามย้ายวิธีการเปลี่ยนสถานะใด ๆ ไปยังวัตถุนั้น - ซึ่งจะเป็นการละเมิด SRP ซึ่งจะยังคงใช้ได้แม้ว่าคุณจะมีคลาสของคุณเองแทนที่จะเป็นคลาสไลบรารีเช่น "ไฟล์" - การใส่ตรรกะการเปลี่ยนสถานะทุกครั้งลงในคลาสของวัตถุนั้นไม่สมเหตุสมผล
Doc Brown

@ Tetastyn ฉันรู้ว่านี่เป็นคำตอบที่เก่า แต่ฉันมีปัญหาในการแสดงคำแนะนำของคุณในวรรคสุดท้ายในแง่ที่เป็นรูปธรรม คุณช่วยอธิบายหรือยกตัวอย่างได้ไหม?
AaronLS

@AaronLS - แทนที่จะเป็นBar(something)(และแก้ไขสถานะของsomething) ให้Barเป็นสมาชิกsomethingประเภทของ something.Bar(42)มีแนวโน้มที่จะกลายพันธุ์มากขึ้นsomethingในขณะที่ยังช่วยให้คุณใช้เครื่องมือ OO (สถานะส่วนตัวอินเทอร์เฟซและอื่น ๆ ) เพื่อป้องกันsomethingสถานะของ
Telastyn

14

เมื่อวิธีการไม่ได้ตั้งชื่ออย่างเหมาะสม

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

void AddMessageToLog(Logger logger, string msg)
{
    //...
}

หรือ

void StripInvalidCharsFromName(Person p)
{
// ...
}

หรือ

void AddValueToRepo(Repository repo,int val)
{
// ...
}

หรือ

void TransferMoneyBetweenAccounts(Account source, Account destination, decimal amount)
{
// ...
}

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

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


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

2
@michielvoo: หากการตั้งชื่อวิธี consise ดูเหมือนจะเป็นไปไม่ได้วิธีการของคุณจะรวมกลุ่มสิ่งที่ไม่ถูกต้องเข้าด้วยกันแทนที่จะสร้างสิ่งที่เป็นนามธรรมสำหรับงานที่ทำ (และนั่นเป็นจริงที่มีหรือไม่มีผลข้างเคียง)
Doc Brown

4

ใช่ดูhttp://codebetter.com/matthewpodwysocki/2008/04/30/side-effecting-functions-are-code-smells/สำหรับตัวอย่างหนึ่งในหลาย ๆ คนที่ชี้ให้เห็นว่าผลข้างเคียงที่ไม่คาดคิดไม่ดี

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


ฉันจะอธิบายสิ่งที่ OP อธิบายว่าเป็น "ผลข้างเคียงที่คาดหวัง" ตัวอย่างเช่นผู้รับมอบสิทธิ์คุณสามารถส่งต่อไปยังเอนจินของการเรียงลำดับบางอย่างที่ทำงานกับแต่ละองค์ประกอบในรายการ นั่นเป็นสิ่งที่ForEach<T>ทำ
Robert Harvey

@RobertHarvey ข้อร้องเรียนเกี่ยวกับวิธีการตั้งชื่อไม่ถูกต้องและเกี่ยวกับการอ่านโค้ดเพื่อหาผลข้างเคียงทำให้พวกเขาไม่ได้คาดหวังผลข้างเคียงอย่างแน่นอน
btilly

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

@RobertHarvey ฉันเห็นด้วย กุญแจสำคัญคือผลข้างเคียงที่สำคัญมีความสำคัญอย่างยิ่งที่ต้องทราบและจำเป็นต้องจัดทำเอกสารอย่างรอบคอบ (ควรใช้ชื่อของวิธีการ)
btilly

ฉันจะบอกว่ามันเป็นการผสมผสานของผลข้างเคียงที่ไม่คาดคิดและไม่ชัดเจน ขอบคุณสำหรับลิงค์
Michiel van Oosterhout

3

ก่อนอื่นสิ่งนี้ไม่ได้ขึ้นอยู่กับ "การส่งต่อโดยอ้างอิงธรรมชาติของ" มันขึ้นอยู่กับวัตถุที่เป็นประเภทอ้างอิงที่ไม่แน่นอน ในภาษาที่ไม่สามารถใช้งานได้นั้นมักจะเป็นอย่างนั้น

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

ฉันจะไม่พูดว่ามันเพิ่มขึ้นถึงระดับของรูปแบบการต่อต้าน แต่เป็นสิ่งที่ควรระวัง


2

ฉันเถียงมีความแตกต่างเล็ก ๆ น้อย ๆ ระหว่างA.Do(Something)การปรับเปลี่ยนsomethingและการปรับเปลี่ยนsomething.Do() ไม่ว่าsomethingในกรณีใดมันก็ควรจะชัดเจนจากชื่อของวิธีการเรียกใช้ที่somethingจะถูกแก้ไข หากไม่ชัดเจนจากชื่อวิธีการไม่ว่าsomethingจะเป็นพารามิเตอร์thisหรือส่วนหนึ่งของสภาพแวดล้อมก็ไม่ควรแก้ไข


1

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

var users = Dependency.Resolve<IGetUsersQuery>().GetAll();

var excludeAdminUsersFilter = new ExcludeAdminUsersFilter();
var filterByAnotherCriteria = new AnotherCriteriaFilter();

excludeAdminUsersFilter.Apply(users);
filterByAnotherCriteria.Apply(users); 

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

var users = Dependency.Resolve<IGetUsersQuery>().GetAll();
Filter(users);

ที่Filter(users)จะดำเนินการตัวกรองดังกล่าวข้างต้น

ฉันจำไม่ได้ว่าเคยเจอสิ่งนี้มาก่อน แต่ฉันคิดว่ามันเรียกว่าการกรองไปป์ไลน์


0

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

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

เกี่ยวกับวิธีการแก้ปัญหาของคุณข้อสังเกตบางประการ:

  • แอปพลิเคชันได้รับการออกแบบมาจากแง่มุมต่าง ๆ : เมื่อวัตถุถูกใช้เพียงเพื่อเก็บค่าหรือส่งผ่านข้ามขอบเขตขององค์ประกอบก็ควรเปลี่ยนอุปกรณ์ภายในของวัตถุภายนอกแทนที่จะเติมด้วยรายละเอียดของวิธีการเปลี่ยนแปลง
  • วัตถุโคลนนำไปสู่การท้องอืดต้องการหน่วยความจำและในหลายกรณีที่นำไปสู่การดำรงอยู่ของวัตถุเทียบเท่าในรัฐเข้ากันไม่ได้ (ที่Xกลายเป็นYหลังf()แต่Xเป็นจริงY) และความไม่สอดคล้องกันอาจจะเป็นชั่วคราว

ปัญหาที่คุณพยายามแก้ไขคือปัญหาที่ถูกต้อง อย่างไรก็ตามถึงแม้จะมีการทำ overengineering อย่างมากปัญหาก็คือการหลีกเลี่ยงไม่สามารถแก้ไขได้


2
นี่จะเป็นคำตอบที่ดีกว่าถ้าคุณเชื่อมโยงการสังเกตของคุณกับคำถามของ OP เช่นเดียวกับมันเป็นความคิดเห็นมากกว่าคำตอบ
Robert Harvey

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