Skip to main content
Innovation|Innovation

Java में SOLID Principles: कोड को आसानी से बदलने योग्य बनाने वाले पाँच नियम

Java में SOLID पाँच सिद्धांतों का व्यावहारिक, code-first walkthrough — Single Responsibility, Open/Closed, Liskov Substitution, Interface Segregation, और Dependency Inversion — messy-then-clean उदाहरणों और सच में कब लागू करना है इस पर ईमानदार मार्गदर्शन के साथ।

6 मई 202612 min read

ज़्यादातर कोड इसलिए नहीं टूटता कि वह ग़लत है। टूटता इसलिए है कि उसे बदलना मुश्किल है। SOLID उसी दूसरी समस्या के लिए पाँच छोटे नियम हैं।

SOLID आज भी क्यों मायने रखता है

मुझे वह पहली बार याद है जब मैंने किसी और के लिखे हुए codebase में एक feature जोड़ने की कोशिश की थी। मैंने एक file में तीन lines बदलीं, और project के किसी और कोने में चार tests red हो गए। कोड तकनीकी रूप से ग़लत नहीं था। बस उसे अगले व्यक्ति को ध्यान में रखकर नहीं लिखा गया था।

SOLID उसी अनुभव का जवाब है। यह पाँच सिद्धांत हैं, जिन्हें Robert C. Martin ने 2000 के दशक की शुरुआत में नाम दिया, जो object-oriented कोड को बढ़ने के साथ flexible बनाए रखने में मदद करते हैं। ये क़ानून नहीं हैं। पहले दिन से सही चुनाव भी ज़रूरी नहीं। लेकिन Java में किसी भी non-trivial चीज़ को तीन साल maintain करने के बाद, समझ में आने लगता है कि ये क्यों मौजूद हैं।

संक्षिप्त रूप का मतलब है:

  • S — Single Responsibility Principle
  • O — Open/Closed Principle
  • L — Liskov Substitution Principle
  • I — Interface Segregation Principle
  • D — Dependency Inversion Principle

यह guide हर एक को एक concrete Java example के साथ समझाती है: messy version, SOLID version, और एक one-line जवाब "मैं इसे असल में कब इस्तेमाल करूँगा?" के लिए। examples जान-बूझकर छोटे रखे गए हैं — sketch के आकार में देखने के बाद patterns पहचानना आसान हो जाता है।

S — Single Responsibility Principle

एक class में बदलाव का सिर्फ़ एक कारण होना चाहिए।

समस्या

एक ऐसी class लीजिए जो बहुत कुछ करती है। editor में reasonable दिखती है, लेकिन हर requirement बदलाव उसे छूता है।

public class Invoice {
    private List<LineItem> items;
    private Customer customer;

    public double calculateTotal() {
        double total = 0;
        for (LineItem item : items) {
            total += item.getPrice() * item.getQuantity();
        }
        return total;
    }

    public String generatePdf() {
        // builds a PDF using iText
        return "...pdf bytes...";
    }

    public void sendEmail(String address) {
        // opens an SMTP connection, attaches the PDF, sends
    }

    public void saveToDatabase() {
        // opens a JDBC connection, runs INSERT
    }
}

इस class के बदलने के चार कारण: pricing rules बदलते हैं, PDF library बदली जाती है, marketing को अलग email templates चाहिए, database MySQL से Postgres पर migrate होती है। एक ही file को चार teams एक साथ edit कर सकती हैं।

हल

responsibility के अनुसार बाँटिए। हर class एक concern की मालिक।

public class Invoice {
    private final List<LineItem> items;
    private final Customer customer;

    public Invoice(List<LineItem> items, Customer customer) {
        this.items = items;
        this.customer = customer;
    }

    public double calculateTotal() {
        return items.stream()
            .mapToDouble(i -> i.getPrice() * i.getQuantity())
            .sum();
    }

    public List<LineItem> getItems() { return items; }
    public Customer getCustomer() { return customer; }
}

public class InvoicePdfRenderer {
    public byte[] render(Invoice invoice) { /* ... */ }
}

public class InvoiceEmailer {
    public void send(Invoice invoice, String address, byte[] pdf) { /* ... */ }
}

public class InvoiceRepository {
    public void save(Invoice invoice) { /* ... */ }
}

