الفريق العربي للبرمجةأرشيف المنتديات · 2000 – 2023
نسخة أرشيفية للقراءة فقط — التسجيل والمشاركة مغلقان، والمحتوى محفوظ كما كان.

ال Refactoring في السي شارب

بدأه HassanAlattas في 7 فبراير 2009 · 14 رد · 2,202 مشاهدة · في Microsoft Visual C#.NET
مشاركة: واتساب X فيسبوك تيليجرام
#1 صاحب الموضوع

تقدم سي شارب مجموعة من الوظائف التي تساعدك في تعديل وتحسين الكود ، وتندرج هذه الوظائف تحت ما يسمى بالـ Refactoring. لكن ما هو الـ Refactoring وما هي هذه الوظائف ؟ هذا ما سوف نحاول ان نفهمه هنا.

ما هو الـ Refactoring؟

يركز الـ Refactoring على تعديل الكود بدون تعديل الوظائف التي يقوم بها، والتعديل هنا بغرض تحسينه بحيث تسهل قراءته وصيانته ويزيد اداءه.

فمن الأمثلة الشائعة ، عندما تحتاج في برنامجك إلى رقم ذو دلالة ما ان تقوم باستخدام رمز ثابت يدل عليه عوضاً عن استخدامه مباشرة، كاستبدال الرقم 3.14 بالثابت PI (ط او النسبة التقريبية).

تأمل الكود:

class SimpleMath
		{
			public static double CalcCircumference(double diameter)
			{ return 3.14 * diameter; }
		}

يمكن تعديله ليصبح:

class SimpleMath
		{
			public const double PI = 3.14;
			public static double CalcCircumference(double diameter)
			{ return PI * diameter; }
		}

وبالرغم من ان وظيفة البرنامج لم تتغير إلا ان هذا التعديل قام بتحسين الكود من حيث:

- تحسين قابلية القراءة ، فالرمز PI أكثر وضوحاً من القيمة 3.14 مما يسهل معه فهم الكود.

- إذا احتجنا ان نعدل في قيمة الثابت (لربما لزيادة الدقة كـ 3.14159 ) فما علينا إلا ان نقوم بتغيير قيمة الرمز الثابت عوضاً عن تغيير الرقم في كل مكان في البرنامج .

مثل هذا التعديل وغيره يندرج ضمن الـ Refactoring ، ويمكنك معرفة المزيد من خلال التالي:

- Fowler, Martin (1999). Refactoring. Improving the Design of Existing Code. Addison-Wesley

- www.refactoring.com

- en.wikipedia.org/wiki/Refactoring

وظائف الـ Refactoring في السي شارب:

ارتبط الـ Refactoring في بدايته بالكثير من الخطوات التي يجب القيام بها يدوياً ، فبمجرد ان يحدد المبرمج الكود اللازم تعديله كان عليه القيام بالكثير من عمليات النسخ، اللصق، البحث، الاستبدال ، التجميع (compile)، و الكثير الكثير من عمليات الاختبار. وبلا شك كان هذا يعني ضياع الوقت و الكثير من الأخطاء.

الجيد مع فيجوال استديو ( 2005 وما بعده) انها وفرت بعض وظائف الـ Refactoring التلقائية لتوفر عليك الوقت والجهد، هذه الوظائف:

- Extract Method: إنشاء طريقة جديدة من الكود المحدد

- Encapsulate Field : تحويل حقل إلى خاصية

- Extract Interface: إنشاء واجهة جديدة من الأعضاء المعرفة في فئة.

- Reorder Parameters : إعادة ترتيب الوسائط

- Remove Parameters :إزالة الوسائط

- Rename : إعادة تسمية فئة او حقل او خاصية ...الخ

- Promote Local Variable to Parameter : تحويل متغير محلي إلى وسيط

وقبل ان نقوم بشرح هذه الوظئف ، يجب ان تعلم انه يمكن الوصول إليها من عدة أماكن في الفيجوال استديو مثل : قائمة Refactoring او القائمة المختصرة او اختصارات لوحة المفاتيح .

تم تعديل هذه المشاركة بواسطة HassanAlattas في 8 فبراير 2009 في 15:42

