solid_principles.py

Download
python 472 lines 11.8 KB
  1"""
  2SOLID Principles Demonstration
  3
  4SOLID is an acronym for five design principles that make software more:
  5- Understandable
  6- Flexible
  7- Maintainable
  8
  9S - Single Responsibility Principle
 10O - Open/Closed Principle
 11L - Liskov Substitution Principle
 12I - Interface Segregation Principle
 13D - Dependency Inversion Principle
 14"""
 15
 16from abc import ABC, abstractmethod
 17from typing import List, Protocol
 18from dataclasses import dataclass
 19from enum import Enum
 20
 21
 22# =============================================================================
 23# S - SINGLE RESPONSIBILITY PRINCIPLE (SRP)
 24# A class should have only ONE reason to change
 25# =============================================================================
 26
 27print("=" * 70)
 28print("S - SINGLE RESPONSIBILITY PRINCIPLE")
 29print("=" * 70)
 30
 31
 32# ❌ BEFORE: God class with multiple responsibilities
 33class UserManagerBad:
 34    """This class violates SRP - it does TOO MUCH"""
 35
 36    def __init__(self, username: str, email: str):
 37        self.username = username
 38        self.email = email
 39
 40    def save_to_database(self):
 41        """Responsibility 1: Database operations"""
 42        print(f"Saving {self.username} to database...")
 43
 44    def send_welcome_email(self):
 45        """Responsibility 2: Email sending"""
 46        print(f"Sending welcome email to {self.email}...")
 47
 48    def generate_report(self) -> str:
 49        """Responsibility 3: Report generation"""
 50        return f"User Report: {self.username}"
 51
 52    # Problem: If email system changes, database logic changes,
 53    # or reporting format changes, we modify the SAME class!
 54
 55
 56# ✅ AFTER: Each class has a single responsibility
 57@dataclass
 58class User:
 59    """Only responsible for user data"""
 60    username: str
 61    email: str
 62
 63
 64class UserRepository:
 65    """Single responsibility: Database operations"""
 66
 67    def save(self, user: User):
 68        print(f"Saving {user.username} to database...")
 69
 70
 71class EmailService:
 72    """Single responsibility: Email sending"""
 73
 74    def send_welcome_email(self, user: User):
 75        print(f"Sending welcome email to {user.email}...")
 76
 77
 78class ReportGenerator:
 79    """Single responsibility: Report generation"""
 80
 81    def generate_user_report(self, user: User) -> str:
 82        return f"User Report: {user.username}"
 83
 84
 85# =============================================================================
 86# O - OPEN/CLOSED PRINCIPLE (OCP)
 87# Open for extension, closed for modification
 88# =============================================================================
 89
 90print("\n" + "=" * 70)
 91print("O - OPEN/CLOSED PRINCIPLE")
 92print("=" * 70)
 93
 94
 95# ❌ BEFORE: Must modify class to add new shapes
 96class AreaCalculatorBad:
 97    """Violates OCP - must modify to add new shapes"""
 98
 99    def calculate_area(self, shape_type: str, **kwargs) -> float:
