Reputation: 401
I have a class that looks something like the following:
# Class violates the Single Responsibility Principle
class Baz:
data = [42]
def do_foo_to_data(self):
# call a dozen functions that do complicated stuff to data
def do_bar_to_data(self):
# call other functions that do different stuff to data
I want to break it into two separate classes because it violates the SRP. The functions called by do_foo_to_data()
are completely distinct from those called by do_bar_to_data()
. Yet they must operate on the same data
.
I've come up with a bunch of solutions, but they're all ugly. Is there a way to do this cleanly, preferably in Python 3 (though 2.7 is OK too)?
The best of my "solutions" is below:
# I find this hard to read and understand
class Baz:
data = [42]
def create_foo(self):
return Baz.Foo()
def create_bar(self):
return Baz.Bar()
class Foo:
def do_foo_to_data(self):
# call foo functions
class Bar:
def do_bar_to_data(self):
# call bar functions
Note: It's not essential to me that the data
member be a class member.
I only expect to create one instance of Baz
; but I didn't want to ask two questions in one post and start a discussion about singletons.
Upvotes: 0
Views: 173
Reputation: 9994
You can pull out the distinct groups of functionality to separate mix-in classes:
class Foo:
"""Mixin class.
Requires self.data (must be provided by classes extending this class).
"""
def do_foo_to_data(self):
# call a dozen functions that do complicated stuff to data
class Bar:
"""Mixin class.
Requires self.data (must be provided by classes extending this class).
"""
def do_bar_to_data(self):
# call other functions that do different stuff to data
class Baz(Foo, Baz):
data = [42]
This relies on Python's duck-typing behavior. You should only apply the Foo
and Bar
mix-ins to classes that actually provide self.data
, like the Baz
class here does.
This might be suitable where certain classes are by convention required to provide certain attributes anyway, such as customized view classes in Django. However, when such conventions aren't already in place, you might not want to introduce new ones. It's too easy to miss the documentation and then have NameError
s at runtime. So let's make the dependency explicit, rather than only documenting it. How? With a mix-in for the mix-ins!
class Data:
"""Mixin class"""
data = [42]
class Foo(Data):
"""Mixin class"""
def do_foo_to_data(self):
# call a dozen functions that do complicated stuff to data
class Bar(Data):
"""Mixin class"""
def do_bar_to_data(self):
# call other functions that do different stuff to data
class Baz(Foo, Baz):
pass
Whether this is appropriate for your use-case is difficult to say at this level of abstraction. As RayLuo's answer shows, you might not need classes at all. Instead, you could put the different groups of functions into different modules or packages, to organize them.
Upvotes: 1
Reputation: 19210
Why do you even need a class for that? All you want is two separated functions which do some job on some data.
data = [42]
def foo(data):
data.append('sample operation foo')
def bar(data):
data.append('sample operation bar')
Problem solved.
Upvotes: 4
Reputation: 476574
This is not an elegant solution. You better pass a reference to the object you want them to operate on. So something like:
class Foo:
def __init__(self,data):
self.data = data
def do_foo_to_data(self):
#...
self.data[0] = 14
pass
class Bar:
def __init__(self,data):
self.data = data
def do_bar_to_data(self):
#...
self.data.append(15)
pass
(I added sample manipulations like self.data[0] = 14
and self.data.append(15)
)
And now you construct the data. For instance:
data = [42]
Next you construct a Foo
and a Bar
and pass a reference to data
like:
foo = Foo(data)
bar = Bar(data)
__init__
is what most programming languages call the constructor and as you have seen in the first fragment, it requires an additional parameter data
(in this case it is a reference to our constructed data
).
and then you can for instance call:
foo.do_foo_to_data()
which will set data
to [14]
and
bar.do_bar_to_data()
which will result in data
being equal to [14,15]
.
Mind that you cannot state self.data = ['a','new','list']
or something equivalent in do_foo_to_data
or do_bar_to_data
because this would change the reference to a new object. Instead you could for instance .clear()
the list, and append new elements to it like:
def do_foo_to_data(self): #alternative version
#...
self.data.clear()
self.data.append('a')
self.data.append('new')
self.data.append('list')
Finally to answer your remark:
preferably in Python 3 (though 2.7 is OK too)?
The technique demonstrated is almost universal (meaning it is available in nearly every programming language). So this will work in both python-3.x and python-2.7.
Upvotes: 4