Classes are factories
In Ruby, classes are objects responding to the .new method for instantiation, so we can freely pass them to collaborators and use them as "factories", without having to bother with the rigmarole of the factory design pattern as commonly seen in statically typed object oriented languages like Java:
Invoice = Struct.new(:number, :customer, :total)
class PdfRenderer
def initialize(invoice)
@invoice = invoice
end
def render
"PDF invoice #{@invoice.number} for #{@invoice.customer}"
end
end
class CsvRenderer
def initialize(invoice)
@invoice = invoice
end
def render
[
@invoice.number,
@invoice.customer,
@invoice.total,
].join(",")
end
end
class InvoiceExporter
def initialize(renderer_classes)
@renderer_classes = renderer_classes
end
def export(invoice)
@renderer_classes.map do |renderer_class|
renderer_class.new(invoice).render
end
end
end
invoice = Invoice.new(
number: "INV-123",
customer: "Acme AB",
total: 1200,
)
exporter = InvoiceExporter.new([PdfRenderer, CsvRenderer])
puts exporter.export(invoice)
# Output:
# PDF invoice INV-123 for Acme AB
# INV-123,Acme AB,1200
import java.util.ArrayList;
import java.util.List;
class Invoice {
final String number;
final String customer;
final int total;
Invoice(String number, String customer, int total) {
this.number = number;
this.customer = customer;
this.total = total;
}
}
interface InvoiceRenderer {
String render();
}
interface InvoiceRendererFactory {
InvoiceRenderer create(Invoice invoice);
}
class PdfRenderer implements InvoiceRenderer {
private final Invoice invoice;
PdfRenderer(Invoice invoice) {
this.invoice = invoice;
}
public String render() {
return "PDF invoice " + invoice.number + " for " + invoice.customer;
}
}
class CsvRenderer implements InvoiceRenderer {
private final Invoice invoice;
CsvRenderer(Invoice invoice) {
this.invoice = invoice;
}
public String render() {
return invoice.number + "," + invoice.customer + "," + invoice.total;
}
}
class PdfRendererFactory implements InvoiceRendererFactory {
public InvoiceRenderer create(Invoice invoice) {
return new PdfRenderer(invoice);
}
}
class CsvRendererFactory implements InvoiceRendererFactory {
public InvoiceRenderer create(Invoice invoice) {
return new CsvRenderer(invoice);
}
}
class InvoiceExporter {
private final List<InvoiceRendererFactory> factories;
InvoiceExporter(List<InvoiceRendererFactory> factories) {
this.factories = factories;
}
List<String> export(Invoice invoice) {
List<String> files = new ArrayList<>();
for (InvoiceRendererFactory factory : factories) {
files.add(factory.create(invoice).render());
}
return files;
}
}
public class Main {
public static void main(String[] args) {
Invoice invoice = new Invoice("INV-123", "Acme AB", 1200);
InvoiceExporter exporter = new InvoiceExporter(List.of(
new PdfRendererFactory(),
new CsvRendererFactory()
));
for (String file : exporter.export(invoice)) {
System.out.println(file);
}
// Output:
// PDF invoice INV-123 for Acme AB
// INV-123,Acme AB,1200
}
}
Note how in the Java example, we have to define separate factory classes for each renderer, and then instantiate those factories to pass them to the InvoiceExporter. In contrast, in Ruby we can just pass the renderer classes directly. Also note how this allows us to express the same logic in less than half the lines of code.
To be fair to modern Java, simple cases like this can skip the factory pattern by means of method references, e.g. List.of(PdfRenderer::new, CsvRenderer::new) typed as List<Function<Invoice, InvoiceRenderer>>.
The more important lesson is to to KISS, whatever language we're using.
History
The ability to treat classes as first-class objects has been a core feature of Ruby since its inception