كتاب "النهج الكينوني. الخامس الدولي. إد. "

صورةتقع البرمجة الكائنية (OOP) في قلب لغات C ++ و Java و C # و Visual Basic .NET و Ruby و Objective-C وحتى Swift. لا يمكنهم الاستغناء عن كائنات تقنية الويب ، لأنهم يستخدمون JavaScript و Python و PHP.

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

تمت كتابة هذا الكتاب من قبل المطور للمطورين ويسمح لك باختيار أفضل الأساليب لحل مشاكل معينة. سوف تتعلم كيفية تطبيق الميراث والتكوين بشكل صحيح ، وفهم الفرق بين التجميع والارتباط ، والتوقف عن الخلط بين الواجهة والتنفيذ.

تقنيات البرمجة تتغير باستمرار وتتطور ، لكن المفاهيم الموجهة للكائنات مستقلة عن المنصة وتبقى فعالة باستمرار. يركز هذا المنشور على الأسس الأساسية لـ OOP: أنماط التصميم والتبعيات ومبادئ SOLID التي ستجعل الشفرة مفهومة ومرنة ومُحافظ عليها جيدًا.

SOLID مبادئ التصميم الكينوني


1. SRP: مبدأ المسؤولية الوحيدة


ينص مبدأ المسؤولية الوحيدة على أنه يلزم سبب واحد فقط لإجراء تغييرات على الطبقة. يجب أن يكون لكل وحدة نمطية للفصل والبرنامج مهمة واحدة في الأولوية. لذلك ، لا يجب أن تقدم أساليب يمكن أن تسبب تغييرات في الفصل لأكثر من سبب. إذا كان وصف الصف يحتوي على كلمة "و" ، فقد يتم انتهاك مبدأ SRP. بعبارة أخرى ، يجب أن تكون كل وحدة أو فئة مسؤولة عن جزء واحد من وظائف البرنامج ، ويجب أن تكون هذه المسؤولية مغلفة بالكامل في الفصل.

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

abstract class Shape{
     protected String name;
     protected double area;
     public abstract double calcArea();
}

في هذا المثال ، توفر فئة الدائرة ، التي ترث من فئة الشكل ، تنفيذها لأسلوب calcArea () ، إذا لزم الأمر:

class Circle extends Shape{
     private double radius;

     public Circle(double r) {
           radius = r;
     }
     public double calcArea() {
           area = 3.14*(radius*radius) ;
           return (area);
     };
}

الفئة الثالثة ، CalculateAreas ، تحسب مساحة الأشكال المختلفة الموجودة في مصفوفة الشكل. صفيف الشكل غير محدود الحجم ويمكن أن يحتوي على أشكال مختلفة ، مثل المربعات والمثلثات.

class CalculateAreas {
     Shape[] shapes;
     double sumTotal=0;
     public CalculateAreas(Shape[] sh) {
           this.shapes = sh;
     }
     public double sumAreas() {
           sumTotal=0;
           for (inti=0; i<shapes.length; i++) {
           sumTotal = sumTotal + shapes[i].calcArea() ;
           }
           return sumTotal ;
     }
     public void output() {
           System.out.printIn("Total of all areas = " + sumTotal);
     }
}

لاحظ أن فئة CalculateAreas تعالج أيضًا إخراج التطبيق ، مما قد يتسبب في حدوث مشكلات. يرتبط سلوك عد المنطقة وسلوك الإخراج لأنها موجودة في نفس الفئة.

يمكننا التحقق من وظائف هذا الرمز باستخدام تطبيق الاختبار المناسب TestShape:

public class TestShape {
      public static void main(String args[]) {

            System.out.printin("Hello World!");

            Circle circle = new Circle(1);

            Shape[] shapeArray = new Shape[1];
            shapeArray[0] = circle;

            CalculateAreas ca = new CalculateAreas(shapeArray) ;

            ca.sumAreas() ;
            ca.output();
      }
}

