Skip to content

Commit ee47e6f

Browse files
committed
Initial commit
0 parents  commit ee47e6f

33 files changed

+1456
-0
lines changed

.github/workflows/pages.yml

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
name: Deploy to GitHub Pages
2+
3+
on:
4+
push:
5+
branches: [main]
6+
7+
permissions:
8+
contents: read
9+
pages: write
10+
id-token: write
11+
12+
concurrency:
13+
group: "pages"
14+
cancel-in-progress: false
15+
16+
jobs:
17+
build:
18+
runs-on: ubuntu-latest
19+
steps:
20+
- uses: actions/checkout@v4
21+
22+
- uses: ruby/setup-ruby@v1
23+
with:
24+
ruby-version: "3.3"
25+
bundler-cache: true
26+
27+
- name: Build site
28+
run: bundle exec jekyll build
29+
env:
30+
JEKYLL_ENV: production
31+
32+
- uses: actions/upload-pages-artifact@v3
33+
with:
34+
path: _site
35+
36+
deploy:
37+
needs: build
38+
runs-on: ubuntu-latest
39+
environment:
40+
name: github-pages
41+
url: ${{ steps.deployment.outputs.page_url }}
42+
steps:
43+
- id: deployment
44+
uses: actions/deploy-pages@v4

.gitignore

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
# IDEs
2+
.idea/
3+
.vscode/
4+
5+
# Python
6+
__pycache__/
7+
*.py[cod]
8+
*.pyo
9+
*.egg-info/
10+
dist/
11+
build/
12+
13+
# Type-checking / linting caches
14+
.mypy_cache/
15+
.ruff_cache/
16+
17+
# Environment
18+
.env
19+
.venv/
20+
venv/
21+
22+
# Jekyll
23+
_site/
24+
.jekyll-cache/
25+
.jekyll-metadata
26+
Gemfile.lock
27+
28+
# OS
29+
.DS_Store
30+
Thumbs.db

