ชั้นเรียนขนาดใหญ่ที่มีความรับผิดชอบเดียว


13

ฉันมีCharacterคลาสline 2,500 ที่:

  • ติดตามสถานะภายในของตัวละครในเกม
  • โหลดและยืนยันสถานะนั้น
  • จัดการคำสั่งที่เข้ามา ~ 30 (โดยปกติ = ส่งต่อคำสั่งไปยังGameแต่คำสั่งแบบอ่านอย่างเดียวบางคำสั่งจะตอบกลับทันที)
  • รับสายประมาณ 80 สายจากGameการกระทำที่ต้องทำและการกระทำที่เกี่ยวข้องของผู้อื่น

ดูเหมือนว่าฉันCharacterมีความรับผิดชอบเดียว: การจัดการสถานะของตัวละคร, การไกล่เกลี่ยระหว่างคำสั่งที่เข้ามาและเกม

มีความรับผิดชอบอื่น ๆ อีกสองสามอย่างที่ถูกทำลายไปแล้ว:

  • Characterมีสิ่งOutgoingที่เรียกร้องให้สร้างการอัพเดตขาออกสำหรับแอปพลิเคชันไคลเอนต์
  • CharacterมีTimerแทร็กใดที่อนุญาตให้ทำอะไรต่อไป คำสั่งที่เข้ามาจะถูกตรวจสอบกับสิ่งนี้

ดังนั้นคำถามของฉันคือเป็นที่ยอมรับหรือไม่ที่จะมีชั้นเรียนขนาดใหญ่ภายใต้ SRP และหลักการที่คล้ายกัน? มีวิธีปฏิบัติที่ดีที่สุดในการทำให้ยุ่งยากน้อยลงหรือไม่ (เช่นอาจแบ่งวิธีเป็นไฟล์แยกกัน) หรือฉันขาดอะไรบางอย่างไปและมีวิธีที่ดีมากที่จะแยกมันออกมา? ฉันรู้ว่านี่เป็นอัตนัยและต้องการความคิดเห็นจากผู้อื่น

นี่คือตัวอย่าง:

class Character(object):
    def __init__(self):
        self.game = None
        self.health = 1000
        self.successful_attacks = 0
        self.points = 0
        self.timer = Timer()
        self.outgoing = Outgoing(self)

    def load(self, db, id):
        self.health, self.successful_attacks, self.points = db.load_character_data(id)

    def save(self, db, id):
        db.save_character_data(self, health, self.successful_attacks, self.points)

    def handle_connect_to_game(self, game):
        self.game.connect(self)
        self.game = game
        self.outgoing.send_connect_to_game(game)

    def handle_attack(self, victim, attack_type):
        if time.time() < self.timer.get_next_move_time():
            raise Exception()
        self.game.request_attack(self, victim, attack_type)

    def on_attack(victim, attack_type, points):
        self.points += points
        self.successful_attacks += 1
        self.outgoing.send_attack(self, victim, attack_type)
        self.timer.add_attack(attacker=True)

    def on_miss_attack(victim, attack_type):
        self.missed_attacks += 1
        self.outgoing.send_missed_attack()
        self.timer.add_missed_attack()

    def on_attacked(attacker, attack_type, damage):
        self.start_defenses()
        self.take_damage(damage)
        self.outgoing.send_attack(attacker, self, attack_type)
        self.timer.add_attack(victim=True)

    def on_see_attack(attacker, victim, attack_type):
        self.outgoing.send_attack(attacker, victim, attack_type)
        self.timer.add_attack()


class Outgoing(object):
    def __init__(self, character):
        self.character = character
        self.queue = []

    def send_connect_to_game(game):
        self._queue.append(...)

    def send_attack(self, attacker, victim, attack_type):
        self._queue.append(...)

class Timer(object):
    def get_next_move_time(self):
        return self._next_move_time

    def add_attack(attacker=False, victim=False):
        if attacker:
            self.submit_move()
        self.add_time(ATTACK_TIME)
        if victim:
            self.add_time(ATTACK_VICTIM_TIME)

