Tek sorumluluğa sahip büyük sınıf


13

2500 Satır Charactersınıfı var:

  • Oyundaki karakterin dahili durumunu izler.
  • Bu durumu yükler ve devam ettirir.
  • ~ 30 gelen komutu işler (genellikle = komut satırına iletir Game, ancak bazı salt okunur komutlar hemen yanıtlanır).
  • Yaptığı Gameeylemler ve diğerlerinin ilgili eylemleri hakkında ~ 80 çağrı alır.

Bana göre Charactertek bir sorumluluğu var: karakterin durumunu yönetmek, gelen komutlar ve Oyun arasında aracılık etmek.

Zaten dağılmış olan birkaç sorumluluk daha vardır:

  • CharacterBir sahiptir Outgoingistemci uygulaması için giden güncelleştirmeleri oluşturmak için içine çağırır hangi.
  • Characterdaha sonra bir Timerşey yapmasına izin verildiğinde izler. Gelen komutlar buna göre doğrulanır.

Benim sorum şu, SRP ve benzeri ilkeler altında bu kadar büyük bir sınıfa sahip olmak kabul edilebilir mi? Daha az hantal yapmak için en iyi uygulamalar var mı (örneğin, yöntemleri ayrı dosyalara bölmek)? Yoksa bir şey mi kaçırıyorum ve onu ayırmanın gerçekten iyi bir yolu var mı? Bunun oldukça öznel olduğunu ve başkalarından geri bildirim almak istediğini anlıyorum.

İşte bir örnek:

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
Bunun bir yazım hatası olduğunu düşünüyorum: db.save_character_data(self, health, self.successful_attacks, self.points)Öyle self.healthmi demek istiyorsun?
candied_orange

5
Karakterin doğru soyutlama seviyesinde kalırsa, bir sorun görmüyorum. Öte yandan, yükleme ve kendi kendini devam ettirme sözlerinin tüm ayrıntılarını gerçekten ele alıyorsa, tek sorumluluğu takip etmiyorsunuzdur. Delegasyon burada gerçekten önemli. Karakterin zamanlayıcı gibi bazı düşük seviyeli detaylar hakkında bilgi sahibi olduğunu görünce, zaten çok fazla bildiği hissine sahibim.
Philip Stuyck

1
Sınıf tek bir soyutlama düzeyinde çalışmalıdır. Örneğin devletin depolanması gibi ayrıntılara girmemelidir. İç kısımlardan sorumlu daha küçük parçaları parçalayabilmelisiniz. Komut deseni burada yararlı olabilir. Ayrıca bkz. Google.pl/url?sa=t&source=web&rct=j&url=http://…
Piotr Gwiazda

Tüm yorumlar ve cevaplar için teşekkürler. Sanırım bir şeyleri yeterince parçalamıyordum ve büyük belirsiz sınıflarda çok fazla şey tutmaya devam ediyordum. Komut desenini kullanmak şimdiye kadar çok yardımcı oldu. Ayrıca farklı soyutlama düzeylerinde (ör. Soket, oyun mesajları, oyun komutları) çalışan katmanlara bir şey ayırıyorum. İlerleme kaydediyorum!
user35358

1
Bununla başa çıkmanın bir başka yolu da "CharacterState" i bir sınıf, "CharacterInputHandler" olarak başka bir karakter, "CharacterPersistance" olarak başka bir karaktere sahip olmaktır
T. Sar

Yanıtlar:


14

Bir soruna SRP uygulama girişimlerimde, genellikle sınıf başına tek sorumluluk için iyi bir yolun, sorumluluklarını ifade eden sınıf isimlerini seçmek olduğunu düşünüyorum, çünkü çoğu zaman bazı işlevlerin daha açık olup olmadığını düşünmeye yardımcı olur. gerçekten o sınıfa ait.

Dahası, ben gibi basit isimler hissetmek Character(veya Employee, Person, Car, Animal, vb) onlar gerçekten tarif çünkü çoğu zaman çok kötü sınıf isimleri yapmak varlıkları uygulamanızda (veri) ve sınıflara muamelesi zaman çok kolay ile bitirmek için sık sık bulunuyor çok şişkin bir şey.

'İyi' sınıf adlarının, programınızın davranışının bazı yönlerini anlamlı bir şekilde ileten etiketler olduğunu görüyorum - başka bir programcı sınıfınızın adını gördüğünde, o sınıfın davranışı / işlevselliği hakkında zaten temel bir fikir ediniyorlar.