01-single-responsibility/README.md

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
---
2+
layout: default
3+
title: "Single Responsibility"
4+
nav_order: 2
5+
permalink: /01-single-responsibility/
6+
---
7+
8+
{: .note }
9+
> **Language:** **English** | [O'zbek]({{ site.baseurl }}/uz/01-single-responsibility/)
10+
11+
# Single Responsibility Principle (SRP)
12+
13+
> A class should have only one reason to change.
14+
15+
Each class or module should encapsulate a **single responsibility**.
16+
When a class handles multiple concerns (e.g. business logic, persistence, formatting), a change in any one area forces the entire class to be modified and re-tested.
17+
18+
## Diagram
19+
20+
<p align="center">
21+
<img src="{{ site.baseurl }}/assets/01-single-responsibility.png" width="700" alt="SRP class diagram" />
22+
</p>
23+
24+
## Violation
25+
26+
In [`violation.py`](violation.py) the `Order` class handles three unrelated responsibilities:
27+
28+
- **Price calculation** — computing the order total
29+
- **Discount logic** — applying business discount rules
30+
- **Persistence** — saving the order to a database
31+
32+
```python
33+
class Order:
34+
def __init__(self, items: list[Item]) -> None:
35+
self.items = items
36+
self.total_price = self.calculate_total()
37+
self.total_price = self.apply_discount(self.total_price)
38+
self.save()
39+
```
40+
41+
Any change to discount rules, pricing formulas, or storage backends forces modification of this single class.
42+
43+
## Correct
44+
45+
In [`correct.py`](correct.py) each responsibility is extracted into its own class:
46+
47+
| Class | Responsibility |
48+
|-------|---------------|
49+
| `Order` | Holds order data |
50+
| `PriceCalculator` | Computes the total price |
51+
| `DiscountApplier` | Applies discount rules |
52+
| `OrderRepository` | Handles persistence |
53+
54+
```python
55+
@dataclass
56+
class Order:
57+
items: list[Item] = field(default_factory=list)
58+
total_price: float = 0.0
59+
60+
def finalize(self) -> None:
61+
self.total_price = PriceCalculator.total(self.items)
62+
self.total_price = DiscountApplier.apply(self.total_price)
63+
OrderRepository.save(self)
64+
```
65+
66+
Now each class has exactly **one reason to change**, and they can be tested, extended, or replaced independently.
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
---
2+
layout: default
3+
title: "Single Responsibility (UZ)"
4+
nav_exclude: true
5+
permalink: /uz/01-single-responsibility/
6+
---
7+
8+
{: .note }
9+
> **Til:** [English]({{ site.baseurl }}/01-single-responsibility/) | **O'zbek**
10+
11+
# Single Responsibility Principle (SRP)
12+
13+
> Har bir class faqat **bitta o'zgarish sababiga** ega bo'lishi kerak.
14+
15+
Har bir class yoki modul **bitta vazifani** bajarishi lozim.
16+
Agar bitta class bir nechta vazifani bajara boshlasa (masalan, biznes-logika, ma'lumotlarni saqlash, formatlash), ulardan birortasi o'zgarganda butun class'ni qayta yozish va test qilish kerak bo'ladi.
17+
18+
## Diagramma
19+
20+
<p align="center">
21+
<img src="{{ site.baseurl }}/assets/01-single-responsibility.png" width="700" alt="SRP class diagramma" />
22+
</p>
23+
24+
## Noto'g'ri yondashuv
25+
26+
[`violation.py`](violation.py) dagi `Order` class'i uchta turli vazifani bajaradi:
27+
28+
- **Narxni hisoblash** — buyurtma summasini hisoblash
29+
- **Chegirma** — biznes-qoidalari bo'yicha chegirma qo'llash
30+
- **Saqlash** — buyurtmani bazaga saqlash
31+
32+
```python
33+
class Order:
34+
def __init__(self, items: list[Item]) -> None:
35+
self.items = items
36+
self.total_price = self.calculate_total()
37+
self.total_price = self.apply_discount(self.total_price)
38+
self.save()
39+
```
40+
41+
Chegirma qoidalari, narx formulasi yoki saqlash tizimi o'zgarganda — aynan shu bitta class'ni o'zgartirish kerak bo'ladi.
42+
43+
## To'g'ri yondashuv
44+
45+
[`correct.py`](correct.py) dagi har bir vazifa alohida class'ga ajratilgan:
46+
47+
| Class | Vazifasi |
48+
|-------|----------|
49+
| `Order` | Buyurtma ma'lumotlarini saqlaydi |
50+
| `PriceCalculator` | Umumiy narxni hisoblaydi |
51+
| `DiscountApplier` | Chegirma qoidalarini qo'llaydi |
52+
| `OrderRepository` | Bazaga saqlash bilan shug'ullanadi |
53+
54+
```python
55+
@dataclass
56+
class Order:
57+
items: list[Item] = field(default_factory=list)
58+
total_price: float = 0.0
59+
60+
def finalize(self) -> None:
61+
self.total_price = PriceCalculator.total(self.items)
62+
self.total_price = DiscountApplier.apply(self.total_price)
63+
OrderRepository.save(self)
64+
```
65+
66+
Endi har bir class'ning **faqat bitta o'zgarish sababi** bor va ularni mustaqil ravishda test qilish, kengaytirish yoki almashtirish mumkin.
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
"""Single Responsibility Principle -- Correct.
2+
3+
Each class has exactly one reason to change:
4+
- Order: holds order data
5+
- PriceCalculator: pricing logic
6+
- DiscountApplier: discount rules
7+
- OrderRepository: persistence
8+
"""
9+
10+
from dataclasses import dataclass, field
11+
12+
13+
@dataclass
14+
class Item:
15+
name: str
16+
price: float
17+
quantity: int
18+
19+
20+
class PriceCalculator:
21+
@staticmethod
22+
def total(items: list[Item]) -> float:
23+
return sum(item.price * item.quantity for item in items)
24+
25+
26+
class DiscountApplier:
27+
@staticmethod
28+
def apply(total: float) -> float:
29+
if total > 200:
30+
return total * 0.9
31+
return total
32+
33+
34+
class OrderRepository:
35+
@staticmethod
36+
def save(order: "Order") -> None:
37+
print(f"[DB] Saving order — total: ${order.total_price:.2f}")
38+
39+
40+
@dataclass
41+
class Order:
42+
items: list[Item] = field(default_factory=list)
43+
total_price: float = 0.0
44+
45+
def finalize(self) -> None:
46+
self.total_price = PriceCalculator.total(self.items)
47+
self.total_price = DiscountApplier.apply(self.total_price)
48+
OrderRepository.save(self)
49+
50+
51+
if __name__ == "__main__":
52+
items = [Item("Keyboard", 75.00, 1), Item("Mouse", 50.00, 3)]
53+
order = Order(items=items)
54+
order.finalize()
55+
print(f"Order total: ${order.total_price:.2f}")
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
"""Single Responsibility Principle -- Violation.
2+
3+
The Order class below handles three unrelated concerns:
4+
1. Price calculation
5+
2. Discount logic
6+
3. Database persistence
7+
8+
Any change to the discount rules, the pricing formula, or the storage
9+
backend forces a modification to this single class.
10+
"""
11+
12+
from dataclasses import dataclass
13+
14+
15+
@dataclass
16+
class Item:
17+
name: str
18+
price: float
19+
quantity: int
20+
21+
22+
class Order:
23+
"""Violates SRP: one class, three reasons to change."""
24+
25+
def __init__(self, items: list[Item]) -> None:
26+
self.items = items
27+
self.total_price = self.calculate_total()
28+
self.total_price = self.apply_discount(self.total_price)
29+
self.save()
30+
31+
def calculate_total(self) -> float:
32+
return sum(item.price * item.quantity for item in self.items)
33+
34+
def apply_discount(self, total: float) -> float:
35+
if total > 200:
36+
return total * 0.9
37+
return total
38+
39+
def save(self) -> None:
40+
print(f"[DB] Saving order — total: ${self.total_price:.2f}")
41+
42+
43+
if __name__ == "__main__":
44+
items = [Item("Keyboard", 75.00, 1), Item("Mouse", 50.00, 3)]
45+
order = Order(items)
46+
print(f"Order total: ${order.total_price:.2f}")

02-open-closed/README.md

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
---
2+
layout: default
3+
title: "Open/Closed"
4+
nav_order: 3
5+
permalink: /02-open-closed/
6+
---
7+
8+
{: .note }
9+
> **Language:** **English** | [O'zbek]({{ site.baseurl }}/uz/02-open-closed/)
10+
11+
# Open/Closed Principle (OCP)
12+
13+
> Software entities should be **open for extension** but **closed for modification**.
14+
15+
You should be able to add new behavior without editing existing, tested code.
16+
This is typically achieved through **inheritance** and **polymorphism**.
17+
18+
## Diagram
19+
20+
<p align="center">
21+
<img src="{{ site.baseurl }}/assets/02-open-closed.png" width="700" alt="OCP class diagram" />
22+
</p>
23+
24+
## Violation
25+
26+
In [`violation.py`](violation.py) an `AreaCalculator` uses `isinstance` checks to handle each shape type:
27+
28+
```python
29+
class AreaCalculator:
30+
@staticmethod
31+
def calculate(shape: object) -> float:
32+
if isinstance(shape, Rectangle):
33+
return shape.width * shape.height
34+
if isinstance(shape, Circle):
35+
return math.pi * shape.radius ** 2
36+
raise TypeError(f"Unknown shape: {type(shape).__name__}")
37+
```
38+
39+
Adding a `Triangle` requires modifying the existing `AreaCalculator` — it is **not closed for modification**.
40+
41+
## Correct
42+
43+
In [`correct.py`](correct.py) a `Shape` abstract base class defines the `area()` contract.
44+
Each shape implements its own calculation:
45+
46+
```python
47+
class Shape(ABC):
48+
@abstractmethod
49+
def area(self) -> float: ...
50+
51+
class Triangle(Shape):
52+
def __init__(self, base: float, height: float) -> None:
53+
self.base = base
54+
self.height = height
55+
56+
def area(self) -> float:
57+
return 0.5 * self.base * self.height
58+
```
59+
60+
Adding a new shape is just a new subclass — **zero changes to existing code**.

0 commit comments

Comments
 (0)