अब हर बदलाव का एक ही घर है। SMTP server बदलता है, तो सिर्फ़ InvoiceEmailer खुलती है।

कब इस्तेमाल करें

जिस दिन एक class job description में "और" इकट्ठा करने लगे — "totals calculate करती है और emails भेजती है और DB में लिखती है" — बाँट दीजिए। "और" वाक्यों का आकार ही SRP का सबसे साफ़ संकेत है।

O — Open/Closed Principle

कोड extension के लिए open, modification के लिए closed होना चाहिए।

समस्या

आपने एक discount calculator लिखा। हर नया customer tier उसी method में एक और if branch माँगता है।

public class DiscountCalculator {
    public double apply(Customer customer, double total) {
        if (customer.getTier().equals("REGULAR")) {
            return total;
        } else if (customer.getTier().equals("PREMIUM")) {
            return total * 0.90;
        } else if (customer.getTier().equals("VIP")) {
            return total * 0.80;
        } else if (customer.getTier().equals("EMPLOYEE")) {
            return total * 0.70;
        }
        return total;
    }
}

"STUDENT" tier जोड़ना मतलब इस class को edit करना। edit मतलब हर पुराने tier को दोबारा test करना। class बदलाव के विरुद्ध बंद नहीं है।

हल

हर rule को interface के पीछे रखिए। नए tiers नई classes के रूप में आते हैं।

public interface DiscountPolicy {
    boolean appliesTo(Customer customer);
    double apply(double total);
}

public class RegularDiscount implements DiscountPolicy {
    public boolean appliesTo(Customer c) { return c.getTier().equals("REGULAR"); }
    public double apply(double total) { return total; }
}

public class PremiumDiscount implements DiscountPolicy {
    public boolean appliesTo(Customer c) { return c.getTier().equals("PREMIUM"); }
    public double apply(double total) { return total * 0.90; }
}

public class DiscountCalculator {
    private final List<DiscountPolicy> policies;

    public DiscountCalculator(List<DiscountPolicy> policies) {
        this.policies = policies;
    }

    public double apply(Customer customer, double total) {
        return policies.stream()
            .filter(p -> p.appliesTo(customer))
            .findFirst()
            .map(p -> p.apply(total))
            .orElse(total);
    }
}

student tier जोड़ना अब एक नई file है: StudentDiscount implements DiscountPolicy। पुरानी classes अछूती रहती हैं। Spring के साथ आप उसे @Component के रूप में register कर देते हैं और framework list inject कर देता है।

कब इस्तेमाल करें

जब आप else if branches की एक श्रृंखला देखें जो "उसी प्रकार की चीज़" का नया variant आते ही बढ़ती है, वही वृद्धि smell है। हर branch एक class बनना चाहती है।

L — Liskov Substitution Principle

Subclasses को parents की जगह बिना surprise के इस्तेमाल किया जा सके।

समस्या

classic उदाहरण Rectangle और Square है। ज्यामिति में square एक rectangle है, तो कोड में क्यों नहीं?

public class Rectangle {
    protected int width, height;

    public void setWidth(int w) { this.width = w; }
    public void setHeight(int h) { this.height = h; }
    public int area() { return width * height; }
}

public class Square extends Rectangle {
    @Override
    public void setWidth(int w) {
        this.width = w;
        this.height = w;
    }
    @Override
    public void setHeight(int h) {
        this.width = h;
        this.height = h;
    }
}

अब एक method लिखिए जो rectangle का इस्तेमाल करती है:

void resize(Rectangle r) {
    r.setWidth(5);
    r.setHeight(4);
    assert r.area() == 20;  // FAILS for Square — area is 16
}

square extends Rectangle को satisfy करता है, लेकिन width और height स्वतंत्र होने का अनुमान लगाने वाले हर caller को तोड़ता है। यह inheritance एक झूठ है।

हल

उस relationship को मजबूर मत कीजिए। दोनों shapes एक common interface को implement करें; कोई किसी से inherit न करे।

public interface Shape {
    int area();
}

public class Rectangle implements Shape {
    private final int width, height;
    public Rectangle(int w, int h) { this.width = w; this.height = h; }
    public int area() { return width * height; }
}

public class Square implements Shape {
    private final int side;
    public Square(int side) { this.side = side; }
    public int area() { return side * side; }
}