[وسط]لا اله الا انت سبحانك انى كنت من الظالمين

[/وسط]

#2

Extract Method

كم مرة أحسست فيها ان الطريقة (Method) التي كتبتها تقوم بأكثر من اللازم او أنها وصلت إلى الحد الذي تقول فيه أنها كبيرة او بالغة في الكبر؟

تأمل معي في الكود التالي الطريقة Main التي تقوم بالحصول على اسم المستخدم وكلمة السر:

class Program
		{
			static void Main(string[] args)
			{
				Console.WriteLine("*** Please enter your credentials ***");

				// Get user name and password. 
				string userName;
				string passWord;
				Console.Write("Enter User Name: ");
				userName = Console.ReadLine();
				Console.Write("Enter Password: ");
				passWord = Console.ReadLine();

			}
		}

في الواقع لا يوجد أي خطأ (بمعنى الكلمة) في استخدام الطريقة Main بهذا الشكل ، لكن ما الذي سيحدث إذا احتاج جزء آخر في برنامجك إلى الحصول على اسم المستخدم وكلمة السر ؟ هل نكتب نفس الكود ؟ القيام بهذا يعني الإسهاب والازدواج في الكود مما يزيد من صعوبة فهمه وصيانته. لكن أليس الأفضل ان نقوم بإنشاء طريقة جديدة وظيفتها الحصول على اسم المستخدم وكلمة السر وان نعطيها اسماً مثل GetCredentials (او حتى GetUserNameAndPassword) ، وإذا احتاج أي جزء في البرنامج إلى اسم المستخدم وكلمة السر فيمكنه ببساطة استدعاء هذه الطريقة. كذلك ، فإن استبدال جمل الكود في Main باستدعاء إلى GetCredentials يجعل قراءتها واستيعابها أسهل بكثير.

وعوضاً عن إنشاء GetCredentials يدوياً ، فإن وظيفة Extract Method في الفيجوال استديو سوف تقوم تلقائياً بتوليدها و واستبدال الكود المحدد باستدعاء للطريقة الجديدة. وأول خطوة هي تحديد الكود الذي نريد تضمينه في الطريقة الجديدة ولنفترض أننا حددنا الكود كما هو واضح في الصورة

post-173477-1234022652_thumb.gif

بعدها انقر على التحديد بزر الماوس الأيمن واختر الأمر Extract Method من قائمة الأختصارات كما هو موضح.

عند هذه اللحظة يظهر مربع حوار Extract Method يسمح لك بإدخال اسم الطريقة الجديدة ومعاينتها، وفي حالتنا تكون static لأن الكود الذي استخدمناه لا يشير إلى أي متغير على مستوى المثيل (instance level variables) .

post-173477-1234022711_thumb.gif

بمجرد ان تضغط على OK ، يعدل الكود تلقائياً بهذا الشكل:

class Program
		{
			static void Main(string[] args)
			{
				Console.WriteLine("*** Please enter your name ***");
				GetCredentials();
			}
			private static void GetCredentials()
			{
				// Get user name and password. 
				string userName;
				string passWord;
				Console.Write("Enter User Name: ");
				userName = Console.ReadLine();
				Console.Write("Enter Password: ");
				passWord = Console.ReadLine();
			}
		}

تأمل كيف أصبحت Main من السهولة بحيث يمكن استيعابها بنظرة واحدة .وكيف أصبح كل من يقرأها في غناً عن الخوض في تفاصيل GetCredentials وأسلوب إنجازها.

وإذا قبلت Main فأظنك مثلي لن تقبل GetCredentials بهذا الشكل لأنها تخزن اسم المستخدم وكلمة السر في متغيراتها المحلية ولا تعيدها إلى Main كما هو مطلوب. لكن يجب ان تعلم ان السبب في إنشائها بهذا الشكل هو أننا حددنا المتغيرات userName و passWord في عملية Extract Method السابقة ، وان شكلها سيتغير إذا كان الكود المحدد:

				Console.Write("Enter User Name: ");
				userName = Console.ReadLine();
				Console.Write("Enter Password: ");
				passWord = Console.ReadLine();

وتأمل مربع الحوار Extract Method في هذه الحالة

post-173477-1234022825_thumb.gif