class Game(object):
    def connect(self, character):
        if not self._accept_character(character):
           raise Exception()
        self.character_manager.add(character)

    def request_attack(character, victim, attack_type):
        if victim.has_immunity(attack_type):
            character.on_miss_attack(victim, attack_type)
        else:
            points = self._calculate_points(character, victim, attack_type)
            damage = self._calculate_damage(character, victim, attack_type)
            character.on_attack(victim, attack_type, points)
            victim.on_attacked(character, attack_type, damage)
            for other in self.character_manager.get_observers(victim):
                other.on_see_attack(character, victim, attack_type)

1
ฉันคิดว่านี่เป็นตัวพิมพ์ผิด: db.save_character_data(self, health, self.successful_attacks, self.points)คุณหมายถึงself.healthใช่มั้ย
candied_orange

5
หากตัวละครของคุณอยู่ในระดับที่ถูกต้องฉันไม่เห็นปัญหา หากในอีกแง่หนึ่งมันคือการจัดการรายละเอียดทั้งหมดของการพูดว่าการโหลดและการคงอยู่ของตัวเองจริงๆแล้วคุณจะไม่ปฏิบัติตามความรับผิดชอบเดียว การมอบหมายเป็นกุญแจสำคัญที่นี่จริงๆ เมื่อเห็นว่าตัวละครของคุณรู้รายละเอียดในระดับต่ำเช่นตัวจับเวลาและสิ่งนี้ฉันรู้สึกว่ามันรู้มากเกินไปแล้ว
Philip Stuyck

1
ชั้นควรดำเนินการในระดับเดียวของสิ่งที่เป็นนามธรรม ไม่ควรดูรายละเอียดของตัวอย่างเช่นการจัดเก็บสถานะ คุณควรจะสามารถย่อยสลายชิ้นเล็ก ๆ ที่รับผิดชอบในการ internals รูปแบบคำสั่งอาจมีประโยชน์ที่นี่ โปรดดูgoogle.pl/url?sa=t&source=web&rct=j&url=http://…
Piotr Gwiazda

ขอบคุณทุกความคิดเห็นและคำตอบ ฉันคิดว่าฉันไม่ได้แยกแยะสิ่งต่าง ๆ มากพอและยึดติดกับชั้นเรียนขนาดใหญ่ที่คลุมเครือมากเกินไป การใช้รูปแบบคำสั่งช่วยได้มากแล้ว ฉันได้แยกสิ่งต่าง ๆ ออกเป็นเลเยอร์ที่ทำงานในระดับต่าง ๆ ของสิ่งที่เป็นนามธรรม (เช่นซ็อกเก็ตข้อความในเกมคำสั่งเกม) ฉันกำลังก้าวหน้า!
user35358

1
วิธีการที่จะแก้ไขปัญหานี้ก็คือการมี "CharacterState" เป็นชั้น "CharacterInputHandler" เป็นอื่น "CharacterPersistance" เป็นอื่น ...
T. Sar

คำตอบ:


14

ในความพยายามของฉันที่จะใช้ SRP กับปัญหาฉันมักพบว่าวิธีที่ดีในการติดกับความรับผิดชอบต่อชั้นคือการเลือกชื่อชั้นที่พูดถึงความรับผิดชอบของพวกเขาเพราะมันมักช่วยให้คิดได้ชัดเจนขึ้นว่าฟังก์ชั่นบางอย่าง จริงๆ 'เป็น' ในชั้นเรียนนั้น

นอกจากนี้ผมรู้สึกว่าคำนามง่ายๆเช่นCharacter(หรือEmployee, Person, Car, Animalฯลฯ ) มักจะทำให้ชื่อชั้นน่าสงสารมากเพราะพวกเขาจริงๆอธิบายหน่วยงาน (ข้อมูล) ในการประยุกต์ใช้ของคุณและเมื่อได้รับการรักษาเป็นชั้นเรียนก็มักจะง่ายเกินไปที่จะจบลงด้วย ป่องมาก

ฉันพบว่าชื่อคลาส 'ดี' มักจะเป็นเลเบลซึ่งสื่อความหมายบางแง่มุมของพฤติกรรมของโปรแกรมของคุณเช่นเมื่อโปรแกรมเมอร์คนอื่นเห็นชื่อคลาสของคุณพวกเขาได้รับแนวคิดพื้นฐานเกี่ยวกับพฤติกรรม / การทำงานของคลาสนั้น