Genel bir kural olarak, Varlıkları veri modelleri ve Sınıfları da davranış temsilcileri olarak düşünme eğilimindeyim . (Tabii ki çoğu programlama dili classher ikisi için de bir anahtar kelime kullanmasına rağmen, 'düz' varlıkları uygulama davranışından ayrı tutma fikri dil açısından tarafsızdır)

Karakter sınıfınız için bahsettiğiniz çeşitli sorumlulukların dökümü göz önüne alındığında, isimleri yerine getirdikleri şartı temel alan sınıflara yaslanmaya başlarım. Örneğin:

  • CharacterModelDavranışı olmayan ve sadece Karakterlerinizin durumunu tutan (verileri tutan) bir varlığı düşünün .
  • Kalıcılık / ES için CharacterReaderve CharacterWriter (veya belki de CharacterRepository/ CharacterSerialiser/ etc) gibi adları düşünün .
  • Komutlarınız arasında ne tür kalıplar olduğunu düşünün; 30 komutunuz varsa potansiyel olarak 30 ayrı sorumluluğunuz vardır; bazıları örtüşebilir, ama ayrılmak için iyi bir aday gibi görünüyorlar.
  • Aynı yeniden düzenleme işlemini Eylemlerinize de uygulayıp uygulayamayacağınızı düşünün - tekrar 80 eylem, muhtemelen bir miktar örtüşme ile birlikte 80'e kadar ayrı sorumluluk önerebilir.
  • Komutların ve eylemlerin ayrılması, bu komutları / eylemleri çalıştırmak / tetiklemekten sorumlu olan başka bir sınıfa da yol açabilir; belki uygulamanızın "ara katman yazılımı" gibi davranan CommandBrokerveya ActionBrokerfarklı nesneler arasındaki bu komutları ve eylemleri gönderme / alma / yürütme gibi davranabilir

Ayrıca, davranışla ilgili her şeyin bir sınıfın parçası olarak var olması gerekmediğini unutmayın; örneğin, düzinelerce durum bilgisi olmayan tek yöntemli sınıf yazmak yerine eylemlerinizi / komutlarınızı kapsüllemek için bir harita / işlev işaretçileri / delegeler / kapaklar sözlüğü kullanmayı düşünebilirsiniz.

İmza / arabirimi paylaşan statik yöntemler kullanılarak oluşturulan sınıfları yazmadan 'komut deseni' çözümlerini görmek oldukça yaygındır:

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

Son olarak, tek bir sorumluluk elde etmek için ne kadar ileri gitmeniz gerektiğine dair zor ve hızlı kurallar yoktur. Karmaşıklık uğruna karmaşıklık iyi bir şey değildir, ancak megalitik sınıflar kendi içlerinde oldukça karmaşık olma eğilimindedir. SRP'nin ve aslında diğer SOLID ilkelerinin temel amacı, yapı, tutarlılık sağlamak ve kodu daha sürdürülebilir hale getirmektir - bu genellikle daha basit bir şeyle sonuçlanır.


Bu cevabın sorunumun temelini oluşturduğunu düşünüyorum, teşekkür ederim. Uygulamamın bazı bölümlerini yeniden düzenlemede kullanıyorum ve işler şimdiye kadar çok daha temiz görünüyor.
user35358

1
Sen dikkatli olmak zorunda anemik modelleri Karakter modeli gibi davranış olması için, mükemmel kabul edilebilir Walk, Attackve Duck. İyi olmayan, sahip olmak Saveve Load(sebat etmek). SRP, bir sınıfın yalnızca bir sorumluluğa sahip olması gerektiğini belirtir, ancak Karakter'in sorumluluğu bir veri kabı değil bir karakter olmaktır.
Chris Wohlert

1
O isim nedeni @ChrisWohlert CharacterModelsorumluluğu olduğu mantığı katmanlarıyla ayrışamayacağını Veri Katmanı kaygıları veri konteyner olmak. Davranışsal bir Charactersınıfın bir yerde de var olması hala arzu edilebilir , ancak 80 eylem ve 30 komutla onu daha da yıkmaya eğilimliydim. Çoğu zaman varlık isimlerinin sınıf isimleri için bir "kırmızı ringa" olduğunu görüyorum, çünkü bir varlık isminden sorumluluğu tahmin etmek zordur ve onların bir çeşit İsviçre-ordu bıçağı haline gelmeleri çok kolaydır.
Ben Cottrell

10

