נט2יו - איכות ברשת משנת 2004
  עקרון אחריות יחידה
16/2/2017 1:32

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

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


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

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


בגיל 14, פתחתי את הדפדפן ורשמתי בגוגל:


how to build websites


מפה לשם התגלגלתי ל – PHP ומשם הדרך לתכנות מונחה עצמים הייתה קצרה (יחסית).

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

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


במסגרת המשרד אותו אני מנהל, החלטנו לאחרונה לבצע רענון טכנולוגי, ובמהלך הלמידה נתקלתי בראשי התיבות S.O.L.I.D:


SOLID? סולידי? מה?


S.O.L.I.D הינם ראשי תיבות של חמשת העקרונות הראשונים מתוך 11 עקרונות  לעיצוב תוכנה מונחית עצמים, שהם חלק מגישת 'פיתוח תוכנה זריז'.

(הערת אגב: רוברט מרטין קרא לקבוצת העקרונות בשם – "חמשת העקרונות הראשונים". מיכאל פיטרס טבע את המונח S.O.L.I.D)

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


חמשת העקרונות הם:



  • עקרון אחריות יחידה

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

  • עקרון ההחלפה של ליסקוב

  • עקרון הפרדה-בין-ממשקים

  • עקרון היפוך תלות


הפוסט הזה הוא אחד מתוך חמישה בסדרת SOLID POSTS: הסבר מעמיק על חמשת העקרונות שתיארתי.


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

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


נצא לדרך – Single Responsibility, או בעברית:


עקרון אחריות יחידה


צריכה להיות סיבה אחת ויחידה לשינוי מחלקה.


על-פי עקרון אחריות יחידה, לכל מחלקה יש תפקיד אחד בלבד.

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

abstract class Rectangle {
abstract public function draw();
abstract public function area();
}

המחלקה מכילה שתי פונקציות:


draw מציירת מלבן על המסך

area מחשבת את השטח של המלבן


מכאן ניתן לומר שלמחלקה שני סוגי אחריות:



  • רנדור גראפי (draw)

  • חישוב מתמטי (area)


עכשיו, נתאר מצב שבו שתי אפליקציות עושות שימוש במחלקה Rectangle:


Geometric Calculator


מבצעת חישובים גיאומטריים.

נעזרת במחלקה Rectangle כדי לבצע חישוב שטח מלבן (area).


Geometric Drawer


מדפיסה צורות גיאומטריות.

נעזרת במחלקה Rectangle לחישוב שטח המלבן (area) וציורו (draw)


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



  • בלא מעט שפות, המחלקה הגראפית שבאמצעותה הפונקציה draw תצייר את המלבן תיטען, גם אם לא ייעשה בה שימוש

  • האפליקציות שבירות: שינוי באפליקציה Geometric Drawer עשוי לגרום לשינוי במחלקה Rectangle, מה שעשוי להצריך בנייה מחדש של האפליקציה Geometric Calculator.

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


ריבוי תפקידים זה רע. אז מה כן?


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


GeometricRectangle


<?php

abstract class GeometricRectangle {
abstract public function draw();
}


Rectangle


<?php

abstract class Rectangle {
abstract public function area();
}

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



  • GeometricRectangle לא תטען את המחלקה הגראפית שמציירת את המלבן

  • שינוי בדרך שבה מלבן מרונדר באפליקציה Geometric Drawer לא ישפיעו על האפליקציה Geometric Calculator


אבל בינינו, למי מכם יצא לאחרונה לכתוב מחלקה שמטפלת במלבן?

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


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

<?php

CONST ACCEPTABLE_USER_FIELDS = array(
'username',
'email',
'password',
'gravatar'
);

class User {
private $id;
private $userFields;
private $db;

public function __construct($id, $info) {
$this->id = $id;
$this->initializeUser($info);
$this->db = new PDO('
mysql:dbname=masterdb;host=localhost',
'masteruser',
'mAst3rPassWord'
);
}

private function initializeUser($info) {
foreach($info as $key=>$val)
$this->updateField($key, $val);
}

public function updateField($key, $value) {
if(in_array($key, ACCEPTABLE_USER_FIELDS)) {
// update user field
$this->userFields[$key] = $value;

// update database
try {
$q = "UPDATE user SET {$key}='${value}' WHERE id={$this->id}";
$this->db->prepare($q)->execute();
return true;
}
catch(PDOException $e) {
return array(
'query' => $q,
'error' => $e->getMessage()
);
}

}
else;
// error handling
}

public function HasAccessTo($area) {
try {
$areaQuery = "SELECT id FROM area WHERE name={$area}";
$areaID = $this->db->prepare($areaQuery)->fetchColumn();

if(!empty($areaID)) {
try {
$accessQuery = "SELECT COUNT(user_id) FROM areaAccess WHERE user_id = {$this->id} AND area_id = {$areaID}";
return $this->db->prepare($accessQuery)->fetchColumn() === 1;
}
catch(PDOException $e) {
return exceptionHandler::pdo($e); // dummy class/method
}
}
}
catch(PDOException $e) {
return exceptionHandler::pdo($e); // dummy class/method
}

}

public function delete() {
try {
$q = "DELETE FROM user WHERE id={$this->id}";
$this->db->exec($q);
return true;
}
catch(PDOException $e) {
return array(
'query' => $q,
'error' => $e->getMessage()
);
}

}

public function printUserInfo() {
$output =
"User ID: {$this->id}<br />
Username: {$this->userFields['username']}<br />
Email: {$this->userFields['email']}<br />
";

echo $output;
}
}


