views:

104

answers:

5

Besides using the Single Responsibility Principle, when designing classes for an application one is writing, what should one keep in mind, to keep the code maintainable, reusable and adhere to OOP principles?

I'm finding it hard to design the classes of applications I'm trying to write, because when does one decide what (functionality) goes in which class and whether it should really be in a derived class or there should be an abstract class or interface for this class?

I know this is probably a topic with many answers, but does anyone have any good guidelines (preferably simple) to designing classes and class hierarchies that are simple to maintain and don't make a mess when creating big applications?

EDIT:

When there's classes that have 10 methods and over and have an abstract base class & interfaces which it derives from. Also has 3 Singleton classes referenced globally inside the class and much more. Sounds like it needs a bit of 'refactoring' applied?

Sorry if it's a long example, but you see the problem I'm facing and I want some input on it. Please just look at design, not at technicalities.

I give an example:

I created this class: (a while back)

  class ExistingUserLogon : Logon, ILogonUser
    {
        #region Member Variables

        LogonEventArgs _logoneventargs;
        LogonData lgndata;
        Factory f = Factory.FactoryInstance;
        PasswordEncrypt.Collections.AppLoginDataCollection applogindatacollection;
        PasswordEncrypt.Collections.SQlLoginDataCollection sqllogindatacollection;
        bool? compare;
        static ExistingUserLogon existinguserlogon;
        PasswordEncrypt.SQLDatabase.DatabaseLogin dblogin;
        string databasename = string.Empty;

        #endregion

        #region Properties

        public static ExistingUserLogon ExistingUserLogonInstance
        {
            get
            {
                if (existinguserlogon == null)
                    existinguserlogon = new ExistingUserLogon();
                return existinguserlogon;
            }
        }
        public string loginname
        {
            get;
            set;
        }
        #endregion

        #region Contructors
        public ExistingUserLogon(bool? compare, LogonData logondata)
        {
            this.compare = compare;
            this.lgndata = logondata;
            this.applogindatacollection = f.AppLoginDataCollection;
            this.sqllogindatacollection = f.SqlLoginDataCollection;
        }

        public ExistingUserLogon()
        {
            this.applogindatacollection = f.AppLoginDataCollection;
            this.sqllogindatacollection = f.SqlLoginDataCollection;
        }

        #endregion

        #region Delegates

        public delegate void ConnStrCreated( object sender, LogonEventArgs e );

        #endregion

        #region Events

        public event ConnStrCreated ConnectionStringCreated;

        #endregion

        private void OnConnectionStringCreated( object sender, LogonEventArgs e )
        {
            if (ConnectionStringCreated != null)
            {
                ConnectionStringCreated(sender, e);
            }
        }

        public void LoginNewUser()
        {
            dblogin = new PasswordEncrypt.SQLDatabase.DatabaseLogin();
            if (!string.IsNullOrEmpty(loginname))
            {
                string temp = dblogin.GenerateDBUserName(loginname);
                if (temp != "Already Exists")
                {

                    if (compare == true)
                    {
                        IterateCollection(lgndata.HahsedUserName.HashedUserName, new Action<string>(OnDatabaseName));
                        IterateCollection(temp, new Action<bool, string, string>(OnIterateSuccess));
                    }
                }

            }
            else
            {
                LogonEventArgs e = new LogonEventArgs();
                e.Success = false;
                e.ErrorMessage = "Error! No Username";
                OnError(this, e);

            }

        }

        private void OnDatabaseName(string name)
        {
            this.databasename = name;
        }

        private void OnIterateSuccess( bool succeed, string psw, string userid )
        {
            if (succeed)
            {
                // Create connectionstring
                ConnectionStringCreator cnstrCreate = ConnectionStringCreator.ConnectionStringInstance;
                if (databasename != string.Empty)
                {
                    string conn = ConnectionStringCreator.CreateConnString(databasename, userid, psw);
                    bool databaseExists;

                    databaseExists = DataManagementVerification.DoDatabaseExists(conn);

                    if (databaseExists)
                    {
                        FormsTransfer.ConnectionString = conn;

                        conn = string.Empty;
                    }
                    else
                    {
                        LogonEventArgs e = new LogonEventArgs();
                        e.Success = false;
                        e.ErrorMessage = "Database does not Exist!";
                        OnError(this, e);
                    }

                    //OnConnectionStringCreated(this, e);

                   // PasswordEncrypt.LINQtoSQL.PasswordDatabase db = new PasswordEncrypt.LINQtoSQL.PasswordDatabase(conn);

                /*    PasswordEncrypt.LINQtoSQL.EncryptData _encryptdata = new PasswordEncrypt.LINQtoSQL.EncryptData()
                    {
                        EncryptID = 1,
                        IV = "Test",
                        PrivateKey = "Hello",
                        PublicKey = "Tony"
                    };

                    db.EncryptionData.InsertOnSubmit(_encryptdata);

                   // db.SubmitChanges();*/

                    //PasswordEncrypt.LINQtoSQL.Data _data = new PasswordEncrypt.LINQtoSQL.Data()
                    //{
                    //    EncryptionID = 1,
                    //    Username = "Tbone",
                    //    Password = "worstje",
                    //    IDCol = 2
                    //};

                    //db.Data.InsertOnSubmit(_data);

                    //db.SubmitChanges();

                    //IEnumerable<PasswordEncrypt.LINQtoSQL.Data> _ddata = db.Data.Where(data => data.Password == "worstje"); ;
                    //foreach (PasswordEncrypt.LINQtoSQL.Data data in _ddata)
                    //{

                    //    MessageBox.Show("Data Found: " + data.Username + "," + data.Password);
                    //}
                }
                else
                {
                    MessageBox.Show("Found no Database name", "Database name error");
                }
            }
            else
            {
                // Unable to create connectionstring
            }
        }

        private void IterateCollection( string username, Action<bool, string, string> OnSucceed )
        {
            bool succeed = false;
            dblogin = new PasswordEncrypt.SQLDatabase.DatabaseLogin();

            string psw;
            string userid;

            foreach (KeyValuePair<PasswordEncrypt.Collections.GItem<string>, PasswordEncrypt.Collections.GItem<string>> kv in sqllogindatacollection)
            {
                List<char> tempa = new List<char>();
                List<char> tempb = new List<char>();

                tempa = dblogin.GetNumber(username);
                tempb = dblogin.GetNumber(kv.Key.Item);

                if (tempa.Count == tempb.Count)
                {
                    for (int i = 0; i < tempa.Count ; i++)
                    {
                        if (tempa.ElementAt(i).Equals(tempb.ElementAt(i)))
                        {
                            if ( i == (tempa.Count -1) )
                                succeed = true;
                            continue;
                        }
                        else
                        {
                            KeyValuePair<PasswordEncrypt.Collections.GItem<string>, PasswordEncrypt.Collections.GItem<string>> last =  sqllogindatacollection.Last();
                            if (kv.Key.Item.Equals(last.Key.Item))
                            {
                                MessageBox.Show("Failed to match usernames for Database", "Error", MessageBoxButtons.OK, MessageBoxIcon.Exclamation);
                                succeed = false;
                                // Let GUI Know...
                                LogonEventArgs e = new LogonEventArgs();
                                e.Success = succeed;
                                OnError(this, e);
                                break;
                            }
                            else
                            {
                                break;
                            }

                        }

                    }


                   // MessageBox.Show("Found a sql username match");
                }
                // Now go execute method to logon into database...
                if (succeed)
                {
                    psw = kv.Value.Item;
                    userid = kv.Key.Item;
                    OnSucceed(succeed, psw, userid);
                }
                succeed = false;
            }


        }

        private void IterateCollection(string key, Action<string> OnDatabaseName )
        {
            foreach (KeyValuePair<PasswordEncrypt.Collections.GItem<string>, PasswordEncrypt.Collections.GItem<string>> kv in applogindatacollection)
            {
                if (key == kv.Key.Item)
                {
                    MessageBox.Show("Found a match");
                    OnDatabaseName(kv.Value.Item);
                }
                else
                {
                   // MessageBox.Show("No Match");
                }
            }
        }

        #region Public Methods

        public bool? ReadFromRegistry( HashedUsername username, HashedPassword hashedpassword )
        {
            return RegistryEdit.ReadFromRegistry(username, hashedpassword);
        }

        public bool WriteToRegistry( HashedUsername username, HashedPassword hashedpassword )
        {
            return RegistryEdit.WriteToRegistry(username, hashedpassword);
        }

        public override void Login(TextBox username, TextBox password)
        {
            base.Login(username, password);
            Login(username.Text, password.Text);
        }

        protected override void Login(string username, string password)
        {
            base.Login(username, password);
            lgndata = base._logondata;
            compare = base._regRead;

            if (compare == true)
            {
                loginname = username;
                LoginNewUser();
                /// on login succeeded let UI class know
                _logoneventargs = new LogonEventArgs();
                _logoneventargs.Success = true;

                OnExistingUserLoggedIn(this, _logoneventargs);
            }
            else
            {
                _logoneventargs = new LogonEventArgs();
                _logoneventargs.Success = false;
                _logoneventargs.ErrorMessage = "Cannot Login this user, please try again.";
                OnError(this, _logoneventargs);
            }
            /// Get username and password for database... 
            /// to login using correct user data & permissions
            /// Login data for database is generated at runtime
            /// then by checking if database with such a name exists
            /// login...

        }
        #endregion
    }