ตามกฎของหัวแม่มือฉันมักจะคิดว่าเอนทิตีเป็นแบบจำลองข้อมูลและคลาสเป็นตัวแทนของพฤติกรรม (แม้ว่าแน่นอนว่าภาษาการเขียนโปรแกรมส่วนใหญ่ใช้classคำหลักสำหรับทั้งคู่ แต่แนวคิดในการรักษาเอนทิตี 'ธรรมดา' แยกจากพฤติกรรมของแอปพลิเคชันนั้นเป็นกลางทางภาษา)

จากการแบ่งความรับผิดชอบที่หลากหลายที่คุณได้กล่าวถึงในชั้นเรียนของตัวละครของคุณฉันจะเริ่มเรียนในชั้นเรียนที่มีชื่อตามความต้องการที่พวกเขาปฏิบัติตาม ตัวอย่างเช่น:

  • พิจารณาCharacterModelเอนทิตีที่ไม่มีพฤติกรรมและรักษาสถานะของตัวละครของคุณ (เก็บข้อมูล)
  • สำหรับการคงอยู่ / IO ให้พิจารณาชื่อเช่นCharacterReaderและCharacterWriter (หรืออาจเป็นCharacterRepository/ CharacterSerialiser/ ฯลฯ )
  • คิดเกี่ยวกับรูปแบบที่มีอยู่ระหว่างคำสั่งของคุณ หากคุณมี 30 คำสั่งคุณอาจมีความรับผิดชอบแยกต่างหาก 30 ข้อ บางอย่างอาจทับซ้อนกัน แต่ดูเหมือนว่าจะเป็นตัวเลือกที่ดีสำหรับการแยก
  • พิจารณาว่าคุณสามารถใช้การปรับโครงสร้างเดียวกันกับการกระทำของคุณได้หรือไม่ - อีกครั้งการกระทำ 80 รายการอาจเสนอความรับผิดชอบแยกกันมากถึง 80 รายการหรืออาจทับซ้อนกันบ้าง
  • การแยกคำสั่งและการกระทำอาจนำไปสู่คลาสอื่นที่รับผิดชอบในการรัน / การยิงคำสั่ง / การกระทำเหล่านั้น อาจเป็นบางประเภทCommandBrokerหรือActionBrokerที่ทำงานเหมือนกับ "มิดเดิลแวร์" ของแอปพลิเคชันของคุณในการส่ง / รับ / ดำเนินการคำสั่งและการกระทำระหว่างวัตถุที่แตกต่างกัน

จำไว้ด้วยว่าไม่ใช่ทุกสิ่งที่เกี่ยวข้องกับพฤติกรรมจำเป็นต้องมีอยู่ในชั้นเรียน ตัวอย่างเช่นคุณอาจพิจารณาใช้แผนที่ / พจนานุกรมของพอยน์เตอร์ของฟังก์ชั่น / ผู้ได้รับมอบหมาย / ปิดเพื่อห่อหุ้มการกระทำ / คำสั่งของคุณมากกว่าที่จะเขียนคลาสเดียววิธีการไร้สัญชาติ

เป็นเรื่องธรรมดาที่จะเห็น 'รูปแบบคำสั่ง' โดยไม่ต้องเขียนคลาสใด ๆ ที่สร้างขึ้นโดยใช้วิธีสแตติกที่ใช้ลายเซ็น / อินเตอร์เฟสร่วมกัน:

 void AttackAction(CharacterModel) { ... }
 void ReloadAction(CharacterModel) { ... }
 void RunAction(CharacterModel) { ... }
 void DuckAction(CharacterModel) { ... }
 // etc.

ท้ายที่สุดไม่มีกฎที่ยากและรวดเร็วว่าคุณควรไปถึงความรับผิดชอบเพียงใด ความซับซ้อนเพื่อประโยชน์ของความซับซ้อนไม่ได้เป็นสิ่งที่ดี แต่คลาส megalithic มีแนวโน้มที่จะค่อนข้างซับซ้อนในตัวเอง เป้าหมายสำคัญของ SRP และหลักการอื่น ๆ ของ SOLID ก็คือการจัดโครงสร้างความมั่นคงและทำให้รหัสสามารถบำรุงรักษาได้ดีขึ้นซึ่งมักจะส่งผลให้บางสิ่งง่ายขึ้น


