נט2יו - איכות ברשת משנת 2004
  עקרון יישום פתוח-סגור
26/2/2017 2:20

כל המערכות משתנות במהלך מחזור החיים שלהן.

יש לזכור עובדה זו כאשר מפתחים מערכות שמצופה מהן לשרוד מעבר לגרסה הראשונה.

ד"ר איבר יאקובסון, מדען מחשב ומהנדס תוכנה


ברוכים הבאים והשבים לפוסט מספר 2 מתוך 5 בסדרת S.O.L.I.D / posts.

לאלה מכם שהצטרפו אלינו עכשיו – עצרו רגע! על עקרון אחריות יחידה קראתם?


כן-לא-שחור-לבן, אנחנו יוצאים לדרך:


עקרון יישום פתוח-סגור


באנגלית: Open-Closed Principle


בתחילת הפוסט, ציטטתי את איבר יאקובסון.

הציטוט הזה מתייחס לדבר שהוא דיי מובן מאליו, אם חושבים על זה לעומק:


מחזור החיים של מערכת, לרוב, אורך יותר מגרסה אחת.

אם להוציא את החריגים, מערכת נוצרת כדי לשרת לקוח במשך תקופה ארוכה, לעיתים אף עשרות שנים! (מישהו אמר צה"ל?)

אם נקודת המוצא הזו נכונה גם לך, אז אחת ממשימות-העל שלך היא לפתח אותה ככה שניתן יהיה לתחזק, לשנות ולהרחיב אותה גם בעוד 4, 7, 10 שנים.


ובשביל להשיג את המטרה הזו, עקרון יישום פתוח-סגור הוא קריטי.

הצעד הראשון להבנת העקרון הוא, באופן מפתיע, לתת לו הגדרה:


מרכיבי תוכנה (מחלקות, מודולים, פונקציות וכו') צריכים להיות פתוחים להרחבה, אבל סגורים לעריכה


ההיגיון שמאחורי ההיגיון מאד פשוט: הוספת התנהגות חדשה תיעשה על-ידי הוספת קוד חדש ולא על ידי שינוי הקוד הקיים.

על כל רכיב לענות על שתי הדרישות הבאות:



  1. פתוח להרחבה

    ניתן להוסיף לרכיב פונקציונליות ואף להתאים את ההתנהגות שלו כך שיתאים לדרישות התוכנה

  2. סגור לעריכה

    קוד-הבסיס של הרכיב בלתי ניתן לשינוי ולאף אחד אין הרשאה לבצע בו שינויים.


כדי להבין מה הכוונה –

נסתכל על שתי המחלקות הבאות, שמתארות עובדים בפרוייקט:

<?php

class Programmer {
public function code() {
return 'coding..';
}
}

class Designer {
public function design() {
return 'designing..';
}
}

העקרון דיי פשוט: מחלקה שמתארת מפתח ומחלקה שמתארת מעצב.

לכל עובד יש פונקציית עבודה:



  • Programmer מתכנת באמצעות הפונקציה code

  • Designer מעצב באמצעות הפונקציה design


המחלקה ProjectManagement אחראית לתפעול העובדים בפרוייקט:

class ProjectManagement {
public function proccess($employee) {
if($employee instanceof Programmer)
$employee->code();
elseif($employee instanceof Designer)
$employee->design();
}
}

בבירור, מתרחשת הפרה של עקרון יישום פתוח-סגור. טענתי ואוכיח:


לפרוייקט שלנו הצטרף משווק שאחראי בין היתר על פרסום הפרויקט:

class Marketer {
public function advertise() {
return 'advertising..';
}
}

כדי לשלב אותו בתהליך העבודה, עלינו להוסיף אותו לפונקציה proccess במחלקה ProjectManagement:

class ProjectManagement {
public function proccess($employee) {
if($employee instanceof Programmer)
$employee->code();
elseif($employee instanceof Designer)
$employee->design();
elseif($employee instanceof Marketer)
$employee->advertise();
}
}

הפרה! זוכרים מה אמרנו על עקרון יישום פתוח-סגור? קבלו תזכורת:


הוספת התנהגות חדשה תיעשה על-ידי הוספת קוד חדש ולא על ידי שינוי הקוד הקיים


הדוגמה גם ממחישה למה העקרון חשוב: התנהגות של מחלקה משתנה כל הזמן. אם בהמשך הפרוייקט יצטרפו גם עובדים אחרים (בודקי תוכנה, מהנדסים, מנהל כספים וכו'), נצטרך להמשיך ולהתאים את הפונקציה עד אין-סוף.

אז מה כן עושים?


פשטות היא התשובה


הדרך הנכונה היא ליצוק את המחלקות שמתארות עובדים לתוך תבנית, או במילים פחות פואטיות – לכתוב ממשק:

interface Employee {
function work();
}

כל מחלקה של עובד תיישם את הממשק; כפועל יוצא – על מחלקה ליישם את הפונקציה Work.

ככה זה יראה:

class Programmer implements Employee {
public function work() {
return 'coding..';
}
}

class Designer implements Employee {
public function work() {
return 'designing..';
}
}

class Marketer implements Employee {
public function work() {
return 'advertising..';
}
}

ועכשיו לקסם: קבלו את המחלקה ProjectManagement, אחרי מייק-אובר:

class ProjectManagement {
public function proccess(Employee $employee) {
$employee->work();
}
}


מה קרה פה?


הפונקציה proccess מקבלת פרמטר מסוג Employee שאנחנו יודעים שבוודאות מיישם פונקציה בשם work.


ולצד צמצום הקוד, שהוא חשוב בפני עצמו, עקרון יישום פתוח-סגור מאפשר לנו להוסיף קוד ללא צורך בעריכת הקוד הקיים.

רוצים הוכחה? אין בעיה: לפרוייקט שלנו הצטרף מנהל כספים ועלינו להוסיף אותו לתהליך העבודה, אבל בלי להפר את עקרון יישום פתוח-סגור!


ככה זה נראה:

class CFO implements Employee {
public function work() {
return 'embezzling..';
}
}

וזה הכל! הקוד הקיים לא השתנה, אלא הוספנו עליו קוד חדש שמביא איתו התנהגות חדשה..


אבל רגע, הבטחתי לכם בפוסט הקודם שאני לא אשאיר אתכם עם דוגמאות כלליות.

מכיוון שהובא לידיעתי שלא רק תוכניתני PHP קוראים את סדרת הפוסטים.. ומכיוון שאני רוצה שכולם יאהבו אותי – אני שמח להגיש לכם את הדוגמה ב – TypeScript!

ועל הדרך אאזכר ואקדם (בלי בושה) את מדריך ה – TypeScript שכתבתי בשני חלקים, לאלו מכם שעדיין לא למדו את השפה.


דוגמה ראשונה ליישום פתוח סגור ✅

יח"צ לעקרון הראשון ✅


נראה לי שאפשר לסיים את הפוסט כאן.. שכחתי משהו?


נכון! הבטחתי לכם עוד דוגמה; אז קבלו במחיאות כפיים (לא-סוערות-מידי-כי-המחלקה-הזאת-מפרה-את-עקרון-יישום-פתוח-סגור) את MasterValidator:


MasterValidator – אימות שדות טופס


פעולת האימות של הרכיב MasterValidator עובדת כך:



  1. הרכיב מקבל את קלט המשתמש

  2. הרכיב יוצר מופע חדש של סוג האימות שהוגדר

  3. הרכיב מפעיל את פעולת האימות


בואו נתחיל לכתוב את האפליקציה;

מתחילים עם יצירת עצם מסוג קלט:

interface InputInterface {
readonly validator: any;
getType(): string;
getValue(): string;
}

class SingleValueInput implements InputInterface {
public readonly validator: any;
private readonly type: string;
private value: string;

public constructor(validator: string, type: string, value: string) {
validator = this.buildValidatorName(validator);
console.log(window[validator].prototype);
this.validator = Object.create(window[validator].prototype);
this.type = type;
this.value = value;
}

private buildValidatorName(validator: string) {
return validator.charAt(0).toUpperCase() + validator.slice(1) + "Validator";
}

public getType(): string {
return this.type;
}

public getValue(): string {
return this.value;
}
}

בואו נבין מה מתרחש במחלקה Input:

המחלקה Input מיישמת את הממשק InputInterface; היא מכילה שלוש תכונות:



  • validator: any מקבלת את מחלקת האימות המתאימה

  • type: string מקבלת את סוג הקלט (text, radio וכו')

  • value: string מקבלת את הערך של הקלט


דיי ברור מה תפקידן של הפונקציות getType ו – getValue.

אבל מה קורה ב – constructor? הפונקציה מקבלת שלושה פרמטרים:



  • validator: string מקבלת את שם המאמת

  • type: string מקבלת את סוג הקלט

  • value: string מקבלת את ערך הקלט


בתוך הפונקציה, נעשית קריאה לפונקציה הפרטית buildValidatorName שלוקחת את שם המאמת, הופכת את האות הראשונה לאות גדולה ומצרפת אליו את המחרוזת Validator.

בחזרה ל – constructor: התכונה validator מקבלת באופן דינמי מופע חדש מסוג המאמת המתאים, באמצעות הפונקציה Object.create שמקבלת כפרמטר את אב-הטיפוס של המחלקה המתאימה (לחצו לקריאה נוספת).

שתי השורות הבאות מיישמות ערך לתכונות type ו – value בהתאמה -שום דבר מיוחד.


בנוסף עבור כל תכונה הוגדרה פונקציה שמושכת את המידע הרלוונטי – כפי שנדרש בעקבות יישום הממשק InputInterface.


אוקיי! יש לנו מחלקה שמתארת קלט וזה הצעד הראשון.

בואו נתקדם הלאה ונגדיר את המחלקות שמבצעות את האימות.


לצורך הדוגמה, כתבתי שתי מחלקות שכאלה שמבצעות אימות לשם ואימות למספר טלפון:

class NameValidator {
private pattern: RegExp;

public constructor() {
this.pattern = new RegExp("/^[a-zA-Zא-ת ' "\s -]+$/");
}

public test(input: string): boolean {
return this.pattern.test(input);
}
}

class PhoneValidator {

public test(input: string): boolean {
input = this.clean(input);
return input.length == 10;
}

private clean(phone: string): string {
return phone.replace(/[^0-9]/g, '');
}
}

היישום עצמו פחות רלוונטי ולכן אציין רק שכפי שניתן לראות, לכל מחלקה יש פונקציה בשם test שמבצעת בדיקה האם המחרוזת תקינה ומחזירה ערך בוליאני בהתאם.


החלק האחרון בפאזל הוא המחלקה MasterValidator שמתפקדת כבקר:

class MasterValidator {
public check(input: Input): boolean {
if(input.validator instanceof NameValidator)
return input.validator.test(input.getValue());
else if(input.validator instanceof PhoneValidator)
return input.validator.test(input.getValue());
else
return false;
}
}

וככה הגיח לו לרכיב העולם רכיב שמפר את עקרון יישום פתוח-סגור.

עכשיו, יכול להיות שלחלקכם עולה עברה המחשבה הבאה בראש, חלקכם אולי אף נזפו בי דרך המסך:


הלו, השמות של הפונקציות זהים, אפשר להעיף את התנאים, להישאר עם שורה אחת ולחסל את הפרת עקרון יישום פתוח-סגור.


ואתם צודקים! (לא בדיוק.. זו מלכודת) בקלות יכולתי לכתוב את הפונקציה check של MasterValidator ככה:

class MasterValidator {
public check(input: InputInterface): boolean {
return input.validator.test(input.getValue());
}
}

עכשיו, כל מה שעלי לעשות כדי להוסיף סוג אימות חדש, הוא לכתוב מחלקה, לקרוא לה בשם תקין (כלומר שתסתיים ב – Validator) ולרשום פונקציה בשם test שתבחן האם המחרוזת שהתקבלה תקינה ולהחזיר ערך בוליאני מתאים.


אבל מי אוכף את כל זה? אני? חוברת-הדרכה? תיעוד בקוד?

מטרת העל של עקרון יישום פתוח-סגור היא להעלות את רמת השרידות של תוכנה; כלומר לאפשר לה להתקיים לאורך זמן ולאור שינויים שמבוצעים בה תוך כדי שמירה על יכולת תחזוקה סבירה.

המטרה מושגת על-ידי כתיבה תבניתית שמאפשרת לנו לדעת מראש מהן אפשרויות הבסיס שכל מחלקה מציעה לנו.


אבל להחליט לכתוב באופן תבניתי – זה לא מספיק.

עקרון יישום פתוח-סגור דורש שנמנה מבוגר אחראי שידאג לאכוף את דרך הכתיבה שהגדרנו;

כל עוד לא קיים מנגנון שמבצע אכיפה אמיתית – עקרון יישום פתוח-סגור לא מתקיים.


בחזרה ל – MasterValidator, למרות שאנחנו יכולים להגיע למצב שבו אין צורך להוסיף שורת קוד אחת בשביל להוסיף סוג אימות (התנהגות חדשה),

אנחנו לא אוכפים מצב שכזה.


ממשק (interface) בעצם מגדיר עבור המיישם שלו דרישות סף שעליו למלא, אם המיישם לא יעמוד בדרישות הסף הללו – תתרחש שגיאה.

ולכן ממשק הוא-הוא אמצעי האכיפה של עקרון יישום פתוח-סגור. בואו ניישם את העקרון ברכיב MasterValidator:


קודם כל, נכתוב את הממשק שיגדיר את דרישות הסף לכל מאמת:

interface ValidatorInterface {
test(input: string): boolean;
}

השלב השני הוא ליישם בכל מחלקה את הממשק ValidatorInterface.

מכיוון שהקוד נכתב בצורה אחידה, אנחנו נדרשים להוסיף לחתימת כל מחלקת אימות את ההכרזה על היישום:

class NameValidator implements ValidatorInterface {
private pattern: RegExp;

public constructor() {
this.pattern = new RegExp("/^[a-zA-Zא-ת ' "\s -]+$/");
}

public test(input: string): boolean {
return this.pattern.test(input);
}
}

class PhoneValidator implements ValidatorInterface {
private pattern: RegExp;

public constructor() {
this.pattern = new RegExp("/^[\d - +]{8,15}$/");
}

לפני שאנחנו ממשיכים, בממשק InputInterface יש בעיה. ככה הוא נראה:

interface InputInterface {
readonly validator: any;
getType(): string;
getValue(): string;
}

הגדרתי בו תכונה ציבורית (public) בשם validator. בטח יצא לכם לשמוע שתכונת מחלקה לא צריכה להיות ציבורית.

יש אמת באמירה הזו ובסוף הפוסט – מחכה לכם הסבר למה.


זה לא נגמר כאן. התכונה יכולה לקבל כל סוג נתונים (any), למרות שהדרישה בפועל היא שהיא תקבל מחלקת אימות.

מקודם, לא יכולנו לדעת מה סוג הנתונים שהתכונה תכיל, אבל עכשיו כשמיסדנו את העניינים עם הממשק ValidatorInterface, נתקן את העוולה הכפולה:

interface InputInterface {
getValidator(): ValidatorInterface;
getType(): string;
getValue(): string;
}

הדרישה לתכונת מחלקה ציבורית נעלמה כלא הייתה והוחלפה בפונקציה שמחזירה מופע מסוג ValidatorInterface.

בעקבות השינוי, גם המחלקה SingleValueInput מתחדשת והופכת לפשוטה הרבה יותר:

class SingleValueInput implements InputInterface {
private readonly validator: ValidatorInterface;
private readonly type: string;
private value: string;

public constructor(validator: ValidatorInterface, type: string, value: string) {
this.validator = validator;
this.type = type;
this.value = value;
}

public getValidator(): ValidatorInterface {
return this.validator;
}

public getType(): string {
return this.type;
}

public getValue(): string {
return this.value;
}
}

שימו לב לפונקציית הבנאי שמקבלת פרמטר מסוג ValidatorInterface ומיישמת אותו לתכונה validator, במקום לקבל מחרוזת ולנחש את שם המאמת.


אחרונה חביבה היא הפונקצייה clean של MasterValidator:

class MasterValidator {
public clean(input: InputInterface) {
return input.getValidator().test(input.getValue());
}
}

כעת הפונקציה מקבלת מראש רק את סוג הנתונים InputInterface והוספת מאמת חדש מצריכה אחת ורק את כתיבתו.


בונוס – סגנון טוב יותר לקוד טוב יותר


לא סתם עקרון יישום פתוח-סגור הוא בין 'חמשת העקרונות הראשונים' לעיצוב תוכנה מונחה עצמים.

הוא מביא לתוכנה יעילה יותר, יציבה יותר וגמישה יותר.


אבל זה לא הכל! לא מעט מוסכמות נוצרו בהשראת העקרון.

בחרתי לכתוב על שתיים כאלה, שישפרו הקוד שלכם על-ידי כתיבה נכונה יותר:


משתני מחלקה יוגדרו כ – private בלבד


הבטחתי ואקיים:

אולי החוק-הלא-רשמי הכי מפורסם שנולד מתוך העקרון: משתני מחלקה לעולם יוגדרו כ – private.

אני בטוח שהרוב הגדול של הקוראים שמע על זה. אבל עצרתם פעם לשאול למה?


בואו ניזכר שוב מה ההיגיון שמאחורי עקרון יישום פתוח-סגור:


הוספת התנהגות חדשה תיעשה על-ידי הוספת קוד חדש ולא על ידי שינוי הקוד הקיים


כשמשתנה מחלקה מוגדר כ – public, הוא חשוף לשינוי גם על-ידי כל מי שרק יחפוץ בכך.

כל שינוי שכזה יצריך מאיתנו לשנות גם את כל הפונקציות שמסתמכות על משתנה המחלקה, כלומר: לשנות את הקוד הקיים.

כשמשתנה מחלקה מוגדר כ – private, לעומת זאת, רק מחלקת האם יכולה לבצע בו שינויים.


משתנים גלובליים החוצה!


הטענה כלפי משתנים גלובליים דומה לזו נגד משתנים שמוגדרים כ- public: לעולם לא נוכל להבטיח שרכיב שמסתמך על משתנה שיכול להשתנות מכל מקום,

ימשיך לתפקד באותו האופן שבו תכננו אותו. פוטנציאל ההרס עצום!


כמעט בחצי הדרך..


כתיבת הפוסט לוותה בקשיים – הרגשתי אחריות כבדה להעביר את מלוא ההיגיון שנמצא מאחורי עקרון יישום פתוח-סגור, אני מקווה שהצלחתי במשימה.

2 עקרונות מתוך 5 כבר מאחורינו, נתראה בפעם הבאה עם העקרון השלישי: עקרון ההחלפה של ליסקוב


 


מקורות



הפוסט עקרון יישום פתוח-סגור הופיע ראשון בMasterScripter







האחריות על התגובות למאמרים השונים חלה על שולחיהן. הנהלת האתר אינה אחראית על תוכנן.
שליחת תגובה
חוקי שליחת תגובות*
תגובות חברי האתר מאושרות אוטומטית
כותרת*
_CM_USER*
_CM_EMAIL*
_CM_URL*
הודעה*
קוד אבטחה*

 הערה: התכנים המוצגים בעמוד זה ...

התכנים המופיעים במדור זה מועברים אוטומטית מבלוגים ואתרי מידע ברשת, כל הזכויות על התכנים: כולל טקסטים, תמונות, סרטים וכל מדיה נוספת, הנם של יוצרי החומר המקורי בלבד. אתר נט2יו אינו טוען לבעלות או לזכויות יוצרים על התכנים, אלא רק מצביע עליהם בצורה נוחה ומרוכזת, וכן מקשר אל האתר המקורי, עם המאמרים המלאים. אם למרות האמור לעיל, נתקלת בחשש להפרת זכויות יוצרים, או שתוכן מסויים באתר אינו לרוחך, אנא דווח לנו באמצעות טופס יצירת הקשר ונורידו בהקדם האפשרי מהאתר.
×

הצהרת נגישות

אתר זה מונגש לאנשים עם מוגבלויות על פי Web Content Accessibility Guidelines 2 ברמה AA.
האתר נמצא תמידית בתהליכי הנגשה: אנו עושים כל שביכולתנו שהאתר יהיה נגיש לאנשים עם מוגבלות.
אם בכל זאת נתקלתם בבעיית נגישות אנא שלחו לנו הערתכם במייל (אל תשכחו בבקשה לציין את כתובת האתר).

אודות ההנגשה באתר:

  • אמצעי הניווט וההתמצאות באתר פשוטים ונוחים לשימוש.
  • תכני האתר כתובים בשפה פשוטה וברורה ומאורגנים היטב באמצעות כותרות ורשימות.
  • מבנה קבוע ואחיד לנושאים, תתי הנושאים והדפים באתר.
  • האתר מותאם לצפייה בסוגי הדפדפנים השונים (כמו כרום, פיירפוקס ואופרה)
  • האתר מותאם לסביבות עבודה ברזולוציות שונות.
  • לאובייקטים הגרפיים באתר יש חלופה טקסטואלית (alt).
  • האתר מאפשר שינוי גודל הגופן על ידי שימוש במקש CTRL וגלגלת העכבר וכן בלחיצה על הכפתור המתאים בערכת ההנגשה הנגללת בצד האתר ונפתחת בלחיצה על הסמלון של כסא הגלגלים.
  • הקישורים באתר ברורים ומכילים הסבר להיכן הם מקשרים.
    לחיצה על הכפתור המתאים בערכת ההנגשה שבצד האתר, מסמנת את כל קישורי האתר בקו תחתון.
  • אנימציות ותכנים מהבהבים: הכפתור המתאים לכך בערכת ההנגשה שבצד האתר , מאפשר להסתיר בלחיצה אחת את כל התכנים באתר הכוללים היבהובים או תכנים המכילים תנועה מהירה (אנימציות, טקסט נע).
  • למתקשי ראיה: מתקשי הראיה שבנינו יכולים להעזר בשני כפתורים הנמצאים בערכת ההנגשה בצדו הימני של האתר, האחד מסב את האתר כולו לגוונים של שחור ולבן, השני מעביר את האתר כולו למצב של ניגודיות גבוהה.