Lớp lớn với trách nhiệm duy nhất


13

Tôi có một Characterlớp 2500 dòng :

  • Theo dõi trạng thái nội bộ của nhân vật trong trò chơi.
  • Tải và tồn tại trạng thái đó.
  • Xử lý ~ 30 lệnh đến (thường = chuyển tiếp chúng tới Game, nhưng một số lệnh chỉ đọc được phản hồi ngay lập tức).
  • Nhận được ~ 80 cuộc gọi từ Gamecác hành động liên quan đến nó và các hành động liên quan của người khác.

Dường như với tôi Charactercó một trách nhiệm duy nhất: quản lý trạng thái của nhân vật, làm trung gian giữa các lệnh đến và Trò chơi.

Có một vài trách nhiệm khác đã bị phá vỡ:

  • Charactercó một Outgoingcái mà nó gọi vào để tạo ra các bản cập nhật gửi đi cho ứng dụng khách.
  • Charactercó một dấu Timervết khi nó tiếp theo được phép làm một cái gì đó. Các lệnh đến được xác nhận chống lại điều này.

Vì vậy, câu hỏi của tôi là, có thể chấp nhận có một lớp lớn như vậy theo SRP và các nguyên tắc tương tự không? Có cách thực hành tốt nhất nào để làm cho nó bớt cồng kềnh hơn (ví dụ: có thể chia các phương thức thành các tệp riêng biệt) không? Hay tôi đang thiếu một cái gì đó và thực sự có một cách tốt để chia nó ra? Tôi nhận ra điều này khá chủ quan và muốn phản hồi từ người khác.

Đây là một mẫu:

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
Tôi đoán đây là một lỗi đánh máy: Ý db.save_character_data(self, health, self.successful_attacks, self.points)bạn là self.healthđúng?
candied_orange

5
Nếu nhân vật của bạn ở mức trừu tượng chính xác, tôi không thấy vấn đề gì. Mặt khác, nếu nó thực sự xử lý tất cả các chi tiết về việc nói và tải chính nó, thì bạn không tuân theo trách nhiệm đơn lẻ. Đoàn là thực sự quan trọng ở đây. Thấy rằng nhân vật của bạn biết về một số chi tiết cấp thấp như bộ đếm thời gian và như vậy, tôi có cảm giác nó đã biết quá nhiều.
Philip Stuyck

1
Các lớp nên hoạt động trên một mức độ trừu tượng duy nhất. Nó không nên đi vào chi tiết ví dụ như lưu trữ trạng thái. Bạn sẽ có thể phân hủy các phần nhỏ hơn chịu trách nhiệm cho nội bộ. Mẫu lệnh có thể hữu ích ở đây. Ngoài ra, hãy xem google.pl/url?sa=t&source=web&rct=j&url=http://iêu
Piotr Gwiazda

Cảm ơn tất cả các ý kiến ​​và câu trả lời. Tôi nghĩ rằng tôi chỉ không phân hủy đủ mọi thứ, và đã bám vào việc giữ quá nhiều trong các lớp học lớn. Sử dụng mẫu lệnh đã giúp ích rất nhiều cho đến nay. Tôi cũng đã chia mọi thứ thành các lớp hoạt động ở các mức độ trừu tượng khác nhau (ví dụ: socket, thông điệp trò chơi, lệnh trò chơi). Tôi đang tiến bộ!
3558

1
Một cách khác để giải quyết vấn đề này là lấy "CharacterState" làm một lớp, "CharacterInputHandler" như một thứ khác, "CharacterPersistance" như một ...
T. Sar - Tái lập lại

Câu trả lời:


14

Trong các nỗ lực của tôi để áp dụng SRP cho một vấn đề, tôi thường thấy rằng một cách tốt để gắn bó với một lớp chịu trách nhiệm duy nhất là chọn các tên lớp liên quan đến trách nhiệm của họ, bởi vì nó thường giúp suy nghĩ rõ ràng hơn về việc liệu một số chức năng thực sự 'thuộc về' trong lớp đó.

Hơn nữa, tôi cảm thấy rằng danh từ đơn giản như Character(hoặc Employee, Person, Car, Animal, vv) thường làm cho tên lớp rất nghèo vì họ thực sự mô tả thực thể (dữ liệu) trong ứng dụng của bạn, và khi đối xử như lớp nó thường quá dễ dàng để kết thúc với một cái gì đó rất bồng bềnh.

Tôi thấy rằng các tên lớp 'tốt' có xu hướng là các nhãn truyền tải một cách có ý nghĩa một số khía cạnh của hành vi chương trình của bạn - tức là khi một lập trình viên khác nhìn thấy tên của lớp bạn, họ đã có được ý tưởng cơ bản về hành vi / chức năng của lớp đó.

