జావాలో SOLID Principles: కోడ్ను సులభంగా మార్చుకోగలిగేలా చేసే ఐదు సూత్రాలు
Java లో SOLID ఐదు సూత్రాల ఆచరణాత్మక, code-first walkthrough — Single Responsibility, Open/Closed, Liskov Substitution, Interface Segregation, Dependency Inversion — messy-then-clean examples మరియు నిజంగా ఎప్పుడు apply చేయాలో నిజాయితీ guidance తో.
చాలా కోడ్ తప్పుగా ఉండడం వల్ల విరగదు. మార్చడం కష్టంగా ఉండడం వల్ల విరుగుతుంది. SOLID అనేది ఆ రెండవ సమస్యకు ఐదు చిన్న సూత్రాల సమాహారం.
SOLID ఇప్పటికీ ఎందుకు ముఖ్యం
మరొకరు రాసిన codebase కు ఒక feature జోడించడానికి నేను మొదటిసారి ప్రయత్నించిన రోజు ఇంకా గుర్తుంది. ఒక file లో మూడు lines మార్చాను, మరెక్కడో project అంతటా నాలుగు tests red అయ్యాయి. కోడ్ సాంకేతికంగా తప్పు కాదు. తరువాతి వ్యక్తి దృష్టిలో పెట్టుకొని రాయలేదు, అంతే.
SOLID అనేది ఆ అనుభవానికి జవాబు. 2000ల ప్రారంభంలో Robert C. Martin పేర్లు పెట్టిన ఐదు సూత్రాలు ఇవి — object-oriented కోడ్ పెరుగుతున్నప్పుడు flexibility ను నిలబెట్టుకోవడానికి సహాయపడతాయి. ఇవి చట్టాలు కావు. మొదటి రోజు సరైన ఎంపికగా ఎప్పుడూ ఉండకపోవచ్చు. కానీ Java లో non-trivial ఏదైనా మూడేళ్ళు maintain చేశాక, వాటి అవసరం ఎందుకో అర్థమవుతుంది.
ఈ acronym ఇలా విస్తరిస్తుంది:
- 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, మరియు "నేను దీన్ని నిజంగా ఎప్పుడు వాడాలి?" అనే ప్రశ్నకు ఒక-line సమాధానం. examples ఉద్దేశపూర్వకంగా చిన్నవే — pattern లను sketch పరిమాణంలో చూశాక గుర్తుపట్టడం సులభం అవుతుంది.
S — Single Responsibility Principle
ఒక class కు మార్పుకు ఒకే కారణం ఉండాలి.
సమస్య
చాలా పనులు చేసే ఒక class తీసుకోండి. editor లో reasonable గానే కనిపిస్తుంది, కానీ ప్రతి requirement మార్పూ దానిని touch చేస్తుంది.
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 లెక్కిస్తుంది మరియు emails పంపుతుంది మరియు DB కు రాస్తుంది" — విభజించండి. SRP మీకు ఇచ్చే అత్యంత స్పష్టమైన signal "మరియు" వాక్యాల ఆకారం.
O — Open/Closed Principle
code 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 చేయడం అంటే ప్రతి existing tier ను మళ్ళీ test చేయడం. class మార్పుకు closed కాదు.
పరిష్కారం
ప్రతి rule ను interface వెనుక పెట్టండి. కొత్త tier లు కొత్త 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. existing classes ను touch చేయడం లేదు. Spring తో, దానిని @Component గా register చేస్తే framework list ను inject చేస్తుంది.
ఎప్పుడు ఉపయోగించాలి
"ఒకే రకం వస్తువు" కు కొత్త variant వచ్చిన ప్రతిసారీ పెరిగే else if branches sequence కనిపిస్తే, ఆ పెరుగుదల smell. ప్రతి branch ఒక class అవ్వాలని కోరుకుంటోంది.
L — Liskov Substitution Principle
Subclasses ను parents స్థానంలో ఎలాంటి surprises లేకుండా వాడగలగాలి.
సమస్య
classic example Rectangle మరియు Square. geometry లో square అనేది rectangle, మరి code లో ఎందుకు కాకూడదు?
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;
}
}
ఇప్పుడు rectangle ను వాడే ఒక method రాయండి:
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 ను break చేస్తుంది. ఈ 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 వాడబోతున్నప్పుడు, మిమ్మల్ని మీరు అడగండి: parent పనిచేసే ఎక్కడైనా calling code ను మార్చకుండా child ను వాడగలనా? methods ను override చేసి UnsupportedOperationException విసరాల్సి వస్తే, మీరు inheritance చేయడం లేదు — apology చేస్తున్నారు. interface లేదా composition వైపు మొగ్గు చూపండి.
I — Interface Segregation Principle
Clients తాము వాడని methods పై depend అవ్వాల్సిన అవసరం రాకూడదు.
సమస్య
ప్రతి implementer satisfy చేయాల్సిన ఒక భారీ interface.
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 break అవుతుంది — వారు మరో empty 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 తన nature కు సరిపడే contracts ను మాత్రమే implement చేస్తుంది. చాలా అడిగిన parent ను satisfy చేయడానికి exceptions విసరడం అక్కరలేదు.
ఎప్పుడు ఉపయోగించాలి
smell ఏమంటే empty methods, UnsupportedOperationException, లేదా "ఈ type కు applicable కాదు" అని చెప్పే comments. ప్రతిదీ interface చెబుతోంది — చాలా కాలం క్రితమే విభజించాల్సింది.
D — Dependency Inversion Principle
Concrete classes మీద కాకుండా abstractions మీద depend అవ్వండి.
సమస్య
ఒక high-level class నేరుగా low-level దానిలోకి చొరబడుతుంది.
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 రాయడం అంటే ఒక real MySQL instance లేపడం. high-level policy (orders place చేయడం) ఒక low-level detail (ఏ database) చేతిలో ఉంది.
పరిష్కారం
service ఒక interface పై depend అవుతుంది; 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 కు ఏమీ తెలియదు.
ఎప్పుడు ఉపయోగించాలి
class ఏ విషయం గురించీ care చేయకూడని దాని పక్కన new పదం కనిపించే ప్రతిచోట — database driver, HTTP client, clock, random number generator. ఆ new ఒక దాచిన dependency. దానిని constructor parameter మరియు ఒక interface తో మార్చండి, testability మరియు replaceability ఉచితంగా వస్తాయి.
ఈ ఐదు ఎలా కలిసి పనిచేస్తాయి
ఈ సూత్రాలు ఐదు వేర్వేరు exercises కావు. ఒకే ఆలోచన యొక్క ఐదు దృక్కోణాలు: మార్పులను plug in చేయడానికి స్థలం ఇచ్చే code.
- SRP unit ను నిర్వచిస్తుంది. మార్పుకు ఒకే కారణం.
- OCP seam ను నిర్వచిస్తుంది. కొత్త behaviour కొత్త class లోకి వెళుతుంది.
- LSP seam ను నిజాయితీగా ఉంచుతుంది. మీరు ఆధారపడగల substitutability.
- ISP seam ను narrow గా ఉంచుతుంది. నాకు అవసరం లేని దాని మీద depend అవ్వకుండా చేయండి.
- DIP seam ను బయటకు చూపిస్తుంది. policy abstraction పై depend అవుతుంది; details బయటి నుండి plug in అవుతాయి.
కలిసి ఇవి ఒక చిన్న, మర్యాదగల class ను వర్ణిస్తాయి — focused job, స్పష్టమైన contract, చదవగలిగే wiring diagram. Spring Boot, Quarkus, మరియు రోజువారీ వాడే Java frameworks చాలా SOLID factories — సరైన పని చేయడాన్ని తక్కువ resistance మార్గంగా చేస్తాయి.
సాధారణ తప్పులు
మొదటి రోజే అతిగా apply చేయడం. ఒక developer మరియు మూడు classes ఉన్న weekend project కు ప్రతి collaborator కు ఒక interface అక్కర్లేదు. SOLID దాని విలువను సంపాదించుకుంటుంది codebase తన మొదటి author ను దాటి జీవించినప్పుడు. premature abstraction తనదైన debt.
SRP ను "class కు ఒక method" గా అపార్థం చేసుకోవడం. ఒక class కు చాలా methods ఉండవచ్చు — అవన్నీ ఒకే responsibility కు సేవ చేస్తే. SRP మార్పుకు కారణాల గురించి, method count గురించి కాదు.
LSP ను inheritance puzzle గా treat చేయడం. deep rule "parent వాగ్దానం చేసిన contract ను గౌరవించండి." subclass ఒక guarantee ను బలహీనపరిస్తే — accepted inputs ను narrow చేస్తే, కొత్త exception విసిరితే, parent ముట్టుకోని state ను mutate చేస్తే — callers break అవుతారు.
DI containers DIP ను మీ కోసం చేస్తాయని అనుకోవడం. concrete class పై @Autowired DIP కాదు. principle abstractions పై depend అవ్వడం; framework wiring ను తక్కువ verbose గా చేస్తుంది.
interfaces ను అతిగా విభజించడం. ISP "interface కు ఒక method" కాదు. real callers లో మూడు methods ఎల్లప్పుడూ కలిసి ప్రయాణిస్తే, అవి కలిసి ఉండాలి. smell call sites వద్ద unused methods, interface కు ఒకటి కంటే ఎక్కువ method ఉండడం కాదు.
ఒక చివరి ఆలోచన
SOLID 2000లో ఒక paper లో పేరు పెట్టబడింది — ఆ paper ఒక దశాబ్దంగా people ఇప్పటికే muddling toward చేస్తున్న practices ను ఎక్కువగా వర్ణించింది. ఇది ప్రతి మంచి engineering principle యొక్క నిజమైన origin story: ఎవరో చాలా code rot చూశారు, అతి తక్కువగా rot అయిన shapes కు పేరు పెట్టారు, వాటిని రాశారు. acronym ను memorise చేయడం point కాదు. shapes ను గుర్తించడమే point — four-job class, పెరుగుతున్న if-ladder, అబద్ధం చెప్పే subclass, bloated interface, welded dependency — వాటిని fix చేయడం చౌక అయ్యేంత త్వరగా.
Software fragile గా age కావాల్సిన అవసరం లేదు. భవిష్యత్తులోని మీరు, లేదా తరువాతి వ్యక్తి, ఒక Tuesday morning తీసుకొని flinch కాకుండా మార్చగలిగే విధంగా age అవ్వగలదు. ఈ ఐదు సూత్రాలు మీకు ఇచ్చేది అదే.
తరచుగా అడిగే ప్రశ్నలు
నేను ప్రతి class కు SOLID apply చేయాలా?
లేదు. చాలా మంది వ్యక్తులు, చాలా కాలం, చాలాసార్లు edit చేసే code లో SOLID తన బరువును సంపాదిస్తుంది. throwaway script లేదా 50-line utility కు constructor-injected abstraction layer అక్కర్లేదు. complexity లేదా change pressure కనిపించినప్పుడు principles వాడండి — తరువాత refactoring cost నిజమే, కానీ ఇప్పుడు premature abstraction cost కూడా.
SOLID Strategy లేదా Factory లాంటి design patterns నుండి ఎలా భిన్నం?
SOLID ఎందుకు. design patterns SOLID ను satisfy చేయడానికి సాధారణ shapes. Strategy pattern OCP ను obey చేయడానికి ఒక మార్గం. dependency injection DIP ను obey చేయడానికి ఒక మార్గం. Adapter pattern తరచుగా LSP violation కు fix. principles లేకుండా patterns cargo cult అవుతాయి; patterns లేకుండా principles wheel ను reinvent చేస్తాయి.
Spring Boot నా code ను automatically SOLID గా చేస్తుందా?
కొంత friction తొలగిస్తుంది, కానీ principles ను enforce చేయదు. concrete class పై @Autowired ఇంకా మిమ్మల్ని ఆ class కు couple చేస్తుంది. 1,000-line god-class పై @Service ఇంకా SRP ను violate చేస్తుంది. Spring ఒక tool — principles మీకు చెబుతాయి, మీరు దానిని బాగా వాడుతున్నారా అని.
SOLID మరియు clean architecture మధ్య సంబంధం ఏమిటి?
Clean architecture (మరియు సంబంధిత "ports and adapters" / hexagonal style) system level వద్ద SOLID ఎలా కనిపిస్తుందో అది. SRP "ఒక layer లో domain logic, మరో layer లో infrastructure" అవుతుంది. DIP "domain interfaces పై depend అవుతుంది; adapters edges వద్ద plug in అవుతాయి" అవుతుంది. SOLID grammar అయితే, clean architecture paragraph.
abstractions తో నేను మరీ ఎక్కువ వెళ్ళానని ఎలా తెలుస్తుంది?
మూడు సంకేతాలు: ఒక method ఏం చేస్తుందో follow చేయడానికి మూడు files చదవాలి, మీ interfaces కు ఒక implementation ఉంది మరియు plausible రెండవది లేదు, మరియు కొత్త teammate ను onboard చేయడం వారు ఏదైనా మార్చడానికి ముందు చాలా సమయం తీసుకుంటుంది. abstractions తమ cost ను సంపాదిస్తాయి అవి నిజమైన రెండవ variation ను దాచినప్పుడు. లేకుంటే, ఒక layer ను delete చేయడం తరచుగా relief గా చదువుతుంది.