Skip to content

Latest commit

 

History

History

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 
 
 
 
 
 
 
 
 

README.md

Factory — Creational Design Pattern

What is it?

The Factory pattern delegates the responsibility of choosing and constructing a concrete type to a dedicated function, hiding that decision from the caller. The caller works only against an interface — it never imports or names the concrete struct. It sits in the Creational family because its sole concern is controlled object construction.vjkj


When to use it

  • When the exact type to create is determined at runtime (e.g., from config, user input, or a feature flag).
  • When you want callers to depend on an abstraction rather than a concrete implementation.
  • When adding a new variant should not require changes outside the factory itself.

Avoid it when there is genuinely only one implementation — the indirection adds complexity with no payoff.


Go's approach vs. OOP languages

In Java or C++, the Factory pattern often involves abstract classes, inheritance, and instanceof checks. Go's approach is leaner:

Mechanism Purpose
Interface (Notifier) Defines the contract every concrete type must fulfill
Unexported concrete structs Callers outside the package cannot instantiate them directly
Exported factory function (NewNotifier) The single controlled entry point — returns the interface, not the concrete type
switch on a channel string Runtime dispatch to the correct concrete constructor

Because Go interfaces are satisfied implicitly, adding a new notifier type requires no changes to the interface or any existing concrete type — only the factory's switch needs updating.


Structure of this implementation

factory/
├── notifier_interface.go      # The Notifier interface
├── notifier_factory.go        # The factory function
├── email_notifier.go          # Concrete: emailNotifier
├── sms_notifier.go            # Concrete: smsNotifier
├── push_notifier.go           # Concrete: pushNotifier
└── notifier_factory_test.go   # Type-correctness and error-path tests
main.go                        # Usage demonstration (concurrent dispatch)

The interface

factory/notifier_interface.go declares the Notifier interface with a single method:

Send(message string) error

Every concrete notifier satisfies this interface implicitly. The caller never needs to know which concrete type it holds.

The concrete types

Each notifier lives in its own file and is an unexported struct, so code outside the factory package cannot write emailNotifier{} directly — the only path to obtaining one is through NewNotifier.

File Struct Output
email_notifier.go emailNotifier [Email] sent with message: …
sms_notifier.go smsNotifier [SMS] sent with message: …
push_notifier.go pushNotifier [Push] sent with message: …

The factory function

NewNotifier is the sole entry point. Its logic is:

  1. Accept a channel string ("EMAIL", "SMS", or "PUSH").
  2. switch on the string and return a pointer to the matching unexported struct, typed as Notifier.
  3. If no case matches, return nil and a descriptive error — the caller is responsible for handling it.

The return type is always the Notifier interface, never a concrete type, enforcing the abstraction at compile time.


Known improvement points

This implementation has two areas worth discussing in an interview:

1. Magic strings in the switch

The switch compares raw string literals ("EMAIL", "SMS", "PUSH"). A typo at the call site compiles fine but fails at runtime. The idiomatic fix is to define typed constants:

const (
    ChannelEmail = "EMAIL"
    ChannelSMS   = "SMS"
    ChannelPush  = "PUSH"
)

Or use an int-based iota type to make invalid values unrepresentable at compile time.

2. Receiver logic in the caller

Each concrete struct holds a Reciver (recipient) field, but NewNotifier has no way to set it — the caller cannot reach the unexported struct after construction. A cleaner approach is an options struct or variadic functional options passed into NewNotifier, so recipient and service configuration flow through the factory.


Concurrency in main.go

main.go demonstrates one real-world usage pattern: dispatching notifications concurrently with a sync.WaitGroup. Each goroutine calls NewNotifier independently — because the factory creates a fresh instance on every call (stateless construction), there is no shared mutable state and no synchronization is needed inside the factory itself.


Testing

TestValidTypes uses a table-driven approach and covers:

Scenario What is verified
Known channel ("PUSH") Returns the correct concrete type (*notifier_factory.pushNotifier)
Known channel ("EMAIL") Returns the correct concrete type (*notifier_factory.emailNotifier)
Unknown channel ("JabbaDabbba") Returns nil notifier and a non-nil error

reflect.TypeOf is used to assert the concrete type behind the returned interface, which is the canonical way to inspect runtime types in Go tests without breaking the encapsulation the factory provides.


Key interview talking points

Q: How is Factory different from just calling a constructor directly? The factory centralizes the creation decision. Callers are coupled only to the interface, not to any concrete type. Adding a new channel means changing one place (the switch) rather than every call site.

Q: Why are the concrete structs unexported? Unexported structs enforce the abstraction. If emailNotifier were exported, callers could bypass the factory and instantiate it directly, undermining the pattern's purpose of hiding the concrete type behind the interface.

Q: Is the factory function itself thread-safe? Yes — it creates a new instance on every call with no writes to shared state, so concurrent calls are safe without any locking.

Q: When would you use Abstract Factory instead? When you have families of related types that must be used together (e.g., a LinuxButton and LinuxCheckbox that must always pair). The simple Factory handles a single product type; Abstract Factory handles a suite of related products.

Q: How would you make this production-ready? Replace magic strings with typed constants or an iota-based channel type, add functional options to NewNotifier for recipient/service configuration, and consider returning a more structured error (e.g., fmt.Errorf("unsupported channel %q", channel)) to aid debugging.