Theo nguyên tắc thông thường, tôi có xu hướng nghĩ Thực thể là mô hình dữ liệu và Lớp học là đại diện cho hành vi. (Mặc dù tất nhiên hầu hết các ngôn ngữ lập trình đều sử dụng classtừ khóa cho cả hai, nhưng ý tưởng giữ các thực thể 'đơn giản' tách biệt khỏi hành vi ứng dụng là ngôn ngữ trung lập)

Khi phân tích các trách nhiệm khác nhau mà bạn đã đề cập cho lớp nhân vật của mình, tôi sẽ bắt đầu nghiêng về các lớp có tên dựa trên yêu cầu mà họ thực hiện. Ví dụ:

  • Xem xét một CharacterModelthực thể không có hành vi và chỉ duy trì trạng thái Nhân vật của bạn (giữ dữ liệu).
  • Đối với tính bền vững / IO, hãy xem xét các tên như CharacterReaderCharacterWriter (hoặc có thể là CharacterRepository/ CharacterSerialiser/ etc).
  • Hãy suy nghĩ về loại mô hình tồn tại giữa các lệnh của bạn; nếu bạn có 30 lệnh thì bạn có khả năng có 30 trách nhiệm riêng biệt; một số trong đó có thể chồng lấp, nhưng chúng có vẻ như là một ứng cử viên tốt để tách.
  • Xem xét liệu bạn có thể áp dụng cùng cấu trúc lại cho Hành động của mình không - một lần nữa, 80 hành động có thể đề xuất tới 80 trách nhiệm riêng biệt, cũng có thể với một số chồng chéo.
  • Việc tách các lệnh và hành động cũng có thể dẫn đến một lớp khác chịu trách nhiệm chạy / bắn các lệnh / hành động đó; có thể một số loại CommandBrokerhoặc ActionBrokerhoạt động giống như "phần mềm trung gian" gửi / nhận / thực thi các lệnh và hành động giữa các đối tượng khác nhau của ứng dụng của bạn

Cũng nên nhớ rằng không phải mọi thứ liên quan đến hành vi nhất thiết cần phải tồn tại như một phần của một lớp; ví dụ, bạn có thể xem xét sử dụng bản đồ / từ điển của các con trỏ hàm / đại biểu / bao đóng để đóng gói các hành động / lệnh của bạn thay vì viết hàng chục lớp phương thức đơn không trạng thái.

Khá phổ biến để xem các giải pháp 'mẫu lệnh' mà không cần viết bất kỳ lớp nào được xây dựng bằng các phương thức tĩnh chia sẻ chữ ký / giao diện:

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

Cuối cùng, không có quy tắc cứng và nhanh nào về việc bạn nên đi bao xa để đạt được trách nhiệm duy nhất. Sự phức tạp vì sự phức tạp không phải là một điều tốt, nhưng bản thân các lớp cự thạch có xu hướng khá phức tạp. Mục tiêu chính của SRP và các nguyên tắc RẮN khác là cung cấp cấu trúc, tính nhất quán và làm cho mã dễ bảo trì hơn - điều này thường dẫn đến một cái gì đó đơn giản hơn.


Tôi nghĩ rằng câu trả lời này đã giải quyết được mấu chốt của vấn đề của tôi, cảm ơn bạn. Tôi đã đưa nó vào sử dụng để tái cấu trúc các phần trong ứng dụng của tôi và mọi thứ trông có vẻ sạch sẽ hơn nhiều cho đến nay.
3558

1
Bạn phải cẩn thận của mô hình thiếu máu , nó là hoàn toàn có thể chấp nhận được cho các mô hình nhân vật có hành vi như Walk, AttackDuck. Điều không ổn, là có SaveLoad(kiên trì). SRP tuyên bố rằng một lớp chỉ nên có một trách nhiệm, nhưng trách nhiệm của Nhân vật là trở thành một nhân vật, không phải là một thùng chứa dữ liệu.
Chris Wohlert

1
@ChrisWohlert Đó là lý do cho cái tên CharacterModel, có trách nhiệm một thùng chứa dữ liệu để giải mã các mối quan tâm của Lớp dữ liệu từ lớp Logic nghiệp vụ. Thực sự có thể vẫn còn mong muốn một Characterlớp hành vi tồn tại ở đâu đó, nhưng với 80 hành động và 30 lệnh tôi sẽ nghiêng về việc phá vỡ nó hơn nữa. Hầu hết thời gian tôi thấy rằng các danh từ thực thể là một "cá trích đỏ" cho các tên lớp vì khó có thể ngoại suy trách nhiệm từ một danh từ thực thể, và tất cả đều quá dễ dàng để chúng biến thành một loại dao quân đội Thụy Sĩ.
Ben Cottrell

10