A: 

This is one of those things that they rarely teach in schools, and even when they do its usually a contrived example that still doesnt give you a good idea of how to do it.

The bottom line is that there really isnt a good concrete, scientific way to do this, after all computer programming is still very much an art. :)

What I like to do is:

  1. Write a brief outline of what the application is suppose to do, high level stuff in bullet point form
  2. Divide the bullet points up into "required", "optional", "nice-to-have". Where "required" is stuff that must be there to even "demo" and optional is the stuff that basically makes it a complete application in the customers eyes
  3. If required do my drawings, diagrams, UML, specs etc at this point (depends on whats required)
  4. Start writing code. I like to draft out what I am doing in code and start figuring out how things lay out and what functionality needs to be where
  5. Work to an initial prototype (the "required" stuff) once I reach this point I freeze and start looking at where the design is at, and what the feedback is
  6. Start prioritizing refactoring and redesign with fixing issues in the prototype.
  7. Refactor! Redesign and reimplement

Rinse, lather and repeat.

For some things its obvious what goes into which objects, which classes are needed and what designs are working well. For other things it takes an iteration or two to work out the bugs.

I learn something new on ever project, and I find new ways to apply concepts I thought I understood well. Its a constant learning experience, and there is always something to learn.

GrayWizardx
+2  A: 

