Chapterبەش 4 · Lectureوانە 4.1
Layers and Business Logic: Where Each Rule Livesچینەکان و لۆجیکی کاروبار: هەر یاسایەک لە کوێ دەژی
Why an all-in-one program hurts; business logic versus presentation and data access; the three layers (UI, business logic, data access) and the one-way dependency rule; layers versus tiers; refactoring Bazaar Cart into a repository, a service and two presentations in Python; testing the rules with a fake repository; where validation belongs; layers in PHP and mobile apps; DRY, coupling and cohesion; clean architecture as an extra; and when not to layer.
بۆچی هەموو شت لە یەکدا ئازار دەدات؛ بەرانبەر پیشاندان و دەستڕاگەیشتن بە ؛ سێ (، ، ) و یاسای یەک ئاراستە؛ بەرانبەر چینی ئامێر؛ ڕێکخستنەوەی Bazaar Cart بۆ ، و دوو لە Pythonـدا؛ تاقیکردنەوەی یاساکان بە ساختە؛ لە کوێ بێت؛ لە PHP و ئەپی مۆبایلدا؛ DRY، و ؛ تەلارسازیی Clean وەک زیادە؛ و کەی چینبەندی مەکە.
What you will learnچی فێر دەبیت
- Tell business logic apart from presentation logic and data access in real code.
- Explain the three layers, the one-way dependency rule and the difference between layers and tiers.
- Refactor a small Python program into a repository, a service and a presentation, and add a second presentation.
- Test business rules alone with a fake data layer, and place validation in the right layers.
- Judge when layering helps (DRY, coupling, cohesion) and when it is too much.
- لە کۆدی ڕاستەقینەدا لە لۆجیکی پیشاندان و دەستڕاگەیشتن بە جیا بکەیتەوە.
- سێ ، یاسای یەک ئاراستە و جیاوازیی نێوان و چینی ئامێر ڕوون بکەیتەوە.
- بچووکی Python بۆ ، و ڕێک بخەیتەوە، پاشان دووەم زیاد بکەیت.
- یاساکانی کار بە ساختە بە تەنیا تاقی بکەیتەوە و لە دروستەکاندا دابنێیت.
- هەڵبسەنگێنیت کەی چینبەندی یارمەتی دەدات (DRY، ، ) و کەی زیادە.
One file that does everythingیەک فایل کە هەموو شتێک دەکات
Karwan's first Bazaar Cart program for Soran's shop is one short file. It asks for a product and the grams, reads the price from an SQLite database, applies the shop's rules and prints the bill. The prices are made up, in Iraqi dinars (IQD). Here it is, with a sample run:
یەکەم Bazaar Cart کە کاروان بۆ دوکانی سۆرانی نووسی، یەک کورتە. بەرهەم و گرام دەپرسێت، نرخەکە لە دەخوێنێتەوە، یاساکانی دوکان جێبەجێ دەکات و پسوولەکە چاپ دەکات. نرخەکان دروستکراون و بە دیناری عێراقین (IQD). ئەمە کۆدەکە و نموونەیەکی کارپێکردنییەتی:
import math
import sqlite3
db = sqlite3.connect(":memory:")
db.execute("CREATE TABLE products (code TEXT, name TEXT, price_per_kg INT, stock_g INT)")
db.executemany("INSERT INTO products VALUES (?, ?, ?, ?)", [
("walnuts", "Walnuts", 18000, 20000), ("sumac", "Sumac", 8000, 5000),
("figs", "Dried figs", 12000, 8000), ("tea", "Black tea", 14000, 10000),
("honey", "Honey", 30000, 3000)])
goods = 0
while True:
code = input("Product code (empty to finish): ")
if code == "":
break
grams = int(input("Grams: "))
row = db.execute("SELECT name, price_per_kg, stock_g FROM products WHERE code = ?",
(code,)).fetchone()
name, price_per_kg, stock_g = row
if grams < 100 or grams > 10000 or grams > stock_g:
print(" Not allowed")
continue
price = math.ceil(price_per_kg * grams / 1000 / 250) * 250
db.execute("UPDATE products SET stock_g = stock_g - ? WHERE code = ?", (grams, code))
goods += price
print(f" {name} {grams} g: {price:,} IQD")
delivery = 0 if goods >= 50000 else 3000
print(f"Goods {goods:,} IQD + delivery {delivery:,} IQD = {goods + delivery:,} IQD")Product code (empty to finish): sumac Grams: 350 Sumac 350 g: 3,000 IQD Product code (empty to finish): walnuts Grams: 1500 Walnuts 1500 g: 27,000 IQD Product code (empty to finish): honey Grams: 50 Not allowed Product code (empty to finish): Goods 30,000 IQD + delivery 3,000 IQD = 33,000 IQD
It works, and for one person on one laptop it is fine. Then the requests arrive. Soran wants orders from a web page too: the rules would have to be copied into it. Dilan wants an automatic check that delivery is free from 50,000 IQD: she cannot test that without typing into input(). Aram wants the products on a server database: every SQL line is mixed with print() calls. And when the shop refuses honey 50 g, the customer only reads «Not allowed».
کار دەکات و بۆ یەک کەس لەسەر یەک باشە. پاشان داواکارییەکان دێن. سۆران دەیەوێت داواکاری لە لاپەڕەتۆڕێکیشەوە وەربگرێت: دەبێت یاساکان کۆپی بکرێن بۆ ئەوێ. دیلان دەیەوێت بە شێوەیەکی خۆکار بپشکنێت کە گەیاندن لە 50,000 دینارەوە بێبەرانبەرە: بێ تایپکردن لە input()ـدا ناتوانێت تاقی بکاتەوە. ئارام دەیەوێت بەرهەمەکان ببات بۆ لەسەر : هەموو دێڕێکی تێکەڵی print() بووە. کاتێکیش دوکان 50 گرام هەنگوین ڕەت دەکاتەوە، کڕیار تەنیا «Not allowed» دەبینێت.
This lesson shows where each piece belongs: the business logic (the shop's rules) in its own layer, between the screen and the database, and how that makes the program easier to test and to grow.
ئەم وانەیە پیشان دەدات هەر پارچەیەک شوێنی لە کوێیە: (یاساکانی دوکان) لە خۆیدا، لە نێوان شاشە و . هەروەها پیشان دەدات چۆن ئەمە ئاسانتر دەکات بۆ تاقیکردنەوە و گەورەبوون.
Business logic: the shop’s rulesلۆجیکی کاروبار: یاساکانی دوکان
Every program exists for a real job. The rules of that job are its business logic. For Bazaar Cart they are Soran's rules: R1 a line costs price per kg × grams ÷ 1000, rounded up to the next 250 IQD; R2 each line is 100 g to 10,000 g; R3 never more than the stock; R4 delivery inside Sulaimani costs 3,000 IQD and is free from 50,000 IQD of goods; and, in version 1.1.0, R5 a 5 % discount from 100,000 IQD. Opening hours or «only the owner may change prices» would be business rules too.
هەر بۆ کارێکی ڕاستەقینە هەیە. یاساکانی ئەو کارە ئەون. بۆ Bazaar Cart یاساکانی سۆرانن: R1 نرخی دێڕێک = نرخی کیلۆیەک × گرام ÷ 1000، کە بۆ سەرەوە دەبرێت بۆ 250 دیناری دواتر ()؛ R2 هەر دێڕێک 100 تا 10,000 گرامە؛ R3 هەرگیز زیاتر لە بڕی کۆگا نا؛ R4 گەیاندن لە ناو سلێمانی 3,000 دینارە و لە 50,000 دیناری کاڵاوە بێبەرانبەرە؛ لە وەشانی 1.1.0ـیشدا R5 داشکاندنی 5 لە سەد لە 100,000 دینارەوە. کاتی کردنەوەی دوکان یان «تەنیا خاوەن دوکان دەتوانێت نرخ بگۆڕێت» یاسای کاریشن.
The rules of the real-world job that the software exists for — prices, discounts, delivery fees, stock, who may do what — kept apart from the screens and from the database code.
یاساکانی ئەو کارە ڕاستەقینەیەی بۆی دروست کراوە — نرخ، داشکاندن، کرێی گەیاندن، بڕی بەردەست، کێ دەتوانێت چی بکات — جیا لە شاشەکان و لە کۆدی .
Android's architecture guide says business logic is what gives an app its value. Showing the bill in green, or the SQL that reads a price, is not business logic: the first is presentation, the second is data access.
ڕێنمایی تەلارسازیی Android دەڵێت ئەوەیە کە بەهای ئەپەکە دروست دەکات. پیشاندانی پسوولە بە سەوز، یان ئەو نرخێک دەخوێنێتەوە، نییە: یەکەمیان پیشاندانە و دووەمیان دەستڕاگەیشتنە بە .
In a pharmacy in the bazaar, the counter assistant talks to customers, takes the prescription and hands over the bag. The pharmacist checks the rules: is a prescription needed, is the dose safe, is the medicine on the shelf? The storeroom keeps the boxes and the stock book. An order that comes by phone or WhatsApp goes to the same pharmacist, so the rules are checked the same way.
لە دەرمانخانەیەکی بازاڕدا، یاریدەدەری مێزی پێشەوە قسە لەگەڵ کڕیار دەکات، ڕەچەتەکە وەردەگرێت و کیسەکە دەداتەوە. دەرمانساز یاساکان دەپشکنێت: ڕەچەتە پێویستە؟ ژەمەکە سەلامەتە؟ دەرمانەکە لە ڕەفدا هەیە؟ کۆگا قوتووەکان و دەفتەری کۆگا ڕادەگرێت. داواکارییەک کە بە تەلەفۆن یان واتسئاپ دێت، دەچێتە لای هەمان دەرمانساز، بۆیە یاساکان بە هەمان شێوە دەپشکنرێن.
Where the analogy stops workingسنووری ئەم نموونەیە
In a small pharmacy one person may do all three jobs. In a program the three jobs stay in separate code even when one developer writes all of them.
لە دەرمانخانەیەکی بچووکدا لەوانەیە یەک کەس هەر سێ کارەکە بکات. لە سێ کارەکە لە کۆدی جیادا دەمێننەوە، تەنانەت ئەگەر یەک هەموویان بنووسێت.
Business logic: the shop’s rulesلۆجیکی کاروبار: یاساکانی دوکان
Three kinds of code in one scriptسێ جۆر کۆد لە یەک دەسنووسدا
Colour each line of all_in_one.py by its job and the problem becomes visible. Blue lines ask and show (presentation), green lines are the shop's rules (business logic) and orange lines talk to the database (data access). The colours are mixed from top to bottom.
هەر دێڕێکی all_in_one.py بەپێی کارەکەی ڕەنگ بکە و کێشەکە دیار دەبێت. دێڕە شینەکان دەپرسن و پیشان دەدەن (پیشاندان)، دێڕە سەوزەکان یاساکانی دوکانن () و دێڕە پرتەقاڵییەکان قسە لەگەڵ دەکەن (دەستڕاگەیشتن بە ). ڕەنگەکان لە سەرەوە تا خوارەوە تێکەڵن.
- Presentation logic: how to ask, what to show, the words and the layout.
- Business logic: what the shop allows and how it calculates money.
- Data access: how products are stored and found: SQL, files or a web API.
- لۆجیکی پیشاندان: چۆن بپرسیت، چی پیشان بدەیت، وشەکان و ڕێکخستنی شاشە.
- : دوکان چی ڕێگە پێ دەدات و پارە چۆن دەژمێرێت.
- دەستڕاگەیشتن بە : بەرهەمەکان چۆن هەڵدەگیرێن و دەدۆزرێنەوە: ، یان وێب.
Three layers and one directionسێ چین و یەک ئاراستە
The usual cure is to give each kind of code its own layer. Martin Fowler calls this presentation–domain–data layering, and Microsoft's architecture guide calls the layers UI, BLL and DAL:
چارەسەری باو ئەوەیە کە هەر جۆرە کۆدێک خۆی هەبێت. Martin Fowler ئەمە ناو دەنێت چینبەندیی پیشاندان، domain و و ڕێنمایی تەلارسازیی Microsoft بە دەڵێت UI، BLL و DAL:
| Layer | Its jobئەرکەکەی | In Bazaar Cartلە Bazaar Cartـدا |
|---|---|---|
| (presentation) | Asks, shows, turns clicks and key presses into callsدەپرسێت، پیشان دەدات، و دوگمەلێدان دەکات بە بانگکردن | cli.py |
| (domain) | Applies the rules, decides what is allowed, calculatesیاساکان جێبەجێ دەکات، بڕیار دەدات چی ڕێگەپێدراوە، دەژمێرێت | services.py |
| Reads and writes stored data هەڵگیراو دەخوێنێتەوە و دەنووسێت | repository.py |
The dependency rule: each layer calls only the layer below it, never the one above. The UI calls the business layer, the business layer calls the data layer, and results come back up as return values. The business layer never calls input() or print(), and the data layer never decides a price.
یاسای : هەر تەنیا ژێرەوەی خۆی بانگ دەکات، هەرگیز ئەوەی سەرەوە نا. بانگ دەکات، ئەویش و ئەنجامەکان وەک دەگەڕێنەوە سەرەوە. هەرگیز input() یان print() بانگ ناکات و هەرگیز بڕیاری نرخ نادات.
Three layers and one directionسێ چین و یەک ئاراستە
Layers are not tiersچین و چینی ئامێر جیاوازن
A layer is how the code is organised. A tier is a machine or process where code runs. (In Kurdish both are often called «چین»; this lesson says «چینی کۆد» and «چینی ئامێر» to keep them apart.) Today Bazaar Cart has three layers in one tier: everything runs on one laptop, with SQLite in memory.
شێوەی ڕێکخستنی کۆدە. tier ئامێر یان پرۆسەیەکە کە کۆدی لەسەر کار دەکات. (لە کوردیدا زۆرجار بە هەردووکیان دەوترێت «چین»؛ ئەم وانەیە بۆ جیاکردنەوەیان دەڵێت «چینی کۆد» و «چینی ئامێر».) ئێستا Bazaar Cart سێ چینی کۆدی هەیە لە یەک چینی ئامێردا: هەموو شت لەسەر یەک کار دەکات و SQLite لە بیرگەدایە.
A web version could put the same code on three machines: the browser, a web server and a database server. That is the three-tier architecture of the database course. Fowler stresses that the layers stay logical: you can run all three on a laptop or spread them out, and the code inside does not change.
وەشانێکی وێب دەتوانێت هەمان کۆد لەسەر سێ ئامێر دابنێت: ، و . ئەمە تەلارسازیی سێچینیـی خولی . Fowler جەخت دەکاتەوە کە لۆجیکی دەمێننەوە: دەتوانیت هەر سێکیان لەسەر کار پێ بکەیت یان بڵاویان بکەیتەوە و کۆدی ناویان ناگۆڕێت.
From one script to a packageلە دەسنووسێکەوە بۆ پاکێجێک
Shilan splits the script into a small Python package, one file per job, plus a folder of tests. We build it from the bottom up: first the shared data shapes, then the data layer, then the rules, then the screens. The files of the earlier lessons are reorganised the same way (Versions and Builds): prices.py (the rules) becomes services.py, storage.py (SQLite) becomes repository.py, cart.py becomes cli.py, and models.py is new.
شیلان دابەش دەکات بۆ بچووکی Python، بۆ هەر کارێک یەک ، لەگەڵ بوخچەیەک بۆ . لە خوارەوە بۆ سەرەوە دروستی دەکەین: سەرەتا شێوەی هاوبەشی ، پاشان ، پاشان یاساکان، پاشان شاشەکان. وانەکانی پێشووش بە هەمان شێوە ڕێک دەخرێنەوە (وەشانەکان و بیلدەکان): prices.py (یاساکان) دەبێت بە services.py، storage.py () دەبێت بە repository.py، cart.py دەبێت بە cli.py و models.py نوێیە.
models.py holds the plain data that every layer passes around. It has no input, no SQL and no rules (classes and objects explains class; @dataclass writes __init__ for you):
models.py ئەو سادەیە لە خۆ دەگرێت کە هەموو دەیگوازنەوە. نە ، نە و نە یاسای تێدایە (کلاسەکان و ئۆبجێکتەکان class ڕوون دەکاتەوە؛ @dataclass خۆی __init__ت بۆ دەنووسێت):
"""The data that every layer talks about. No input, no SQL, no rules."""
from dataclasses import dataclass
@dataclass
class Product:
code: str
name: str
price_per_kg: int # IQD (made-up prices)
stock_g: int # grams in the storeroom
@dataclass
class OrderLine:
name: str
grams: int
price: int
@dataclass
class Order:
lines: list[OrderLine]
goods_total: int
delivery: int
@property
def total(self):
return self.goods_total + self.deliveryExplain the codeڕوونکردنەوەی کۆدەکە
- Lineدێڕی 26-28
totalis computed when you read it, so it can never disagree with the two numbers.totalکاتی خوێندنەوە دەژمێردرێت، بۆیە هەرگیز لەگەڵ دوو ژمارەکەدا ناکۆک نابێت.
Step 1: the data access layerهەنگاوی 1: چینی دەستڕاگەیشتن بە دراوە
The data access layer is the only place that knows how products are stored. Here it is a repository class: other code asks it for a product by code and gets a Product object back, never a row or a cursor. It uses sqlite3 with ? placeholders, as in SQLite basics.
تاکە شوێنە کە دەزانێت بەرهەمەکان چۆن هەڵدەگیرێن. لێرەدا : کۆدی تر بە کۆدی بەرهەم داوای بەرهەمێکی لێ دەکات و Product وەردەگرێتەوە، هەرگیز ڕیز یان cursor نا. sqlite3 بە شوێنگری ? بەکار دەهێنێت، وەک لە بنەماکانی SQLite.
"""Data access layer: the only file that knows SQL."""
import sqlite3
from .models import Product
class ProductRepository:
def __init__(self, db):
self.db = db
def get(self, code):
row = self.db.execute(
"SELECT code, name, price_per_kg, stock_g FROM products WHERE code = ?",
(code,)).fetchone()
return Product(*row) if row else None
def reduce_stock(self, code, grams):
self.db.execute(
"UPDATE products SET stock_g = stock_g - ? WHERE code = ?", (grams, code))
self.db.commit()
def open_demo_db():
"""Soran's five products in a database in memory (made-up numbers)."""
db = sqlite3.connect(":memory:")
db.execute("CREATE TABLE products (code TEXT PRIMARY KEY, name TEXT,"
" price_per_kg INTEGER, stock_g INTEGER)")
db.executemany("INSERT INTO products VALUES (?, ?, ?, ?)", [
("walnuts", "Walnuts", 18000, 20000), ("sumac", "Sumac", 8000, 5000),
("figs", "Dried figs", 12000, 8000), ("tea", "Black tea", 14000, 10000),
("honey", "Honey", 30000, 3000)])
return dbExplain the codeڕوونکردنەوەی کۆدەکە
- Lineدێڕی 11-15
get()returns aProduct, orNonewhen the code does not exist. Deciding what «not found» means is left to the rules.get()Productدەگەڕێنێتەوە، یانNoneکاتێک کۆدەکە نییە. بڕیاردان لەسەر ئەوەی «نەدۆزرایەوە» چی دەگەیەنێت بۆ یاساکان جێدەهێڵرێت.- Lineدێڕی 23-32
The demo database with made-up stock in grams. A real shop would open a file or a server here, and only this file would change.
نموونە بە بڕی کۆگای دروستکراو بە گرام. دوکانێکی ڕاستەقینە لێرەدا یان دەکاتەوە و تەنیا ئەم دەگۆڕێت.
A hand-written repository is one way to build this layer. An ORM library can write the SQL for you; object–relational mapping in PHP shows Eloquent and Doctrine. Either way, the layers above see objects only.
دەستنووس یەک ڕێگەیە بۆ دروستکردنی ئەم . دەتوانێت بۆت بنووسێت؛ ORM لە PHPـدا Eloquent و Doctrine پیشان دەدات. بە هەر ڕێگەیەک بێت، سەرەوە تەنیا دەبینن.
Step 2: the business logic layerهەنگاوی 2: چینی لۆجیکی کاروبار
Now every rule moves into one file. OrderService receives a repository when it is created; it does not care whether the repository uses SQLite or something else. (Handing an object what it needs from outside has a name, dependency injection, the topic of the next-but-one lesson.) A rule break raises a ValueError with a clear message, as in raising your own exceptions.
ئێستا هەموو یاسایەک دەچێتە یەک . OrderService کاتی دروستبوون وەردەگرێت؛ گرنگ نییە بۆی کە ئەو SQLite بەکار دەهێنێت یان شتێکی تر. (دانی ئەوەی پێویستیەتی لە دەرەوە ناوی هەیە: تێخستنی وابەستەیی، بابەتی دوو وانە دواتر.) کاتێک یاسایەک دەشکێت، ValueError بە پەیامێکی ڕوونەوە هەڵدەدرێت، وەک لە هەڵدانی هەڵاوێردەکانی خۆت.
"""Business logic layer: Soran's rules R1-R4 live here, and only here."""
import math
from .models import Order, OrderLine
MIN_GRAMS, MAX_GRAMS = 100, 10_000 # R2
DELIVERY_FEE = 3_000 # R4: inside Sulaimani, IQD
FREE_DELIVERY_FROM = 50_000 # R4: goods total for free delivery
def round_up_250(amount):
"""R1: round UP to the next 250 IQD."""
return math.ceil(amount / 250) * 250
def line_price(price_per_kg, grams):
"""R1: the price of one line."""
return round_up_250(price_per_kg * grams / 1000)
def delivery_fee(goods_total):
"""R4: 3,000 IQD, free from 50,000 IQD of goods."""
return 0 if goods_total >= FREE_DELIVERY_FROM else DELIVERY_FEE
class OrderService:
def __init__(self, repo):
self.repo = repo # any object with get() and reduce_stock()
def price_order(self, items):
"""Check every line (R2, R3) and price the order (R1, R4). Saves nothing."""
lines = []
for code, grams in items:
product = self.repo.get(code)
if product is None:
raise ValueError(f"unknown product: {code}")
if not MIN_GRAMS <= grams <= MAX_GRAMS:
raise ValueError(f"{product.name}: {grams} g is outside 100-10,000 g")
if grams > product.stock_g:
raise ValueError(f"{product.name}: only {product.stock_g:,} g in stock")
lines.append(OrderLine(product.name, grams,
line_price(product.price_per_kg, grams)))
goods = sum(line.price for line in lines)
return Order(lines, goods, delivery_fee(goods))
def place_order(self, items):
"""Price the order, take the goods from stock, return the total in IQD."""
order = self.price_order(items)
for code, grams in items:
self.repo.reduce_stock(code, grams)
return order.totalExplain the codeڕوونکردنەوەی کۆدەکە
- Lineدێڕی 6-8
The numbers of R2 and R4 are named constants, written once.
ژمارەکانی R2 و R4 ناودارن و تەنیا یەک جار نووسراون.
- Lineدێڕی 11-23
Small pure functions: easy to read and to test one by one.
بچووک و ڕوون: خوێندنەوە و تاقیکردنەوەیان یەک بە یەک ئاسانە.
- Lineدێڕی 30-44
price_order()checks R2 and R3 for every line and prices the order. It saves nothing, so a screen can show the bill first.price_order()بۆ هەر دێڕێک R2 و R3 دەپشکنێت و نرخی داواکارییەکە دادەنێت. هیچ پاشەکەوت ناکات، بۆیە شاشە دەتوانێت سەرەتا پسوولەکە پیشان بدات.- Lineدێڕی 46-51
place_order()reusesprice_order(), then takes the goods from stock through the repository.place_order()دووپاتەprice_order()بەکار دەهێنێتەوە، پاشان لە ڕێی کاڵاکان لە کۆگا کەم دەکاتەوە.
Step 3: the presentation layerهەنگاوی 3: چینی نێوبەستەی بەکارهێنەر
The command-line UI now only asks, shows and passes things on. It checks one thing itself, that the grams are a whole number, so it can ask again politely. Every real rule comes from the service, and every refusal arrives as a ValueError message.
، کە لێرەدا ، ئێستا تەنیا دەپرسێت، پیشان دەدات و شتەکان دەگەیەنێتە شوێنی خۆیان. تەنیا یەک شت خۆی دەپشکنێت: ئایا گرامەکان ، بۆ ئەوەی بە ڕێزەوە دووپاتە بپرسێت. هەموو یاسایەکی ڕاستەقینە لە دێت و هەموو ڕەتکردنەوەیەک وەک پەیامی ValueError دەگات.
"""Presentation layer: asks and shows. No rules, no SQL."""
from .repository import ProductRepository, open_demo_db
from .services import OrderService
def ask_items():
items = []
while True:
code = input("Product code (empty to finish): ").strip()
if not code:
return items
text = input("Grams: ").strip()
if not text.isdigit():
print(" Please type the grams as a whole number.")
continue
items.append((code, int(text)))
def show_bill(order):
for line in order.lines:
print(f" {line.name:<10} {line.grams:>5} g {line.price:>7,} IQD")
print(f" Goods {order.goods_total:,} + delivery {order.delivery:,}"
f" = {order.total:,} IQD")
def main():
service = OrderService(ProductRepository(open_demo_db()))
items = ask_items()
try:
show_bill(service.price_order(items))
if input("Confirm (y/n)? ").strip().lower() == "y":
total = service.place_order(items)
print(f"Order placed: {total:,} IQD. Thank you!")
except ValueError as error:
print("Sorry,", error)
if __name__ == "__main__":
main()Product code (empty to finish): sumac Grams: 350 Product code (empty to finish): walnuts Grams: 1.5 kg Please type the grams as a whole number. Product code (empty to finish): walnuts Grams: 1500 Product code (empty to finish): Sumac 350 g 3,000 IQD Walnuts 1500 g 27,000 IQD Goods 30,000 + delivery 3,000 = 33,000 IQD Confirm (y/n)? y Order placed: 33,000 IQD. Thank you!
Explain the codeڕوونکردنەوەی کۆدەکە
- Lineدێڕی 12-15
A format check for friendliness only. «1.5 kg» is not refused by a rule; the screen just asks again.
پشکنینی شێوە تەنیا بۆ دۆستایەتی. «1.5 kg» لەلایەن یاسایەکەوە ڕەت ناکرێتەوە؛ شاشەکە تەنیا دووپاتە دەپرسێت.
- Lineدێڕی 26-35
main()connects the three layers once. Theexceptshows the business layer's message to the customer.main()سێ یەک جار بە یەکەوە دەبەستێت.exceptپەیامی بە کڕیار پیشان دەدات.
Follow one order through the layers: the UI reads «sumac 350», the business layer asks the repository for sumac, the rules run, and an Order travels back up to be printed.
یەک داواکاری بە ناو بەدوادا بچۆ: «sumac 350» دەخوێنێتەوە، لە داوای سوماق دەکات، یاساکان جێبەجێ دەبن و Orderـێک دەگەڕێتەوە سەرەوە بۆ چاپکردن.
![An animation in six frames over three stacked layers and an SQLite database. Frame 1: the UI layer reads Product code: sumac, Grams: 350. Frame 2: an arrow goes down to the business logic layer with service.price_order([("sumac", 350)]). Frame 3: the business layer asks the repository, which sends SELECT … WHERE code = ? to the database. Frame 4: the repository returns Product(name="Sumac", price_per_kg=8000, stock_g=5000). Frame 5: the rules run only in the business layer: R2 350 g OK, R3 350 ≤ 5000 OK, R1 2,800 → 3,000, R4 plus 3,000 delivery. Frame 6: an Order comes back up and the UI prints Goods 3,000 + delivery 3,000 = 6,000 IQD.](/_astro/se-layers-and-business-logic-order-trip.en._G2wCWK-.gif)
![An animation in six frames over three stacked layers and an SQLite database. Frame 1: the UI layer reads Product code: sumac, Grams: 350. Frame 2: an arrow goes down to the business logic layer with service.price_order([("sumac", 350)]). Frame 3: the business layer asks the repository, which sends SELECT … WHERE code = ? to the database. Frame 4: the repository returns Product(name="Sumac", price_per_kg=8000, stock_g=5000). Frame 5: the rules run only in the business layer: R2 350 g OK, R3 350 ≤ 5000 OK, R1 2,800 → 3,000, R4 plus 3,000 delivery. Frame 6: an Order comes back up and the UI prints Goods 3,000 + delivery 3,000 = 6,000 IQD.](/_astro/se-layers-and-business-logic-order-trip.ckb.DQwFavPk.gif)
Step 3: the presentation layerهەنگاوی 3: چینی نێوبەستەی بەکارهێنەر
A second presentation, the same rulesچینی نێوبەستەی بەکارهێنەرێکی دووەم، هەمان یاساکان
Soran's nephew builds a website that needs the bill as JSON. Karwan adds a second presentation in a few lines. It reuses the same service, so every rule, R1 to R4, is already there:
برازای سۆران دروست دەکات کە پسوولەکەی وەک پێویستە. کاروان بە چەند دێڕێک دووەم زیاد دەکات. هەمان دووپاتە بەکار دەهێنێتەوە، بۆیە هەموو یاساکان، R1 تا R4، لە پێشدا ئامادەن:
"""A second presentation on the same business layer: a JSON receipt for a website or an app."""
import json
from .repository import ProductRepository, open_demo_db
from .services import OrderService
def receipt_json(service, items):
order = service.price_order(items)
return json.dumps({
"lines": [{"product": line.name, "grams": line.grams, "price": line.price}
for line in order.lines],
"goods_total": order.goods_total,
"delivery": order.delivery,
"total": order.total,
}, indent=2)
if __name__ == "__main__":
service = OrderService(ProductRepository(open_demo_db()))
print(receipt_json(service, [("honey", 1000), ("tea", 1500)])){
"lines": [
{
"product": "Honey",
"grams": 1000,
"price": 30000
},
{
"product": "Black tea",
"grams": 1500,
"price": 21000
}
],
"goods_total": 51000,
"delivery": 0,
"total": 51000
}Testing the rules without a screen or a databaseتاقیکردنەوەی یاساکان بێ شاشە و بێ بنکەدراوە
Dilan can finally test R1–R4 directly. In the test she gives the service a fake repository, a dictionary in memory, instead of SQLite. The service only calls get() and reduce_stock(), so it cannot tell the difference. No typing, no database, and the tests run in a moment. (Automated testing teaches unittest and test doubles in full.)
دیلان ئێستا دەتوانێت R1 تا R4 ڕاستەوخۆ تاقی بکاتەوە. لە ساختە دەداتە ، لە بیرگەدا، لە جیاتی SQLite. تەنیا get() و reduce_stock() بانگ دەکات، بۆیە جیاوازییەکە نازانێت. نە تایپکردن، نە و لە چاوتروکانێکدا تەواو دەبن. (تاقیکردنەوەی خۆکار unittest و بە تەواوی فێر دەکات.)
import unittest
from bazaar_cart.models import Product
from bazaar_cart.services import OrderService
class InMemoryProductRepository:
"""A fake data layer: a dictionary instead of a database."""
def __init__(self, products):
self.products = {p.code: p for p in products}
def get(self, code):
return self.products.get(code)
def reduce_stock(self, code, grams):
self.products[code].stock_g -= grams
class OrderServiceTest(unittest.TestCase):
def setUp(self):
self.repo = InMemoryProductRepository([
Product("sumac", "Sumac", 8000, 5000),
Product("honey", "Honey", 30000, 3000)])
self.service = OrderService(self.repo)
def test_r1_line_price_rounds_up_to_250(self):
order = self.service.price_order([("sumac", 350)])
self.assertEqual(order.lines[0].price, 3000)
def test_r2_less_than_100_g_is_refused(self):
with self.assertRaises(ValueError):
self.service.price_order([("sumac", 50)])
def test_r3_not_more_than_the_stock(self):
with self.assertRaises(ValueError):
self.service.price_order([("honey", 3500)])
def test_r4_delivery_costs_3000_below_50000(self):
order = self.service.price_order([("sumac", 350)])
self.assertEqual(order.delivery, 3000)
def test_r4_free_delivery_from_50000(self):
order = self.service.price_order([("honey", 1700)]) # 51,000 IQD
self.assertEqual(order.delivery, 0)
def test_place_order_takes_goods_from_stock(self):
self.service.place_order([("honey", 1000)])
self.assertEqual(self.repo.products["honey"].stock_g, 2000)
if __name__ == "__main__":
unittest.main()test_place_order_takes_goods_from_stock (tests.test_services.OrderServiceTest.test_place_order_takes_goods_from_stock) ... ok test_r1_line_price_rounds_up_to_250 (tests.test_services.OrderServiceTest.test_r1_line_price_rounds_up_to_250) ... ok test_r2_less_than_100_g_is_refused (tests.test_services.OrderServiceTest.test_r2_less_than_100_g_is_refused) ... ok test_r3_not_more_than_the_stock (tests.test_services.OrderServiceTest.test_r3_not_more_than_the_stock) ... ok test_r4_delivery_costs_3000_below_50000 (tests.test_services.OrderServiceTest.test_r4_delivery_costs_3000_below_50000) ... ok test_r4_free_delivery_from_50000 (tests.test_services.OrderServiceTest.test_r4_free_delivery_from_50000) ... ok ---------------------------------------------------------------------- Ran 6 tests in 0.000s OK
Explain the codeڕوونکردنەوەی کۆدەکە
- Lineدێڕی 7-17
The fake data layer: the same two methods as
ProductRepository, backed by a dictionary.ساختە: هەمان دوو
ProductRepository، بە لە پشتیەوە.- Lineدێڕی 21-25
setUp()builds a fresh service before every test.setUp()پێش هەر نوێ دروست دەکات.
Validation: a friendly check and a strict checkپشتڕاستکردنەوە: پشکنینێکی دۆستانە و پشکنینێکی توند
Where should «grams must be 100 to 10,000» be checked? In both places, for different reasons. The UI checks the format to be friendly: «1.5 kg» is not a number, ask again. The business layer enforces the rules always, because tomorrow a different screen may call it with no checks at all. This is the same idea as client-side and server-side validation in PHP: never trust what arrives from a screen.
«گرام دەبێت 100 تا 10,000 بێت» لە کوێ بپشکنرێت؟ لە هەردوو شوێن، بە دوو هۆی جیاواز. شێوەکە دەپشکنێت بۆ دۆستایەتی: «1.5 kg» ژمارە نییە، دووپاتە بپرسە. هەمیشە یاساکان جێبەجێ دەکات، چونکە سبەی لەوانەیە شاشەیەکی تر بێ هیچ پشکنینێک بانگی بکات. ئەمە هەمان بیرۆکەی پشتڕاستکردنەوەی لای ڕاژەخواز و پشتڕاستکردنەوەی لای ڕاژەکارـە لە PHPـدا: هەرگیز متمانە بەوە مەکە کە لە شاشەیەکەوە دێت.
# A careless web page sends whatever the visitor typed, with no checks.
from bazaar_cart.repository import ProductRepository, open_demo_db
from bazaar_cart.services import OrderService
service = OrderService(ProductRepository(open_demo_db()))
for grams in [-200, 350, 25000]:
try:
order = service.price_order([("sumac", grams)])
print(grams, "g -> accepted, total", order.total)
except ValueError as error:
print(grams, "g -> refused:", error)-200 g -> refused: Sumac: -200 g is outside 100-10,000 g 350 g -> accepted, total 6000 25000 g -> refused: Sumac: 25000 g is outside 100-10,000 g
Layers in websites and mobile appsچینەکان لە تۆڕگەکان و ئەپی مۆبایلدا
The same three jobs appear everywhere, under different names. Microsoft's guide notes that a new ASP.NET Core project starts as one «all-in-one» project and suggests separate layers as it grows. Android's guide asks for at least a UI layer and a data layer, with an optional domain layer of use cases for complex or shared business logic; it places business logic in the data layer's repositories or in the domain layer, never in the screens.
هەمان سێ کار لە هەموو شوێنێکدا هەن، بە ناوی جیاواز. ڕێنمایی Microsoft دەڵێت پڕۆژەیەکی نوێی ASP.NET Core وەک یەک پڕۆژەی «هەموو شت لە یەکدا» دەست پێ دەکات و پێشنیار دەکات کە بە گەورەبوونی جیا هەبێت. ڕێنمایی Android لانیکەم و چینی دەوێت، لەگەڵ چینی domainـی ئارەزوومەندانە کە use caseی تێدایە بۆ ئاڵۆز یان هاوبەش؛ لە چینی یان لە چینی domainـدا دادەنێت، هەرگیز لە شاشەکاندا نا.
- PHP: the pages of the CRUD application mix HTML, rules and SQL in each file. Moving the SQL into one
ProductRepository.phpand the rules into oneOrderService.phpis the same refactoring you did here. - Mobile: in MVVM for mobile apps the view and the view model form the UI layer, and services and repositories form the data layer.
- PHP: لاپەڕەکانی بەرنامەی CRUD لە هەر ، یاسا و تێکەڵ دەکەن. گواستنەوەی بۆ یەک
ProductRepository.phpو یاساکان بۆ یەکOrderService.phpهەمان ئەو کە لێرەدا کردت. - مۆبایل: لە MVVM بۆ ئەپی مۆبایل و پێک دەهێنن و و چینی پێک دەهێنن.
One rule, one place: DRYیەک یاسا، یەک شوێن: DRY
Karwan's first web page did not ask the service for the delivery fee. He typed the rule again, from memory. Watch what happens with goods of exactly 50,000 IQD (walnuts 2,000 g + black tea 1,000 g):
یەکەم کاروان کرێی گەیاندنی لە نەپرسی. یاساکەی لە بیرەوە دووپاتە نووسییەوە. سەیر بکە چی ڕوو دەدات بە کاڵای ڕێک 50,000 دینار (2,000 گرام گوێز + 1,000 گرام چای ڕەش):
# Karwan's web page copied the delivery rule instead of asking the business layer.
from bazaar_cart.repository import ProductRepository, open_demo_db
from bazaar_cart.services import OrderService
service = OrderService(ProductRepository(open_demo_db()))
order = service.price_order([("walnuts", 2000), ("tea", 1000)])
print("Goods total: ", order.goods_total)
print("Business layer:", order.delivery) # the one real rule (R4)
copied = 0 if order.goods_total > 50000 else 3000 # a copy, typed again
print("Copied rule: ", copied)Goods total: 50000 Business layer: 0 Copied rule: 3000
"Don't repeat yourself": every rule or piece of knowledge should live in one place in the code, so that a change is made once and cannot be forgotten in a copy.
«دووپاتە مەکەرەوە»: هەر یاسایەک یان زانیارییەک دەبێت لە یەک شوێنی کۆددا بژی، بۆ ئەوەی گۆڕانکاری یەک جار بکرێت و لە کۆپییەکدا لەبیر نەچێت.
The fix is one line: show order.delivery, which the business layer already calculated. The DRY principle («don't repeat yourself», named by Andy Hunt and Dave Thomas in The Pragmatic Programmer, 1999) says every rule should have one home. Microsoft's guide lists it as a reason for layers: shared logic is written once.
چارەسەرەکە یەک دێڕە: order.delivery پیشان بدە، کە پێشتر ژماردوویەتی. (DRY، کورتکراوەی «don't repeat yourself»، کە Andy Hunt و Dave Thomas لە 1999 لە The Pragmatic Programmerـدا ناویان لێنا) دەڵێت هەر یاسایەک دەبێت یەک ماڵی هەبێت. ڕێنمایی Microsoft ئەمە وەک هۆیەک بۆ دادەنێت: لۆجیکی هاوبەش یەک جار دەنووسرێت.
Why the split works: coupling and cohesionبۆچی دابەشکردنەکە کار دەکات: پێکەوەبەستراوی و یەکگرتوویی
Requirements and design introduced two words for judging a design. Cohesion: how well the things in one part belong together. Coupling: how much one part depends on another part's insides. Good designs aim for high cohesion and low coupling. Compare before and after:
داواکاری و دیزاین دوو وشەی ناساند بۆ هەڵسەنگاندنی دیزاینێک. : شتەکانی یەک بەش تا چەند پێکەوە دەگونجێن. : بەشێک تا چەند پشت بە ناوەوەی بەشێکی تر دەبەستێت. دیزاینی باش زۆر و کەمی دەوێت. پێش و دوای بەراورد بکە:
| Questionپرسیار | all_in_one.pyall_in_one.py | The layered package |
|---|---|---|
| What does one file do?یەک چی دەکات؟ | Everything (low cohesion)هەموو شت ( کەم) | One job each (high cohesion)هەریەکە یەک کار ( زۆر) |
| What do the rules know about SQLite?یاساکان چی لەبارەی SQLite دەزانن؟ | Its rows and columns (high coupling)ڕیز و ستوونەکانی ( زۆر) | Only get() and reduce_stock() (low coupling)تەنیا get() و reduce_stock() ( کەم) |
| A new screen needs…شاشەیەکی نوێ پێویستی بە… | a copy of the rulesکۆپییەکی یاساکان | one call to the serviceیەک بانگکردنی |
| Testing R4 needs…تاقیکردنەوەی R4 پێویستی بە… | a person typingکەسێک کە تایپ بکات | a fake repository ساختە |
Fowler gives the main reason for layering in one phrase: it reduces the scope of your attention. While you change the delivery rule, you can forget about screens and SQL. That is what makes software more maintainable.
Fowler هۆی سەرەکیی چینبەندی لە یەک ڕستەدا دەڵێت: سنووری سەرنجت بچووک دەکاتەوە. کاتێک یاسای گەیاندن دەگۆڕیت، دەتوانیت شاشە و لەبیر بکەیت. ئەمە ئەوەیە کە زیاد دەکات.
Extra: business rules at the centreزیادە: یاساکانی کار لە ناوەڕاستدا
In our layers the business layer still knows that a repository exists below it. Many teams go one step further. In clean architecture (also called hexagonal, ports-and-adapters or onion architecture), the business rules sit at the centre and define the interfaces they need; the database code and the screens are outer details that depend on the centre. Arrows point inward.
لە ئێمەدا هێشتا دەزانێت لە ژێریەوە هەیە. زۆر تیم هەنگاوێکی تر دەڕۆن. لە تەلارسازیی Cleanـدا (کە پێی دەوترێت hexagonal، ports-and-adapters یان onion)، یاساکانی کار لە ناوەڕاستدان و پێویستی خۆیان دیاری دەکەن؛ کۆدی و شاشەکان وردەکاریی دەرەوەن و پشت بە ناوەڕاست دەبەستن. تیرەکان بەرەو ناوەوەن.
When layers are too muchکەی چینەکان زیادن
Layers cost files and a little ceremony. For a ten-line script you run once, they are not worth it. Bazaar Cart needed them because it has several screens, a database, a team of four and rules that change. Fowler adds a second warning for large systems: do not make «presentation», «domain» and «data» the top-level folders of a huge program. Split it first by feature (orders, products, delivery), and layer each feature inside.
و هەندێک ڕێوڕەسمیان دەوێت. بۆ دە دێڕی کە یەک جار کاری پێ دەکەیت، شایەنی نین. Bazaar Cart پێویستی پێیان بوو چونکە چەند شاشەیەک، ، تیمێکی چوار کەسی و یاسای گۆڕاوی هەیە. Fowler ئاگادارکردنەوەیەکی دووەم بۆ گەورە زیاد دەکات: «پیشاندان»، «domain» و «» مەکە بە بوخچە سەرەکییەکانی زۆر گەورە. سەرەتا بەپێی بەش دابەشی بکە (داواکاری، بەرهەم، گەیاندن) و پاشان هەر بەشێک لە ناوەوە چینبەند بکە.
- Tiny script or a one-off tool: one file is fine; split it the day a second screen or a test is needed.
- An app like Bazaar Cart: three layers in one package.
- A large system with many teams: features first, layers inside each feature. Fowler also warns against teams split by layer: «developers don't have to be full-stack but teams should be».
- بچووک یان ئامرازێکی یەکجاری: یەک باشە؛ ئەو ڕۆژەی شاشەی دووەم یان پێویست بوو، دابەشی بکە.
- ئەپێکی وەک Bazaar Cart: سێ لە یەک .
- گەورە بە چەندین تیم: سەرەتا بەش، لە ناو هەر بەشێکدا. Fowler هەروەها ئاگادار دەکاتەوە لە دابەشکردنی تیم بەپێی : بەرنامەساز پێویست ناکات هەموو شت بزانێت، بەڵام تیم دەبێت هەموو شتی تێدا بێت.
Common mistakesهەڵە باوەکان
Here is the third mistake for real. Someone adds a confirmation question inside the service, then a test runs it on a computer with no keyboard input, as a test server does:
ئەمە هەڵەی سێیەمە بە ڕاستی. کەسێک پرسیارێکی دڵنیاکردنەوە دەخاتە ناو ، پاشان لەسەر کۆمپیوتەرێک بێ تەختەکلیل کاری پێ دەکات، وەک تاقیکردنەوە:
from bazaar_cart.services import OrderService
class ChattyOrderService(OrderService):
def place_order(self, items):
order = self.price_order(items)
print(f"Total: {order.total:,} IQD") # UI work...
if input("Confirm (y/n)? ") != "y": # ...inside the business layer
return 0
for code, grams in items:
self.repo.reduce_stock(code, grams)
return order.totalTotal: 33,000 IQD
Confirm (y/n)? E
======================================================================
ERROR: test_place_order (mistake.test_chatty.ChattyTest.test_place_order)
----------------------------------------------------------------------
Traceback (most recent call last):
File "~/se/bazaar-cart/mistake/test_chatty.py", line 10, in test_place_order
self.assertEqual(service.place_order([("honey", 1000)]), 33000)
~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^
File "~/se/bazaar-cart/mistake/chatty_service.py", line 8, in place_order
if input("Confirm (y/n)? ") != "y": # ...inside the business layer
~~~~~^^^^^^^^^^^^^^^^^^^
EOFError: EOF when reading a line
----------------------------------------------------------------------
Ran 1 test in 0.002s
FAILED (errors=1)Fix: keep the question in cli.py, as in step 3: the UI shows the bill from price_order(), asks «Confirm (y/n)?» and only then calls place_order(). The service stays silent and testable.
چارەسەر: پرسیارەکە لە cli.pyـدا بهێڵەوە، وەک لە هەنگاوی 3: پسوولەکە لە price_order()ـەوە پیشان دەدات، دەپرسێت «Confirm (y/n)?» و تەنیا پاشان place_order() بانگ دەکات. بێدەنگ و تاقیکراو دەمێنێتەوە.
Exercisesڕاهێنانەکان
Say which layer each line belongs in: (a) print(f"Total {order.total:,} IQD"); (b) if goods_total >= 100_000: discount = ...; (c) db.execute("SELECT stock_g FROM products WHERE code = ?", (code,)); (d) if not text.isdigit(): print("Please type a number"); (e) return Product(*row).
بڵێ هەر دێڕێک هی کام : (a) print(f"Total {order.total:,} IQD")؛ (b) if goods_total >= 100_000: discount = ...؛ (c) db.execute("SELECT stock_g FROM products WHERE code = ?", (code,))؛ (d) if not text.isdigit(): print("Please type a number")؛ (e) return Product(*row).
Show answerوەڵامەکە پیشان بدە
(a) UI layer: it shows. (b) Business logic layer: it is rule R5. (c) Data access layer: SQL. (d) UI layer: a friendly format check. (e) Data access layer: it turns a row into an object.
(a) : پیشان دەدات. (b) : یاسای R5ـە. (c) : . (d) : پشکنینێکی دۆستانەی شێوە. (e) : ڕیزێک دەکات بە .
There are 3,000 g of honey in stock. Dilan orders honey 500 g and then honey 2,600 g in the same order, and the program accepts it (3,100 g!). Run it, find which layer must change, fix it and add a test.
3,000 گرام هەنگوین لە کۆگادا هەیە. دیلان لە یەک داواکاریدا 500 گرام هەنگوین و پاشان 2,600 گرام هەنگوین داوا دەکات و پەسەندی دەکات (3,100 گرام!). کاری پێ بکە، بدۆزەوە کام دەبێت بگۆڕێت، چاکی بکە و زیاد بکە.
Hintئاماژە
Each line passes R3 alone. What does the rule really say about the whole order?
هەر دێڕێک بە تەنیا R3 تێدەپەڕێنێت. یاساکە بە ڕاستی لەبارەی هەموو داواکارییەکە چی دەڵێت؟
Show answerوەڵامەکە پیشان بدە
Product code (empty to finish): honey
Grams: 500
Product code (empty to finish): honey
Grams: 2600
Product code (empty to finish):
Honey 500 g 15,000 IQD
Honey 2600 g 78,000 IQD
Goods 93,000 + delivery 0 = 93,000 IQD
Confirm (y/n)? y
Order placed: 93,000 IQD. Thank you!R3 is a business rule, so the fix goes into services.py: add up the grams of each product over all lines before comparing with the stock.
R3 یاسای کارە، بۆیە چاککردنەکە دەچێتە services.py: پێش بەراوردکردن لەگەڵ بڕی کۆگا، گرامی هەر بەرهەمێک لە هەموو دێڕەکاندا کۆ بکەرەوە.
def price_order(self, items):
"""Check every line (R2, R3) and price the order (R1, R4). Saves nothing."""
lines = []
wanted = {} # grams per product code, over all lines
for code, grams in items:
product = self.repo.get(code)
if product is None:
raise ValueError(f"unknown product: {code}")
if not MIN_GRAMS <= grams <= MAX_GRAMS:
raise ValueError(f"{product.name}: {grams} g is outside 100-10,000 g")
wanted[code] = wanted.get(code, 0) + grams
if wanted[code] > product.stock_g:
raise ValueError(f"{product.name}: only {product.stock_g:,} g in stock")
lines.append(OrderLine(product.name, grams,
line_price(product.price_per_kg, grams)))
goods = sum(line.price for line in lines)
return Order(lines, goods, delivery_fee(goods)) def test_r3_two_lines_of_one_product(self):
with self.assertRaises(ValueError):
self.service.price_order([("honey", 500), ("honey", 2600)])test_place_order_takes_goods_from_stock (tests.test_services.OrderServiceTest.test_place_order_takes_goods_from_stock) ... ok test_r1_line_price_rounds_up_to_250 (tests.test_services.OrderServiceTest.test_r1_line_price_rounds_up_to_250) ... ok test_r2_less_than_100_g_is_refused (tests.test_services.OrderServiceTest.test_r2_less_than_100_g_is_refused) ... ok test_r3_not_more_than_the_stock (tests.test_services.OrderServiceTest.test_r3_not_more_than_the_stock) ... ok test_r3_two_lines_of_one_product (tests.test_services.OrderServiceTest.test_r3_two_lines_of_one_product) ... ok test_r4_delivery_costs_3000_below_50000 (tests.test_services.OrderServiceTest.test_r4_delivery_costs_3000_below_50000) ... ok test_r4_free_delivery_from_50000 (tests.test_services.OrderServiceTest.test_r4_free_delivery_from_50000) ... ok ---------------------------------------------------------------------- Ran 7 tests in 0.000s OK
Product code (empty to finish): honey
Grams: 500
Product code (empty to finish): honey
Grams: 2600
Product code (empty to finish):
Sorry, Honey: only 3,000 g in stockVersion 1.1.0 adds R5: 5 % off goods totals of 100,000 IQD or more, rounded up to 250 IQD. Which files change, and which do not? Sketch the change.
وەشانی 1.1.0 یاسای R5 زیاد دەکات: داشکاندنی 5 لە سەد بۆ کۆی کاڵای 100,000 دینار یان زیاتر، کە بۆ سەرەوە دەبرێت بۆ 250 دینار (). کام دەگۆڕێن و کامیان ناگۆڕێن؟ گۆڕانەکە بە کورتی بنووسە.
Show answerوەڵامەکە پیشان بدە
Only services.py and its tests change: add a discount(goods_total) function that returns round_up_250(goods_total * 5 / 100) from 100,000 IQD and 0 below, store it in the Order and subtract it in total. cli.py and receipt.py need at most one new line to show the discount; repository.py does not change. Write tests for 99,750 and 100,000 IQD first (test first).
تەنیا services.py و دەگۆڕێن: discount(goods_total) زیاد بکە کە لە 100,000 دینارەوە round_up_250(goods_total * 5 / 100) دەگەڕێنێتەوە و لە خوارتر 0، لە Orderـدا هەڵی بگرە و لە totalـدا لێی دەربکە. cli.py و receipt.py زۆرترین یەک دێڕی نوێیان دەوێت بۆ پیشاندانی داشکاندنەکە؛ repository.py ناگۆڕێت. سەرەتا بۆ 99,750 و 100,000 دینار بنووسە (سەرەتا حاڵەتی تاقیکردنەوە).
Open create.php in the CRUD application. List which lines are presentation, which are rules (validation) and which are data access, and say which new files you would create.
create.php لە بەرنامەی CRUDـدا بکەرەوە. بڵێ کام دێڕانە پیشاندانن، کامانە یاسان () و کامانە دەستڕاگەیشتن بە . پاشان بڵێ چ نوێ دروست دەکەیت.
Show answerوەڵامەکە پیشان بدە
The HTML form and the echoed messages are presentation; the checks on the submitted fields are business rules; the mysqli/PDO INSERT is data access. A layered version keeps the form in create.php, moves the checks into a StudentService.php (it throws an exception with a message) and the INSERT into a StudentRepository.php. The page then calls only the service.
فۆرمی و ئەو پەیامانەی بە echo چاپ دەکرێن پیشاندانن؛ پشکنینی خانە نێردراوەکان یاسای کارن؛ INSERTـی mysqli/PDO دەستڕاگەیشتنە بە . وەشانێکی چینبەندکراو فۆرمەکە لە create.phpـدا دەهێڵێتەوە، پشکنینەکان دەگوازێتەوە بۆ StudentService.php (کە بە پەیامێکەوە هەڵدەدات) و INSERT بۆ StudentRepository.php. پاشان لاپەڕەکە تەنیا بانگ دەکات.
Summaryپوختە
- Business logic is the real job's rules (R1–R5, stock, who may do what). It is the reason the software exists.
- Three layers: UI (asks and shows), business logic (rules), data access (storage). Calls go down one step; results come back up.
- Layers are logical; tiers are machines. The same three layers can run on one laptop or on three servers (three tiers).
- A repository hides SQL; a service holds the rules; the UI only asks, shows and passes errors on. A second screen reuses every rule.
- Rules are tested alone with a fake repository; the UI checks format kindly, the business layer enforces rules always; one rule has one home (DRY).
- Do not layer a tiny script; split huge systems by feature first, then layer each feature.
- یاساکانی کارە ڕاستەقینەکەیە (R1 تا R5، کۆگا، کێ دەتوانێت چی بکات). هۆی بوونی .
- سێ : (دەپرسێت و پیشان دەدات)، (یاسا)، (هەڵگرتن). بانگکردن یەک هەنگاو بەرەو خوار دەچێت؛ ئەنجام دەگەڕێتەوە سەرەوە.
- لۆجیکین؛ چینی ئامێر ئامێرن. هەمان سێ دەتوانن لەسەر یەک یان سێ کار بکەن (سێ چین).
- دەشارێتەوە؛ یاساکان هەڵدەگرێت؛ تەنیا دەپرسێت، پیشان دەدات و هەڵەکان دەگەیەنێت. شاشەی دووەم هەموو یاساکان دووپاتە بەکار دەهێنێتەوە.
- یاساکان بە ساختە بە تەنیا تاقی دەکرێنەوە؛ بە نەرمی شێوە دەپشکنێت، هەمیشە یاسا جێبەجێ دەکات؛ هەر یاسایەک یەک ماڵی هەیە (DRY).
- بچووک چینبەند مەکە؛ زۆر گەورە سەرەتا بەپێی بەش دابەش بکە، پاشان هەر بەشێک چینبەند بکە.
Next: the UI layer itself can be organised in three classic ways: MVC, MVP and MVVM.
داهاتوو: خۆی دەتوانرێت بە سێ ڕێگەی ناسراو ڕێک بخرێت: MVC، MVP و MVVM.
Lesson complete!وانەکە تەواو بوو!
Well done. Review the key terms, then go on to the next lesson.دەستخۆش. سەیرێکی زاراوە سەرەکییەکان بکەرەوە، پاشان بڕۆ بۆ وانەی داهاتوو.
Key termsزاراوە سەرەکییەکان
MVC, MVP and MVVM: Three Ways to Organise the ScreenMVC، MVP و MVVM: سێ ڕێگە بۆ ڕێکخستنی شاشە
Related lecturesوانە پەیوەندیدارەکان
- MVC, MVP and MVVM: Three Ways to Organise the ScreenMVC، MVP و MVVM: سێ ڕێگە بۆ ڕێکخستنی شاشە
- Dependency Injection: Handing Objects What They Needتێخستنی وابەستەیی: دانی ئەوەی ئۆبجێکتەکان پێویستیانە
- Requirements and Design: Deciding What to Buildپێداویستییەکان و دیزاین: بڕیاردان لەسەر ئەوەی چی دروست بکەین
- App Architecture: the MVVM Patternتەلارسازیی نەرمەواڵە: شێوازی MVVM
- Database Systems, Abstraction and Data Modelsسیستەمەکانی بنکەدراوە، ڕووتکردنەوەی دراوە و مۆدێلەکانی دراوە
- ORM: Working with Tables as ObjectsORM: کارکردن لەگەڵ خشتەکان وەک ئۆبجێکتەکان
- Building a CRUD Application with PHP and MySQLدروستکردنی بەرنامەیەکی CRUD بە PHP و MySQL
- Form Validationپشتڕاستکردنەوەی فۆرم
- Handling Errors with try and exceptمامەڵەکردن لەگەڵ هەڵاوێردە بە try و except
- OSI Layers 6 and 7: Presentation and Applicationچینەکانی 6 و 7ی OSI: چینی پێشکەشکردن و چینی ئەپ
References and further readingسەرچاوەکان و خوێندنەوەی زیاتر
- Martin Fowler: Presentation Domain Data Layering (26 August 2015)
- Microsoft Learn: Common web application architectures (all-in-one apps, layers, UI/BLL/DAL, layers vs tiers, DRY, clean architecture, composition root)
- Android Developers: Guide to app architecture (UI layer, data layer with business logic, optional domain layer and use cases)
- Python 3.13 documentation: sqlite3 (":memory:", placeholders against SQL injection, fetchone, commit)
- Python 3.13 documentation: dataclasses
- Python 3.13 documentation: unittest (setUp, assertRaises, test discovery needs __init__.py since 3.11)
- Wikipedia: Don't repeat yourself (Hunt and Thomas, The Pragmatic Programmer, 1999)
This lecture is a first edition. The Kurdish text and terminology are waiting for review by Kurdish IT teachers.ئەم وانەیە چاپی یەکەمە. دەقە کوردییەکە و زاراوەکانی چاوەڕێی پێداچوونەوەی مامۆستایانی کوردی بواری IT ن.