GRASP (General Responsibility Assignment Software Patterns)
GRASP (General Responsibility Assignment Software Patterns) սկզբունքը ծրագրավորման մեջ։
GRASP-ը ներառում է 9 հիմնական օրինակ, որոնք օգնում են ճիշտ բաշխել պարտականությունները կոդում։ Ահա դրանցից յուրաքանչյուրը մանրամասն բացատրված՝ օրինակներով։
Սկզբունք. Պատասխանատվությունը պետք է տրվի այն դասին, որը ունի բավարար տեղեկատվություն այն իրականացնելու համար։
Վատ Օրինակ
class Order {
items: number[];
constructor(items: number[]) {
this.items = items;
}
}
class OrderProcessor {
calculateTotal(order: Order): number {
let total = 0;
for (const item of order.items) {
total += item;
}
return total;
}
}
Խնդիրը. OrderProcessor-ն անում է մի բան, որի համար ինքը բավարար տվյալներ չունի. ավելի տրամաբանական է, որ հենց Order-ը ունենա այդ լոգիկան։
Լավ Օրինակ
class Order {
items: number[];
constructor(items: number[]) {
this.items = items;
}
calculateTotal(): number {
return this.items.reduce((sum, item) => sum + item, 0);
}
}
Այժմ Order-ը ունի իր տվյալները և կարող է ինքնուրույն հաշվարկել գումարը։
Սկզբունք. Եթե մեկ օբյեկտ օգտագործում է մեկ ուրիշ օբյեկտ և կախվածություն ունի նրանից, ապա այն պետք է նաև ստեղծի այն։
Օրինակ
class Engine {}
class Car {
engine: Engine;
constructor() {
this.engine = new Engine(); // Car-ը ստեղծում է Engine, քանի որ օգտագործում է այն
}
}
Car-ը ստեղծում է Engine, քանի որ այն նրա բաղկացուցիչ մասն է։
Սկզբունք. Վերահսկիչը (Controller) պետք է լինի մի օբյեկտ, որը կառավարում է use case-ները և ծառայում է որպես կապառուկ միջնորդ։
Օրինակ
class OrderController {
placeOrder(items: number[]) {
const order = new Order(items);
console.log("Order placed:", order.calculateTotal());
}
}
const controller = new OrderController();
controller.placeOrder([10, 20, 30]); // Controller-ը կառավարում է լոգիկան
Այժմ OrderController-ը վերահսկում է Order-ի ստեղծումը։
Սկզբունք. Օբյեկտները պետք է ունենան նվազագույն կախվածություններ իրարից, որպեսզի համակարգը լինի ավելի ճկուն։
Վատ Օրինակ
class Order {
paymentProcessor: PaymentProcessor;
constructor() {
this.paymentProcessor = new PaymentProcessor(); // Ուղղակի կախվածություն
}
processPayment() {
this.paymentProcessor.process();
}
}
Խնդիրն այն է, որ Order-ը ուղղակիորեն կախված է PaymentProcessor-ից։ Եթե այն փոխվի, Order-ը նույնպես պետք է փոխվի։
Լավ Օրինակ
class Order {
processPayment(paymentProcessor: PaymentProcessor) {
paymentProcessor.process(); // Կախվածությունը տրվում է արտաքինից
}
}
Այժմ Order-ը ավելի անկախ է, և մենք կարող ենք փոխանցել ցանկացած payment processor։
Սկզբունք. Մեկ դասի մեթոդները պետք է միմյանց հետ առնչություն ունենան և չպարունակեն ավելորդ ֆունկցիոնալություն։
Վատ Օրինակ
class Order {
items: number[];
constructor(items: number[]) {
this.items = items;
}
calculateTotal() { /* ... */ }
printReceipt() { /* ... */ } // Պետք է լինի առանձին class-ի մեջ
}
Խնդիրը. Order-ը կատարում է ավելորդ գործողություն (printReceipt), որը պետք է լինի ուրիշ դասում։
Լավ Օրինակ
class ReceiptPrinter {
print(order: Order) { /* ... */ }
}
Այժմ Order-ը չի կատարում ավելորդ գործողություն։
Սկզբունք. Տարբեր տեսակի օբյեկտները պետք է կարողանան օգտագործվել նույն կերպ՝ առանց իմանալու նրանց կոնկրետ տիպը։
Օրինակ
interface PaymentMethod {
processPayment(amount: number): void;
}
class CreditCardPayment implements PaymentMethod {
processPayment(amount: number) {
console.log(`Paid ${amount} with Credit Card`);
}
}
class PayPalPayment implements PaymentMethod {
processPayment(amount: number) {
console.log(`Paid ${amount} with PayPal`);
}
}
class Order {
processPayment(method: PaymentMethod) {
method.processPayment(100);
}
}
Այժմ Order-ը կարող է ընդունել ցանկացած վճարման մեթոդ առանց փոփոխությունների։
Սկզբունք. Երբ լոգիկան որևէ կոնկրետ դերի չի համապատասխանում, այն պետք է տեղափոխվի առանձին class-ի՝ առանց խախտելու օբյեկտ-կենտրոնացված դիզայնը։
Վատ օրինակ (Տվյալներ պահող օբյեկտը նաև գրում է ֆայլի մեջ)
class Report {
generate() {
return "This is a report";
}
saveToFile() {
console.log("Saving report to file..."); // Լոգիկան պետք է լինի այլ տեղ
}
Խնդիրը. Report-ը պետք է զբաղվի միայն report-ի ստեղծմամբ, ոչ թե ֆայլեր գրելով։
Լավ օրինակ
class Report {
generate() {
return "This is a report";
}
}
class FileSaver {
save(content: string) {
console.log("Saving to file:", content);
}
}
const report = new Report();
const fileSaver = new FileSaver();
fileSaver.save(report.generate()); // Այժմ լոգիկան բաժանված է
Այժմ Report-ը չունի ավելորդ ֆունկցիաներ, իսկ ֆայլի պահպանումը կատարվում է FileSaver-ի կողմից։
Սկզբունք. Երբ երկու մասի միջև պետք է ապահովվի թույլ կապ (low coupling), օգտագործվում է միջնորդ (mediator) կամ այլ մոդել, որը կառավարում է նրանց միջև փոխազդեցությունը։
Վատ օրինակ (Ուղղակի կապ)
class UserService {
getUser() {
return { id: 1, name: "John Doe" };
}
}
class Profile {
userService: UserService;
constructor() {
this.userService = new UserService(); // Ուղղակի կախվածություն
}
showProfile() {
const user = this.userService.getUser();
console.log("User:", user.name);
}
}
Խնդիրը. Profile-ը ուղղակիորեն կախված է UserService-ից, ինչը դժվարացնում է փոփոխությունները և թեստավորումը։
Լավ օրինակ
interface IUserService {
getUser(): { id: number; name: string };
}
class UserService implements IUserService {
getUser() {
return { id: 1, name: "John Doe" };
}
}
class Profile {
userService: IUserService;
constructor(userService: IUserService) {
this.userService = userService; // Միջնորդություն, հիմա ավելի հեշտ է փոխել `UserService`-ը
}
showProfile() {
const user = this.userService.getUser();
console.log("User:", user.name);
}
}
const userService = new UserService();
const profile = new Profile(userService);
profile.showProfile();
Այժմ Profile-ը ուղղակիորեն կախված չէ կոնկրետ UserService-ից, այլ միայն IUserService ինտերֆեյսից։ Սա թույլ է տալիս հեշտությամբ փոխարինել UserService-ը, օրինակ՝ mock-ով թեստավորման ժամանակ։
Սկզբունք. Փոփոխվող տարրերը պետք է պաշտպանվեն ինտերֆեյսների, աբստրակցիաների, կամ դիզայնի օրինաչափությունների միջոցով։
Վատ օրինակ (Փոփոխվող dependency)
class MySQLDatabase {
query(sql: string) {
console.log(`Executing query on MySQL: ${sql}`);
}
}
class UserRepository {
db: MySQLDatabase;
constructor() {
this.db = new MySQLDatabase(); // Ուղղակի կախվածություն
}
getUser(id: number) {
this.db.query(`SELECT * FROM users WHERE id = ${id}`);
}
}
Խնդիրը. Եթե վաղը որոշենք օգտագործել PostgreSQL, կոդը պետք է ամբողջությամբ վերափոխվի։
Լավ օրինակ
interface Database {
query(sql: string): void;
}
class MySQLDatabase implements Database {
query(sql: string) {
console.log(`Executing query on MySQL: ${sql}`);
}
}
class PostgreSQLDatabase implements Database {
query(sql: string) {
console.log(`Executing query on PostgreSQL: ${sql}`);
}
}
class UserRepository {
db: Database;
constructor(db: Database) {
this.db = db; // Փոփոխական տարբերակ. MySQL-ից PostgreSQL անցումը հեշտ է
}
getUser(id: number) {
this.db.query(`SELECT * FROM users WHERE id = ${id}`);
}
}
const db = new MySQLDatabase();
const userRepository = new UserRepository(db);
userRepository.getUser(1);
Այժմ UserRepository-ն պաշտպանված է MySQL-ից PostgreSQL անցման դեպքում, քանի որ այն ուղղակիորեն կախված չէ կոնկրետ տվյալների բազայից։
Սկզբունք | Նպատակ |
Information Expert | Տվյալներ ունեցող օբյեկտը պետք է իրականացնի դրանց հետ աշխատող ֆունկցիոնալությունը։ |
Creator | Եթե մի օբյեկտ օգտագործում է մյուսին, այն պետք է ստեղծի այն։ |
Controller | Վերահսկիչները պետք է կառավարեն use case-ները։ |
Low Coupling | Օբյեկտները պետք է ունենան նվազագույն կախվածություն իրարից։ |
High Cohesion | Օբյեկտները պետք է ունենան միայն իրենց վերաբերող պարտականություններ։ |
Polymorphism | Կոդը պետք է օգտագործի ինտերֆեյսներ կամ աբստրակցիաներ՝ ընդլայնելիություն ապահովելու համար։ |
Pure Fabrication | Հատուկ լոգիկա ունեցող կոդը պետք է առանձնացվի առանձին class-ի։ |
Indirection | Կախվածությունները պետք է նվազեցվեն միջնորդության (mediator) միջոցով։ |
Protected Variations | Փոփոխվող տարրերը պետք է պաշտպանվեն ինտերֆեյսների կամ աբստրակցիաների միջոցով։ |