الآن بعد أن أصبح لدينا تطبيق اختبار تحت تصرفنا ، يمكننا التركيز على مشكلة مبدأ المسؤولية الوحيدة. مرة أخرى ، تكمن المشكلة في فئة CalculateAreas وحقيقة أن هذه الفئة تحتوي على سلوكيات لإضافة مناطق الأشكال المختلفة ، وكذلك لإخراج البيانات.

السؤال الأساسي (وفي الواقع ، المشكلة) هو أنه إذا كنت بحاجة إلى تغيير وظيفة طريقة الإخراج () ، فستحتاج إلى إجراء تغييرات على فئة CalculateAreas بغض النظر عما إذا كانت طريقة حساب مساحة الأشكال تتغير. على سبيل المثال ، إذا أردنا فجأة إخراج البيانات إلى وحدة تحكم HTML ، وليس إلى نص عادي ، فسوف نحتاج إلى إعادة تجميع الكود وإعادة تضمينه ، مما يضيف مساحة الأشكال. كل ذلك بسبب المسؤولية ذات الصلة.

وفقًا لمبدأ المسؤولية الوحيدة ، فإن المهمة هي تغيير طريقة واحدة لا تؤثر على الطرق الأخرى ولا يجب إعادة تجميعها. "يجب أن يكون للفصل سبب واحد ، واحد فقط ، للتغيير - المسؤولية الوحيدة التي يجب تغييرها."

لحل هذه المشكلة ، يمكنك وضع طريقتين في فئات منفصلة ، واحدة لإخراج وحدة التحكم الأصلية ، والأخرى لإخراج HTML:

class CaiculateAreas {;
     Shape[] shapes;
     double sumTotal=0;

     public CalculateAreas(Shape[] sh) {
           this.shapes = sh;
     }

     public double sumAreas() {
           sumTotal=0;

           for (inti=0; i<shapes.length; i++) {

                sumTotal = sumTotal + shapes[i].calcArea();

           }

                return sumTotal;
           }
}
class OutputAreas {
     double areas=0;
     public OutputAreas (double a) {
           this.areas = a;
     }

           public void console() {
           System.out.printin("Total of all areas = " + areas);
     }
     public void HTML() {
           System.out.printIn("<HTML>") ;
           System.out.printin("Total of all areas = " + areas);
           System.out.printin("</HTML>") ;
     }
}

خلاصة القول هنا أنه يمكنك الآن إرسال استنتاج في اتجاهات مختلفة حسب الحاجة. إذا كنت ترغب في إضافة إمكانية طريقة إخراج أخرى ، على سبيل المثال ، JSON ، يمكنك إضافتها إلى فئة OutputAreas دون الحاجة إلى إجراء تغييرات على فئة CalculateAreas. ونتيجة لذلك ، يمكنك إعادة توزيع فئة CalculateAreas دون التأثير على الفئات الأخرى بأي شكل من الأشكال.

2. OCP: مبدأ مفتوح / مغلق


ينص مبدأ الانفتاح / التقارب على أنه يمكنك توسيع سلوك الفصل دون إجراء تغييرات.

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

class Rectangle{
     protected double length;
     protected double width;

     public Rectangle(double 1, double w) {
           length = 1;
           width = w;
     };
}
class CalculateAreas {
     private double area;

     public double calcArea(Rectangle r) {

           area = r.length * r.width;

           return area;
     }
}
public class OpenClosed {
      public static void main(String args[]) {

            System.out.printin("Hello World");

            Rectangle r = new Rectangle(1,2);

            CalculateAreas ca = new CalculateAreas ();

            System.out.printin("Area = "+ ca.calcArea(r));
      }
}

حقيقة أن هذا التطبيق يعمل فقط في حالة المستطيلات يؤدي إلى قيود توضح بوضوح مبدأ الانفتاح / الإغلاق: إذا أردنا إضافة فئة Circle إلى فئة CalculateArea (تغيير ما تفعله) ، فنحن بحاجة إلى إجراء تغييرات على الوحدة النمطية نفسها. من الواضح أن هذا يتعارض مع مبدأ الانفتاح / التقارب ، والذي ينص على أنه لا يجب إجراء تغييرات على الوحدة النمطية لتغيير ما تفعله.

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