One thing I wish I learned early was to design your code to interact through very simple interfaces rather than massive APIs. I would recommend you even do this internally. Create simple interfaces that interact between your modules. This makes testing SO much easier.

private interface IPerformWork
{
    void DoThis(String value);
    void DoThat(String value);  
}

Remember, the less coupled your code is the better.

Code Smells

Why don't you browse through this question. It may not be C# specific but a lot of it is completely language agnostic.

http://stackoverflow.com/questions/114342/what-are-code-smells-what-is-the-best-way-to-correct-them

ChaosPandion
+5  A: 

Hear I post some of sentence that I did take from my favorite book "Architecting Microsoft® .NET Solutions for the Enterprise" that I strongly recommend to read this book even if you’re not a Software Architect.

It Depends It always depends. As an architect, you are never sure about anything. There's always the possibility that you're missing something. However, the role requires that decisions be made, so you must be able to evaluate all options and make an informed decision, and to do this promptly, when a decision is required. To buy yourself some time and activate your mental processes in the background, first say, "It depends," and then explain why and what the answer depends on. If you are unsure about what a point depends on, the default answer is, "It depends on the context."

Requirements Are Lord Over All The architect is just one link in the natural chain of actors in a software project. The customer says what he wants. If the customer doesn't know what he wants, someone will be there to prompt him for specifics. The analyst formalizes what the customer wants. The project manager prepares the groundwork for the formally-defined project. The architect gets the bundle of requirements and sorts them out. Developers follow the architect. The database administrator does his best to make the database support the application effectively. Note that the customer leads the chain, and what the customer wants is the law. What the customer wants goes under the name of requirements. Of course, only few customers know what it is they want. So requirements change.

Program to an Interface Even if you make a living out of implemented code, you should leverage interfaces wherever possible. Repeat with us: "No implementation is possible without an interface." Look around, there's always an interface that can be extracted.

