SOLID disainipõhimõtted: Lihtsustatud selgitus
SOLID põhimõtted juhendavad arendajaid hästi struktureeritud tarkvara loomisel. Robert C. Martini poolt tutvustatud, on need hädavajalikud vastupidava objektorienteeritud disaini jaoks.
Sissejuhatus
Kui on juttu tugeva ja hooldatava tarkvara loomisest, siis SOLID disainipõhimõtted on arendajatele majakaks. Robert C. Martini poolt välja töötatud need põhimõtted pakuvad raamistikku objektorienteeritud disainile, rõhutades loetavust, skaleeritavust ja hooldatavust. Sukeldugem nende mängureegleid muutvate kontseptsioonide lihtsustatud lahtiseletamisse.
S: Üksikvastutuse põhimõte (Single Responsibility Principle, SRP)
Kujutage ette Šveitsi armee nuga: see on üks tööriist, mis suudab täita palju ülesandeid nagu lõikamine, pudelite avamine ja isegi kruvide keeramine. Kuigi see on mitmekülgne, on noa igal osal ainult üks peamine funktsioon. Tera on mõeldud ainult lõikamiseks, korgitser ainult veinipudelite avamiseks jne. See on sarnane Üksikvastutuse põhimõttega tarkvaraarenduses.
SRP viitab sellele, et just nagu iga tööriist Šveitsi armee noal on pühendatud kindlale ülesandele, peaks igal klassil teie koodis olema ainult üks põhjus muutumiseks, tähendades, et sellel peaks olema ainult üks töö või vastutus. Näiteks, kui teil on klass, mis käitleb kasutaja informatsiooni rakenduses, peaks see tegelema ainult kasutaja informatsiooniga, mitte muude asjadega nagu e-kirjade saatmine või andmete logimine.
Miks see kasulik on? Mõelge, mis juhtuks, kui Šveitsi armee noa tööriist katki läheks. Kui iga tööriist on eraldi, peate parandama ainult seda ühte osa. Samamoodi koodis, kui klassil on ainult üks vastutus, on seda palju lihtsam hooldada ja uuendada, mõjutamata rakenduse teisi osi. See viib puhtama, korralikuma koodini, mis on lihtsamini mõistetav ja vähem vigadele kalduv.
Kokkuvõttes julgustab Üksikvastutuse põhimõte kirjutama klasse, mis on keskendunud ja pühendunud ühe asja hästi tegemisele, palju nagu iga tööriist Šveitsi armee noal.
Halva näide: Klass, millel on mitu kohustust
interface Device {
display(data: string): void;
}
class WindowsConsole implements Device {
display(text: string): void {
console.log(text);
}
}
class Book {
private bookName: string;
private authorName: string;
private content: string;
constructor(bookName: string, authorName: string, content: string) {
this.bookName = bookName;
this.authorName = authorName;
this.content = content;
}
getBookName(): string {
return this.bookName;
}
setBookName(bookName: string): void {
this.bookName = bookName;
}
getAuthorName(): string {
return this.authorName;
}
setAuthorName(authorName: string): void {
this.authorName = authorName;
}
getContent(): string {
return this.content;
}
setContent(content: string): void {
this.content = content;
}
// Bad example: A class with multiple responsibilities
show(device: Device): void {
const text = `Name: ${this.bookName}, author: ${this.authorName}, content: ${this.content}`;
device.display(text);
}
}
// Usage
const book = new Book("Book Name", "Author Name", "text text text");
const device = new WindowsConsole();
book.show(device);
Hea näide: Kohustuste jagamine eri klassidesse
interface Device {
display(data: string): void;
}
class WindowsConsole implements Device {
display(text: string): void {
console.log(text);
}
}
class BookDisplay {
show(book: Book, device: Device): void {
const text = `Name: ${book.getBookName()}, author: ${book.getAuthorName()}, content: ${book.getContent()}`;
device.display(text);
}
}
class Book {
private bookName: string;
private authorName: string;
private content: string;
constructor(bookName: string, authorName: string, content: string) {
this.bookName = bookName;
this.authorName = authorName;
this.content = content;
}
getBookName(): string {
return this.bookName;
}
setBookName(bookName: string): void {
this.bookName = bookName;
}
getAuthorName(): string {
return this.authorName;
}
setAuthorName(authorName: string): void {
this.authorName = authorName;
}
getContent(): string {
return this.content;
}
setContent(content: string): void {
this.content = content;
}
}
// Usage
const book = new Book("Book Name", "Author Name", "text text text");
const device: Device = new WindowsConsole();
const bookDisplay = new BookDisplay();
bookDisplay.show(book, device);
O: Avatud/Suletud põhimõte (Open/Closed Principle)
Mõelge LEGO komplektile. Saate lisada uusi tükke, et luua midagi uut, kuid te ei muuda olemasolevaid tükke. Sarnaselt soovitab Avatud/Suletud põhimõte, et klassid peaksid olema avatud laiendamiseks, kuid suletud muutmiseks. See lähenemine võimaldab teil tutvustada uusi funktsionaalsusi ilma olemasolevat koodi häirimata.
Halva näide: Klass, mis on avatud muutmisele
enum ReportingType {
CSV = "CSV",
XML = "XML",
EXCEL = "EXCEL"
}
class ReportingService {
private typeReport: ReportingType;
constructor(typeReport: ReportingType) {
this.typeReport = typeReport;
}
getTypeReport(): ReportingType {
return this.typeReport;
}
setTypeReport(typeReport: ReportingType): void {
this.typeReport = typeReport;
}
generateReportBasedOnType(reportingType: ReportingType): void {
if (reportingType === ReportingType.CSV) {
this.generateCSVReport();
} else if (reportingType === ReportingType.XML) {
this.generateXMLReport();
}
}
private generateCSVReport(): void {
console.log("Generate CSV Report");
}
private generateXMLReport(): void {
console.log("Generate XML Report");
}
}
// Usage
const rs = new ReportingService(ReportingType.CSV);
// Generate CSV report
rs.generateReportBasedOnType(ReportingType.CSV);
// Generate XML report
rs.generateReportBasedOnType(ReportingType.XML);
Hea näide: Klass, mis on avatud laiendamiseks ja suletud muutmiseks.
interface Reporting {
generateReport(): void;
}
class XMLReporting implements Reporting {
generateReport(): void {
console.log("Generate XML Report");
}
}
class CSVReporting implements Reporting {
generateReport(): void {
console.log("Generate CSV Report");
}
}
class ReportingService {
generateReportBasedOnStrategy(reportingStrategy: Reporting): void {
reportingStrategy.generateReport();
console.log();
}
}
// Usage
const rs = new ReportingService();
// Generate CSV report
const csvReportingStrategy: Reporting = new CSVReporting();
rs.generateReportBasedOnStrategy(csvReportingStrategy);
// Generate XML report
const xmlReportingStrategy: Reporting = new XMLReporting();
rs.generateReportBasedOnStrategy(xmlReportingStrategy);
L: Liskovi asenduspõhimõte (Liskov Substitution Principle, LSP)
Mõelge Liskovi asenduspõhimõttest (Liskov Substitution Principle, LSP) kui usaldusväärsest kohvimasinast teie lemmikkohvikus. Olenemata masina mudelist (kas see on uusim või vanem), ootate, et see valmistaks teie tavalist kohvi sama hästi. Programmeerimises tagab LSP midagi sarnast klassidele ja nende alamklassidele.
Tehnilisemalt öeldes ütleb LSP, et ülemklassi objekte peaks olema võimalik asendada nende alamklasside objektidega ilma, et see muudaks programmi toimimist. See tähendab, et kui teil on klass (nagu kohvimasin) ja alamklass (teatud mudel masinast), peaksite saama kasutada alamklassi ülemklassi asemel ilma probleemideta.
Näiteks kui teil on klass nimega Bird, millel on meetod fly(), ja alamklass nimega Sparrow, mis laiendab Bird‘i, peaksite saama kasutada Sparrow‘d seal, kus oodatakse Bird‘i, ilma probleemideta. Kui Sparrow ei suuda korrektselt sooritada fly() funktsiooni, rikub see LSP-d. See on nagu omamine kindlast mudelist kohvimasinast, mis ei valmista kohvi ootuspäraselt.
LSP järgimine aitab luua paindlikku ja hooldatavat koodi, kus alamklasse saab hõlpsasti vahetada, põhjustamata vigu või ootamatut käitumist rakenduses.
Halva näide: Välised protsessid kas katkevad, käituvad valesti või peavad teadma liiga palju teavet.
abstract class Boiler {
private currentTemperature: number;
private desirableTemperature: number;
constructor(currentTemperature: number, desirableTemperature: number) {
this.currentTemperature = currentTemperature;
this.desirableTemperature = desirableTemperature;
}
public setDesirableTemperature(temp: number): void {
this.desirableTemperature = temp;
}
public getDesirableTemperature(): number {
return this.desirableTemperature;
}
public getCurrentTemperature(): number {
return this.currentTemperature;
}
public setCurrentTemperature(currentTemperature: number): void {
this.currentTemperature = currentTemperature;
}
public abstract heat(): void;
}
class BoilerA extends Boiler {
constructor(temperature: number, desirableTemperature: number) {
super(temperature, desirableTemperature);
}
public heat(): void {
this.setCurrentTemperature(this.getCurrentTemperature() + 1);
}
}
class BoilerB extends Boiler {
constructor(temperature: number, desirableTemperature: number) {
super(temperature, desirableTemperature);
}
public heat(): void {
this.setCurrentTemperature(this.getCurrentTemperature() + 2);
}
}
class BoilerC extends Boiler {
private desirableTemperature: number = 0;
constructor(temperature: number, desirableTemperature: number) {
super(temperature, desirableTemperature);
}
public setDesirableTemperature(temp: number): void {
this.desirableTemperature = temp;
}
public getDesirableTemperature(): number {
return this.desirableTemperature;
}
public heat(): void {
this.setCurrentTemperature(this.getCurrentTemperature() + 3);
}
}
interface TemperatureController {
setDevice(boiler: Boiler): void;
getWaterTemperature(): number;
heatWater(): void;
}
class AutomaticTemperatureController implements TemperatureController {
private boiler: Boiler;
public setDevice(boiler: Boiler): void {
this.boiler = boiler;
}
public getWaterTemperature(): number {
return this.boiler.getCurrentTemperature();
}
public heatWater(): void {
this.boiler.heat();
}
}
class ManualTemperatureController implements TemperatureController {
private boiler: Boiler;
public setDevice(boiler: Boiler): void {
this.boiler = boiler;
}
public getWaterTemperature(): number {
return this.boiler.getCurrentTemperature();
}
public heatWater(): void {
this.boiler.heat();
}
}
// Usage
const controller1: TemperatureController = new ManualTemperatureController();
const boilerA: BoilerA = new BoilerA(0, 37);
controller1.setDevice(boilerA);
controller1.heatWater();
console.log("Desirable Temperature: " + boilerA.getDesirableTemperature());
console.log("Temperature: " + controller1.getWaterTemperature());
const controller2: TemperatureController = new ManualTemperatureController();
const boilerB: BoilerB = new BoilerB(5, 40);
controller2.setDevice(boilerB);
controller2.heatWater();
console.log("Desirable Temperature: " + boilerB.getDesirableTemperature());
console.log("Temperature: " + controller2.getWaterTemperature());
/**
* Violating Liskov Substitution Principle (Desirable Temperature is 0-zero, boiler will not heat water).
*
* @description:
* If you don't follow the LSP, external processes will either break, behave improperly, or need to know too much information.
*/
const controller3: TemperatureController = new AutomaticTemperatureController();
const boilerC: BoilerC = new BoilerC(3, 39);
controller3.setDevice(boilerC);
console.log("Desirable Temperature: " + boilerC.getDesirableTemperature());
console.log("Temperature: " + controller3.getWaterTemperature());
Hea näide
abstract class Boiler {
private currentTemperature: number
private desirableTemperature: number
constructor(currentTemperature: number, desirableTemperature: number) {
this.currentTemperature = currentTemperature
this.desirableTemperature = desirableTemperature
}
public setDesirableTemperature(temp: number): void {
this.desirableTemperature = temp
}
public getDesirableTemperature(): number {
return this.desirableTemperature
}
public getCurrentTemperature(): number {
return this.currentTemperature
}
public setCurrentTemperature(currentTemperature: number): void {
this.currentTemperature = currentTemperature
}
public abstract heat(): void
}
class BoilerA extends Boiler {
constructor(temperature: number, desirableTemperature: number) {
super(temperature, desirableTemperature)
}
public heat(): void {
this.setCurrentTemperature(this.getCurrentTemperature() + 1)
}
}
class BoilerB extends Boiler {
constructor(temperature: number, desirableTemperature: number) {
super(temperature, desirableTemperature)
}
public heat(): void {
this.setCurrentTemperature(this.getCurrentTemperature() + 2)
}
}
class BoilerC extends Boiler {
constructor(temperature: number, desirableTemperature: number) {
super(temperature, desirableTemperature)
}
public heat(): void {
this.setCurrentTemperature(this.getCurrentTemperature() + 3)
}
}
interface TemperatureController {
setDevice(boiler: Boiler): void
getWaterTemperature(): number
heatWater(): void
}
class ManualTemperatureController implements TemperatureController {
private boiler: Boiler
public setDevice(boiler: Boiler): void {
this.boiler = boiler
}
public getWaterTemperature(): number {
return this.boiler.getCurrentTemperature()
}
public heatWater(): void {
this.boiler.heat()
}
}
class AutomaticTemperatureController implements TemperatureController {
private boiler: Boiler
public setDevice(boiler: Boiler): void {
this.boiler = boiler
}
public getWaterTemperature(): number {
return this.boiler.getCurrentTemperature()
}
public heatWater(): void {
this.boiler.heat()
}
}
// Usage
const controller1: TemperatureController = new ManualTemperatureController()
const boilerA: BoilerA = new BoilerA(0, 37)
controller1.setDevice(boilerA)
controller1.heatWater()
console.log('Desirable Temperature: ' + boilerA.getDesirableTemperature())
console.log('Temperature: ' + controller1.getWaterTemperature())
const controller2: TemperatureController = new ManualTemperatureController()
const boilerB: BoilerB = new BoilerB(5, 40)
controller2.setDevice(boilerB)
controller2.heatWater()
console.log('Desirable Temperature: ' + boilerB.getDesirableTemperature())
console.log('Temperature: ' + controller2.getWaterTemperature())
const controller3: TemperatureController = new AutomaticTemperatureController()
const boilerC: BoilerC = new BoilerC(3, 39)
controller3.setDevice(boilerC)
console.log('Desirable Temperature: ' + boilerC.getDesirableTemperature())
console.log('Temperature: ' + controller3.getWaterTemperature())
I: Liideste eraldamise põhimõte (Interface Segregation Principle, ISP)
Kujutage ette, et olete suure valikuga Rootsi lauas. Te ju ei soovi oma taldrikut kõigega täita, eks? Eelistaksite valida ainult seda, mis teile meeldib või vajate. Liideste eraldamise põhimõte (Interface Segregation Principle, ISP) programmeerimises on sarnane sellele kontseptsioonile.
Programmeerimises on ‘liides’ nagu menüü funktsioonidest, mida klass saab täita. Liideste eraldamise põhimõte ütleb, et selle asemel, et omada ühte suurt liidest paljude meetoditega, on parem luua väiksemaid, spetsiifilisemaid liideseid. Nii saab klass (nagu te Rootsi lauas) valida, milliseid konkreetseid funktsionaalsusi ta vajab, ilma et oleks koormatud nendega, mida ta ei kasuta.
Näiteks, kui teil on mitmefunktsiooniline printer, võib teil olla üks liides printimiseks, teine skaneerimiseks ja veel üks faksimiseks. Lihtne printeriklass rakendaks ainult printimisliidest, samas kui multifunktsionaalne võib rakendada kõiki kolme. Nii ei pea lihtne printer sisaldama koodi skaneerimiseks ja faksimiseks, mida ta kunagi ei kasuta.
ISP järgimine tagab, et teie klassid ei ole ülekoormatud tarbetute funktsioonidega, muutes teie koodi puhtamaks, arusaadavamaks ja lihtsamini hallatavaks.
Halva näide: Suur liides, mis sisaldab ebavajalikke meetodeid
interface Animal {
eat(): void;
sleep(): void;
fly(): void;
swim(): void;
}
class Bird implements Animal {
public eat(): void {
// Implementation...
}
public sleep(): void {
// Implementation...
}
public fly(): void {
// Implementation...
}
public swim(): void {
// Implementation...
}
}
Hea näide: Väiksemad kasutajaliidesed, mis on spetsiifilisemad iga kliendi vajaduste jaoks
interface IEatable {
eat(): void;
}
interface ISleepable {
sleep(): void;
}
interface IFlyable {
fly(): void;
}
class Bird implements IEatable, ISleepable, IFlyable {
public eat(): void {
// Implementation...
}
public sleep(): void {
// Implementation...
}
public fly(): void {
// Implementation...
}
}
D: Sõltuvuste inversiooni põhimõte (Dependency Inversion Principle, DIP)
Kujutage ette, et kasutate oma telefonis muusikarakendust. Saate ühendada erinevaid kõrvaklappe ja ikka kuulata oma muusikat, olgu need lihtsad kõrvasisesed või kvaliteetsed mürasummutavad kõrvaklapid. Sõltuvuste inversiooni põhimõte (Dependency Inversion Principle, DIP) programmeerimises töötab sarnaselt.
DIP kohaselt on teie peamine programm (nagu muusikarakendus) kavandatud olema ühilduv erinevate komponentidega (nagu erinevad kõrvaklapid). Seda saavutate mitte sidudes oma koodi otseselt konkreetse komponendiga, vaid kasutades liidest ( nagu kõrvaklappide pistik), mis võimaldab paindlikkust.
Nii saate hõlpsasti muuta või täiendada oma koodi osi (vahetada kõrvaklappe) ilma, et peaksite muutma oma peaprogrammi ( muusikarakendust). DIP on selliste kohandatavate ühenduste loomisest teie koodis, mis viib süsteemini, mida on lihtne muuta ja uuendada.
Halva näide: Kõrgetasemeline moodul sõltub madala taseme moodulist
enum MailerType {
SMTP,
SEND_GRID
}
class SendGridMailer {
send(): void {
console.log("Send: SendGridMailer");
}
}
class SmtpMailer {
send(): void {
console.log("Send: SmtpMailer");
}
}
class MailerService {
private smtpMailer: SmtpMailer;
private sendGridMailer: SendGridMailer;
constructor() {
this.smtpMailer = new SmtpMailer();
this.sendGridMailer = new SendGridMailer();
}
send(mailerType: MailerType): void {
if (mailerType === MailerType.SMTP) {
this.smtpMailer.send();
} else if (mailerType === MailerType.SEND_GRID) {
this.sendGridMailer.send();
}
}
}
const mailerService = new MailerService();
mailerService.send(MailerType.SMTP);
mailerService.send(MailerType.SEND_GRID);
Hea näide: Nii kõrgetasemeline moodul kui ka madala taseme moodul sõltuvalt abstraktsioonist
interface Mailer {
send(): void;
}
class SendGridMailer implements Mailer {
send(): void {
console.log("Send: SendGridMailer");
}
}
class SmtpMailer implements Mailer {
send(): void {
console.log("Send: SmtpMailer");
}
}
class MailerService {
private mailer: Mailer;
constructor(mailer: Mailer) {
this.mailer = mailer;
}
send(): void {
this.mailer.send();
}
}
const smtpMailer: Mailer = new SmtpMailer();
const mailerService: MailerService = new MailerService(smtpMailer);
mailerService.send();
Järeldus
SOLID põhimõtete omaksvõtmine võib muuta teie lähenemist kodeerimisele, viies tarkvara loomiseni, mis pole mitte ainult funktsionaalne, vaid ka puhas ja hallatav. Kuigi need kontseptsioonid võivad esmapilgul tunduda ülekaalukad, võib nende praktiline rakendamine oluliselt parandada teie koodi kvaliteeti ja hooldatavust. Jätkates oma teekonda tarkvaraarenduses, laske nendel põhimõtetel juhatada teid tõhusama ja osavama programmeerija poole.