अब Shape सिर्फ़ वही कहता है जो ईमानदारी से सच है: हर shape का एक area होता है। कोई caller misled नहीं होता।

कब इस्तेमाल करें

जब भी आप extends की तरफ़ बढ़ें, ख़ुद से पूछिए: क्या मैं child को parent की जगह कहीं भी, calling code बदले बिना, इस्तेमाल कर सकता हूँ? अगर methods को override करके UnsupportedOperationException फेंकना पड़े, तो आप inheritance नहीं कर रहे — माफ़ी माँग रहे हैं। interface या composition की तरफ़ झुकिए।

I — Interface Segregation Principle

Clients को उन methods पर निर्भर नहीं होना चाहिए जो वे इस्तेमाल नहीं करते।

समस्या

एक विशाल interface जिसे हर implementer satisfy करता है।

public interface Worker {
    void work();
    void eat();
    void sleep();
    void attendMeeting();
    void writeCode();
    void writeReport();
}

public class Robot implements Worker {
    public void work() { /* OK */ }
    public void writeCode() { /* OK */ }

    public void eat() { throw new UnsupportedOperationException(); }
    public void sleep() { throw new UnsupportedOperationException(); }
    public void attendMeeting() { throw new UnsupportedOperationException(); }
    public void writeReport() { throw new UnsupportedOperationException(); }
}

robot को अपने आधे methods के बारे में झूठ बोलना पड़ता है। और बुरा यह कि जब-जब interface बढ़ता है, हर implementer टूटता है — जब तक वे एक और ख़ाली method न जोड़ लें।

हल

कई छोटे interfaces, जिन्हें वही classes उठाएँ जिन्हें वाक़ई ज़रूरत हो।

public interface Workable { void work(); }
public interface Eatable   { void eat(); }
public interface Sleepable { void sleep(); }
public interface Coder     { void writeCode(); }

public class Robot implements Workable, Coder {
    public void work() { /* ... */ }
    public void writeCode() { /* ... */ }
}

public class Human implements Workable, Eatable, Sleepable, Coder {
    public void work() { /* ... */ }
    public void eat() { /* ... */ }
    public void sleep() { /* ... */ }
    public void writeCode() { /* ... */ }
}

हर class ठीक उन्हीं contracts को implement करती है जो उसकी प्रकृति से मेल खाते हैं। बहुत कुछ माँगने वाले parent को संतुष्ट करने के लिए exceptions फेंकने की कोई ज़रूरत नहीं।

कब इस्तेमाल करें

smell हैं ख़ाली methods, UnsupportedOperationException, या comments जो कहें "इस type के लिए लागू नहीं।" हर एक interface के बताने का तरीक़ा है कि उसे बहुत पहले बाँट देना चाहिए था।

D — Dependency Inversion Principle

Concrete classes पर नहीं, abstractions पर निर्भर रहें।

समस्या

एक high-level class सीधे low-level class के अंदर पहुँचती है।

public class MySqlOrderRepository {
    public void save(Order order) {
        // JDBC calls to MySQL
    }
}

public class OrderService {
    private final MySqlOrderRepository repository = new MySqlOrderRepository();

    public void placeOrder(Order order) {
        // business rules
        repository.save(order);
    }
}

service MySQL से चिपकी हुई है। database बदलना मतलब OrderService को edit करना। unit test लिखना मतलब एक असली MySQL instance खड़ा करना। high-level policy (orders place करना) low-level detail (कौन-सी database) के रहम पर है।

हल

service एक interface पर निर्भर रहे; database का चुनाव बाहर से inject किया जाने वाला detail हो।

public interface OrderRepository {
    void save(Order order);
}

public class MySqlOrderRepository implements OrderRepository {
    public void save(Order order) { /* ... */ }
}

public class PostgresOrderRepository implements OrderRepository {
    public void save(Order order) { /* ... */ }
}

public class InMemoryOrderRepository implements OrderRepository {
    private final List<Order> saved = new ArrayList<>();
    public void save(Order order) { saved.add(order); }
    public List<Order> all() { return saved; }
}

public class OrderService {
    private final OrderRepository repository;

    public OrderService(OrderRepository repository) {
        this.repository = repository;
    }

    public void placeOrder(Order order) {
        // business rules
        repository.save(order);
    }
}