Bạn luôn có thể sử dụng một định nghĩa trừu tượng hơn về "trách nhiệm". Đó không phải là một cách hay để đánh giá những tình huống này, ít nhất là cho đến khi bạn có nhiều kinh nghiệm về nó. Lưu ý rằng bạn dễ dàng thực hiện bốn điểm đạn, mà tôi sẽ gọi là điểm khởi đầu tốt hơn cho mức độ chi tiết của lớp bạn. Nếu bạn thực sự theo dõi SRP, thật khó để tạo ra những gạch đầu dòng như thế.

Một cách khác là nhìn vào các thành viên trong lớp của bạn và phân tách dựa trên các phương pháp thực sự sử dụng chúng. Ví dụ, tạo một lớp trong số tất cả các phương thức thực sự sử dụng self.timer, một lớp khác trong số tất cả các phương thức thực sự sử dụng self.outgoingvà một lớp khác ngoài phần còn lại. Tạo một lớp khác trong số các phương thức của bạn lấy tham chiếu db làm đối số. Khi các lớp học của bạn quá lớn, thường có những nhóm như thế này.

Đừng sợ chia nhỏ nó ra như bạn nghĩ là hợp lý như một thử nghiệm. Đó là những gì kiểm soát phiên bản dành cho. Điểm cân bằng bên phải dễ nhìn hơn nhiều sau khi đưa nó đi quá xa.


3

Định nghĩa về "trách nhiệm" nổi tiếng là mơ hồ, nhưng nó trở nên hơi mơ hồ nếu bạn nghĩ đó là một "lý do để thay đổi". Vẫn còn mơ hồ, nhưng một cái gì đó bạn có thể phân tích trực tiếp hơn một chút. Lý do thay đổi phụ thuộc vào tên miền của bạn và cách phần mềm của bạn sẽ được sử dụng, nhưng trò chơi là trường hợp ví dụ hay vì bạn có thể đưa ra các giả định hợp lý về điều đó. Trong mã của bạn, tôi đếm năm trách nhiệm khác nhau trong năm dòng đầu tiên:

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

Việc triển khai của bạn sẽ thay đổi nếu các yêu cầu trò chơi thay đổi theo bất kỳ cách nào sau đây:

  1. Khái niệm về những gì cấu thành một "trò chơi" thay đổi. Đây có thể là ít khả năng nhất.
  2. Cách bạn đo lường và theo dõi sự thay đổi điểm sức khỏe
  3. Hệ thống tấn công của bạn thay đổi
  4. Hệ thống điểm của bạn thay đổi
  5. Hệ thống thời gian của bạn thay đổi

Bạn đang tải từ cơ sở dữ liệu, giải quyết các cuộc tấn công, liên kết với các trò chơi, điều chỉnh thời gian; đối với tôi, danh sách trách nhiệm đã có từ rất lâu và chúng tôi chỉ thấy một phần nhỏ trong Characterlớp học của bạn . Vì vậy, câu trả lời cho một phần câu hỏi của bạn là không: lớp của bạn gần như chắc chắn không tuân theo SRP.

Tuy nhiên, tôi sẽ nói rằng có những trường hợp được SRP chấp nhận để có lớp 2.500 dòng hoặc dài hơn. Một số ví dụ có thể là:

  • Một phép tính toán học rất phức tạp nhưng được xác định rõ ràng, lấy đầu vào được xác định rõ và trả về đầu ra được xác định rõ. Đây có thể là mã được tối ưu hóa cao, cần hàng ngàn dòng. Các phương pháp toán học đã được chứng minh cho các tính toán được xác định rõ ràng không có nhiều lý do để thay đổi.
  • Một lớp hoạt động như một kho lưu trữ dữ liệu, chẳng hạn như một lớp chỉ có yield return <N>10.000 số nguyên tố đầu tiên hoặc 10.000 từ tiếng Anh phổ biến nhất. Có nhiều lý do tại sao việc triển khai này sẽ được ưu tiên hơn là lấy từ kho lưu trữ dữ liệu hoặc tệp văn bản. Các lớp này có rất ít lý do để thay đổi (ví dụ: bạn thấy bạn cần hơn 10.000).

2

Bất cứ khi nào bạn làm việc chống lại một số thực thể khác, bạn có thể giới thiệu một đối tượng thứ ba thực hiện việc xử lý thay thế.

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)

Tại đây, bạn có thể giới thiệu 'AttackResolver' hoặc thứ gì đó dọc theo những dòng xử lý việc gửi và thu thập số liệu thống kê. Là on_attack ở đây chỉ về trạng thái nhân vật là nó làm nhiều hơn?

Bạn cũng có thể xem lại trạng thái và tự hỏi nếu một số trạng thái bạn thực sự cần phải có trên nhân vật. 'Thành công_ tấn công' nghe có vẻ như một cái gì đó bạn có khả năng có thể theo dõi trên một số lớp khác.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.