简体   繁体   中英

Is this a valid use of the builder pattern in Java (or even good OO design)?

Being fairly new to OO, I often feel I understand a concept until I try to move from a simplified example to actual requirements I am given. I'd appreciate any help understanding how to think about this particular problem.

I have a GUI which has a panel that defines a container and items that go in it. Right now, there are three types of containers. The containers have some properties (like size) and can contain one to three different types of items (two are optional). Once enough information is entered, I use the information to make a graph.

I implemented an Observer pattern. When the user enters information, it updates an observable, which notifies the graph that it has changed.

I'm happy so far. Now the wrinkles. My containers have a size, but sometimes it is entered explicitly and sometimes it is determined by what the container is holding. That is determined by the type of container. How the size is determined, if not entered explicitly, depends on whether one of the optional items is in the container. I'm not sure if the requirements writer just hates me or I am lacking enough OO experience, but those wrinkles are giving me fits. Right now, my observable just has variables to hold all the assorted information and I use a bunch of switch statements to handle the special cases.

I am thinking that I could use the builder pattern. The director would produce the data that was graphed. I would have a concrete builder for each type of container and I would instantiate the class with the container properties and the items inside it. I would have methods of the abstract builder class to return to the director the values needed for the graph, for example getContainerSize() and combine these to produce the actual data points. Also, the director could return null if the user had not yet entered enough data to complete a graph.

Am I getting close to a usable OO design? I'm not sure I didn't just bury the special casing a bit deeper.

One other wrinkle. One of the item types goes in all three containers. Right now, my observable keeps track of the container and items separately and the method that creates the graph decides what to ask for (the graph changes a lot as users play around with the values). How's that work if I have multiple builder patterns?

Maybe I am missing a step? The observable updates the builder of the current container then lets the graph know it should call the director to get its coordinates? Which would then also need to ask what the current container was?

All comments welcome that help me get my head around OO design or this problem in particular. The actual requirements have more special cases, but are variations on this basic theme.

Thanks for the replies. I think I am guilty of mixing two questions together. Here is an attempt to provide a minimal code example focusing on the Builder pattern. Note IE8 I see no identation, FireFox 8, I do- so sorry to anyone reading the code in IE8.

interface MyContainerBuilder
{
     void   setContents( MyContents contents );

     Double myVolume();
     Double myDensity();   
}

class SmallContainerBuilder implements MyContainerBuilder
{
    Double     volume   = null;
    Double     density  = null;
    MyContents contents = null;

    public void   setVolume()
    {
        if (contents != null)
        {
            volume = contents.myDensity() / 3.0;
        }
    }

    public void   setContents( MyContents contents )
    {
        this.contents = contents;
    }

    public Double myVolume()
    {
        if (volume == null)
            setVolume();
        return volume;
    }

    public Double myDensity()   
    {
        return contents.myDensity();
    }
}

class BigContainerBuilder implements MyContainerBuilder
{
    Double     volume   = null;
    Double     density  = null;
    MyContents contents = null;

    public void   setVolume( Double volume )
    {
        this.volume = volume;
    }

    public void   setContents( MyContents contents )
    {
        this.contents = contents;
    }

    public Double myVolume()
    {
        return volume;
    }

    public Double myDensity()   
    {
        return contents.myDensity();
    }
}

class ContainerDirector
{
    Double myResult( MyContainerBuilder container )
    {
        return container.myVolume() * container.myDensity();
    }
}

class MyContents
{
    Double density;

    MyContents( Double density )
    {
        this.density = density;
    }

    public Double myDensity()
    {
        return density;
    }
}

class Test
{
    public static void main(String[] args)
    {
        SmallContainerBuilder smallContainer = new SmallContainerBuilder();
        BigContainerBuilder   bigContainer   = new BigContainerBuilder();
        ContainerDirector     director       = new ContainerDirector();
//
// Assume this comes from the GUI, where an ActionListener knows which Builder
// to use based on the user's action. I'd be having my observable store this.
       Double        density       = 15.0;
       MyContents    contents      = new MyContents( density );
       smallContainer.setContents( contents );
//
// Then I would need to tell my observer to do this.
        Double       results       = director.myResult( smallContainer );
        System.out.println( "Use this result: " + results );
    }
}

I have two types of containers that use a different method to calculate the volume. So let's say I have radiobuttons to select the container type and under each radiobutton a combobox of items that can go in the selected container. The ActionListener on the combobox will put the item in the right container and save it to my observable (there are lots of other things that actually get set) and it tells my observer to use the director to get an appropriate value and the observer then updates some view component of the GUI.