ฉันคิดว่าคำตอบนี้ตอบปัญหาของฉันขอบคุณ ฉันได้ใส่มันเพื่อใช้ในการ refactoring บางส่วนของแอปพลิเคชันของฉันและสิ่งต่าง ๆ ดูสะอาดตามากขึ้นจนถึงตอนนี้
35358

1
คุณจะต้องระมัดระวังของรุ่นโลหิตจางก็เป็นที่ยอมรับอย่างสมบูรณ์สำหรับรูปแบบตัวอักษรที่จะมีพฤติกรรมเช่นWalk, และAttack Duckสิ่งที่ไม่เป็นไรคือการมีSaveและLoad(การคงอยู่) SRP ระบุว่าคลาสควรมีความรับผิดชอบเพียงอย่างเดียว แต่ความรับผิดชอบของตัวละครคือการเป็นตัวละครไม่ใช่ที่เก็บข้อมูล
Chris Wohlert

1
@ChrisWohlert นั่นคือเหตุผลว่าชื่อCharacterModelที่มีความรับผิดชอบเป็นที่จะเป็นที่เก็บข้อมูลจะแยกข้อมูลความกังวลชั้นจากชั้นตรรกะทางธุรกิจ มันอาจจะเป็นที่พึงปรารถนาสำหรับCharacterคลาสพฤติกรรมที่มีอยู่ด้วยเช่นกัน แต่ด้วยการกระทำ 80 รายการและคำสั่ง 30 รายการฉันก็จะพึ่งพามันต่อไป ส่วนใหญ่เวลาที่ฉันพบว่าคำนามเอนทิตีเป็น "ปลาเฮอริ่งแดง" สำหรับชื่อคลาสเพราะมันยากที่จะคาดการณ์ความรับผิดชอบจากคำนามเอนทิตีและมันง่ายเกินไปสำหรับพวกเขากลายเป็นมีดสวิสกองทัพกองทัพ
Ben Cottrell

10

คุณสามารถใช้คำจำกัดความที่เป็นนามธรรมของ "ความรับผิดชอบ" ได้เสมอ นั่นไม่ใช่วิธีที่ดีในการตัดสินสถานการณ์เหล่านี้อย่างน้อยก็จนกว่าคุณจะมีประสบการณ์มากมาย ขอให้สังเกตว่าคุณทำสัญลักษณ์สี่หัวข้อได้ง่ายซึ่งฉันจะเรียกว่าเป็นจุดเริ่มต้นที่ดีกว่าสำหรับกลุ่มย่อยของคุณ หากคุณกำลังติดตาม SRP อย่างแท้จริงมันเป็นเรื่องยากที่จะทำสัญลักษณ์แสดงหัวข้อย่อยแบบนั้น

อีกวิธีคือดูสมาชิกชั้นเรียนของคุณและแยกออกตามวิธีการที่ใช้งานจริง ตัวอย่างเช่นทำคลาสหนึ่งจากวิธีการทั้งหมดที่ใช้จริงself.timerอีกคลาสหนึ่งจากวิธีทั้งหมดที่ใช้จริงself.outgoingและคลาสอื่นออกจากส่วนที่เหลือ สร้างคลาสอื่นจากเมธอดของคุณที่ใช้การอ้างอิง db เป็นอาร์กิวเมนต์ เมื่อชั้นเรียนของคุณใหญ่เกินไปมักจะมีการจัดกลุ่มแบบนี้

อย่ากลัวที่จะแยกให้เล็กกว่าที่คุณคิดว่าเหมาะสมสำหรับการทดสอบ นั่นคือสิ่งที่ใช้ควบคุมเวอร์ชัน จุดสมดุลที่ถูกต้องนั้นง่ายกว่าที่จะมองเห็นหลังจากถ่ายไกลเกินไป


3

คำจำกัดความของ "ความรับผิดชอบ" นั้นคลุมเครือ แต่มันก็คลุมเครือน้อยลงถ้าคุณคิดว่ามันเป็น "เหตุผลในการเปลี่ยนแปลง" ยังคลุมเครือ แต่มีบางสิ่งที่คุณสามารถวิเคราะห์ได้โดยตรงมากขึ้น เหตุผลในการเปลี่ยนแปลงขึ้นอยู่กับโดเมนของคุณและวิธีการใช้ซอฟต์แวร์ของคุณ แต่เกมเป็นตัวอย่างที่ดีเพราะคุณสามารถตั้งสมมติฐานได้อย่างสมเหตุสมผล ในรหัสของคุณฉันนับความรับผิดชอบที่แตกต่างกันห้าประการในห้าบรรทัดแรก:

