เขียนโค้ด Java แบบ Fat-free ด้วย Project Lombok
เผยแพร่แล้ว: 2022-03-11มีเครื่องมือและไลบรารีจำนวนมากที่ฉันไม่สามารถจินตนาการได้ว่าตัวเองกำลังเขียนโค้ด Java หากไม่มีวันนี้ ตามเนื้อผ้า สิ่งต่าง ๆ เช่น Google Guava หรือ Joda Time (อย่างน้อยก็ในยุค Java 8 ก่อน) อยู่ท่ามกลางการพึ่งพาที่ฉันลงเอยด้วยการโยนลงในโปรเจ็กต์ของฉันเกือบตลอดเวลา โดยไม่คำนึงถึงโดเมนเฉพาะที่อยู่ในมือ
ลอมบอกสมควรได้รับตำแหน่งใน POM ของฉันหรือ Gradle builds เช่นกันแม้ว่าจะไม่ใช่ยูทิลิตี้ไลบรารี / กรอบงานทั่วไป ลอมบอกมีมาระยะหนึ่งแล้ว (เปิดตัวครั้งแรกในปี 2552) และเติบโตเต็มที่ตั้งแต่นั้นเป็นต้นมา อย่างไรก็ตาม ฉันรู้สึกเสมอว่ามันสมควรได้รับความสนใจมากขึ้น ซึ่งเป็นวิธีที่ยอดเยี่ยมในการจัดการกับการใช้คำฟุ่มเฟือยตามธรรมชาติของ Java
ในโพสต์นี้ เราจะมาสำรวจสิ่งที่ทำให้ลอมบอกเป็นเครื่องมือที่มีประโยชน์
Java มีหลายสิ่งหลายอย่างที่นอกเหนือไปจาก JVM ซึ่งเป็นซอฟต์แวร์ที่โดดเด่น Java เติบโตเต็มที่และมีประสิทธิภาพ ชุมชนและระบบนิเวศรอบๆ นั้นมีขนาดใหญ่และมีชีวิตชีวา
อย่างไรก็ตาม ในฐานะที่เป็นภาษาโปรแกรม Java มีความแปลกประหลาดบางอย่างในตัวมันเอง เช่นเดียวกับตัวเลือกการออกแบบที่สามารถทำให้มันค่อนข้างละเอียด เพิ่มโครงสร้างและรูปแบบคลาสที่นักพัฒนา Java มักจำเป็นต้องใช้ และเรามักจะจบลงด้วยโค้ดหลายบรรทัดที่ให้คุณค่าที่แท้จริงเพียงเล็กน้อยหรือไม่มีเลย นอกจากการปฏิบัติตามข้อจำกัดบางชุดหรือแบบแผนของเฟรมเวิร์ก
นี่คือจุดที่ลอมบอกเข้ามาเล่น ช่วยให้เราลดจำนวนโค้ด "boilerplate" ที่เราต้องเขียนได้อย่างมาก ครีเอเตอร์ของลอมบอกเป็นคู่รักที่ฉลาดมาก และมีรสนิยมด้านอารมณ์ขันอย่างแน่นอน คุณไม่ควรพลาดอินโทรนี้ที่เคยทำในการประชุมครั้งก่อน!
มาดูกันว่าลอมบอกใช้เวทมนตร์และตัวอย่างการใช้งานได้อย่างไร
ลอมบอกทำงานอย่างไร
ลอมบอกทำหน้าที่เป็นตัวประมวลผลคำอธิบายประกอบที่ "เพิ่ม" โค้ดให้กับชั้นเรียนของคุณในเวลารวบรวม การประมวลผลคำอธิบายประกอบเป็นคุณลักษณะที่เพิ่มลงในคอมไพเลอร์ Java ในเวอร์ชัน 5 แนวคิดคือผู้ใช้สามารถใส่ตัวประมวลผลคำอธิบายประกอบ (เขียนด้วยตัวเองหรือผ่านการพึ่งพาบุคคลที่สาม เช่น Lombok) ลงใน build classpath จากนั้น เมื่อกระบวนการคอมไพล์ดำเนินไป เมื่อใดก็ตามที่คอมไพเลอร์พบคำอธิบายประกอบ มันก็จะถามว่า: “เฮ้ มีใครใน classpath สนใจ @Annotation นี้ไหม” สำหรับโปรเซสเซอร์เหล่านั้นที่ยกมือขึ้น คอมไพเลอร์จะถ่ายโอนการควบคุมไปยังตัวประมวลผลพร้อมกับคอมไพล์บริบทสำหรับพวกเขา อืม... ประมวลผล
กรณีที่พบบ่อยที่สุดสำหรับตัวประมวลผลคำอธิบายประกอบคือการสร้างไฟล์ต้นทางใหม่หรือดำเนินการตรวจสอบเวลาคอมไพล์บางประเภท
ลอมบอกไม่ได้จัดอยู่ในหมวดหมู่เหล่านี้จริงๆ: สิ่งที่ทำคือการปรับเปลี่ยนโครงสร้างข้อมูลคอมไพเลอร์ที่ใช้เพื่อแสดงโค้ด กล่าวคือ ต้นไม้ไวยากรณ์นามธรรม (AST) การแก้ไข AST ของคอมไพเลอร์ทำให้ลอมบอกเปลี่ยนแปลงการสร้าง bytecode สุดท้ายโดยอ้อม
วิธีการที่ผิดปกติและค่อนข้างเป็นการล่วงล้ำนี้ส่งผลให้ลอมบอกถูกมองว่าเป็นการแฮ็ก แม้ว่าตัวฉันเองจะเห็นด้วยกับคุณลักษณะนี้ในระดับหนึ่ง แทนที่จะมองว่าสิ่งนี้ในความหมายที่ไม่ดีของคำนั้น ฉันจะถือว่าลอมบอกเป็น "ทางเลือกที่ชาญฉลาด มีข้อดีทางเทคนิค และเป็นทางเลือกดั้งเดิม"
ยังมีนักพัฒนาที่มองว่าเป็นแฮ็คและไม่ใช้ลอมบอกด้วยเหตุนี้ นั่นเป็นเรื่องที่เข้าใจได้ แต่จากประสบการณ์ของผม ประโยชน์ของลอมบอกมีประสิทธิผลมากกว่าข้อกังวลใดๆ เหล่านี้ ฉันใช้มันอย่างมีความสุขสำหรับโครงการผลิตมาหลายปีแล้ว
ก่อนที่จะลงรายละเอียด ฉันต้องการสรุปเหตุผลสองประการที่ฉันให้ความสำคัญเป็นพิเศษกับการใช้ลอมบอกในโครงการของฉัน:
- ลอมบอกช่วยให้โค้ดของฉันสะอาด กระชับ และตรงประเด็น ฉันพบว่าคลาสที่มีคำอธิบายประกอบในลอมบอกมีความหมายมาก และโดยทั่วไปแล้วฉันพบว่าโค้ดที่มีคำอธิบายประกอบนั้นค่อนข้างเปิดเผยถึงความตั้งใจ แม้ว่าทุกคนบนอินเทอร์เน็ตจะไม่เห็นด้วยก็ตาม
- เมื่อฉันเริ่มโครงการและคิดเกี่ยวกับโมเดลโดเมน ฉันมักจะเริ่มต้นด้วยการเขียนชั้นเรียนที่มีงานยุ่งมาก และฉันเปลี่ยนแปลงซ้ำๆ เมื่อฉันคิดเพิ่มเติมและปรับแต่งมัน ในช่วงแรกๆ ลอมบอกช่วยให้ฉันเคลื่อนไหวเร็วขึ้นโดยไม่จำเป็นต้องย้ายไปรอบๆ หรือแปลงรหัสสำเร็จรูปที่สร้างขึ้นสำหรับฉัน
รูปแบบถั่วและวิธีการวัตถุทั่วไป
เครื่องมือและเฟรมเวิร์ก Java จำนวนมากที่เราใช้อาศัยรูปแบบบีน Java Beans เป็นคลาสที่ทำให้ซีเรียลไลซ์ได้ซึ่งมีคอนสตรัคเตอร์ Zero-args ดีฟอลต์ (และอาจเป็นเวอร์ชันอื่น) และเปิดเผยสถานะของพวกเขาผ่าน getters และ setters ซึ่งโดยทั่วไปจะได้รับการสนับสนุนโดยฟิลด์ส่วนตัว เราเขียนสิ่งเหล่านี้จำนวนมาก เช่น เมื่อทำงานกับ JPA หรือเฟรมเวิร์กการทำให้เป็นอนุกรม เช่น JAXB หรือ Jackson
พิจารณา User bean นี้ที่มีแอตทริบิวต์ (คุณสมบัติ) สูงสุดห้ารายการ ซึ่งเราต้องการให้มีตัวสร้างเพิ่มเติมสำหรับแอตทริบิวต์ทั้งหมด การแสดงสตริงที่มีความหมาย และกำหนดความเท่าเทียมกัน/การแฮชในแง่ของช่องอีเมล:
public class User implements Serializable { private String email; private String firstName; private String lastName; private Instant registrationTs; private boolean payingCustomer; // Empty constructor implementation: ~3 lines. // Utility constructor for all attributes: ~7 lines. // Getters/setters: ~38 lines. // equals() and hashCode() as per email: ~23 lines. // toString() for all attributes: ~3 lines. // Relevant: 5 lines; Boilerplate: 74 lines => 93% meaningless code :( }
เพื่อความกระชับ แทนที่จะรวมการใช้งานจริงของวิธีการทั้งหมด ฉันเพียงแค่แสดงความคิดเห็นโดยระบุวิธีการและจำนวนบรรทัดของโค้ดที่นำไปใช้จริง รหัสต้นแบบนั้นจะมีทั้งหมดมากกว่า 90% ของรหัสสำหรับคลาสนี้!
ยิ่งกว่านั้น ถ้าในภายหลังฉันต้องการพูด เปลี่ยน email
เป็น emailAddress
หรือให้ registerTs เป็น registrationTs
แทนที่จะเป็น Instant
ฉันจะต้องอุทิศเวลา (ด้วยความช่วยเหลือจาก IDE ของฉันในบางกรณี เป็น Date
ยอมรับ) เพื่อเปลี่ยนแปลงสิ่งต่าง ๆ เช่น รับ /set ชื่อและประเภทของเมธอด แก้ไขตัวสร้างยูทิลิตี้ของฉัน และอื่นๆ อีกครั้งที่เวลาอันมีค่าสำหรับบางสิ่งที่ไม่นำคุณค่าทางธุรกิจมาสู่โค้ดของฉัน
มาดูกันว่าลอมบอกสามารถช่วยได้อย่างไร:
import lombok.AllArgsConstructor; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; import lombok.ToString; @Getter @Setter @NoArgsConstructor @AllArgsConstructor @ToString @EqualsAndHashCode(of = {"email"}) public class User { private String email; private String firstName; private String lastName; private Instant registrationTs; private boolean payingCustomer; }
โว้ว! ฉันเพิ่งเพิ่มคำอธิบายประกอบ lombok.*
และบรรลุสิ่งที่ฉันต้องการ รายการด้านบนนี้เป็นโค้ด ทั้งหมด ที่ฉันต้องเขียนเพื่อสิ่งนี้ ลอมบอกเชื่อมต่อกับกระบวนการคอมไพเลอร์ของฉันและสร้าง ทุกอย่าง ให้ฉัน (ดูภาพหน้าจอด้านล่างของ IDE ของฉัน)
ตามที่คุณสังเกตเห็น ตัวตรวจสอบ NetBeans (และสิ่งนี้จะเกิดขึ้นโดยไม่คำนึงถึง IDE) จะตรวจจับ bytecode ของคลาสที่คอมไพล์แล้ว รวมถึงส่วนเพิ่มเติมของ Lombok ที่นำเข้ามาในกระบวนการ สิ่งที่เกิดขึ้นที่นี่ค่อนข้างตรงไปตรงมา:
- การใช้
@Getter
และ@Setter
ฉันสั่งให้ลอมบอกสร้างตัวรับและตัวตั้งค่าสำหรับแอตทริบิวต์ทั้งหมด นี่เป็นเพราะฉันใช้คำอธิบายประกอบในระดับชั้นเรียน ถ้าฉันต้องการเลือกระบุสิ่งที่จะสร้างสำหรับแอตทริบิวต์ใด ฉันอาจใส่คำอธิบายประกอบลงในช่องเอง - ขอบคุณ
@NoArgsConstructor
และ@AllArgsConstructor
ฉันได้รับคอนสตรัคเตอร์ว่างเริ่มต้นสำหรับคลาสของฉันรวมถึงอีกอันสำหรับแอตทริบิวต์ทั้งหมด - คำอธิบายประกอบ
@ToString
จะสร้างเมธอดtoString()
ที่มีประโยชน์โดยอัตโนมัติ โดยแสดงแอตทริบิวต์คลาสทั้งหมดที่นำหน้าด้วยชื่อโดยค่าเริ่มต้น - สุดท้าย ในการกำหนดให้คู่ของวิธี
equals()
และhashCode()
กำหนดไว้ในแง่ของฟิลด์อีเมล ฉันใช้@EqualsAndHashCode
และกำหนดพารามิเตอร์ด้วยรายการฟิลด์ที่เกี่ยวข้อง (เฉพาะอีเมลในกรณีนี้)
การปรับแต่งคำอธิบายประกอบลอมบอก
ลองใช้การปรับแต่งลอมบอกบางอย่างตามตัวอย่างเดียวกันนี้:
- ฉันต้องการลดการมองเห็นของตัวสร้างเริ่มต้น เนื่องจากฉันต้องการมันด้วยเหตุผลด้านความสอดคล้องของ bean เท่านั้น ฉันจึงคาดหวังให้ผู้บริโภคของคลาสเรียกเฉพาะตัวสร้างที่รับทุกฟิลด์เท่านั้น เพื่อบังคับใช้สิ่งนี้ ฉันกำลังปรับแต่งคอนสตรัคเตอร์ที่สร้างขึ้นด้วย
AccessLevel.PACKAGE
- ฉันต้องการให้แน่ใจว่าฟิลด์ของฉันไม่เคยได้รับค่า null ที่กำหนด ไม่ว่าจะผ่านทางตัวสร้างหรือผ่านเมธอด setter การใส่คำอธิบายประกอบแอตทริบิวต์คลาสด้วย
@NonNull
ก็เพียงพอแล้ว ลอมบอกจะสร้างการตรวจสอบที่เป็นโมฆะโดยโยนNullPointerException
เมื่อเหมาะสมในเมธอดตัวสร้างและตัวตั้งค่า - ฉันจะเพิ่มแอตทริบิวต์
password
แต่ไม่ต้องการให้แสดงเมื่อโทรtoString()
ด้วยเหตุผลด้านความปลอดภัย สิ่งนี้ทำได้ผ่านการยกเว้นอาร์กิวเมนต์ของ@ToString
- ฉันโอเคที่จะเปิดเผยสถานะต่อสาธารณะผ่าน getters แต่ต้องการจำกัดความเปลี่ยนแปลงภายนอก ดังนั้นฉันจึงออกจาก
@Getter
เหมือนเดิม แต่ใช้AccessLevel.PROTECTED
อีกครั้งสำหรับ@Setter
- บางทีฉันอาจต้องการบังคับข้อจำกัดบางอย่างในฟิลด์
email
เพื่อที่ว่าหากได้รับการแก้ไข จะมีการเรียกใช้การตรวจสอบบางประเภท สำหรับสิ่งนี้ ฉันแค่ใช้วิธีsetEmail()
ด้วยตัวเอง ลอมบอกจะละเว้นการสร้างสำหรับวิธีการที่มีอยู่แล้ว
นี่คือลักษณะที่คลาส User จะมีลักษณะ:
import lombok.AccessLevel; import lombok.AllArgsConstructor; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.NonNull; import lombok.Setter; import lombok.ToString; @Getter @Setter(AccessLevel.PROTECTED) @NoArgsConstructor(access = AccessLevel.PACKAGE) @AllArgsConstructor @ToString(exclude = {"password"}) @EqualsAndHashCode(of = {"email"}) public class User { private @NonNull String email; private @NonNull byte[] password; private @NonNull String firstName; private @NonNull String lastName; private @NonNull Instant registrationTs; private boolean payingCustomer; protected void setEmail(String email) { // Check for null (=> NullPointerException) // and valid email code (=> IllegalArgumentException) this.email = email; } }
โปรดทราบว่าสำหรับคำอธิบายประกอบบางรายการ เรากำลังระบุแอตทริบิวต์ของคลาสเป็นสตริงธรรมดา ไม่ใช่ปัญหา เพราะลอมบอกจะโยนข้อผิดพลาดในการคอมไพล์หากเรา เช่น พิมพ์ผิดหรืออ้างถึงฟิลด์ที่ไม่มีอยู่ ด้วยลอมบอก เราปลอดภัย
นอกจากนี้ เช่นเดียวกับ setEmail()
ลอมบอกก็ใช้ได้ และไม่สร้างสิ่งใดสำหรับวิธีการที่โปรแกรมเมอร์ได้นำไปใช้แล้ว สิ่งนี้ใช้กับวิธีการและตัวสร้างทั้งหมด
โครงสร้างข้อมูลที่ไม่เปลี่ยนรูป
กรณีการใช้งานอื่นที่ Lombok มีความยอดเยี่ยมคือเมื่อสร้างโครงสร้างข้อมูลที่ไม่เปลี่ยนรูปแบบ เหล่านี้มักจะเรียกว่า "ประเภทค่า" บางภาษามีการสนับสนุนในตัวสำหรับสิ่งเหล่านี้ และยังมีข้อเสนอสำหรับการรวมสิ่งนี้เข้ากับเวอร์ชัน Java ในอนาคต
สมมติว่าเราต้องการสร้างแบบจำลองการตอบสนองต่อการดำเนินการเข้าสู่ระบบของผู้ใช้ นี่คือประเภทของอ็อบเจ็กต์ที่เราต้องการที่จะสร้างอินสแตนซ์และกลับไปยังเลเยอร์อื่นๆ ของแอปพลิเคชัน (เช่น เพื่อให้เป็น JSON ต่อเนื่องเป็นเนื้อหาของการตอบสนอง HTTP) LoginResponse ดังกล่าวไม่จำเป็นต้องเปลี่ยนแปลงเลย และลอมบอกสามารถช่วยอธิบายได้อย่างกระชับ แน่นอนว่ายังมีกรณีการใช้งานอื่นๆ อีกมากสำหรับโครงสร้างข้อมูลที่ไม่เปลี่ยนรูปแบบ (เป็นแบบมัลติเธรดและเป็นมิตรกับแคช ท่ามกลางคุณสมบัติอื่นๆ) แต่มาดูตัวอย่างง่ายๆ นี้กัน:
import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.NonNull; import lombok.RequiredArgsConstructor; import lombok.ToString; import lombok.experimental.Wither; @Getter @RequiredArgsConstructor @ToString @EqualsAndHashCode public final class LoginResponse { private final long userId; private final @NonNull String authToken; private final @NonNull Instant loginTs; @Wither private final @NonNull Instant tokenExpiryTs; }
น่าสังเกตที่นี่:

- มีการแนะนำคำอธิบายประกอบ
@RequiredArgsConstructor
ตั้งชื่ออย่างเหมาะเจาะ สิ่งที่มันทำคือสร้างคอนสตรัคเตอร์สำหรับฟิลด์สุดท้ายทั้งหมดที่ยังไม่ได้เริ่มต้น - ในกรณีที่เราต้องการนำ LoginResonse ที่ออกก่อนหน้านี้มาใช้ใหม่ (ลองนึกภาพ เช่น การดำเนินการ "รีเฟรชโทเค็น") เราไม่ต้องการแก้ไขอินสแตนซ์ที่มีอยู่ของเราอย่างแน่นอน แต่เราต้องการสร้างอินสแตนซ์ใหม่ตามนั้น . ดูว่าคำอธิบายประกอบ
@Wither
ช่วยเราได้อย่างไร: มันบอกให้ลอมบอกสร้างwithTokenExpiryTs(Instant tokenExpiryTs)
ที่สร้างอินสแตนซ์ใหม่ของ LoginResponse โดยมีค่าอินสแตนซ์ with'ed ทั้งหมด ยกเว้นค่าใหม่ที่เรากำลังระบุ คุณต้องการพฤติกรรมนี้สำหรับทุกฟิลด์หรือไม่? เพียงเพิ่ม@Wither
ในการประกาศคลาสแทน
@Data และ @Value
กรณีการใช้งานทั้งสองที่กล่าวถึงจนถึงตอนนี้เป็นเรื่องธรรมดามากที่ลอมบอกจัดส่งคำอธิบายประกอบสองสามรายการเพื่อให้สั้นลง: การทำคำอธิบายประกอบคลาสด้วย @Data
จะทำให้ลอมบอกทำงานเหมือนกับว่าได้รับการใส่คำอธิบายประกอบด้วย @Getter
+ @Setter
+ @ToString
+ @EqualsAndHashCode
+ @RequiredArgsConstructor
ในทำนองเดียวกัน การใช้ @Value
จะทำให้ชั้นเรียนของคุณเป็นแบบที่ไม่เปลี่ยนรูปแบบ (และเป็นขั้นสุดท้าย) อีกครั้ง ราวกับว่ามีคำอธิบายประกอบในรายการด้านบน
รูปแบบตัวสร้าง
กลับไปที่ตัวอย่าง User ของเรา หากเราต้องการสร้างอินสแตนซ์ใหม่ เราจำเป็นต้องใช้ Constructor ที่มีอาร์กิวเมนต์ไม่เกิน 6 อาร์กิวเมนต์ นี่เป็นจำนวนที่ค่อนข้างมากแล้ว ซึ่งจะยิ่งแย่ลงไปอีกหากเราเพิ่มแอตทริบิวต์ในชั้นเรียนเพิ่มเติม สมมติว่าเราต้องการตั้งค่าเริ่มต้นบางอย่างสำหรับฟิลด์ lastName
และ payingCustomer
เงิน
ลอมบอกใช้คุณลักษณะ @Builder
ที่มีประสิทธิภาพมาก ทำให้เราสามารถใช้รูปแบบตัวสร้างเพื่อสร้างอินสแตนซ์ใหม่ได้ มาเพิ่มในคลาสผู้ใช้ของเรา:
import lombok.AccessLevel; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.NonNull; import lombok.Setter; import lombok.ToString; @Getter @Setter(AccessLevel.PROTECTED) @NoArgsConstructor(access = AccessLevel.PACKAGE) @AllArgsConstructor @ToString(exclude = {"password"}) @EqualsAndHashCode(of = {"email"}) @Builder public class User { private @NonNull String email; private @NonNull byte[] password; private @NonNull String firstName; private @NonNull String lastName = ""; private @NonNull Instant registrationTs; private boolean payingCustomer = false; }
ตอนนี้เราสามารถสร้างผู้ใช้ใหม่ได้อย่างคล่องแคล่วดังนี้:
User user = User .builder() .email("[email protected]") .password("secret".getBytes(StandardCharsets.UTF_8)) .firstName("Miguel") .registrationTs(Instant.now()) .build();
เป็นเรื่องง่ายที่จะจินตนาการว่าโครงสร้างนี้สะดวกเพียงใดเมื่อชั้นเรียนของเราเติบโตขึ้น
การมอบหมาย/องค์ประกอบ
หากคุณต้องการทำตามกฎที่มีเหตุผลของ "องค์ประกอบที่โปรดปรานมากกว่าการสืบทอด" นั่นเป็นสิ่งที่ Java ไม่ได้ช่วยจริงๆ ใช้คำฟุ่มเฟือย ถ้าคุณต้องการเขียนออบเจ็กต์ คุณมักจะต้องเขียนการเรียกเมธอดการมอบหมายทั้งหมด
ลอมบอกเสนอวิธีแก้ปัญหานี้ผ่าน @Delegate
ลองมาดูตัวอย่างกัน
ลองนึกภาพว่าเราต้องการแนะนำแนวคิดใหม่ของ ContactInformation
นี่คือข้อมูลบางส่วนที่ User
ของเรามี และเราอาจต้องการให้ชั้นเรียนอื่นๆ มีด้วยเช่นกัน จากนั้นเราสามารถจำลองสิ่งนี้ผ่านอินเทอร์เฟซดังนี้:
public interface HasContactInformation { String getEmail(); String getFirstName(); String getLastName(); }
จากนั้นเราจะแนะนำคลาส ContactInformation
ใหม่โดยใช้ลอมบอก:
import lombok.Data; @Data public class ContactInformation implements HasContactInformation { private String email; private String firstName; private String lastName; }
และสุดท้าย เราสามารถ refactor User
เพื่อเขียนด้วย ContactInformation
และใช้ Lombok เพื่อสร้างการเรียกผู้รับมอบสิทธิ์ที่จำเป็นทั้งหมดเพื่อให้ตรงกับสัญญาอินเทอร์เฟซ:
import lombok.AccessLevel; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.NonNull; import lombok.Setter; import lombok.ToString; import lombok.experimental.Delegate; @Getter @Setter(AccessLevel.PROTECTED) @NoArgsConstructor(access = AccessLevel.PACKAGE) @AllArgsConstructor @ToString(exclude = {"password"}) @EqualsAndHashCode(of = {"contactInformation"}) public class User implements HasContactInformation { @Getter(AccessLevel.NONE) @Delegate(types = {HasContactInformation.class}) private final ContactInformation contactInformation = new ContactInformation(); private @NonNull byte[] password; private @NonNull Instant registrationTs; private boolean payingCustomer = false; }
โปรดทราบว่าฉันไม่จำเป็นต้องเขียนการใช้งานสำหรับวิธีการของ HasContactInformation
: นี่คือสิ่งที่เรากำลังบอกให้ลอมบอกทำ โดยมอบหมายการโทรไปยังอินสแตนซ์ ContactInformation
ของเรา
นอกจากนี้ เนื่องจากฉันไม่ต้องการให้เข้าถึงอินสแตนซ์ที่ได้รับมอบสิทธิ์จากภายนอก ฉันกำลังปรับแต่งด้วย @Getter(AccessLevel.NONE)
เพื่อป้องกันการสร้าง getter อย่างมีประสิทธิภาพ
ตรวจสอบข้อยกเว้น
ดังที่เราทราบกันดี Java แยกความแตกต่างระหว่างข้อยกเว้นที่ตรวจสอบแล้วและไม่ได้ตรวจสอบ นี่เป็นแหล่งดั้งเดิมสำหรับการโต้เถียงและวิพากษ์วิจารณ์ภาษา เนื่องจากการจัดการข้อยกเว้นบางครั้งอาจส่งผลมากเกินไป โดยเฉพาะอย่างยิ่งเมื่อจัดการกับ API ที่ออกแบบมาเพื่อโยนข้อยกเว้นที่ตรวจสอบแล้ว ดังนั้นจึงบังคับให้นักพัฒนาซอฟต์แวร์จับพวกเขาหรือประกาศวิธีการของเรา โยนพวกเขา
พิจารณาตัวอย่างนี้:
public class UserService { public URL buildUsersApiUrl() { try { return new URL("https://apiserver.com/users"); } catch (MalformedURLException ex) { // Malformed? Really? throw new RuntimeException(ex); } } }
นี่เป็นรูปแบบทั่วไป: เราทราบดีว่า URL ของเรามีรูปแบบที่ดี แต่เนื่องจากตัวสร้าง URL
ส่งข้อยกเว้นที่ตรวจสอบแล้ว เราจึงถูกบังคับให้จับหรือประกาศวิธีการของเราที่จะโยนมันทิ้งและนำผู้โทรออกในสถานการณ์เดียวกัน การรวมข้อยกเว้นที่ตรวจสอบเหล่านี้ไว้ใน RuntimeException
เป็นแนวทางปฏิบัติที่ขยายออกไปมาก และสิ่งนี้จะยิ่งแย่ลงไปอีกหากจำนวนของข้อยกเว้นที่ตรวจสอบที่เราต้องจัดการเพิ่มขึ้นเมื่อเราเขียนโค้ด
ดังนั้นนี่คือสิ่งที่ @SneakyThrows
ของลอมบอกมีไว้สำหรับ มันจะรวมข้อยกเว้นที่ตรวจสอบแล้วซึ่งจะถูกโยนลงในวิธีการของเราให้เป็นแบบที่ไม่ถูกตรวจสอบและทำให้เราพ้นจากความยุ่งยาก:
import lombok.SneakyThrows; public class UserService { @SneakyThrows public URL buildUsersApiUrl() { return new URL("https://apiserver.com/users"); } }
การบันทึก
คุณเพิ่มอินสแตนซ์ตัวบันทึกในชั้นเรียนเช่นนี้บ่อยเพียงใด (ตัวอย่าง SLF4J)
private static final Logger LOG = LoggerFactory.getLogger(UserService.class);
ฉันจะเดาค่อนข้างมาก เมื่อทราบสิ่งนี้ ผู้สร้าง Lombok ได้ใช้คำอธิบายประกอบที่สร้างอินสแตนซ์ตัวตัดไม้ด้วยชื่อที่ปรับแต่งได้ (ค่าเริ่มต้นคือบันทึก) ซึ่งสนับสนุนเฟรมเวิร์กการบันทึกที่ใช้บ่อยที่สุดบนแพลตฟอร์ม Java เช่นนี้ (อีกครั้งตาม SLF4J):
import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; @Slf4j public class UserService { @SneakyThrows public URL buildUsersApiUrl() { log.debug("Building users API URL"); return new URL("https://apiserver.com/users"); } }
คำอธิบายประกอบรหัสที่สร้าง
หากเราใช้ลอมบอกเพื่อสร้างโค้ด ดูเหมือนว่าเราจะสูญเสียความสามารถในการใส่คำอธิบายประกอบวิธีการเหล่านั้นเนื่องจากเราไม่ได้เขียนโค้ดเหล่านั้นจริงๆ แต่นี่ไม่เป็นความจริงเลย ในทางกลับกัน ลอมบอกช่วยให้เราสามารถบอกได้ว่าเราต้องการสร้างคำอธิบายประกอบโค้ดอย่างไร โดยใช้สัญกรณ์ที่ค่อนข้างแปลก
ลองพิจารณาตัวอย่างนี้ ที่กำหนดเป้าหมายการใช้เฟรมเวิร์กการฉีดการพึ่งพา: เรามีคลาส UserService
ที่ใช้การแทรกคอนสตรัคเตอร์เพื่อรับการอ้างอิงไปยัง UserRepository
และ UserApiClient
package com.mgl.toptal.lombok; import javax.inject.Inject; import lombok.RequiredArgsConstructor; @RequiredArgsConstructor(onConstructor = @__(@Inject)) public class UserService { private final UserRepository userRepository; private final UserApiClient userApiClient; // Instead of: // // @Inject // public UserService(UserRepository userRepository, // UserApiClient userApiClient) { // this.userRepository = userRepository; // this.userApiClient = userApiClient; // } }
ตัวอย่างด้านบนแสดงวิธีการใส่คำอธิบายประกอบให้กับ Constructor ที่สร้างขึ้น ลอมบอกช่วยให้เราทำสิ่งเดียวกันสำหรับเมธอดและพารามิเตอร์ที่สร้างขึ้นได้เช่นกัน
เรียนรู้เพิ่มเติม
การใช้งานลอมบอกที่อธิบายในโพสต์นี้เน้นที่คุณสมบัติเหล่านั้นที่ฉันพบว่ามีประโยชน์มากที่สุดในช่วงหลายปีที่ผ่านมา อย่างไรก็ตาม ยังมีฟีเจอร์และการปรับแต่งอื่นๆ อีกมากมายที่พร้อมใช้งาน
เอกสารของลอมบอกให้ข้อมูลและละเอียดถี่ถ้วน พวกเขามีหน้าเฉพาะสำหรับทุกคุณลักษณะ (คำอธิบายประกอบ) พร้อมคำอธิบายและตัวอย่างที่ละเอียดมาก หากคุณพบว่าโพสต์นี้น่าสนใจ เราขอแนะนำให้คุณเจาะลึกลงไปในลอมบอกและเอกสารประกอบเพื่อหาข้อมูลเพิ่มเติม
ไซต์โครงการจัดทำเอกสารวิธีใช้ลอมบอกในสภาพแวดล้อมการเขียนโปรแกรมต่างๆ กล่าวโดยย่อ รองรับ IDE ที่ได้รับความนิยมมากที่สุด (Eclipse, NetBeans และ IntelliJ) ตัวฉันเองเปลี่ยนจากที่หนึ่งไปอีกที่หนึ่งเป็นประจำในแต่ละโครงการและใช้ลอมบอกกับพวกเขาทั้งหมดอย่างไม่มีที่ติ
เดลมบอก!
Delombok เป็นส่วนหนึ่งของ “Lombok toolchain” และมีประโยชน์มาก โดยทั่วไปแล้วสิ่งที่ทำคือสร้าง ซอร์สโค้ด Java สำหรับโค้ดที่มี หมายเหตุประกอบในลอมบอก โดยดำเนินการแบบเดียวกับที่ bytecode สร้างขึ้นในลอมบอก
นี่เป็นตัวเลือกที่ยอดเยี่ยมสำหรับผู้ที่กำลังพิจารณานำลอมบอกมาใช้ แต่ยังไม่ค่อยแน่ใจ คุณสามารถเริ่มใช้งานได้อย่างอิสระและจะไม่มี "การล็อคอินจากผู้ขาย" ในกรณีที่คุณหรือทีมของคุณเสียใจกับการเลือกในภายหลัง คุณสามารถใช้ delombok เพื่อสร้างซอร์สโค้ดที่เกี่ยวข้อง ซึ่งคุณสามารถใช้งานได้โดยไม่ต้องพึ่งพา Lombok ที่เหลืออยู่
Delombok เป็นเครื่องมือที่ยอดเยี่ยมในการเรียนรู้ว่าลอมบอกจะทำอะไร มีวิธีที่ง่ายมากในการเชื่อมต่อกับกระบวนการสร้างของคุณ
ทางเลือก
มีเครื่องมือมากมายในโลกของ Java ที่ใช้ตัวประมวลผลคำอธิบายประกอบที่คล้ายคลึงกันเพื่อปรับปรุงหรือแก้ไขโค้ดของคุณในขณะรวบรวม เช่น Immutables หรือ Google Auto Value สิ่งเหล่านี้ (และอื่น ๆ แน่นอน!) ทับซ้อนกับคุณสมบัติของลอมบอก ฉันชอบแนวทางของ Immutables มากเป็นพิเศษและเคยใช้ในบางโครงการด้วย
นอกจากนี้ ยังควรสังเกตด้วยว่ามีเครื่องมือที่ยอดเยี่ยมอื่นๆ ที่มีคุณสมบัติคล้ายคลึงกันสำหรับ "การเพิ่มประสิทธิภาพโค้ดไบต์" เช่น Byte Buddy หรือ Javassist โดยทั่วไปแล้วสิ่งเหล่านี้จะทำงานที่รันไทม์และประกอบด้วยโลกของตัวเองที่อยู่นอกเหนือขอบเขตของโพสต์นี้
ชวากระชับ
มีภาษาเป้าหมาย JVM ที่ทันสมัยจำนวนหนึ่งที่ให้แนวทางการออกแบบที่มีสำนวนมากขึ้น หรือแม้แต่ระดับภาษา เพื่อช่วยแก้ไขปัญหาเดียวกัน Groovy, Scala และ Kotlin เป็นตัวอย่างที่ดีอย่างแน่นอน แต่ถ้าคุณกำลังทำงานในโครงการ Java เท่านั้น ลอมบอกเป็นเครื่องมือที่ดีที่จะช่วยให้โปรแกรมของคุณกระชับ แสดงออก และสามารถบำรุงรักษาได้มากขึ้น