في الوقت الحالي ، يمكنك إضافة العديد من الفئات المختلفة حسب الحاجة ، دون الحاجة إلى إجراء تغييرات مباشرة على فئة الشكل (على سبيل المثال ، فئة الدائرة). الآن يمكننا القول أن فئة الشكل مغلقة.

يوفر الرمز أدناه تنفيذ الحل للمستطيلات والدوائر ويسمح لك بإنشاء عدد غير محدود من الأشكال:

abstract class Shape {
      public abstract double getArea() ;
}
class Rectangle extends Shape
{

      protected double length;
      protected double width;

      public Rectangle(double 1, double w) {
            length = 1;
            width = w;
      };
      public double getArea() {
            return length*width;
      }

}
class Circle extends Shape
{
      protected double radius;

      public Circle(double r) {
            radius = r;
      };
      public double getArea() {
            return radius*radius*3.14;
      }
}
class CalculateAreas {
      private double area;

      public double calcArea(Shape s) {
            area = s.getArea();
            return area;
      }
}

public class OpenClosed {
      public static void main(String args[]) {

            System.out.printiIn("Hello World") ;

            CalculateAreas ca = new CalculateAreas() ;

            Rectangle r = new Rectangle(1,2);

            System.out.printIn("Area = " + ca.calcArea(r));

            Circle c = new Circle(3);

            System.out.printIn("Area = " + ca.calcArea(c));
}
}

تجدر الإشارة إلى أنه مع هذا التطبيق ، لا يجب تعديل طريقة CalculateAreas () عند إنشاء مثيل جديد لفئة الشكل.

يمكنك تحجيم الكود دون القلق بشأن وجود الكود السابق. مبدأ الانفتاح / التقارب هو أنه يجب عليك تمديد الكود باستخدام الفئات الفرعية بحيث لا تتطلب الفئة الأصلية تعديلات. ومع ذلك ، فإن مفهوم "التمديد" نفسه مثير للجدل في بعض المناقشات المتعلقة بمبادئ SOLID. بصراحة ، إذا فضلنا التكوين بدلاً من الميراث ، فكيف يؤثر ذلك على مبدأ الانفتاح / التقارب؟

عند الالتزام بأحد مبادئ SOLID ، قد تستوفي الكود معايير مبادئ SOLID الأخرى. على سبيل المثال ، عند التصميم وفقًا لمبدأ الانفتاح / التقارب ، قد تتوافق الشفرة مع متطلبات مبدأ المسؤولية الوحيدة.

3. LSP: مبدأ استبدال Lisk


وفقًا لمبدأ الاستبدال Liskov ، يجب أن ينص التصميم على إمكانية استبدال أي مثيل للفئة الأم بمثيل لفئة تابعة. إذا كان بإمكان الفصل الأصل القيام بأي مهمة ، يجب أن يكون الفصل الفرعي قادرًا على ذلك أيضًا.

ضع في اعتبارك بعض التعليمات البرمجية الصحيحة للوهلة الأولى ، ولكنها تنتهك مبدأ استبدال Lisk. يحتوي الرمز أدناه على فئة مجردة الشكل العام. بدورها ، ترث فئة المستطيل سمات من فئة الشكل وتتجاوز طريقة calcArea () المجردة. الطبقة المربعة ، بدورها ، ترث من المستطيل.

abstract class Shape{
      protected double area;

      public abstract double calcArea();
}
class Rectangle extends Shape{
      private double length;
      private double width;

      public Rectangle(double 1, double w) {
            length = 1;
            width = w;
      }
      public double calcArea() {
            area = length*width;
            return (area) ;
      };
}
class Square extends Rectangle{
      public Square(double s) {
            super(s, Ss);
      }
}

public class LiskovSubstitution {
      public static void main(String args[]) {

            System.out.printIn("Hello World") ;

            Rectangle r = new Rectangle(1,2);

            System.out.printin("Area = " + r.calcArea());

            Square s = new Square(2) ;

            System.out.printin("Area = " + s.calcArea());
      }
}

