จงเลือกความซ้ำซ้อนแทนนามธรรมที่ผิดพลาด (2016)
(sandimetz.com)- โค้ดที่ซ้ำกันมีต้นทุนถูกกว่า นามธรรมที่ผิดพลาด มาก และการ รีบทำให้เป็นส่วนกลางเร็วเกินไปจะเพิ่มต้นทุนการบำรุงรักษาในระยะยาว
- แม้การแยกออกมาในตอนแรกจะดูสมเหตุสมผล แต่เมื่อความต้องการค่อย ๆ เปลี่ยนไป ก็จะมี พารามิเตอร์และเงื่อนไข if เพิ่มเข้ามาจนทำให้เจตนาเดิมพร่าเลือน
- เมื่อนามธรรมร่วมเริ่มต้องแบกรับหลายแนวคิด โค้ดจะกลายเป็นกระบวนการที่ขับเคลื่อนด้วยเงื่อนไข และยิ่งเพิ่มฟีเจอร์ใหม่ก็ยิ่งแตกหักง่าย
- ควรระวัง กับดักต้นทุนจม ที่พยายามปกป้องความพยายามที่ลงไปกับโค้ดเดิม และหากจำเป็นควรอินไลน์นามธรรมกลับไปยังจุดเรียกใช้ เพื่อให้เหลือเฉพาะโค้ดที่ต้องใช้จริง
- หากเห็นชัดว่านามธรรมนั้นผิดพลาด การนำความซ้ำกลับมาใหม่เพื่อสังเกตจุดร่วมของความต้องการปัจจุบัน แล้วค่อยแยกออกอีกครั้ง มักจะเร็วกว่า
กระบวนการที่ทำให้เกิดนามธรรมที่ผิดพลาด
- ประโยค “duplication is far cheaper than the wrong abstraction” เป็นส่วนหนึ่งของงานพูดที่ RailsConf 2014 แต่หลังจากนั้นก็ยังถูกอ้างถึงอยู่เรื่อยมา
- เส้นทางความล้มเหลวที่พบบ่อยมีดังนี้
- นักพัฒนา A พบ ความซ้ำซ้อน
- แยกความซ้ำออกเป็นเมธอดหรือคลาส ตั้งชื่อให้ และสร้างนามธรรมใหม่
- แทนที่โค้ดซ้ำในจุดเรียกใช้ด้วยการเรียกนามธรรมใหม่นั้น
- เมื่อเวลาผ่านไป ก็เกิดความต้องการใหม่ที่เกือบจะเหมือนเดิมแต่ไม่เหมือนกันทั้งหมด
- นักพัฒนา B พยายามคงนามธรรมเดิมไว้ จึงเพิ่มพารามิเตอร์และใส่เงื่อนไขที่พาไปตามเส้นทางต่างกันตามค่า
- หลังจากนั้น ทุกครั้งที่มีความต้องการใหม่ พารามิเตอร์และเงื่อนไขก็จะเพิ่มขึ้น ทำให้โค้ดเข้าใจได้ยากขึ้นเรื่อย ๆ
- โค้ดที่สร้างขึ้นมาแล้วมักดูเหมือนเป็น การลงทุนที่ต้องรักษาไว้
- จิตวิทยาที่เสียดายความพยายามที่ลงไปแล้วจะเริ่มทำงาน
- ยิ่งโค้ดซับซ้อนและเข้าใจยาก ก็ยิ่งดูเหมือนเป็นสิ่งสำคัญและใช้เวลานาน จนยิ่งทิ้งได้ยาก
- สิ่งนี้เชื่อมโยงกับ กับดักต้นทุนจม
กลับไปหาความซ้ำ แล้วค่อยแยกออกใหม่
- หากยังคงพัฒนาความต้องการใหม่บนฐานของนามธรรมที่ผิดพลาดต่อไป โค้ดที่ใช้ร่วมกันจะกลายเป็นโค้ดที่ขับเคลื่อนด้วยเงื่อนไข และยิ่งเพิ่มฟีเจอร์ก็ยิ่งไม่เสถียร
- ในเวลานั้น ทางลัดที่เร็วที่สุดไม่ใช่การฝืนเดินหน้าต่อ แต่คือ การถอยกลับ
- อินไลน์โค้ดที่ถูกนามธรรมไว้กลับเข้าไปในแต่ละจุดเรียกใช้ เพื่อนำความซ้ำกลับมาอีกครั้ง
- ดูจากพารามิเตอร์ที่แต่ละจุดเรียกใช้ส่งเข้าไป เพื่อดูว่าโค้ดใดถูกทำงานจริง
- ลบโค้ดที่ไม่จำเป็นต่อจุดเรียกใช้นั้นออก
- กระบวนการอินไลน์นี้จะลบนามธรรมและเงื่อนไขออกพร้อมกัน และย่อแต่ละจุดเรียกใช้ให้เหลือเพียงโค้ดที่ตัวเองต้องการ
- โค้ดที่ดูเหมือนเรียกใช้นามธรรมเดียวกัน แท้จริงแล้วแต่ละจุดเรียกใช้อาจกำลังวิ่งผ่าน เส้นทางโค้ดที่เฉพาะตัว อย่างมาก
- ต้องลบนามธรรมเดิมออกให้หมดก่อน จึงจะกลับมาสังเกตความซ้ำ และแยกนามธรรมใหม่ที่เหมาะกับความต้องการปัจจุบันได้
- หากยังมีการเพิ่มพารามิเตอร์และเส้นทางเงื่อนไขเข้าไปในโค้ดที่ใช้ร่วมกันอย่างต่อเนื่อง ก็มีโอกาสสูงว่านามธรรมนั้นจะไม่เหมาะอีกต่อไป
- ในตอนแรกมันอาจเป็นนามธรรมที่เหมาะสมก็ได้
- แต่เมื่อความต้องการเปลี่ยนไป ก็อาจรักษารูปแบบเดิมไว้ได้ยากอีกต่อไป
- สำหรับนามธรรมที่ผิดพลาด การนำความซ้ำกลับมาใหม่ไม่ใช่การถอยหลัง แต่คือ การเดินหน้าไปสู่สิ่งที่ดีกว่า
5 ความคิดเห็น
ผม/ฉันไม่แน่ใจว่านี่เป็นประเด็นที่จำเป็นต้องตีความแบบแบ่งเป็นสองขั้วหรือไม่
โอ้ เห็นด้วยมากครับ
สิ่งที่ยังไม่เป็นระเบียบก็จัดระเบียบทีหลังได้
แต่สิ่งที่จัดเป็นระเบียบไว้แล้ว ดูเหมือนว่าจะมีต้นทุนในการรื้อกลับมากกว่ามากครับ
ponytail เพิ่งโพสต์ขึ้นไป แล้วก็มีบทความแบบนี้ออกมาทันทีเลย 555
เป็นประเด็นที่ถกเถียงกันอยู่เสมอครับ
ความคิดเห็นจาก Hacker News
มองว่าควรยึดหลัก single source of truth ไว้เสมอ
ถ้าเป็นโค้ดซ้ำที่พอไม่ตรงกันเมื่อไรจะกลายเป็นบั๊ก ก็ควรรีแฟกเตอร์ ไม่อย่างนั้นจะเกิด การพ่วงกันระยะไกล ที่ทำให้นักพัฒนาในอนาคตรู้ตัวยากมากจนกว่าบั๊กจะระเบิด
แต่ถ้าไม่ได้ละเมิดหลักนั้น abstraction ก็เป็นแค่ความสะดวกเท่านั้น และถ้ามันเริ่มใช้งานไม่สะดวก ก็แปลว่ามันไม่ทำหน้าที่ของมันแล้ว จึงไม่มีเหตุผลจะใช้ต่อ ถ้าฟังก์ชันต้องพึ่งหลายแฟลกเพื่อให้ทำงานแบบปรับแต่งได้ ก็มักเป็น abstraction ที่ผิดพลาดหรือไม่ก็ละเมิดหลัก single responsibility
ถ้าจำเป็นต้องปรับแต่งเยอะจริง ๆ การรับฟังก์ชัน/ฟังก์เตอร์เป็นอาร์กิวเมนต์มักดีกว่า เช่นทำเป็น
solve(f:double -> double, stopping_criteria: StoppingCriteriaClass)แทนsolve(f:double -> double, max_iters = 99, x_abs_tol = 1e-15, x_rel_tol = 1e-15, ...)ยังไม่แน่ชัดว่าโค้ดสองจุดใช้ algorithm เดียวกัน หรือเป็นคนละเวอร์ชันที่ต่างกันเล็กน้อย และที่สำคัญกว่านั้นคือมันจะเปลี่ยนด้วยเหตุผลเดียวกันหรือไม่
คำคมในหัวข้อบอกว่า การฝืนทำให้สิ่งที่ต่างกันเป็นสิ่งเดียวกันนั้นเจ็บปวดกว่าการทำของที่เหมือนกันซ้ำไว้ก่อนแล้วค่อยแยกให้ต่างกันทีหลัง ซึ่งผมก็เห็นด้วย อย่างหลังแค่แก้การเปลี่ยนเดียวกันสองครั้ง หรือรีแฟกเตอร์เพื่อใส่ abstraction ก็พอ แต่แบบแรกต้องคอยแปะเพิ่มบน abstraction เดิมหรือไม่ก็ย้อนกลับมัน
โดยเฉพาะเพราะมันทำลาย locality ซึ่งเวลาแก้ไขแล้วนี่คือคุณสมบัติที่สำคัญจริง ๆ แค่อยากเปลี่ยนตรงนี้แล้วไม่ต้องกังวลว่าจะมีผลข้างเคียงกับส่วนที่ไม่เกี่ยวของระบบ
ตัวอย่างชัด ๆ คือการซิงก์ pyproject.toml / requirements.txt ซึ่งบางครั้งก็เป็นทางเลือกที่ดีที่สุดจริง ๆ และน่าจะประยุกต์ใช้ได้กว้างกว่านั้น เงื่อนไขคืองานมันพังมาถึงจุดที่มี single source of truth ไม่ได้แล้ว จึงเป็นการลดความเสียหายมากกว่าการรักษา
เคยเจอบ่อยว่าตอนแรกโค้ดสองก้อนดูคล้ายกันเลย abstract มากเกินไป แล้วสุดท้ายภายหลังก็แยกทางกัน
โดยเฉพาะนักพัฒนารุ่นจูเนียร์ที่บางครั้งมองว่า ความซ้ำซ้อน คือรากเหง้าของความชั่วร้ายทั้งปวง
บางทีก็คิดเรื่องนี้เหมือนกัน เพิ่งเจอในโปรเจกต์ส่วนตัวตอนจัดการ สไปรต์ 2D สำหรับยูนิตในเกม RTS โดยสไปรต์ยูนิตถูกใส่ไว้ในสไปรต์ชีตอย่างสม่ำเสมอ: 8 ทิศทาง ทิศทางละ 5 สไปรต์ โดยมี 3 ทิศทางที่ใช้การ mirror และเรียงเป็น stand, move, attack, die
เลยสร้าง loader ที่รับ action + direction แล้วคืนอาร์เรย์สไปรต์ที่จะเล่นกลับมา
แต่พอมีสไปรต์ระเบิดที่ไม่มีทิศทาง สไปรต์ศพที่มีแค่ 4 ทิศทางและ mirror เพียง 2 แบบ แถมหลังจากสี่ตัวแรกแล้วออร์คกับมนุษย์ยังใช้ร่วมกันเป็นส่วนใหญ่ ก็เริ่มเจอปัญหา
เคยนั่งคิดอยู่พักหนึ่งว่าจริง ๆ abstraction ร่วมของทั้งหมดนี้คืออะไรกันแน่ แต่สุดท้ายก็แยกออกมาแค่บางส่วนของโค้ดโหลด แล้วทำ UnitLoader, CorpseLoader และ EffectLoader จากนั้นก็ไปต่อ ทั้งสาม loader จัดการสิ่งที่คล้ายกันเล็กน้อยจึงอาจมี abstraction ที่ดีกว่าอยู่ก็ได้ แต่ถ้าจะพบก็ค่อยพบทีหลัง ตอนนี้การลบ duplication ทีหลังยังง่ายกว่าการสร้าง EverythingLoader ที่ซับซ้อนเพื่อพยายามรองรับทุกกรณีตั้งแต่แรก
ในการเขียนโปรแกรม เรามีสัญชาตญาณจะทำให้โค้ดง่ายขึ้นด้วยการทำให้เป็น generalization แต่ความจริงมันสกปรกกว่านั้น จึงมักกลายเป็นการทำให้ง่ายเกินไป อย่างในบทความนี้ พอเวลาผ่านไปและมีความต้องการใหม่ ก็จะเห็นว่ามันคือ การทำให้ง่ายเกินไปเร็วเกินควร
จะมีคำคมว่า “abstraction ที่รีบเกินไปคือรากเหง้าของความห่วยจำนวนมาก” ก็คงไม่ผิด
แต่ในชั้นถัดขึ้นไปซึ่งเป็นการตีความการจัดวางในสไปรต์ชีตและโหมดการเล่นนั้นมีรูปแบบย่อยหลายแบบ และอาจไม่มี abstraction ร่วมที่ใช้ได้กับทุกกรณี
ผมชอบวิธีทำแบบตอนนี้มากกว่าการฝืนสร้าง abstraction ที่มองไม่เห็น หรือพยายามยัดทุกอย่างให้เข้ากับ abstraction ที่ยังไม่สมบูรณ์ การรอจนกว่า abstraction จะชัดเจนสมบูรณ์และความจำเป็นเด่นชัดจึงเป็นเรื่องที่ดี
ฝั่งตรงข้ามของ DRY ยังมี WET ซึ่งหมายถึงให้เขียนทุกอย่างซ้ำสอง/สามครั้ง ที่สำคัญกว่านั้นคือผมคิดว่าควร abstract เฉพาะ use case ที่พิสูจน์แล้วจริง ๆ ซึ่งมักจะโผล่มาให้เห็นก่อนในรูปแบบของ duplication โค้ดที่เขียนเผื่อ use case ในอนาคตที่ยังไม่มี มักไปขัดขวางการ abstract สิ่งที่เรามีอยู่จริง และทุกครั้งที่เกิดแบบนั้นก็น่าขำดี
งานยากและน่าเบื่อค่อยไปทำตอนถึง 10% สุดท้ายของโปรเจกต์ก็ได้
แถมบางที “บั๊ก” ที่เกิดจาก duplication ก็กลายเป็นฟีเจอร์สนุก ๆ ที่ผู้เล่นชอบด้วย
ตอนที่ยังใช้ OOP เคยทรมานกับ abstraction มาก แต่หลังจากย้ายมาใช้แนวทางแบบ functional ที่เกือบบริสุทธิ์แล้ว การซ้ำของโค้ดก็แทบไม่ค่อยเกิด
แค่สร้างฟังก์ชันขึ้นมาแล้วเรียกใช้จากสองที่ก็พอ ปัญหา abstraction หลัก ๆ อยู่ที่โครงสร้างข้อมูล ซึ่งอินเทอร์เฟซของ TypeScript ก็เป็น duck typing โดยเนื้อแท้อยู่แล้ว เลยไม่ค่อยมีปัญหามากนัก
เพราะงั้น duplication ของโค้ดที่เกิดจากปัญหา abstraction จึงพบไม่บ่อย สิ่งที่เจอบ่อยกว่ามากคือ duplication ของโค้ดที่เกิดจาก นักพัฒนาทำงานเป็นไซโล
ภาษาสมัยใหม่ส่วนใหญ่สามารถวางอยู่บนทฤษฎีของ functional programming ได้ไม่ยาก และไม่จำเป็นต้องรู้ Haskell ก็ได้ แน่นอนว่าสมองแต่ละคนทำงานต่างกัน แต่สำหรับผมแนวคิดที่ว่าชิ้นส่วนเล็ก ๆ เรียบง่าย และบางครั้งก็ยืดหยุ่นมาประกอบกันเป็นภาพรวม มันเข้าท่ามาก
ซึ่งตรงข้ามกับเครื่องแปลงรูปร่างขนาดใหญ่ ซับซ้อน และทำได้ทุกอย่าง
เมื่อทีมใหญ่เกินระดับหนึ่งจนไม่มีใครรู้ได้ทั้งหมดว่าคนอื่นกำลังทำอะไรอยู่ โค้ดซ้ำ ก็ค่อนข้างหลีกเลี่ยงไม่ได้ ต่อให้ทุกคนเขียนในสไตล์ functional ก็เหมือนกัน
เดือนก่อนที่บริษัทก็เพิ่งเจอแบบนี้ ผมเขียน pure helper function ใหม่ไว้ตรงต้นไฟล์ แล้วอีกสัปดาห์ต่อมาเพื่อนร่วมงานก็มาบอกว่าจริง ๆ มี helper function คล้ายกันมากที่ทำหน้าที่แทบเหมือนกันแต่มี signature ต่างออกไป อยู่ท้ายไฟล์เดียวกันนี่เอง
ในบริบทเดียวกับบทความหลัก ใครที่เคยเจอมาทั้งสองแบบก็น่าจะเห็นด้วยว่า โค้ดเบสที่ออกแบบไม่มากเกินไป จัดการได้ง่ายกว่าโค้ดเบสที่ออกแบบเกินจำเป็นมาก
โค้ดที่แย่ที่สุดที่เคยต้องดูแลคือโค้ดที่พยายามทำตาม DRY แต่ไม่ได้พยายามเข้าใจเจตนาเดิมของหลักการนั้นจริง ๆ
วิธีเดียวที่จะหนีออกจากความเละเทะนั้นได้คือการนำ การทำโค้ดซ้ำ ในวงกว้างกลับเข้ามาอีกครั้ง
ตรงนี้ทำให้นึกถึงสองงานบรรยาย: Mike Acton กับ Data-Oriented Design and C++ [1] และ Brian Cantrill กับ The Complexity of Simplicity [2]
งานของ Mike พูดว่าโซลูชันในโค้ดไม่จำเป็นต้องจำลองโลกความเป็นจริง ข้อมูลที่ต่างกันก็ก่อปัญหาคนละแบบ และจึงต้องการโซลูชันคนละแบบเช่นกัน คงถ่ายทอดงานบรรยายนี้ออกมาได้ไม่ดีพอ แต่สำหรับฉันมันมีอิทธิพลมาก
งานของ Brian ว่าด้วย abstraction โดยรวม และความยากของการหา abstraction ที่ “ถูกต้อง”
ตอนที่ฉันเพิ่งเรียนจบมาได้ไม่กี่ปี เคยกำลังทำ connection pool ใน Rust และแนวทางที่สมเหตุสมผลที่สุดคือให้ออบเจ็กต์ connection ถือ weak reference ไปยัง pool เพื่อจะได้คืนกลับอัตโนมัติเมื่อ
dropผู้จัดการของฉันซึ่งเป็นผู้จัดการที่มีประสบการณ์มาก ไม่ชอบไอเดียนี้เพราะ “ห้องสมุดถือหนังสือ ไม่ใช่หนังสือถือห้องสมุด” แม้ฉันจะไม่รู้สึกว่านี่เป็นเหตุผลที่หนักแน่นพอจะเปลี่ยนแบบได้ แต่เขาก็ไม่ยอมจัดการปัญหานี้นอกกรอบของอุปมานั้น
สุดท้ายทางตันก็คลี่คลายเมื่อผู้จัดการอีกคนเสนอว่า “หนังสือห้องสมุดไม่ได้มีตัวห้องสมุดอยู่ในตัว แต่มีตราประทับชื่อห้องสมุดที่ชี้ไปยังจุดคืนหนังสืออยู่ด้านหลัง” ดูเหมือนผู้จัดการคนนั้นจะเห็นว่าการขยายอุปมานี้สมเหตุสมผล
ถ้าตอนนั้นฉันมีประสบการณ์มากกว่านี้ บางทีอาจหาวิธีคุยกันภายในอุปมานั้นได้โดยไม่ต้องยอมประเด็น แต่จนถึงตอนนี้ก็ยังรู้สึกว่าการยึดอุปมานั้นเป็นกรอบมาตรฐาน แทนที่จะพิจารณาผลลัพธ์ของ abstraction ในโค้ดและประสบการณ์การใช้ไลบรารี เป็นเรื่องประหลาดอย่างยิ่ง
ไม่มีใครฟังหรอก ไม่มีใครฟังจริง ๆ บริษัท 90% มีสิ่งที่เรียกกันว่านักพัฒนา senior ที่เคลิบเคลิ้มทุกครั้งที่ได้สร้าง abstraction ใหม่
การออกแบบเกินจำเป็น, abstraction และการ optimize เร็วเกินไป คือ 3 มหันตภัยของงานวิศวกรรม
แต่ในอีกด้านหนึ่งก็ดีใจที่สิ่งเหล่านี้ยังมีอยู่ เพราะมันทำให้มีงานทำเสมอ
คล้ายกันคือ นักพัฒนาบางคนดูเหมือนจะคิดว่าสตริงแบบ inline หรือค่าคงที่ตัวเลขทั้งหมดเป็นสิ่งชั่วร้าย เคยเห็นแบบนี้ใน PR หนึ่ง
HTTPS_SCHEME = 'https'DOMAIN = 'www.example.com'url = HTTPS_SCHEME + '://' + DOMAINฉันไม่เห็นว่ามันให้อะไรเลยนอกจากทำตามคำว่า “อย่า hardcode ค่าคงที่” แบบ cargo cult แถมการประกาศค่าคงที่ยังอยู่บนสุดของไฟล์ ส่วนโค้ดที่ประกอบ URL อยู่ห่างออกไปอีกหลายร้อยบรรทัด
regex ก็ไม่จำเป็นต้องไว้บนสุดของไฟล์ เอาไว้ตรงที่ใช้ก็ได้ ภาษาโปรแกรมฉลาดพอ น่าจะรู้ได้เองว่าเป็นค่าคงที่
ถ้าเป็นฟังก์ชันเล็กมาก ก็ใช้ lambda ไปเลย อยากไม่ให้มีการสร้างฟังก์ชันหนึ่งบรรทัดที่ใช้แค่ครั้งสองครั้งไว้ไกลลิบ
ถ้าใน test หรือ staging ต้องเปลี่ยนจาก https เป็น http การแยก scheme กับ domain ออกมาและวางค่าคงที่ไว้ด้านบนหรือในอีกไฟล์หนึ่งก็สมเหตุสมผล
urlถูกประกอบหลายที่หรือที่เดียวก็สำคัญเหมือนกันการมี named constants อยู่บนสุดของไฟล์เป็นสไตล์ที่พบได้บ่อยมาก และบางครั้งก็เป็นส่วนหนึ่งของมาตรฐานการเขียนโค้ดของทีม
อาจมีเหตุผลอื่นอีกด้วย ดังนั้นนึกถึง Chesterton’s Fence ไว้ก็ดี อย่างน้อย การฟันธงว่าเป็น cargo cult ไม่ใช่ความคิดที่ดี ใครบางคนก็อาจบอกได้เหมือนกันว่าการใช้ inline literals ก็เป็น cargo cult แบบเดียวกัน ถ้ามันดูแปลกก็ถามได้ อาจมีเหตุผลดี ๆ หรืออาจเป็นแค่ไม่มีใครสนใจ และถ้าคุณ refactor ให้ inline ค่าคงที่กลับเข้าไป ทุกคนก็อาจยินดีด้วยซ้ำ
แต่ถ้าแยกมันออกไปเป็นค่าคงที่ ก็ต้องกลับไปเปิดแต่ละโปรเจกต์แล้ว ค้นหาจุดใช้งาน ใหม่
ถ้าใช้ microservices คุณก็ทำได้ทั้งสองอย่าง
ถ้าคุณเป็นผู้ดูแลบริการหนึ่ง ก็ไม่มีเหตุผลต้องสนใจโค้ดในอีกบริการหนึ่ง จะไปสนใจทำไม ในเมื่อเป็นโค้ดของอีกทีม? คุณไม่จำเป็นต้องรู้ด้วยซ้ำว่าทีมนั้นมีอยู่ และในระบบใหญ่ บางครั้งก็เป็นไปไม่ได้ในทางปฏิบัติที่จะรู้ว่ามีแอปพลิเคชันทั้งหมดอะไรบ้าง
เพียง $19.95 เราจะเปลี่ยน single point of failure หนึ่งจุดของคุณให้กลายเป็น single points of failure หลายจุด!
ใช้ service-oriented architecture แต่ deploy เป็นโมโนลิธไปเลยยังดีกว่า ทดสอบง่ายกว่า และยังเลี่ยงชั้นส่วนเกินของการ serialize/deserialize ได้ด้วย
คนระดับซีเนียร์ส่วนใหญ่น่าจะรู้ว่าไม่ควรทำตาม DRY แบบมืดบอด ถึงอย่างนั้น พวกเราหลายคนก็ยังรู้สึกไม่สบายใจกับแนวคิดที่ว่าต้องดูแล แหล่งที่มาของโค้ดที่ซ้ำกัน หลายแห่ง
หากจะรับมือกับเรื่องนี้ ต้องพิจารณาโมเดลง่าย ๆ ที่ผู้เรียกใช้สองรายพึ่งพาโค้ดส่วนกลางอย่างละเอียด ถ้าโค้ดส่วนกลางต้องเปลี่ยนเพราะความต้องการของผู้เรียกใช้เพียงรายเดียว แสดงว่าโค้ดนั้นไม่ได้เป็นส่วนกลางจริง ๆ
เป้าหมายที่ผิดของ DRY คือพยายามแก้เรื่องนี้ด้วยการห่อหุ้ม (encapsulation) การห่อหุ้มเป็นการย้ายงาน refactoring จากฝั่งผู้เรียกใช้ไปไว้ที่โค้ดส่วนกลาง แต่ผลกระทบจากการอัปเดตโค้ดส่วนกลางนั้นใหญ่กว่าฝั่งผู้เรียกใช้มาก จึงไม่ใช่ทิศทางที่ต้องการ
เราสามารถรักษา DRY ได้โดยไม่ต้องพึ่งการห่อหุ้ม ทางที่ดีกว่าคือมี abstraction แบบบางหลายชั้นที่ผู้เรียกใช้ต้องรับรู้ ใน OOP เราเรียนรู้สิ่งนี้ผ่าน SRP และ IoC และใน procedural programming มันมักปรากฏขึ้นอย่างเป็นธรรมชาติในรูปแบบของการเรียกชุด helper functions