production में आप PostgresOrderRepository wire करते हैं। unit test में InMemoryOrderRepository wire करके all() पर assert करते हैं। business rules को कुछ पता ही नहीं चला।

कब इस्तेमाल करें

हर वहाँ जहाँ new शब्द किसी ऐसी चीज़ के पास दिखे जिसकी class को परवाह नहीं करनी चाहिए — एक database driver, एक HTTP client, एक clock, एक random number generator। वह new एक छिपी dependency है। उसे एक constructor parameter और एक interface से बदल दीजिए, testability और replaceability मुफ़्त मिल जाती है।

पाँचों कैसे मिलकर काम करते हैं

सिद्धांत पाँच अलग-अलग exercises नहीं हैं। एक ही विचार के पाँच दृश्य हैं: ऐसा कोड जो आपको बदलाव plug in करने की जगह दे

  • SRP unit परिभाषित करता है। बदलाव का एक कारण।
  • OCP seam परिभाषित करता है। नया behaviour नई class में जाता है।
  • LSP seam को ईमानदार बनाए रखता है। ऐसी substitutability जिस पर आप भरोसा कर सकें।
  • ISP seam को संकीर्ण रखता है। मुझे उस पर निर्भर मत बनाइए जिसकी मुझे ज़रूरत नहीं।
  • DIP seam को बाहर की ओर मोड़ता है। policy abstraction पर निर्भर है; details बाहर से plug in होती हैं।

मिलकर ये एक छोटी, सुसंस्कृत class का चित्र बनाते हैं — focused काम, स्पष्ट contract, पढ़ने योग्य wiring diagram। Spring Boot, Quarkus, और जिन Java frameworks के लिए लोग रोज़ाना हाथ बढ़ाते हैं, ज़्यादातर SOLID factories हैं — सही काम करने को कम-से-कम प्रतिरोध का रास्ता बना देते हैं।

सामान्य ग़लतियाँ

पहले दिन से ज़रूरत से ज़्यादा apply करना। एक developer और तीन classes वाले weekend project को हर collaborator के लिए interface नहीं चाहिए। SOLID अपनी क़ीमत तब वसूलता है जब codebase अपने पहले लेखक के बाद भी ज़िंदा रहे। premature abstraction अपने आप में एक तरह का debt है।

SRP को "एक class में एक method" समझ लेना। एक class में कई methods हो सकते हैं — अगर सब एक ही responsibility की सेवा करें। SRP बदलाव के कारणों के बारे में है, method की संख्या के बारे में नहीं।

LSP को inheritance की पहेली मानना। गहरा नियम "parent ने जो contract दिया था उसका सम्मान कीजिए।" अगर आपकी subclass गारंटी कमज़ोर करती है — accepted inputs को संकरा करती है, नया exception फेंकती है, parent ने जो state नहीं छुई उसे mutate करती है — तो callers टूटते हैं।

यह मानना कि DI containers आपके लिए DIP कर देते हैं। एक concrete class पर @Autowired DIP नहीं है। सिद्धांत abstractions पर निर्भर रहने का है; framework बस wiring को कम verbose बनाता है।

interfaces को बहुत आक्रामक रूप से बाँटना। ISP "एक interface में एक method" नहीं है। अगर तीन methods असली callers में हमेशा एक साथ चलते हैं, तो वे एक साथ रहने के क़ाबिल हैं। smell call sites पर unused methods हैं, एक से अधिक method वाला interface नहीं।

एक आख़िरी विचार

SOLID को 2000 में एक paper में नाम मिला जिसने ज़्यादातर वही practices बयान कीं जिनकी ओर लोग एक दशक से बढ़ रहे थे। यह हर अच्छे engineering सिद्धांत की ईमानदार origin story है: किसी ने बहुत सारे code को सड़ते देखा, सबसे धीरे सड़ने वाले shapes को नाम दिया, और उन्हें लिख दिया। मक़सद कभी acronym को रटना नहीं है। मक़सद shapes को पहचानना है — चार-काम वाली class, बढ़ती हुई if-ladder, झूठ बोलने वाली subclass, फूली हुई interface, चिपकी हुई dependency — इतनी जल्दी कि उन्हें ठीक करना सस्ता पड़े।

Software को कमज़ोर होकर बूढ़ा होना ज़रूरी नहीं। वह ऐसा बूढ़ा हो सकता है जिसे भविष्य के आप, या अगला व्यक्ति, मंगलवार की सुबह उठाकर बिना झिझक बदल सके। यही ये पाँच नियम आपको ख़रीदकर देते हैं।