ليصبح الكود في النهاية بالشكل المطلوب تماماً :

class Program
		{
			static void Main(string[] args)
			{
				Console.WriteLine("*** Please enter your credentials ***");

				// Get user name and password. 
				string userName;
				string passWord;
				GetCredentials(out userName, out passWord);

			}

			private static void GetCredentials(out string userName, out string passWord)
			{
				Console.Write("Enter User Name: ");
				userName = Console.ReadLine();
				Console.Write("Enter Password: ");
				passWord = Console.ReadLine();
			}
		}

جدير بالذكر هنا انه إذا كان الكود المحدد يستلزم إرجاع قيمة واحدة فقط فإنها سوف تكون هي القيمة المعادة في الطريقة الجديدة، أما إذا تطلب الكود المحدد إرجاع أكثر من قيمة فإنها تكون على شكل وسائط خارجة (output parameters) في الطريقة الجديدة.

[وسط]لا اله الا انت سبحانك انى كنت من الظالمين

[/وسط]

#3

شرح حلو تسلم

FOTA KHader FAR

سوف اتجة الى عالم الويب

thumb_kujviv1t1.jpg

#4

شكرا جزيلا .. والله يعطيك اعافية ..

غير انني اتساءل .. ألا يُمكننا عوضا عن ذلك كله القيام بتعريف تلك المتغيرات في الجلوبال (خارج اي فنكشن) بحيث تكون بذلك معروفة لدى جميع الفنكشنز؟

دمتم بخير.

تم تعديل هذه المشاركة بواسطة خالد حاتم كخن في 8 فبراير 2009 في 01:02

#5
اقتباس
شكرا جزيلا .. والله يعطيك اعافية ..

غير انني اتساءل .. ألا يُمكننا عوضا عن ذلك كله القيام بتعريف تلك المتغيرات في الجلوبال (خارج اي فنكشن) بحيث تكون بذلك معروفة لدى جميع الفنكشنز؟

نعم اخونا خالد لكن الاخ حسن فقط بيعطى مثال ليوضح الصورة

جزاكم الله جميعا كل خير

ولدي إليك وصيتي عهد الأسودْ

العز غايتنا نعيش لكي نسود

و عريننا في الأرض معروف الحدود

فاحم العرين و صنه عن عبث القرود

أظفارنا للمجد قد خُلقت فدى

و نيوبنا سُنَّت بأجساد العدى

و زئيرنا في الأرض مرهوب الصدى

نعلي على جثث الأعادي السؤددا

#6

االسلام عليكــم ورحمـة الله وبركاتــه

اقتباس
غير انني اتساءل .. ألا يُمكننا عوضا عن ذلك كله القيام بتعريف تلك المتغيرات في الجلوبال (خارج اي فنكشن) بحيث تكون بذلك معروفة لدى جميع الفنكشنز؟

سؤال حلو اخي خالد ،

وأوكد على ما ذكره objectOrienteD على ان هذا مثال لتوضيح الصورة أساساً

ولنقرأ العبارة من جديد

اقتباس
لكن ما الذي سيحدث إذا احتاج جزء آخر في برنامجك إلى الحصول على اسم المستخدم وكلمة السر ؟ هل نكتب نفس الكود ؟....

فالقصد هنا، ما الذي سيحدث إذا احتاج جزء آخر إلى الحصول على اسم المستخدم وكلمة السر مرة اخرى من المستخدم نفسه .

[وسط]لا اله الا انت سبحانك انى كنت من الظالمين

[/وسط]

#7

Encapsulate Field

مما لا شك فيه ان استخدام الحقول (المتغيرات على مستوى الفئة) العامة من الأشياء السيئة.

فـ مبدأ التغليف في البرمجة كائنبة التوجيه ينص على استخدام حقول خاصة وتوفير خصائص عامة (get و set) لقراءة و/او كتابة قيمتها.

ولنفترض انه طلب منك تحسين هذا الكود

class Square
		{
			public int width;
			public int height;

		}
		class MainClass
		{
			public static void Main()
			{
				Square mySquare = new Square();
				mySquare.width = 110;
				mySquare.height = 150;
				Console.WriteLine("width = {0}", mySquare.width);
				Console.WriteLine("height = {0}", mySquare.height);
			}
		}

