In this post, I've described two usages of the GoF's creational patterns, which aren't listed in the book: improving maintainability and ensuring objects are fully initialized.In this post, I've described two usages of the GoF's creational patterns, which aren't listed in the book: improving maintainability and ensuring objects are fully initialized.

Object Creation: The Issues I Faced and the Patterns That Helped Me

2025/09/05 05:24
7분 읽기
이 콘텐츠에 대한 의견이나 우려 사항이 있으시면 crypto.news@mexc.com으로 연락주시기 바랍니다

Creational patterns were first described in the famous Gang of Four's Design Patterns. The book presents each pattern in a dedicated chapter and follows a strict structure for each one: intent, motivation, applicability, structure, participants, collaborations, consequences, implementation, sample codes, known uses, and related patterns. The intent pattern presents a succinct goal of the pattern, while the applicability tells when you should use it.

\ For example, here's an excerpt for the Builder pattern:

The GoF (Gang of Four) has been foundational in the domain of Object-Oriented Programming and has influenced the design of programming languages, including widespread ones such as Java. However, it may come as intimidating or irrelevant to modern software development.

\ As I'm back in an engineering position, I come across newly written Java code that has a lot of improvement potential regarding maintainability. It works, but I imagine engineers having to update it, including the original author's future self and me, and I'm sure I can help them. This week, I refactored code using creational patterns to improve its maintainability. In this post, I want to describe the issues I faced and mention how patterns helped me.

A constructor with many parameters of the same type.

Let's imagine a constructor with many String parameters:

public License (     String id, String licenseeName,     String licenseId, String environment,     LocalDateTime generatedAt ) 

\ When calling this constructor, chances are the caller may unwillingly switch parameter orders:

var license = new License("My license", "XXX-123", "Customer", "User-acceptance tests", new LocalDateTime()); 

\ Oops, I switched the licensee name and the license ID. Your IDE may help here, but there are other ways.

Type Wrappers

Proponents of pure OOP will happily point out that one should never directly use a string. Instead, one should wrap each parameter in a dedicated type, e.g., a Java record:

public record Id(String id) { ... } public record LicenseeName(String licenseeName) { ... } public record LicenseeId(String licenseId) { ... } public record Environment(String environment) { ... } public record GeneratedAt(LocalDateTime generatedAt) { ... } 

\ Now, we can't make a mistake:

var id = new Id("My license"); var licenseeName = new LicenseeName("Customer"); var licenseId = new LicenseeId("XXX-123"); var environment = new Environment("User-acceptance tests"); var generatedAt = new LocalDateTime();  var license = new License(id, licenseId, licenseName, environment, generatedAt); //1 
  1. Compile-time error

While this approach definitely improves maintainability, the wrapper increases the memory size. The exact increase depends on the JDK implementation, but for a single type, it's around 5 times larger.

\ Kotlin makes it a breeze by providing inline value classes: the wrapping is a compile-time check, but the bytecode points to the wrapped type with the following limitation:

\

Named Parameters

Java offers only method calls with positional parameters, but other languages, e.g., Python, Kotlin, and Rust, also offer named parameters.

\ Here's a Kotlin constructor that mirrors the above class:

class License (     val id: String, val licenseeName: String,     val licenseId: String, val environment: String,     val generatedAt: LocalDateTime ) 

\ You can call the constructor by naming the parameters, thus reducing the risks of making a mistake:

val license = License(     id = "My license", licenseeName = "Customer",     licenseId = "XXX-123", environment = "User-acceptance tests",     generatedAt = LocalDateTime() ) 

The Builder Pattern

The Builder pattern is another viable approach, even though it's not part of the use cases described in the GoF.

\ Here's the code:

public class License {      private final String id;     private final String licenseeName;     private final String licenseId;     private final String environment;     private final LocalDateTime generatedAt;      private License (                              //1         String id, String licenseeName,         String licenseId, String environment,         LocalDateTime generatedAt     ) { ... }      public static LicenseBuilder builder() {       //2         return new LicenseBuilder();     }      public static class LicenseBuilder {          private String id;                         //3         private String licenseeName;               //3         private String licenseId;                  //3         private String environment;                //3         private LocalDateTime generatedAt;         //3          private LicenseBuilder() {}                //1          public LicenseBuilder withId(String id) {  //4             this.id = id;                          //5             return this;                           //4         }          // Other `withXXX` methods          public License build() {                   //6             return new License(                 id, licenseeName,                 licenseId, environment,                 generatedAt             );         }     } } 
  1. Prevent direct object instantiation
  2. Create a new builder
  3. The builder fields mimic the object's fields
  4. Each method returns the builder object itself
  5. Assign the attribute
  6. Return the complete object

\ One can now call the builder as such:

val license = License.builder()                      .withId("My license")                      .withLicenseName("Customer")                      .withLicenseId("XXX-123")                      .withEnvironment("User-acceptance tests")                      .withGeneratedAt(new LocalDateTime()) 

Creating the builder code is a pain (unless you use AI), but it allows for better readability. Moreover, one can add validation for every method call, ensuring the object under construction is valid. For more complex objects, one can also implement a Faceted Builder.

Summary

| Approach | Pros | Cons | |----|----|----| | Type wrappers | Object-Oriented Programming | - More verbose- Can be memory-heavy depending on the language | | Named parameters | Easy | Not available in Java | | Builder pattern | Verbose | - Allows creating complex objects- Allows validating |

Constructors Throwing Exceptions

In the same codebase, I found the following code:

public Stuff(UuidService uuidService, FallbackUuidService fallbackUuidService) {      try {         uuid = uuidService.getUuid();     } catch(CannotGetUuidException e) {         try {             uuid = fallbackUuidService.getUuid();         } catch(CannotGetUuidException e1) {             uuid = "UUID can be fetched";         }     } } 

\ With a modicum of experience, you can notice what's wrong in the above snippet. If both services fail, uuid is initialized with a string that's not a UUID. All code that relies on UUID must deal with possibly non-UUID values. One must fail fast. A quick fix would look like this:

public Stuff(UuidService uuidService, FallbackUuidService fallbackUuidService) {      try {         uuid = uuidService.getUuid();     } catch(CannotGetUuidException e) {         try {             uuid = fallbackUuidService.getUuid();         } catch(CannotGetUuidException e1) {             throw new RuntimeException(e1);         }     } } 

Now, every Stuff object has a valid UUID. However, throwing exceptions inside constructors has potential issues:

  • Resource Leaks: If the constructor allocates resources (e.g., files, sockets) and throws an exception, those resources may not be released. It can be mitigated by being careful and using try-catch-finally blocks.
  • Inheritance: If a superclass constructor throws an exception, the subclass constructor won’t run.
  • Checked exceptions: It's impossible to use checked exceptions in constructors, only runtime ones.

\ For these reasons, I think exceptions don't have their place in constructors and I avoid them. One can use the Builder pattern described in the first section, but as mentioned, it's a lot of code; I don't think it's necessary. Another creational pattern to the rescue, Factory Method.

\

\ Note that in this case, we use it for a different reason. Here's the updated code:

public class Stuff {      private final UUID uuid;      private Stuff(UUID uuid) {                               //1         this.uuid = uuid;     }      public static Stuff create(UuidService uuidService, FallbackUuidService fallbackUuidService) throws CannotGetUuidException {          try {             return new Stuff(uuidService.getUuid());         } catch(CannotGetUuidException e) {             return new Stuff(fallbackUuidService.getUuid()); //2         }     } } 
  1. Prevent outside instantiation
  2. If it fails, it throws a new CannotGetUuidException

\ One calls the above like this:

var stuff = Stuff.create(uuidService, fallbackUuidService);  //1 
  1. Need to catch CannotGetUuidException

At this point, we are sure that the object is fully initialized if the call succeeds. If it doesn't, no object is created.

Conclusion

In this post, I've described two usages of the GoF's creational patterns, which aren't listed in the book: improving maintainability and ensuring objects are fully initialized. Knowing your classics allows you to use them in cases that fit, but weren't initially listed for.

To go further:

  • GoF Design Patterns
  • A dive into the Builder pattern
  • The Builder pattern is a finite state machine!

Originally published at A Java Geek on August 31st, 2025

시장 기회
스레숄드 로고
스레숄드 가격(T)
$0.006145
$0.006145$0.006145
-0.88%
USD
스레숄드 (T) 실시간 가격 차트
면책 조항: 본 사이트에 재게시된 글들은 공개 플랫폼에서 가져온 것으로 정보 제공 목적으로만 제공됩니다. 이는 반드시 MEXC의 견해를 반영하는 것은 아닙니다. 모든 권리는 원저자에게 있습니다. 제3자의 권리를 침해하는 콘텐츠가 있다고 판단될 경우, crypto.news@mexc.com으로 연락하여 삭제 요청을 해주시기 바랍니다. MEXC는 콘텐츠의 정확성, 완전성 또는 시의적절성에 대해 어떠한 보증도 하지 않으며, 제공된 정보에 기반하여 취해진 어떠한 조치에 대해서도 책임을 지지 않습니다. 본 콘텐츠는 금융, 법률 또는 기타 전문적인 조언을 구성하지 않으며, MEXC의 추천이나 보증으로 간주되어서는 안 됩니다.

$30,000 in PRL + 15,000 USDT

$30,000 in PRL + 15,000 USDT$30,000 in PRL + 15,000 USDT

Deposit & trade PRL to boost your rewards!