अक्सर पूछे जाने वाले प्रश्न

क्या मुझे हर class पर SOLID लागू करना चाहिए?

नहीं। SOLID अपना वज़न उस कोड में कमाता है जिसे कई बार, कई लोगों द्वारा, लंबे समय तक edit किया जाएगा। एक throwaway script या 50-line utility को constructor-injected abstraction layer नहीं चाहिए। सिद्धांतों का इस्तेमाल तब कीजिए जब complexity या change pressure दिखे — बाद में refactoring की क़ीमत असली है, लेकिन अभी premature abstraction की क़ीमत भी।

SOLID Strategy या Factory जैसे design patterns से कैसे अलग है?

SOLID क्यों है। design patterns SOLID को satisfy करने के सामान्य आकार हैं। Strategy pattern OCP का पालन करने का एक तरीक़ा है। dependency injection DIP का पालन करने का एक तरीक़ा है। Adapter pattern अक्सर LSP उल्लंघन का हल है। सिद्धांतों के बिना patterns cargo cult बन जाते हैं; patterns के बिना सिद्धांत आपको wheel reinvent करवाते हैं।

क्या Spring Boot मेरे कोड को अपने आप SOLID बना देता है?

थोड़ा घर्षण कम करता है, पर सिद्धांतों को मजबूर नहीं करता। एक concrete class पर @Autowired अब भी आपको उसी class से जोड़ देता है। 1,000-line वाली god-class पर @Service अब भी SRP का उल्लंघन करती है। Spring एक tool है — सिद्धांत बताते हैं कि आप उसे अच्छे से इस्तेमाल कर रहे हैं या नहीं।

SOLID और clean architecture का क्या रिश्ता है?

Clean architecture (और संबंधित "ports and adapters" / hexagonal style) system level पर SOLID जैसा दिखता है। SRP बनता है "एक layer में domain logic, दूसरी में infrastructure।" DIP बनता है "domain interfaces पर निर्भर है; adapters edges पर plug in होते हैं।" SOLID अगर grammar है, तो clean architecture paragraph।

मुझे कैसे पता चलेगा कि मैं abstractions के साथ बहुत आगे चला गया हूँ?

तीन संकेत: एक method क्या करता है यह समझने के लिए तीन files पढ़नी पड़ती हैं, आपके interfaces की एक implementation है और दूसरी कोई संभावित नहीं, और एक नए teammate को onboard करने में बहुत समय लग जाता है इससे पहले कि वे कुछ बदल सकें। abstractions अपनी क़ीमत तब कमाते हैं जब वे एक असली दूसरी variation को छिपाएँ। अगर नहीं, तो एक layer हटाना अक्सर राहत जैसा लगता है।

More from Innovation

Innovation

इस साल बुद्धिमत्ता सस्ती हो गई — और मूल्य उस ओर खिसक गया जो आप उससे बनाते हैं

एक करोड़ AI टोकन की लागत एक ही साल में लगभग चौगुनी गिर गई। जब कच्चा माल इतनी तेज़ी से इतना सस्ता हो जाए, तो बढ़त मॉडल के मालिक होने की नहीं, बल्कि उपयोगी चीज़ बनाने वाले की होती है।

Jun 14, 20265 min read
Innovation

वह साल जब रोबोट ने प्रदर्शन करना बंद किया और काम करना शुरू किया

एक दशक तक ह्यूमनॉइड रोबोट सिर्फ़ डेमो वीडियो थे — हमेशा प्रभावशाली, हमेशा पाँच साल दूर। 2026 में कहानी चुपचाप वीडियो से उत्पादन संख्या में बदल गई। यहाँ बताया गया है कि वास्तव में क्या बदला।

Jun 14, 20265 min read
Innovation

AI हर महीने सस्ता हो रहा है। फिर सबके AI बिल क्यों फट रहे हैं?

AI की कीमत एक साल में करीब 4 गुना गिरी — जो काम $20 में होता था वह अब सेंट्स में। फिर भी कंपनियां सालभर का AI बजट महीनों में फूंक रही हैं। दोनों बातें सच हैं, और वजह समझना ज़रूरी है।

Jun 12, 20264 min read