בוחן פתע: כמה תפקידים יש למחלקה ומהם?


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


למחלקה יש חמישה תפקידים שונים


והנה הם לפניכם:



  1. עדכון פרטי המשתמש (initializeUser, updateField)

  2. עדכון מסד נתונים (updateField, delete)

  3. התמודדות עם שגיאות מסד נתונים (updateField, delete)

  4. בדיקת הרשאות (hasAccessTo)

  5. הדפסת פרטי המשתמש (printUserInfo)


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

ולמה זה רע? כי:



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

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


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

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


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

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


לדוגמה, נניח שקיבלנו מהלקוח דרישה למשיכת נתוני משתמש בפורמט JSON דרך כתובת URL.

יש לנו שתי אפשרויות:



  1. להוסיף הסתעפות לפונקציה printUserInfo

  2. ליצור פונקציה נוספת – printUserInfoJSON


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

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


מהלך היפרדות


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

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


User


המחלקה אחראית לעדכון פרטי המשתמש

<?php

CONST ACCEPTABLE_USER_FIELDS = array(
'username',
'email',
'password',
'gravatar'
);

class User {
private $id;
private $userFields;

public function __construct($id, $info) {
$this->id = $id;
$this->initializeUser($info);
}

private function initializeUser($info) {
foreach($info as $key=>$val)
$this->updateField($key, $val);
}

public function updateField($key, $value) {
if(in_array($key, ACCEPTABLE_USER_FIELDS))
// update user field
$this->userFields[$key] = $value;
else
// error handling
}
}


userDB


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

* המחלקה מיישמת את הממשק UserDBLogic ומרחיבה את המחלקה הדמיונית DB

<?php

interface UserDBLogic {
public function select(Integer $id);
public function update(Integer $id, Array $fields);
public function insert(Integer $id, Array $fields);
public function remove(Integer $id);
}

class UserDB implements UserDBLogic {

private $db;

public function __construct($db) {
$this->db = $db;
}

public function select(Integer $id) {
try {
$q = "SELECT * FROM user WHERE id = {$id}";
$statement = $this->db->prepare($q);

$statement->setFetchMode(PDO::FETCH_ASSOC);
return $statement->fetchAll();
}
catch(PDOException $e) {
return exceptionHandler::pdo($e); // dummy class/method
}

}

public function update(Integer $id, Array $fields) {
try {
$q = "UPDATE user SET";

foreach($fields as $field=>$value) {
$q .= $field . '=' . $value . ',';
}

$q = rtrim($q, ',') . " WHERE id = {$id}";

$this->db->prepare($sql);
$this->db->execute();

return true;
}
catch(PDOException $e) {
return ExceptionHandler::pdo($e); // dummy class/method
}
}

public function insert(Array $fields) {
try {
$fieldsStr = implode(', ', array_keys($fields));
$valuesStr = implode(', ', array_values($fields));

$q = "INSERT INTO user ({$fieldsStr}) VALUES({$valuesStr})";

$this->db->exec($q);
return true;
}
catch(PDOException $e) {
return ExceptionHandler::pdo($e);
}
}

public function remove(Integer $id) {
try {
$q = "DELETE FROM user WHERE id={$id}";
$this->db->exec($q);
return true;
}
catch(PDOException $e) {
return ExceptionHandler::pdo($e);
}
}
}


userAccess


המחלקה אחראית לבדוק האם למשתמש יש הרשאת גישה לאיזור מסויים.

(תודה לקורא אלכס רסקין שהראה מה הדרך הנכונה לכתוב את המחלקה)

<?php

class UserAccess {
private $greenAreas;

public function __construct($greenAreas) {
$this->greenAreas = $greenAreas
}

public function HasAccessTo($area) {
return in_array($area, $this->greenAreas);
}
}


הגדרת אחריות


רוברט מרטין הגדיר אחריות (בהקשר של עקרון אחריות יחידה) כ – "סיבה לשינוי".

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



  • בהירות

  • ניגודיות

  • הגדרות אנרגיה

  • הגדרות תצוגה



איפה זה נגמר?


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

וכאן, לדעתי – מגיע החלק החשוב ביותר בעקרון אחריות יחידה:


ניקח לדוגמה את הממשק הבא (נכתב על-ידי רוברט  מרטין), שמתאר מודם:

<?php

interface Modem {
public function dial(String $pno);
public function hangUp();
public function send(String $c);
public function receive();
}

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

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



  1. חיבור (dial, hangUp)

  2. תקשורת (send, recieve)


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

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


האם הסיבה לשינוי תיתכן במציאות?


אם התשובה היא לא חד משמעי – אין סיבה אמיתית ליישם את עקרון אחריות יחידה.


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

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


סיכום וטיפ בונוס


רוברט מרטין כתב בסיכום עקרון אחריות יחידה: (תרגום חופשי)


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

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

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


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

אבל אחרי שתתחילו ליישם אותו – לא תוכלו ללכת אחורה.


הבטחתי בונוס, שהוא בעצם טיפ:


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

הפרידו אחריות כפולה למס' פונקציות שישמרו את הקוד נקי, קצר, חזק וקל לתחזוקה.


נתראה בפוסט השני בסדרה: עקרון יישום פתוח סגור!


מקורות



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







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

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

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

הצהרת נגישות

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

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

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