لاحظ ان الحقول معرفة بشكل عام (public)، وإذا اردنا نطبق مبدأ التغليف فإن علينا القيام بـ:

- إعادة تعريف الحقول بشكل خاص (private)

- إنشاء خاصية (get و set) لكل حقل

- استبدال المراجع إلى الحقل بمراجع إلى الخاصية التي تقابله في الكود.

مرة اخرى، يمكنك القيام بهذا يدوياً إذا اردت ، او ان تستخدم الوظيفة Encapsulate Field للقيام به تلقائياً. وهذا ما سوف نفعله الآن.

في البداية انقر بزر الماوس الأيمن على الحقل width واختر Encapsulate Field من قائمة الأختصارات.

يظهر مربع الحوار لتحديد اسم الخاصية ومجال تعديل المراجع (الخارجية فقط او الكل)

post-173477-1234112596_thumb.jpg

بعد الضغط على OK ، يصبح الكود :

class Square
		{
			private int width;

			public int Width
			{
				get { return width; }
				set { width = value; }
			}
			public int height;

		}
		class MainClass
		{
			public static void Main()
			{
				Square mySquare = new Square();
				mySquare.Width = 110;
				mySquare.height = 150;
				Console.WriteLine("width = {0}", mySquare.Width);
				Console.WriteLine("height = {0}", mySquare.height);
			}
		}

انظر كيف تغير الحقل في الفئة Square

			private int width;

			public int Width
			{
				get { return width; }
				set { width = value; }
			}

وكيف تغيرت المراجع في Main

…..
				mySquare.Width = 110;
…..
				Console.WriteLine("width = {0}", mySquare.Width);

[وسط]لا اله الا انت سبحانك انى كنت من الظالمين

[/وسط]

#8

تحية طيبة :

الاخوة الكرام .. بارك الله فيكم وشكرا جزيلا على الايضاح .. وانا ان شاء الله مستمر في متابعة هذا الموضوع عن الـ Refactoring.

الله يحفظكم جميعا.

سلااااام.

#9

واياكم اخي خالد,,,,,

[وسط]لا اله الا انت سبحانك انى كنت من الظالمين

[/وسط]

#10

Reorder Parameters

تقوم هذه الوظيفة بإعادة ترتيب الوسائط في طريقة معينة. لكن متى نحتاج إلى ذلك ؟ اقصد متى نحتاج إلى إعادة ترتيب الوسلئط؟

انظر معي في هذا المثال لنفهم الجواب:

لنفترض انه لديك فئة تحتوي على طريقة محملة بشكل زائد overloaded :

class ImageRenderer
{
  public void Render(Point topLeft, Point bottomRight, Graphics g)
  {…}
  public void Render(Graphics g, int x, int y)
  { …}
  public void Render(Graphics g, Rectangle boundingBox)
  { …}
}

نلاحظ في الطريقة Render الثانية والثالثة ان الوسيط الأول من نوع Graphics ، بينما ان هذا الوسيط هو الأخير في Render الأولى ، وبينما ان الكومبيلر لن يعترض على هذا ، إلا ان عدم التناسق في ترتيب وسائط هذه الطريقة المحملة بشكل زائد غالباً ما يؤدي إلى الإرباك عندما نقوم باستدعائها في جزء آخر من البرنامج. لذا يفضل ان نعيد ترتيب الوسائط في Render الأولى.

للقيام بهذا علينا استخدام الوظيفة Reorder Parameters كالتالي:

انقر بزر الماوس الأيمن على اسم الطريقة واختر Refactor ثم Reorder Parameters.

نحدد الترتيب الذي نريده في مربع الحوار بهذا الشكل:

post-173477-1234290380_thumb.jpg

النتيجة:

class ImageRenderer
{
  public void Render(Graphics g , Point topLeft, Point bottomRight)
  {…}
  public void Render(Graphics g, int x, int y)
  { …}
  public void Render(Graphics g, Rectangle boundingBox)
  { …}
}

لاحظ ايضاً ان هذه الوظيفة تقوم بتعديل الأستدعاءات إلى الطريقة ، هذا يعني انه إذا كان لدينا أستدعاء مثل:

