تعرّفنا في المقال السابق على مفهوم البرمجة كائنية التوجه - أو اختصارًا OOP- وكيفية تعريف الأصناف classes في لغة بايثون، إضافةً إلى بعض التوابع المفيدة بهذا الخصوص. سننظر في هذا المقال على مثال عملي لتطبيق البرمجة كائنية التوجه في لغة بايثون Python ومن ثم سنطّلع على البرنامج ذاته دون استخدام البرمجة كائنية التوجه Non-OOP.
أمثلة مع البرمجة كائنية التوجه وبدونها: لعبة إكس أو
في البداية، قد يكون من الصعب معرفة كيفية استخدام الأصناف في برامجك. دعنا نلقي نظرةً على مثال قصير للعبة إكس أو لا يستخدم الأصناف، ثم نعيد كتابته باستخدامها.
افتح نافذة محرر ملفات جديدة وأدخل البرنامج التالي؛ ثم احفظه بالاسم tictactoe.py:
# tictactoe.py, A non-OOP tic-tac-toe game. ALL_SPACES = list('123456789') # The keys for a TTT board dictionary. X, O, BLANK = 'X', 'O', ' ' # Constants for string values. def main(): """Runs a game of tic-tac-toe.""" print('Welcome to tic-tac-toe!') gameBoard = getBlankBoard() # Create a TTT board dictionary. currentPlayer, nextPlayer = X, O # X goes first, O goes next. while True: print(getBoardStr(gameBoard)) # Display the board on the screen. # Keep asking the player until they enter a number 1-9: move = None while not isValidSpace(gameBoard, move): print(f'What is {currentPlayer}\'s move? (1-9)') move = input() updateBoard(gameBoard, move, currentPlayer) # Make the move. # Check if the game is over: if isWinner(gameBoard, currentPlayer): # First check for victory. print(getBoardStr(gameBoard)) print(currentPlayer + ' has won the game!') break elif isBoardFull(gameBoard): # Next check for a tie. print(getBoardStr(gameBoard)) print('The game is a tie!') break currentPlayer, nextPlayer = nextPlayer, currentPlayer # Swap turns. print('Thanks for playing!') def getBlankBoard(): """Create a new, blank tic-tac-toe board.""" board = {} # The board is represented as a Python dictionary. for space in ALL_SPACES: board[space] = BLANK # All spaces start as blank. return board def getBoardStr(board): """Return a text-representation of the board.""" return f''' {board['1']}|{board['2']}|{board['3']} 1 2 3 -+-+- {board['4']}|{board['5']}|{board['6']} 4 5 6 -+-+- {board['7']}|{board['8']}|{board['9']} 7 8 9''' def isValidSpace(board, space): """Returns True if the space on the board is a valid space number and the space is blank.""" return space in ALL_SPACES and board[space] == BLANK def isWinner(board, player): """Return True if player is a winner on this TTTBoard.""" b, p = board, player # Shorter names as "syntactic sugar". # Check for 3 marks across the 3 rows, 3 columns, and 2 diagonals. return ((b['1'] == b['2'] == b['3'] == p) or # Across the top (b['4'] == b['5'] == b['6'] == p) or # Across the middle (b['7'] == b['8'] == b['9'] == p) or # Across the bottom (b['1'] == b['4'] == b['7'] == p) or # Down the left (b['2'] == b['5'] == b['8'] == p) or # Down the middle (b['3'] == b['6'] == b['9'] == p) or # Down the right (b['3'] == b['5'] == b['7'] == p) or # Diagonal (b['1'] == b['5'] == b['9'] == p)) # Diagonal def isBoardFull(board): """Return True if every space on the board has been taken.""" for space in ALL_SPACES: if board[space] == BLANK: return False # If a single space is blank, return False. return True # No spaces are blank, so return True. def updateBoard(board, space, mark): """Sets the space on the board to mark.""" board[space] = mark if __name__ == '__main__': main() # Call main() if this module is run, but not when imported.
عند تنفيذ هذا البرنامج، سيبدو الخرج كما يلي:
Welcome to tic-tac-toe! | | 1 2 3 -+-+- | | 4 5 6 -+-+- | | 7 8 9 What is X's move? (1-9) 1 X| | 1 2 3 -+-+- | | 4 5 6 -+-+- | | 7 8 9 What is O's move? (1-9) --snip-- X| |O 1 2 3 -+-+- |O| 4 5 6 -+-+- X|O|X 7 8 9 What is X's move? (1-9) 4 X| |O 1 2 3 -+-+- X|O| 4 5 6 -+-+- X|O|X 7 8 9 X has won the game! Thanks for playing!
باختصار، يعمل هذا البرنامج باستخدام كائنات القاموس لتُمثل المساحات التسع على لوحة إكس أو. مفاتيح القاموس هي السلاسل من "1" إلى "9"، وقيمها هي السلاسل "X" أو "O" أو " ". المسافات المرقمة بنفس ترتيب لوحة مفاتيح الهاتف.
تتمثل وظيفة الدوال في tictactoe.py بما يلي:
-
تحتوي الدالة
main()
على الشيفرة التي تُنشئ بنية بيانات لوحة جديدة (مخزنة في متغيرgameBoard
) وتستدعي دوالًا أخرى في البرنامج. -
تُعيد الدالة
getBlankBoard()
قاموسًا به تسع مسافات مضبوطة على " " للوحة فارغة. -
تقبل الدالة
getBoardStr()
قاموسًا يمثل اللوحة وتعيد تمثيلًا لسلسلة متعددة الأسطر للوحة يمكن طباعتها على الشاشة، وتصيّر هذه الدالة نص لوحة إكس أو tic-tac-toe الذي تعرضه اللعبة. -
تُعيد الدالة
isValidSpace()
القيمةTrue
إذا مُرّر رقم مسافة صالح وكانت تلك المسافة فارغة. -
تقبل معاملات دالة
isWinner()
قاموس لوحة إما "X" أو "O" لتحديد ما إذا كان هذا اللاعب لديه ثلاث علامات متتالية على اللوحة. -
تحدد دالة
isBoardFull()
ما إذا كانت اللوحة لا تحتوي على مسافات فارغة، ما يعني أن اللعبة قد انتهت. تقبل معاملات دالةupdateBoard()
قاموس لوحة ومساحة وعلامة X أو O للاعب وتحدّث القاموس.
لاحظ أن العديد من الدوال تقبل اللوحة المتغيرة في معاملها الأول، وهذا يعني أن هذه الدوال مرتبطة ببعضها بعضًا من حيث أنها تعمل جميعها على بنية بيانات مشتركة.
عندما تعمل العديد من الدوال في الشيفرة على بنية البيانات ذاتها، فمن الأفضل عادةً تجميعها معًا على أنها توابع وسمات للصنف. دعنا نعيد تصميم هذا في برنامج tictactoe.py لاستخدام صنف TTTBoard
الذي سيخزن قاموس board
في سمة تسمى spaces
. ستصبح الدوال التي كان لها board
مثل معامل توابع لصنف TTTBoard
الخاصة بنا وستستخدم المعامل self
بدلًا من معامل board
.
افتح نافذة محرر ملفات جديدة، وأدخل الشيفرة التالي، واحفظه باسم tictactoe_oop.py:
# tictactoe_oop.py, an object-oriented tic-tac-toe game. ALL_SPACES = list('123456789') # The keys for a TTT board. X, O, BLANK = 'X', 'O', ' ' # Constants for string values. def main(): """Runs a game of tic-tac-toe.""" print('Welcome to tic-tac-toe!') gameBoard = TTTBoard() # Create a TTT board object. currentPlayer, nextPlayer = X, O # X goes first, O goes next. while True: print(gameBoard.getBoardStr()) # Display the board on the screen. # Keep asking the player until they enter a number 1-9: move = None while not gameBoard.isValidSpace(move): print(f'What is {currentPlayer}\'s move? (1-9)') move = input() gameBoard.updateBoard(move, currentPlayer) # Make the move. # Check if the game is over: if gameBoard.isWinner(currentPlayer): # First check for victory. print(gameBoard.getBoardStr()) print(currentPlayer + ' has won the game!') break elif gameBoard.isBoardFull(): # Next check for a tie. print(gameBoard.getBoardStr()) print('The game is a tie!') break currentPlayer, nextPlayer = nextPlayer, currentPlayer # Swap turns. print('Thanks for playing!') class TTTBoard: def __init__(self, usePrettyBoard=False, useLogging=False): """Create a new, blank tic tac toe board.""" self._spaces = {} # The board is represented as a Python dictionary. for space in ALL_SPACES: self._spaces[space] = BLANK # All spaces start as blank. def getBoardStr(self): """Return a text-representation of the board.""" return f''' {self._spaces['1']}|{self._spaces['2']}|{self._spaces['3']} 1 2 3 -+-+- {self._spaces['4']}|{self._spaces['5']}|{self._spaces['6']} 4 5 6 -+-+- {self._spaces['7']}|{self._spaces['8']}|{self._spaces['9']} 7 8 9''' def isValidSpace(self, space): """Returns True if the space on the board is a valid space number and the space is blank.""" return space in ALL_SPACES and self._spaces[space] == BLANK def isWinner(self, player): """Return True if player is a winner on this TTTBoard.""" s, p = self._spaces, player # Shorter names as "syntactic sugar". # Check for 3 marks across the 3 rows, 3 columns, and 2 diagonals. return ((s['1'] == s['2'] == s['3'] == p) or # Across the top (s['4'] == s['5'] == s['6'] == p) or # Across the middle (s['7'] == s['8'] == s['9'] == p) or # Across the bottom (s['1'] == s['4'] == s['7'] == p) or # Down the left (s['2'] == s['5'] == s['8'] == p) or # Down the middle (s['3'] == s['6'] == s['9'] == p) or # Down the right (s['3'] == s['5'] == s['7'] == p) or # Diagonal (s['1'] == s['5'] == s['9'] == p)) # Diagonal def isBoardFull(self): """Return True if every space on the board has been taken.""" for space in ALL_SPACES: if self._spaces[space] == BLANK: return False # If a single space is blank, return False. return True # No spaces are blank, so return True. def updateBoard(self, space, player): """Sets the space on the board to player.""" self._spaces[space] = player if __name__ == '__main__': main() # Call main() if this module is run, but not when imported.
يقدّم هذا البرنامج عمل برنامج إكس أو tic-tac-toe السابق ذاته دون استخدام البرمجة كائنية التوجه. يبدو الخرج متطابقًا تمامًا. نقلنا الشيفرة التي كانت في getBlankBoard()
إلى تابع __init __()
لصنف TTTBoard
، لأنها تؤدي المهمة ذاتها لإعداد بنية بيانات اللوحة. حوّلنا الدوال الأخرى إلى توابع، مع استبدال المعامل board
القديم بمعامل self
، لأنها تخدم أيضًا غرضًا مشابهًا؛ إذ أن كلاهما كتلتان من الشيفرة البرمجية التي تعمل على بنية بيانات لوحة إكس أو.
عندما تحتاج الشيفرة البرمجية في هذه التوابع إلى تغيير القاموس المخزن في السمة _spaces
، تستخدم الشيفرة self._spaces
، وعندما تحتاج الشيفرة في هذا التابع إلى استدعاء توابع أخرى، فإن الاستدعاء يسبقه self
وفترة زمنية period. هذا مشابه لكيفية احتواء coinJars.values()
في قسم "إنشاء صنف بسيط" على كائن في متغير coinJars
. في هذا المثال، الكائن الذي يحتوي على طريقة استدعاء موجود في متغير self
.
لاحظ أيضًا أن السمة _spaces
تبدأ بشرطة سفلية، ما يعني أن الشيفرة البرمجية الموجودة داخل توابع TTTBoard
هي فقط التي يجب أن تصل إليها أو تعدلها. يجب أن تكون الشيفرة البرمجية خارج الصنف قادرةً فقط على تعديل المسافات بصورة غير مباشرة عن طريق استدعاء التوابع التي تعدّلها.
قد يكون من المفيد مقارنة الشيفرة المصدرية لبرنامجي إكس أو، إذ يمكنك مقارنة الشيفرة وعرض مقارنة جنبًا إلى جنب من خلال الرابط https://autbor.com/compareoop.
لعبة إكس أو هي برنامج صغير، لذا لا يتطلب فهمه الكثير من الجهد، ولكن ماذا لو كان هذا البرنامج يتكون من عشرات الآلاف من السطور بمئات الدوال المختلفة؟ قد يكون فهم البرنامج الذي يحتوي على بضع عشرات من الأصناف أسهل في الفهم من البرنامج الذي يحتوي على عدة مئات من الدوال المتباينة. تُقسّم البرمجة كائنية التوجه البرنامج المعقد إلى أجزاء يسهل فهمها.
تصميم أصناف في العالم الحقيقي أمر صعب
يبدو تصميم الصنف، تمامًا مثل تصميم الاستمارة الورقية paper form، فهو أمرٌ واضحٌ وبسيط. الاستمارات والأصناف، بحكم طبيعتها، هي تبسيطات لكائنات العالم الحقيقي التي تمثلها. السؤال هو كيف نبسط هذه الأشياء؟ على سبيل المثال، إذا كنا بصدد إنشاء صنف Customer
فيجب أن يكون للعميل سمة firstName
و lastName
، أليس كذلك؟ لكن في الواقع، قد يكون إنشاء أصناف لنمذجة كائنات من العالم الحقيقي أمرًا صعبًا؛ ففي معظم البلدان الغربية، يكون الاسم الأخير للشخص هو اسم عائلته، ولكن في الصين، يكون اسم العائلة أولًا. إذا كنا لا نريد استبعاد أكثر من مليار عميل محتمل، فكيف يجب أن نغير صنف Customer
لدينا؟ هل يجب تغيير firstName
و lastName
إلى givenName
و familyName
؟ لكن بعض الثقافات لا تستخدم أسماء العائلة. على سبيل المثال، الأمين العام السابق للأمم المتحدة يو ثانت U Thant، وهو بورمي، ليس له اسم عائلة: ثانت Thant هو اسمه الأول ويو U هو اختصار لاسم والده. قد نرغب في تسجيل عمر العميل، ولكن سرعان ما ستصبح سمة age
قديمة؛ وبدلًا من ذلك، من الأفضل حساب العمر في كل مرة تحتاج إليها باستخدام سمة birthdate
.
العالم الحقيقي معقد، ومن الصعب تصميم الاستمارات والأصناف لتسجيل هذه الأمور المعقدة في بنية موحدة يمكن لبرامجنا العمل عليها؛ إذ تختلف تنسيقات أرقام الهاتف بين البلدان؛ ولا تنطبق الرموز البريدية على العناوين خارج الولايات المتحدة؛ كما قد يكون تعيين الحد الأقصى لعدد الأحرف لأسماء المدن مشكلةً بالنسبة إلى قرية SchmedeswMorewesterdeich الألمانية. في أستراليا ونيوزيلندا، يمكن أن يكون جنسك المعترف به قانونًا هو X. خلد الماء هو أحد الثدييات التي تبيض. لا ينتمي الفول السوداني للمكسرات. الهوت دوج قد تكون شطيرة أو قد لا تكون، اعتمادًا على من تسأل. بصفتك مبرمجًا يكتب برامج لاستخدامها في العالم الحقيقي، سيتعين عليك تجاوز هذا التعقيد.
ترجمة -وبتصرف- لقسم من الفصل Object-Oriented Programming And Classes من كتاب Beyond the Basic Stuff with Python.
أفضل التعليقات
لا توجد أية تعليقات بعد
انضم إلى النقاش
يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.