My containers have a size, but sometimes it is entered explicitly and sometimes it is determined by what the container is holding. That is determined by the type of container . [...] if not entered explicitly, depends on whether one of the optional items is in the container.

Sounds like you could have different subclasses of an abstract container, each implementing getContainerSize() in a different way. One for explicitly entered, one for the case with optional item and one without it.

... and I use a bunch of switch statements to handle the special cases.

Does not sound great. Replace Conditional with Polymorphism if applicable.

I am thinking that I could use the builder pattern...

I assume that you need to determine a concrete type of object (or null ) based on a set of input variables. The pattern provides a way to build a complex object if it knows what type that is, but the actual problem is to decide which type. So you need conditional code at some place. That place can be a builder but it could be simple factory as well.

Right now, my observable keeps track of the container and items separately[...] observable updates the builder of the current container[...] How's that work if I have multiple builder patterns?

Not really understanding what that Observable is observing and what changes in which case are triggering what, but Observable updating a builder (or multiple) sounds strange. That's more of a gut feeling though :)

Am I getting close to a usable OO design?

If it works, yes. But I actually can't tell you if you have created a good or usable design because I still don't know the details of your problem or your design - after reading your text several times now.

Instead of adding another page of information to your question now, try to break your problem down into smaller pieces and use code snippets / images / graphs or any type of visualization to help people understand your problem and all the connections between those pieces. Just a lot of text is rather scary and a huge OO design like that is as a whole too big and too localized for SO.


Your approach seems fine but it requires IMO quite complex Objects to justify that use.

You create a MyContents instance in your GUI via the observer. That object is then wrapped in a MyContainerBuilder which is then given to a ContainerDirector which then produces a result. That is in my opinion one step too much if MyContents or the result is simple.

Also the way you set the MyContents to the MyContainerBuilder means that you can't reuse the same concrete MyContainerBuilder instance blindly. You either have to make sure that you use it sequentially or you have to construct a new one every time.

Ie this does not work

MyContents content1 = new MyContents( 5 );
MyContents content2 = new MyContents( 6 );
smallContainer.setContents( content1 );
smallContainer.setContents( content2 ); // overwriting old state
Double results1 = director.myResult( smallContainer ); // wrong result
Double results2 = director.myResult( smallContainer );

I assume that MyContents is a generic data holding object that is filled with data in several steps by the user. Once the user is happy with it, it is submitted to be build into a result. As far as I can tell, you know at that point what the result has to be.

Below is an approach using a Strategy Pattern(? - I'm bad with all those names and little differences) which I chose to plug into the MyContents directly so the MyContents object once finalized has all details how it has to be transformed into a result. That way safes one step and you don't need to create / maintain extra builder objects. MyContents is already in a way a Builder now.

interface VolumeStrategy {
     Double calculateVolume(Double density);
}
class SmallVolumeStrategy implements VolumeStrategy {
    public Double calculateVolume(Double density) {
        return density / 3.0;
    }
}
class BigVolumeStrategy implements VolumeStrategy {
    public Double calculateVolume(Double density) {
        return density;
    }
}

class ContainerDirector {
    Double myResult( MyContents container ) {
        Double density = container.myDensity();
        VolumeStrategy strategy = container.myStrategy();
        return density * strategy.calculateVolume(density);
    }
}

class MyContents {
    // built via observer
    Double density;
    MyContents( Double density ) {
        this.density = density;
    }
    public Double myDensity() {
        return density;
    }

    // plugged in at the end.
    VolumeStrategy strategy;
    public void setStrategy(VolumeStrategy strategy) {
        this.strategy = strategy;
    }
    public VolumeStrategy myStrategy() {
        return strategy;
    }
}

public class Test {
    public static void main(String[] args) {
        // all those can be static 
        VolumeStrategy       smallStrategy  = new SmallVolumeStrategy();
        VolumeStrategy       bigStratetgy   = new BigVolumeStrategy();
        ContainerDirector    director       = new ContainerDirector();

       // from the GUI
       Double        density       = 15.0;
       MyContents    contents      = new MyContents( density );
       // building this contents ...
       // ... time to submit, we know what strategy to use
       contents.setStrategy(smallStrategy);

       // can turn contents into result without needing to know anything about it.
        Double       results       = director.myResult( contents );
        System.out.println( "Use this result: " + results );
    }
}

That's a way what I think should work well for the problem I imagine you have. I can be wrong tough.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM