Pythonの「神クラス」をリファクタリングする方法は?


10

問題

私は、メインクラスが少し「神オブジェクト」であるPythonプロジェクトに取り組んでいます。そこされているので、多くの属性とメソッドのfrigginの!

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

これまでのところ…

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

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

getattr

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

で、最初に

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

@property

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

望ましい

  • self.whatever親の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失敗した場合、何かがチェックされstoogeslarryそこにあるかどうかが確認されます。(ただし、サブクラスlarry = 'blah'がクラスレベルで実行しようとした場合にも機能する必要があります。)

概要

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


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


6
実装の一部を分離したとしても、この巨大な神のようなインターフェースを維持することを主張した場合の利点の半分が失われます。ショートカットを提供することはできますが、変数を別の名前空間に配置してそれらに完全にリダイレクトするだけでは、ほとんど何もできません。

1
@delnan:じゃあ、代わりに何を勧めますか?
Zearin 2012年

回答:


9

Pythonの「神オブジェクト」を作成してリファクタリングしたので、私は同情します。私がしたことは、メソッドに基づいて元のオブジェクトをサブセクションに分割することです。たとえば、元のコードは次の疑似コードのように見えました。

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="support@server.com"):
        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」の一部を独自のメソッドに変換する場合は、そうします。ただし、メソッドをその外側に置きます。これにより、コンポーネント間のインターフェースを作成する必要があります。

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

#!/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

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

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


1
素晴らしい答え、スペンサー。ありがとうございました!私には、本質的にあまりに具体的すぎて、ここでは適切ではないいくつかのフォローアップ質問があります。これらについて話し合うために非公開で連絡してもよろしいですか?
Zearin 2012年

@Zearin確かに、私のプロフィールには私のメールアドレスが含まれています。これは会社のプロジェクト用でしたが、そこには独自のものがあるため、リポジトリの完全なコピーを提供することはできません。十分な時間があれば、スナップショットの前または後にクリーンアップできますが、どれだけ役立つかわかりません。
Spencer Rathbun 2012年

プロフィールにメールアドレスが表示されません。あらゆる種類の情報がありますが、連絡先情報はありません。☺連絡方法を教えてください。
Zearin

とった。サイバーマン:「削除!削除!削除!」
Zearin
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.