self.game = None
self.health = 1000
self.successful_attacks = 0
self.points = 0
self.timer = Timer()

การใช้งานของคุณจะเปลี่ยนแปลงหากข้อกำหนดของเกมเปลี่ยนไปด้วยวิธีใดวิธีหนึ่งต่อไปนี้:

  1. แนวคิดของสิ่งที่ถือเป็นการเปลี่ยนแปลง "เกม" นี่อาจเป็นไปได้น้อยที่สุด
  2. คุณวัดและติดตามการเปลี่ยนแปลงของคะแนนสุขภาพได้อย่างไร
  3. ระบบการโจมตีของคุณเปลี่ยนไป
  4. ระบบคะแนนของคุณเปลี่ยนแปลง
  5. ระบบจับเวลาของคุณเปลี่ยนไป

คุณกำลังโหลดจากฐานข้อมูลแก้ไขการโจมตีเชื่อมโยงกับเกมจับเวลาสิ่งต่าง ๆ ดูเหมือนว่าฉันจะมีรายการของความรับผิดชอบมานานแล้วและเราได้เห็นเพียงส่วนเล็ก ๆ ของCharacterชั้นเรียนของคุณ ดังนั้นคำตอบของคำถามส่วนหนึ่งของคุณคือไม่: ชั้นเรียนของคุณแทบจะไม่ปฏิบัติตาม SRP

อย่างไรก็ตามฉันจะบอกว่ามีหลายกรณีที่เป็นที่ยอมรับภายใต้ SRP ที่จะมีระดับ 2,500 บรรทัดหรือนานกว่านั้น ตัวอย่างบางส่วนอาจเป็น:

  • การคำนวณทางคณิตศาสตร์ที่ซับซ้อน แต่กำหนดไว้อย่างดีที่รับอินพุตที่กำหนดไว้อย่างดีและส่งคืนเอาต์พุตที่กำหนดไว้อย่างดี นี่อาจเป็นโค้ดที่ได้รับการปรับให้เหมาะสมที่สุดซึ่งต้องการหลายพันบรรทัด วิธีการทางคณิตศาสตร์ที่ได้รับการพิสูจน์แล้วสำหรับการคำนวณที่ชัดเจนไม่มีเหตุผลมากมายที่จะเปลี่ยนแปลง
  • คลาสที่ทำหน้าที่เป็นแหล่งข้อมูลเช่นคลาสที่มีเพียงyield return <N>จำนวน 10,000 หมายเลขแรกหรือคำภาษาอังกฤษที่พบบ่อยที่สุด 10,000 อันดับแรก มีเหตุผลที่เป็นไปได้ที่ทำให้การใช้งานนี้เป็นที่ต้องการมากกว่าการดึงจากแหล่งข้อมูลหรือไฟล์ข้อความ คลาสเหล่านี้มีเหตุผลน้อยมากที่จะเปลี่ยนแปลง (เช่นคุณพบว่าคุณต้องการมากกว่า 10,000)

2

เมื่อใดก็ตามที่คุณทำงานกับเอนทิตีอื่น ๆ คุณสามารถแนะนำวัตถุที่สามซึ่งจัดการแทน

def on_attack(victim, attack_type, points):
    self.points += points
    self.successful_attacks += 1
    self.outgoing.send_attack(self, victim, attack_type)
    self.timer.add_attack(attacker=True)

ที่นี่คุณสามารถแนะนำ 'AttackResolver' หรือบางสิ่งบางอย่างตามแนวเส้นเหล่านั้นที่จัดการการรวบรวมและรวบรวมสถิติ on_attack ที่นี่เกี่ยวกับสถานะตัวละครจะทำอะไรได้มากกว่านี้ไหม?

นอกจากนี้คุณยังสามารถทบทวนสถานะและถามตัวเองว่าสถานะบางอย่างที่คุณจำเป็นต้องมีกับตัวละครจริงหรือไม่ 'success_attack' ดูเหมือนสิ่งที่คุณสามารถติดตามในชั้นเรียนอื่นได้เช่นกัน

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.