단일 책임을 가진 큰 클래스


13

2500 라인 Character클래스가 있습니다.

  • 게임에서 캐릭터의 내부 상태를 추적합니다.
  • 해당 상태를로드하고 유지합니다.
  • ~ 30 개의 들어오는 명령을 처리합니다 (보통 =는 명령을 전달 Game하지만 일부 읽기 전용 명령은 즉시 응답합니다).
  • Game취한 조치 및 타인의 관련 조치와 관련하여 ~ 80 개의 전화를받습니다 .

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
클래스는 단일 추상화 수준에서 작동해야합니다. 예를 들어 상태 저장과 같은 세부 사항에 들어가서는 안됩니다. 내부를 담당하는 더 작은 청크를 분해 할 수 있어야합니다. 여기서 명령 패턴이 유용 할 수 있습니다. 또한 참조 google.pl/url?sa=t&source=web&rct=j&url=http://...
표트르 Gwiazda

의견과 답변에 감사드립니다. 나는 단지 물건을 충분히 분해하지 않았고, 큰 성가신 수업에 너무 많이 머물러 있다고 생각했습니다. 지금까지 명령 패턴을 사용하는 것이 큰 도움이되었습니다. 또한 다른 추상화 수준 (예 : 소켓, 게임 메시지, 게임 명령)에서 작동하는 레이어로 항목을 분리했습니다. 진행 중입니다!
user35358

1
이것을 해결하는 또 다른 방법은 "CharacterState"를 클래스로, "CharacterInputHandler"를 다른 것으로, "CharacterPersistance"를 다른 것으로 만드는 것입니다.
T. Sar

답변:


14

문제에 SRP를 적용하려는 시도에서 일반적으로 클래스 당 단일 책임을 고수하는 좋은 방법은 책임을 암시하는 클래스 이름을 선택하는 것입니다. 그 수업에서 정말 '포함'합니다.

또한, 나는 다음과 같은 간단한 명사 느낌 Character(또는 Employee, Person, Car, Animal, 등) 그들이 정말 설명하기 때문에 종종 매우 가난한 클래스 이름을 개체 응용 프로그램에서 (데이터), 및 클래스로 처리 할 때 너무 쉽게 끝낼 자주의 매우 부풀어 오른 것.

'좋은'클래스 이름은 프로그램 동작의 일부 측면을 의미있게 나타내는 레이블 인 경향이 있습니다. 즉, 다른 프로그래머가 클래스 이름을 볼 때 이미 해당 클래스의 동작 / 기능에 대한 기본 아이디어를 얻습니다.

경험상, 나는 엔티티 를 데이터 모델로 생각 하고 클래스 를 행동의 대표자 로 생각하는 경향 이 있습니다. (물론 대부분의 프로그래밍 언어 class는 둘 다에 키워드를 사용 하지만 '일반'엔터티를 응용 프로그램 동작과 분리시키는 아이디어는 언어 중립적입니다)

당신이 당신의 캐릭터 클래스에 대해 언급 한 다양한 책임들이 무너지면서, 나는 그들이 충족시켜야 할 요구 사항에 기초한 이름을 가진 클래스에 기대기 시작합니다. 예를 들면 다음과 같습니다.

  • CharacterModel행동이없고 단순히 캐릭터의 상태를 유지 하는 엔티티 (데이터를 보유)를 고려하십시오.
  • 지속성 / IO의 경우 CharacterReaderand CharacterWriter (또는 CharacterRepository/ CharacterSerialiser/ etc 등) 와 같은 이름을 고려하십시오 .
  • 당신의 명령 사이에 어떤 종류의 패턴이 있는지 생각하십시오. 30 개의 명령이있는 경우 30 개의 개별 책임이있을 수 있습니다. 일부는 겹칠 수 있지만 분리하기에 좋은 후보 인 것 같습니다.
  • 액션에 동일한 리팩토링을 적용 할 수 있는지 고려하십시오. 다시 말하지만 80 개의 액션은 80 개의 개별 책임을 제안 할 수 있으며 일부 중복 될 수도 있습니다.
  • 커맨드와 액션의 분리는 커맨드 / 액션 실행 / 발행을 담당하는 다른 클래스로 이어질 수도 있습니다. 어쩌면 응용 프로그램의 "미들웨어"처럼 다른 객체 사이에서 해당 명령과 작업을 보내거나 받고 / 실행하는 것처럼 작동 CommandBroker하거나ActionBroker

또한 행동과 관련된 모든 것이 반드시 수업의 일부로 존재할 필요는 없다는 것을 기억하십시오. 예를 들어, 수십 개의 상태 비 저장 단일 메소드 클래스를 작성하는 대신 맵 / 사전의 함수 포인터 / 위임 / 클로저를 사용하여 조치 / 명령을 캡슐화하는 것을 고려할 수 있습니다.

서명 / 인터페이스를 공유하는 정적 메소드를 사용하여 작성된 클래스를 작성하지 않고 '명령 패턴'솔루션을 보는 것이 일반적입니다.

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

마지막으로, 단일 책임을 달성하기 위해 얼마나 멀리 가야하는지에 대한 어렵고 빠른 규칙은 없습니다. 복잡성을위한 복잡성은 좋지 않지만 거석 클래스는 그 자체로 상당히 복잡한 경향이 있습니다. SRP와 실제로 다른 SOLID 원칙의 주요 목표는 구조, 일관성 및 코드 유지 관리를보다 쉽게하는 것입니다.


이 답변이 내 문제의 핵심을 해결했다고 생각합니다. 감사합니다. 응용 프로그램의 일부를 리팩토링하는 데 사용했으며 지금까지 상황이 훨씬 깨끗해졌습니다.
user35358

1
당신의 조심해야 빈혈 모델 문자 모델은 같은 동작을하는 것은 완벽하게 수용 Walk, Attack그리고 Duck. 옳지 않은 것은 가지고 Save있고 Load(지속성)입니다. SRP는 클래스가 하나의 책임 만 가져야한다고 말하지만 Character의 책임은 데이터 컨테이너가 아닌 캐릭터 여야합니다.
크리스 Wohlert

1
@ChrisWohlert 이것이 바로 비즈니스 로직 계층에서 데이터 계층 문제를 분리하기위한 데이터 컨테이너 되는 CharacterModel책임 의 이름 입니다. 실제로 행동 클래스가 어딘가에 존재 하는 것이 여전히 바람직 할 수 있지만, 80 개의 행동과 30 개의 명령으로 더 세분화하려고합니다. 대부분의 경우, 엔티티 명사에서 엔티티 명사에 대한 책임을 추정하기가 어렵 기 때문에 클래스 명에 대한 "빨간 청어"라는 사실을 알게되었으며, 스위스 명예의 일종으로 전환하기가 너무 쉽습니다. Character
벤 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이 더 많은 일을하고 있습니까?

당신은 또한 상태를 다시 방문하고 당신이 실제로 어떤 상태에 있어야 하는지를 물어볼 수 있습니다. 'successful_attack'은 다른 클래스에서도 추적 할 수있는 것처럼 들립니다.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.