حتى الآن جيد جدًا: المستطيل هو مثال على الشكل ، لذلك لا يوجد ما يدعو للقلق ، حيث أن المربع هو مثال على المستطيل - ومرة ​​أخرى ، كل شيء صحيح ، أليس كذلك؟

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

عندما يتعلق الأمر بالتصميم الكينوني ، فإن المشكلة ليست في الهندسة. تكمن المشكلة في كيفية إنشاء المستطيلات والمربعات بالضبط. هنا مُنشئ لفئة المستطيل:

public Rectangle(double 1, double w) {
      length = 1;
      width = w;
}

من الواضح أن المنشئ يتطلب معلمتين. ومع ذلك ، يتطلب منشئ الفئة المربعة واحدًا فقط ، على الرغم من أن الفئة الأصل ، المستطيل ، تتطلب اثنين.

class Square extends Rectangle{
      public Square(double s) {
      super(s, Ss);
}

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

كيف تجد طريقة للخروج من هذا الوضع؟ ببساطة ، لا يمكنك استبدال فئة مربعة بمستطيل. وبالتالي ، لا ينبغي أن يكون المربع تابعًا لفئة المستطيل. يجب أن تكون فئات منفصلة.

abstract class Shape {
      protected double area;

      public abstract double calcArea();
}

class Rectangle extends Shape {

      private double length;
      private double width;

      public Rectangle(double 1, double w) {
            length = 1;
            width = w;
      }

      public double calcArea() {
            area = length*width;
            return (area);
      };
}

class Square extends Shape {
      private double side;

      public Square(double s) {
            side = s;
      }
      public double calcArea() {
            area = side*side;
            return (area);
      };
}

public class LiskovSubstitution {
      public static void main(String args[]) {

             System.out.printIn("Hello World") ;

             Rectangle r = new Rectangle(1,2);

             System.out.printIn("Area = " + r.calcArea());

             Square s = new Square(2) ;

             System.out.printIn("Area = " + s.calcArea());
      }
}

4. ISP: مبدأ تقاسم الواجهة


ينص مبدأ فصل الواجهات على أنه من الأفضل إنشاء العديد من الواجهات الصغيرة من العديد من الواجهات الكبيرة.

في هذا المثال ، نقوم بإنشاء واجهة واحدة تتضمن العديد من السلوكيات لفئة الثدييات ، وهي الأكل () و makeNoise ():

interface IMammal {
     public void eat();
     public void makeNoise() ;
}
class Dog implements IMammal {
     public void eat() {
           System.out.printIn("Dog is eating");
     }
     public void makeNoise() {
           System.out.printIn("Dog is making noise");
     }
}
public class MyClass {
      public static void main(String args[]) {

            System.out.printIn("Hello World");

            Dog fido = new Dog();
            fido.eat();
            fido.makeNoise()
      }
}

بدلاً من إنشاء واجهة واحدة لفئة Mammal ، تحتاج إلى إنشاء
واجهات منفصلة لجميع السلوكيات:

interface IEat {
     public void eat();
}
interface IMakeNoise {
     public void makeNoise() ;
}
class Dog implements IEat, IMakeNoise {
     public void eat() {
           System.out.printIn("Dog is eating");
     }
     public void makeNoise() {
           System.out.printIn("Dog is making noise");
     }
}
public class MyClass {
      public static void main(String args[]) {

            System.out.printIn("Hello World") ;

            Dog fido = new Dog();
            fido.eat();
            fido.makeNoise();
      }
}

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

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

»يمكن العثور على مزيد من المعلومات حول الكتاب على موقع الناشر على الإنترنت
» جدول المحتويات
» مقتطفات

لـ Habrozhitelami خصم 25 ٪ على القسيمة -OOP

عند دفع النسخة الورقية من الكتاب ، يتم إرسال كتاب إلكتروني عبر البريد الإلكتروني.

Source: https://habr.com/ru/post/undefined/


All Articles