Her zaman "sorumluluk" un daha soyut bir tanımını kullanabilirsiniz. Bu durumları yargılamak için çok iyi bir yol değil, en azından deneyimleyene kadar. Kolayca dört mermi noktası yaptığını fark ettim, ki bu da sınıf ayrıntı düzeyiniz için daha iyi bir başlangıç ​​noktası diyeceğim. SRP'yi gerçekten takip ediyorsanız, böyle mermi noktaları yapmak zor.

Başka bir yol da sınıf üyelerinize bakmak ve onları gerçekten kullanan yöntemlere göre ayrılmaktır. Örneğin, bir sınıfı gerçekte kullanılan tüm yöntemlerden self.timer, başka bir sınıfı gerçekte kullanılan tüm yöntemlerden self.outgoingve başka bir sınıfı geri kalanından çıkarın. Bağımsız değişken olarak db başvurusu alan yöntemlerinizden başka bir sınıf yapın. Sınıflarınız çok büyük olduğunda, genellikle bunun gibi gruplamalar vardır.

Bir deneme olarak makul olduğunu düşündüğünüzden daha küçük bir şekilde bölmekten korkmayın. Sürüm kontrolü bunun içindir. Doğru dengeyi çok ileri götürdükten sonra görmek çok daha kolaydır.


3

"Sorumluluk" tanımı herkesin bildiği gibi belirsizdir, ancak bunu "değişim nedeni" olarak düşünürseniz biraz daha az belirsiz olur. Hala belirsiz, ama biraz daha doğrudan analiz edebileceğiniz bir şey. Değişiklik nedenleri alan adınıza ve yazılımınızın nasıl kullanılacağına bağlıdır, ancak oyunlar bununla ilgili makul varsayımlar yapabileceğiniz için güzel örnek vakalardır. Kodunuzda, ilk beş satırda beş farklı sorumluluk sayıyorum:

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

Oyun gereksinimleri aşağıdaki yollardan biriyle değişirse uygulamanız değişecektir:

  1. “Oyun” u neyin oluşturduğu fikri değişir. Bu en düşük olasılık olabilir.
  2. Sağlık puanı değişikliklerini nasıl ölçersiniz ve izlersiniz
  3. Saldırı sisteminiz değişiyor
  4. Puan sisteminiz değişiyor
  5. Zamanlama sisteminiz değişiyor

Veritabanlarından yüklüyorsunuz, saldırıları çözüyorsunuz, oyunlarla bağlantı kuruyorsunuz, şeyleri zamanlıyorsunuz; Bana öyle geliyor ki sorumluluklar listesi çok uzun ve Charactersınıfınızın sadece küçük bir kısmını gördük . Yani sorunuzun bir kısmının cevabı hayır: sınıfınız neredeyse kesinlikle SRP'yi takip etmiyor.

Ancak, SRP altında 2.500 ya da daha uzun bir sınıfa sahip olmanın kabul edilebilir olduğu durumlar olduğunu söyleyebilirim. Bazı örnekler şunlar olabilir:

  • İyi tanımlanmış girdi alan ve iyi tanımlanmış çıktı döndüren oldukça karmaşık ancak iyi tanımlanmış bir matematiksel hesaplama. Bu, binlerce satıra ihtiyaç duyan oldukça optimize edilmiş bir kod olabilir. İyi tanımlanmış hesaplamalar için kanıtlanmış matematiksel yöntemlerin değişmek için pek bir nedeni yoktur.
  • Yalnızca yield return <N>ilk 10.000 asal sayıya sahip bir sınıf veya en yaygın 10.000 İngilizce sözcük gibi bir veri deposu görevi gören bir sınıf . Bu uygulamanın bir veri deposundan veya metin dosyasından çekme yerine tercih edilmesinin olası nedenleri vardır. Bu sınıfların değiştirmek için çok az nedeni vardır (örneğin 10.000'den fazlasına ihtiyacınız olduğunu görürsünüz).

2

Başka bir varlığa karşı çalıştığınızda, bunun yerine işlemeyi yapan üçüncü bir nesneyi tanıtabilirsiniz.

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)

Burada bu satırlar boyunca istatistiklerin gönderilmesini ve toplanmasını işleyen bir 'AttackResolver' veya benzeri bir şey sunabilirsiniz. Burada on_attack sadece karakter durumu hakkında mı daha fazlasını yapıyor?

Ayrıca, durumu yeniden ziyaret edebilir ve kendinize sahip olduğunuz bazı durumların karakterde olması gerekip gerekmediğini kendinize sorabilirsiniz. 'başarılı_atak' başka bir sınıfta da potansiyel olarak izleyebileceğiniz bir şey gibi geliyor.

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.