imageRenderer.Render(topLeft, bottomRight, g);

فإنه تلقائياً يصبح:

imageRenderer.Render(g, topLeft, bottomRight);

Remove Parameters

استخدم هذه الوظيفة عندما تريد إزالة الوسائط التي لم تعد بحاجة إليها في طريقة معينة. ولنفترض الآن انك تريد إزالة الوسيط bottomRight من الطريقة Render السابقة .

لأستدعاء الوظيفة انقر بزر الماوس الأيمن على اسم الطريقة واختر Refactor ثم Remove Parameters.

بعدها علينا ان نحدد الوسيط المطلوب ونضغط على Remove في مربع الحوار.

post-173477-1234290632_thumb.jpg

[وسط]لا اله الا انت سبحانك انى كنت من الظالمين

[/وسط]

#11

سلام عليكم

شرح جميل جدا ... جزاك الله خيرا !

بالتوفيق

سلام

#12

Promote Local Variable to Parameter

او تحويل متغير محلي في طريقة إلى وسيط ، ففي بعض الحالات تجد انك حددت قيم لمتغيرات محلية ، كان من الأفضل ان يتم تعريفها كوسائط تحدد قيمتها عند استدعاء الطريقة.

مثال:

افترض انك عرفت طريقة بهذا الشكل:

class Program
{
  public static void ParseTextFile()
  {
	//فتح ملف في القرص
	string fileToParse = @"C:\myFile.txt";
	StreamReader sr = File.OpenText(fileToParse);

	//البدأ بعملية الأعراب...
  }

  static void Main(string[] args)
  {
	ParseTextFile();
  }
}

وبعد ان انتهيت من كتابتها وجدت انك قد تحتاج إلى تنفيذ هذه الطريقة لكن مع ملفات اخرى، فماذا تفعل في هذه الحالة؟

الجواب بسيط جداً، عليك ان تحول المتغير fileToParse إلى وسيط.

يمكنك القيام بهذا بخطوة واحدة عن طريق النقر على اسم المتغير بزر الماوس الأيمن واختيار Refactor ثم Promote Local Variable to Parameter.

بعدها مباشرة يعدل الكود بهذا الشكل:

class Program
	{
		public static void ParseTextFile(string fileToParse)
		{
			//فتح ملف في القرص

			StreamReader sr = File.OpenText(fileToParse);

			//البدأ بعملية الأعراب...
		}

		static void Main(string[] args)
		{
			ParseTextFile(@"C:\myFile.txt");
		}
	}

ملاحظــة:

تشترط هذه الوظيفة ان يتم تصريح وتهيئة المتغير بقيمة في نفس السطر كما يوضح المثال.

[وسط]لا اله الا انت سبحانك انى كنت من الظالمين

[/وسط]

#13

Extract Interface

تحدد الواجهة (Interface) مجموعة من الأعضاء (خصائص، طرق، احداث) المجردة التي يمكن ان توفرها فئة او struct معينة. وتتضح اهميتها عندما نفهم ان الفئات المختلفة كلية في مخطط الوراثة يمكنها ان تحقق (implement) نفس الواجهة.

في بعض الأحيان قد نحتاج إلى إنشاء واجهة جديدة من الأعضاء الموجودة في فئة معينة ، وهذا بالضبط ما تقوم به Extract Interface

مثال:

لنفترض انك تقوم بإنشاء مكتبة رسوم، واول فئة كتبتها هي TwoDShape:

class TwoDShape
	{
		public void Draw()
		{ /* Some interesting code. */ }

		public Rectangle GetBoundingRect()
		{ /* More interesting code. */ }

		public Color GetArgb()
		{ /* Even more interesting code. */ }
	}

وبعد ان انتهيت من كتابتها وجدت انك بحاجة إلى إنشاء فئات اخرى (مثلاً ThreeDShape و Printer) تحتاج فيها إلى نفس الأعضاء Draw() و GetBoundingRect() و GetArgb() . في هذه الحالة يكون من المفيد استخراج هذه الأعضاء إلى واجهة جديدة (ليكن اسمها IRender) يتم تحقيقها بواسطة الفئات الثلاث.