Keep It Simple but Not Simplistic You know KISS (Keep It Simple, Stupid), right? This is just our customized version. Simple and concise is usually equivalent to great and well done. Aim at simplicity, but give yourself a boundary for the low end of the range. If you go below that lower boundary, your solution will become simplistic. And this is not a good thing.

Inheritance Is About Polymorphism, Not Reuse Object-oriented programming (OOP) taught us that we should write a class once and reuse it forever and extend it at will. And this is possible thanks to inheritance. Does this naturally extend to class reuse? Reuse is a much subtler concept than you might think at first. Polymorphism is the key aspect of OOP to leverage. Polymorphism means you can use two inherited classes interchangeably. As others have said, "Reuse is a nice side effect to have." But reuse shouldn't be your goal, or put another way, don't reuse a class through inheritance just to reuse the class. It's better to write a new class that more precisely fits the needs than to try to inherit an existing class that wasn't designed for the job.

Not the DAL? Don't Touch SQL Then Repeat with us: "Separation of concerns. Separation of concerns." Push data access code and details (such as connection strings, commands, and table names) to the corner. Sooner or later, you need to take care of them, but consider business and presentation logic separately from persistence. And if possible, delegate persistence to ad hoc tools such as Object/ Relational Mapper (O/RM) tools.

Maintainability First If you could pick only one attribute for your software, what would it be? Scalability? Security? Performance? Testability? Usability? For us, it would be none of the above. For us, what comes first is maintainability. Through maintainability, you can achieve anything else at any time.

All User Input Is Evil You should have heard this already. If there's a way for users to do something wrong, they'll find it. Oh, this sounds like Murphy's Law. Yes, you should have heard this one, too, already.

Post-Mortem Optimization Donald Knuth said that premature optimization is the root of all software evil. We go even further. Do not optimize the system. Instead, design it for being improved and extended at any time. But focus on pure optimization only when the system is dismissed.

Security and Testability Are by Design If you're serious about a system attribute, design for it right from the beginning. Security and testability are no exception to this rule, and there's an International Organization for Standardization (ISO) standard that specifically says so.

I hope you this will help you.

Florim Maxhuni
I happen to have this book on my shelf. I have not read it fully though. Maybe It's time I should pick it up again!
Tony
+1 for many nice points! I would question the need for the analyst/architect/developer division, though; it implies Waterfall and BDUF.
TrueWill
+1 imho nice generalities, but "idealized" compared to real-world software development; but, I'd rather "pray" to those "truths" than Agile/Scrum, or TDD :) "Repeat with us: "No implementation ... without an interface." Sorry, we're not in that cult; of course interfaces are useful, sometimes necessary. imho a "glaring omission" : an architect should have deep understanding of data's structure, dynamics; ways it may change, evolve over time : imagine a house architect not aware of geology, terrain, soil load-bearing strength, winds, rainfall, shadows cast by nearby buildings, etc. :)
BillW
+2  A: 

Well, aside from keeping the code maintainable, reusable, and adhering to OOP principles... :)

I'd be careful to avoid paralysis of analysis here: if you don't make the right choice between a class, derived class, or interface, for example, that's what refactoring is for. In fact, I'd argue that's the whole point of principles like the SRP - not to make it easy to design everything beforehand, but to make it easy to change things as your requirements grow and change shape, because you can't predict everything beforehand.

A tension exists between designing code to be reusable and just creating something that is usable. Design for reuse where you can, but don't let that get in the way of just implementing the requirements that are right in front of you.

One tip I've heard (courtesy of Spolsky, probably) is: when you need perform an operation in one place, write the code. When you need to perform it in another place, write the code again. When you want to perform the same op in a third place, now it's time to think about refactoring.

Don't try to design a vast, all-encompassing system in the hopes of avoiding change - design code that's resilient to change.

djacobson
+1 very eloquently said, imho
BillW
+1  A: 

There is a good question here on Stackoverflow that discusses C# anti-patterns, and how to avoid them.

You should give it a quick read through; it exposes a lot of things that you may be tempted to do/not realize are wrong, and has suggestions for how to accomplish the same functionality in a more elegant way.

instanceofTom