web-dev-qa-db-ja.com

Python“ god class”をリファクタリングする方法は?

問題

Pythonプロジェクトのメインクラスが少し「 God Object 」であるプロジェクトに取り組んでいます。so friggin 'の多くの属性があり、メソッド!

クラスをリファクタリングしたい。

これまでのところ…

最初のステップとして、私は比較的単純なことをしたいと思います。しかし、最も簡単な方法を試したところ、いくつかのテストと既存の例が破られました。

基本的に、クラスには属性のリストがたくさんありますが、私はそれらを明確に調べて考えることができます「これらの5つの属性は関連しています...これらの8つの属性も関連しています...そして残りがあります。」

getattr

基本的に、関連する属性をdictのようなヘルパークラスにグループ化したかっただけです。 __getattr__はこの仕事に理想的だと感じました。そのため、属性を別のクラスに移動し、確かに、__getattr__はその魔法を完全にうまく機能させました…

最初

しかし、私は例の1つを実行してみました。サンプルのサブクラスは、これらの属性の1つを直接設定しようとします(クラスレベル)。しかし、属性が親クラスで「物理的に配置」されなくなったため、属性が存在しないというエラーが発生しました。

@property

次に、@propertyデコレータについて読みます。ただし、xが親クラスのプロパティである場合、self.x = blahを実行したいサブクラスに問題が発生することも読みました。

望まれる

  • 親のwhateverプロパティがクラス(またはインスタンス)自体に「物理的に」配置されていない場合でも、self.whateverを使用してすべてのクライアントコードを引き続き機能させます。
  • 関連する属性をdictのようなコンテナにグループ化します。
  • メインクラスのコードの極端なノイズを減らします。

たとえば、私はこれを変更したいだけです:

larry = 2
curly = 'abcd'
moe   = self.doh()

これに:

larry = something_else('larry')
curly = something_else('curly')
moe   = yet_another_thing.moe()

…それはまだうるさいからです。これにより、simple属性がデータを管理できる何かに正常に変換されますが、元の属性には3つの変数があり、調整されたバージョンにはまだ3つの変数があります。

しかし、私はこのようなもので大丈夫でしょう:

stooges = Stooges()

そして、self.larryの検索が失敗した場合、何かがstoogesをチェックし、larryが存在するかどうかを確認します。 (ただし、サブクラスがクラスレベルでlarry = 'blah'を実行しようとした場合にも機能する必要があります。)

概要

  • 親クラスの関連する属性グループを、他の場所にすべてのデータを格納する単一の属性に置き換えたい
  • クラスレベルで(例)larry = 'blah'を使用する既存のクライアントコードで作業したい
  • 変更されたことを知らずに、サブクラスがこれらのリファクタリングされた属性を拡張、オーバーライド、および変更できるようにしたい


これは可能ですか?または私は間違った木を吠えていますか?

10
Zearin

python "God object"を記述してリファクタリングした後、私は同情しました。私が行ったのは、メソッドに基づいて元のオブジェクトをサブセクションに分割することです。たとえば、元のオブジェクトはこの疑似コードのように見えました:

method A():
    self.bla += 1

method B():
    self.bla += 1

do stuff():
    self.bla = 1
    method A()
    method B()
    print self.bla

スタッフメソッドは、自己完結型の作業の「単位」です。オリジナルをインスタンス化する新しいクラスに移行しました。これにより、必要なプロパティも引き出されました。一部はサブクラスでのみ使用され、まっすぐに移動できました。他のものは共有され、共有クラスに移されました。

「Godオブジェクト」は、起動時に共有クラスの新しいコピーを作成し、新しいサブクラスのそれぞれは、initメソッドの一部としてポインターを受け入れます。たとえば、以下はメーラーのストリップバージョンです。

#!/usr/bin/env python
# -*- coding: ascii -*-
'''Functions for emailing with dirMon.'''

from email.MIMEMultipart import MIMEMultipart
from email.MIMEBase import MIMEBase
from email.MIMEText import MIMEText
from email.Utils import COMMASPACE, formatdate
from email import Encoders
import os
import smtplib
import datetime
import logging

class mailer:
    def __init__(self,SERVER="mail.server.com",FROM="[email protected]"):
        self.server = SERVER
        self.send_from = FROM
        self.logger = logging.getLogger('dirMon.mailer')

    def send_mail(self, send_to, subject, text, files=[]):
        assert type(send_to)==list
        assert type(files)==list
        if self.logger.isEnabledFor(logging.DEBUG):
            self.logger.debug(' '.join(("Sending email to:",' '.join(send_to))))
            self.logger.debug(' '.join(("Subject:",subject)))
            self.logger.debug(' '.join(("Text:",text)))
            self.logger.debug(' '.join(("Files:",' '.join(files))))
        msg = MIMEMultipart()
        msg['From'] = self.send_from
        msg['To'] = COMMASPACE.join(send_to)
        msg['Date'] = formatdate(localtime=True)
        msg['Subject'] = subject
        msg.attach( MIMEText(text) )
        for f in files:
            part = MIMEBase('application', "octet-stream")
            part.set_payload( open(f,"rb").read() )
            Encoders.encode_base64(part)
            part.add_header('Content-Disposition', 'attachment; filename="%s"' % os.path.basename(f))
            msg.attach(part)
        smtp = smtplib.SMTP(self.server)
        mydict = smtp.sendmail(self.send_from, send_to, msg.as_string())
        if self.logger.isEnabledFor(logging.DEBUG):
            self.logger.debug("Email Successfully Sent!")
        smtp.close()
        return mydict

一度作成され、メール機能を必要とするさまざまなクラス間で共有されます。

したがって、必要なプロパティとメソッドを含むクラスlarryを作成します。クライアントがlarry = blahと言っている箇所はすべてlarryObj.larry = blahに置き換えます。これにより、現在のインターフェースを壊すことなく、サブプロジェクトに移行します。

他にすべきことは、「作業単位」を探すことだけです。 「God Object」の一部を独自のメソッドに変換する場合は、そうする。ただし、メソッドをoutsideに配置します。これにより、コンポーネント間のインターフェースを作成する必要があります。

その基礎を築くことは、他のすべてがそれに続くことを可能にします。たとえば、メイラーとのインターフェース方法を示すヘルパーオブジェクトの一部:

#!/usr/bin/env python
'''This module holds a class to spawn various subprocesses'''
import logging, os, subprocess, time, dateAdditionLib, datetime, re

class spawner:
    def __init__(self, mailer):
        self.logger = logging.getLogger('dirMon.spawner')
        self.myMailer = mailer

可能な最小の個別作業単位に集中し、それを外に移動します。これは簡単に実行でき、セットアップをすばやく実行できます。移動するもののプロパティを見てはいけません。ほとんどの場合、それらはtasksで行われている処理の補助的なものです。メソッドを処理した後に残ったものは、共有状態の一部であるため、おそらく元のオブジェクトにstayする必要があります。

But、新しいオブジェクトは必要なプロパティを初期化変数として受け入れるようになり、呼び出し元オブジェクトのプロパティには触れません。次に、呼び出し側が必要に応じて共有プロパティを更新するために使用できる必要な値を返します。これはオブジェクトを分離するのに役立ち、より堅牢なシステムになります。

9
Spencer Rathbun