ولعمل هذا باستخدام Extract Interface :

نقوم بالنقر بزر الماوس الأيمن على اسم الفئة (TwoDShape) ووالتأشير على Refactor ثم اختيار Extract Interface .

عند هذه اللحظة يظهر مربع حوار لتحديد اسم الواجهة ، اسم الملف ، والأعضاء التي نريد ان تكون فيها.

post-173477-1234370714_thumb.jpg

بمجرد ان تضغط على OK ، فإن الواجهة يتم إنشاؤها في ملف جديد:

interface IRender
  {
	void Draw();
	System.Drawing.Color GetArbg();
	System.Drawing.Rectangle GetBoundingRect();
  }

كما يتم تعديل TwoDShape لتصبح بهذا الشكل :

class TwoDShape : IRender
{ ... }

الآن يمكنك ان نعرف بقية الفئات (ThreeDShape و Printer) والأستفادة من العلامات الذكية (SmartTags) في الفيجوال استديو في تحقيقها لـ IRender

post-173477-1234370845_thumb.gif

[وسط]لا اله الا انت سبحانك انى كنت من الظالمين

[/وسط]

#14

Rename

وهي اسهل طريقة لإعادة التسمية. ويمكنك بواسطتها إعادة تسمية:

- الحقول

- المتغيرات المحلية

- الطرق

- فضاء الأسماء

- الخصائص

- الأنواع

كما توفر خيارات لتضمين التعليقات والقيم النصية في عملية التعديل.

مثال:

ليكن لدينا الكود التالي:

class ProtoClassA
		{
			// Invoke on 'MethodB'.
			public void MethodB(int i, bool b) { }
		}

		class ProtoClassC
		{
			void D()
			{
				ProtoClassA MyClassA = new ProtoClassA();

				// Invoke on 'MethodB'.
				MyClassA.MethodB(0, false);
			}
		}

لتغيير اسم الطريقة MethodB إلى MethodC انقر بزر الماوس الأيمن على اسم الطريقة واختر Refactor ثم Rename، بعدها نقوم بكتابة الأسم الجديد في مربع الحوار بهذا الشكل:

post-173477-1234371528_thumb.jpg

بعد الظغط على OK تكون النتيجة:

class ProtoClassA
		{
			// Invoke on 'MethodC'.
			public void MethodC(int i, bool b) { }
		}

		class ProtoClassC
		{
			void D()
			{
				ProtoClassA MyClassA = new ProtoClassA();

				// Invoke on 'MethodC'.
				MyClassA.MethodC(0, false);
			}
		}

لاحظ التغيرات التي حصلت:

في الفئة ProtoClassA يعدل اسم الطريقة في التصريح والتعليق:

قبل:

			// Invoke on 'MethodB'.
			public void MethodB(int i, bool b) { }

بعد:

			// Invoke on 'MethodC'.
			public void MethodC(int i, bool b) { }

وفي الفئة ProtoClassC يعدل اسم الطريقة في الأستدعاء والتعليق:

قبل:

				// Invoke on 'MethodB'.
				MyClassA.MethodB(0, false);

بعد:

				// Invoke on 'MethodC'.
				MyClassA.MethodC(0, false);

=============================

إعادة تسمية الملفات بواسطة Solution Explorer:

يظهر الأمر Rename ضمن القائمة المختصرة في Solution Explorer ، فإذا حددت ملف يتطابق اسمه مع اسم فئة بداخله ، فإنه يمكنك إعادة تسمية الملف وفي نفس الوقت تنفيذ الوظيفة Rename refactoring .

فلو افترضنا انك انشأت تطبيق ويندوز وقمت بتغيير اسم Form1.cs إلى TestForm.cs فإن اسم الملف سوف يعدل من Form1.cs إلى TestForm.cs ، كذلك فإن الفئة Form1 وكل المراجع التي تشير إليها سوف تعدل إلى TestForm.

post-173477-1234371647_thumb.jpg

=============================

بهذا نكون انتهينا من آخر وظيفة من وظائف الـ Refactoring .

اسئل الله الفائدة للجميع .

[وسط]لا اله الا انت سبحانك انى كنت من الظالمين

[/وسط]

مواضيع مشابهة