100        if shape_type == "circle":
101            return 3.14159 * kwargs["radius"] ** 2
102        elif shape_type == "rectangle":
103            return kwargs["width"] * kwargs["height"]
104        elif shape_type == "triangle":
105            return 0.5 * kwargs["base"] * kwargs["height"]
106        # Adding a new shape requires MODIFYING this method!
107        else:
108            raise ValueError(f"Unknown shape: {shape_type}")
109
110
111# ✅ AFTER: Extensible through inheritance/protocols
112class Shape(ABC):
113    """Abstract base - closed for modification"""
114
115    @abstractmethod
116    def area(self) -> float:
117        pass
118
119
120class Circle(Shape):
121    """Extends Shape without modifying it"""
122
123    def __init__(self, radius: float):
124        self.radius = radius
125
126    def area(self) -> float:
127        return 3.14159 * self.radius ** 2
128
129
130class Rectangle(Shape):
131    def __init__(self, width: float, height: float):
132        self.width = width
133        self.height = height
134
135    def area(self) -> float:
136        return self.width * self.height
137
138
139class Triangle(Shape):
140    def __init__(self, base: float, height: float):
141        self.base = base
142        self.height = height
143
144    def area(self) -> float:
145        return 0.5 * self.base * self.height
146
147
148class AreaCalculator:
149    """Closed for modification - works with any Shape"""
150
151    def calculate_total_area(self, shapes: List[Shape]) -> float:
152        return sum(shape.area() for shape in shapes)
153
154
155# =============================================================================
156# L - LISKOV SUBSTITUTION PRINCIPLE (LSP)
157# Subtypes must be substitutable for their base types
158# =============================================================================
159
160print("\n" + "=" * 70)
161print("L - LISKOV SUBSTITUTION PRINCIPLE")
162print("=" * 70)
163
164
165# ❌ BEFORE: Square violates LSP
166class RectangleBad:
167    def __init__(self, width: float, height: float):
168        self.width = width
169        self.height = height
170
171    def set_width(self, width: float):
172        self.width = width
173
174    def set_height(self, height: float):
175        self.height = height
176
177    def area(self) -> float:
178        return self.width * self.height
179
180
181class SquareBad(RectangleBad):
182    """Violates LSP - changes behavior unexpectedly"""
183
184    def set_width(self, width: float):
185        self.width = width
186        self.height = width  # Breaks rectangle's assumption!
187
188    def set_height(self, height: float):
189        self.height = height
190        self.width = height  # Breaks rectangle's assumption!
191
192
193# ✅ AFTER: Proper abstraction respects LSP
194class ShapeLSP(ABC):
195    @abstractmethod
196    def area(self) -> float:
197        pass
198
199
200class RectangleLSP(ShapeLSP):
201    def __init__(self, width: float, height: float):
202        self._width = width
203        self._height = height
204
205    def area(self) -> float:
206        return self._width * self._height
207
208
209class SquareLSP(ShapeLSP):
210    """Independent class - doesn't inherit problematic behavior"""
211
212    def __init__(self, side: float):
213        self._side = side
214
215    def area(self) -> float:
216        return self._side ** 2
217
218
219# =============================================================================
220# I - INTERFACE SEGREGATION PRINCIPLE (ISP)
221# Clients should not depend on interfaces they don't use
222# =============================================================================
223
224print("\n" + "=" * 70)
225print("I - INTERFACE SEGREGATION PRINCIPLE")
226print("=" * 70)
227
228
229# ❌ BEFORE: Fat interface forces unnecessary implementation
230class WorkerBad(ABC):
231    """Fat interface - not all workers do everything"""
232
233    @abstractmethod
234    def work(self):
235        pass
236
237    @abstractmethod
238    def eat(self):
239        pass
240
241    @abstractmethod
242    def sleep(self):
243        pass
244
245
246class HumanWorkerBad(WorkerBad):
247    def work(self):
248        print("Human working...")
249
250    def eat(self):
251        print("Human eating...")
252
253    def sleep(self):
254        print("Human sleeping...")
255
256
257class RobotWorkerBad(WorkerBad):
258    def work(self):
259        print("Robot working...")
260
261    def eat(self):
262        raise NotImplementedError("Robots don't eat!")  # Forced to implement!
263
264    def sleep(self):
265        raise NotImplementedError("Robots don't sleep!")  # Forced to implement!
266
267
268# ✅ AFTER: Segregated interfaces
269class Workable(Protocol):
270    """Small, focused interface"""
271
272    def work(self):
273        pass
274
275
276class Eatable(Protocol):
277    def eat(self):
278        pass
279
280
281class Sleepable(Protocol):
282    def sleep(self):
283        pass
284
285
286class HumanWorker:
287    """Implements only needed interfaces"""
288
289    def work(self):
290        print("Human working...")
291
292    def eat(self):
293        print("Human eating...")
294
295    def sleep(self):
296        print("Human sleeping...")
297
298
299class RobotWorker:
300    """Only implements what makes sense"""
301
302    def work(self):
303        print("Robot working...")
304
305
306# =============================================================================
307# D - DEPENDENCY INVERSION PRINCIPLE (DIP)
308# Depend on abstractions, not concretions
309# =============================================================================
310
311print("\n" + "=" * 70)
312print("D - DEPENDENCY INVERSION PRINCIPLE")
313print("=" * 70)
314
315
316# ❌ BEFORE: High-level module depends on low-level module
317class MySQLDatabase:
318    """Low-level module (concrete implementation)"""
319
320    def save(self, data: str):
321        print(f"Saving to MySQL: {data}")
322
323
324class UserServiceBad:
325    """High-level module depends on concrete MySQL"""
326
327    def __init__(self):
328        self.db = MySQLDatabase()  # Tight coupling!
329
330    def save_user(self, username: str):
331        self.db.save(username)
332        # Can't easily switch to PostgreSQL or MongoDB!
333
334
335# ✅ AFTER: Both depend on abstraction
336class Database(Protocol):
337    """Abstraction (interface)"""
338
339    def save(self, data: str):
340        pass
341
342
343class MySQLDatabaseGood:
344    def save(self, data: str):
345        print(f"Saving to MySQL: {data}")
346
347
348class PostgreSQLDatabase:
349    def save(self, data: str):
350        print(f"Saving to PostgreSQL: {data}")
351
352
353class MongoDBDatabase:
354    def save(self, data: str):
355        print(f"Saving to MongoDB: {data}")
356
357
358class UserService:
359    """High-level module depends on abstraction"""
360
361    def __init__(self, database: Database):
362        self.db = database  # Depends on interface, not concrete class!
363
364    def save_user(self, username: str):
365        self.db.save(username)
366
367
368# =============================================================================
369# DEMONSTRATION
370# =============================================================================
371
372def demonstrate_solid():
373    print("\n" + "=" * 70)
374    print("SOLID PRINCIPLES DEMONSTRATION")
375    print("=" * 70)
376
377    # Single Responsibility
378    print("\n[SRP] Single Responsibility Principle:")
379    user = User("alice", "alice@example.com")
380    repo = UserRepository()
381    email_service = EmailService()
382    report_gen = ReportGenerator()
383
384    repo.save(user)
385    email_service.send_welcome_email(user)
386    print(report_gen.generate_user_report(user))
387
388    # Open/Closed
389    print("\n[OCP] Open/Closed Principle:")
390    shapes = [
391        Circle(5),
392        Rectangle(4, 6),
393        Triangle(3, 8)
394    ]
395    calculator = AreaCalculator()
396    total = calculator.calculate_total_area(shapes)
397    print(f"Total area: {total:.2f}")
398
399    # Liskov Substitution
400    print("\n[LSP] Liskov Substitution Principle:")
401    shapes_lsp: List[ShapeLSP] = [
402        RectangleLSP(4, 6),
403        SquareLSP(5)
404    ]
405    for shape in shapes_lsp:
406        print(f"Area: {shape.area()}")
407
408    # Interface Segregation
409    print("\n[ISP] Interface Segregation Principle:")
410    human = HumanWorker()
411    robot = RobotWorker()
412
413    human.work()
414    human.eat()
415    robot.work()
416    # robot.eat()  # Not forced to implement!
417
418    # Dependency Inversion
419    print("\n[DIP] Dependency Inversion Principle:")
420    mysql_service = UserService(MySQLDatabaseGood())
421    postgres_service = UserService(PostgreSQLDatabase())
422    mongo_service = UserService(MongoDBDatabase())
423
424    mysql_service.save_user("alice")
425    postgres_service.save_user("bob")
426    mongo_service.save_user("charlie")
427
428
429def print_summary():
430    print("\n" + "=" * 70)
431    print("SOLID PRINCIPLES SUMMARY")
432    print("=" * 70)
433
434    print("""
435[S] Single Responsibility Principle
436    ✓ One class = one reason to change
437    ✓ Easier to understand and maintain
438    ✓ Better testability
439
440[O] Open/Closed Principle
441    ✓ Open for extension (add new features)
442    ✓ Closed for modification (don't change existing code)
443    ✓ Use abstraction and polymorphism
444
445[L] Liskov Substitution Principle
446    ✓ Subtypes must be substitutable for base types
447    ✓ Don't violate base class contracts
448    ✓ Prefer composition over problematic inheritance
449
450[I] Interface Segregation Principle
451    ✓ Many small interfaces > one large interface
452    ✓ Clients use only what they need
453    ✓ Reduces coupling
454
455[D] Dependency Inversion Principle
456    ✓ Depend on abstractions, not concretions
457    ✓ High-level modules independent of low-level details
458    ✓ Enables easy swapping of implementations
459
460BENEFITS OF SOLID:
461  • More maintainable code
462  • Easier to test
463  • Better extensibility
464  • Reduced coupling
465  • Improved reusability
466""")
467
468
469if __name__ == "__main__":
470    demonstrate_solid()
471    print_summary()