更好地程式設計

2gua發表於2014-11-14

enter image description here

一晃發現有些日子沒有寫點東西了,再加上這一段遇到的事情,就把自己的思考做個提煉吧。事情的起由是這樣的:

enter image description here

enter image description here

其實,對於一個特定的功能,要很快把它寫出來並非難事,但要考慮周全,如封裝性、複用性、耦合性及便捷性等,其中的難度和平衡,就沒那麼容易的,冰凍三尺非一日之寒,一方面需要自己不斷實踐累積,另一方面也需要不斷拓寬知識面。

加上這兩天.NET開源的勁爆訊息影響,這次就且用C#來做說明。其實,我一直覺得C#的語法比Java先進許多,雖說其借鑑於Java,但語法上已超越Java。這次開源真是大好事,如果沒有開源,我還真不敢如此放心大膽地使用它。

在這裡,我會使用一個常見的影片導購車作為例子:比如你想實現個影片導購車,統計各部影片的總票價。我會按先易後難的步驟一一說明,因此,加上完整程式碼,估計篇幅會比較長,稍需耐心。

需要事先說明一點的是:為了簡化說明,我用了靜態模型資料陣列來模擬從資料庫中獲取的資料,實際專案中這些資料往往是從資料庫中獲取到的。

一. 最簡單的實現

一般這個實現在現實專案中並不常見,除非在練習過程中,或對於一個newbie程式設計師而言。

程式程式碼如下:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace DIApp1
{
    //Model類
    public class Movie
    {
        public int MovieID { get; set; }
        public string MovieName { get; set; }
        public decimal MovieCost { get; set; }
        public decimal MoviePrice { set; get; }
    }

    class Program
    {
        //模型資料陣列,這裡為了簡化資料庫資料獲取,用靜態陣列替代
        private static Movie[] movies = {
            new Movie {MovieID = 1, MovieName = "猩猿崛起II", MovieCost = 80000000.00M, MoviePrice = 80.00M},
            new Movie {MovieID = 2, MovieName = "生化危機", MovieCost = 5000000.00M, MoviePrice = 50.00M},
            new Movie {MovieID = 3, MovieName = "大話西遊", MovieCost = 3000000.00M, MoviePrice = 35.00M},
            new Movie {MovieID = 4, MovieName = "葉問II", MovieCost = 58000000.00M, MoviePrice = 80.00M},
            new Movie {MovieID = 5, MovieName = "魔戒", MovieCost = 180000000.00M, MoviePrice = 60.00M},
            new Movie {MovieID = 6, MovieName = "哈利波特", MovieCost = 170000000.00M, MoviePrice = 60.00M},
            new Movie {MovieID = 7, MovieName = "高考1977", MovieCost = 9000000.00M, MoviePrice = 60.00M},
            new Movie {MovieID = 8, MovieName = "縱橫天下", MovieCost = 3150000.00M, MoviePrice = 40.00M},
            new Movie {MovieID = 9, MovieName = "醉拳", MovieCost = 2000000.00M, MoviePrice = 40.00M}
        };

        static void Main(string[] args)
        {
            Console.WriteLine(movies.Sum(p => p.MoviePrice));
            Console.ReadKey(true);
        }
    }
}

這個程式很簡單,所以實現起來也最快了:通過Lambda直接對陣列進行遍歷,很快就求出了導購車的總票價:

 505.00

但這個程式問題非常多,基本就是寫死了程式,耦合度高,複用性低,更別談什麼封裝性了。這個程式裡處理邏輯:Movies.Sum(p => p.MoviePrice)只呼叫了1次,如果在複雜程式裡呼叫了數十、數百次,一旦要改動它,那改動量可想而知了。

那麼,我想,大多數有些經驗的程式設計師應該都不會這麼做,那會怎麼做呢?我覺得70%(?)會選擇第二種方式吧,也就是前面所說我看到的方式。

二. 簡單的工具類封裝方式

這種方式的程式程式碼如下:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace DIApp2
{
    //Model類
    public class Movie
    {
        public int MovieID { get; set; }
        public string MovieName { get; set; }
        public decimal MovieCost { get; set; }
        public decimal MoviePrice { set; get; }
    }

    //負責Movie計算邏輯的類實現
    public class MoviesCalculator
    {
        public decimal CalculateMovies(IEnumerable<Movie> movies)
        {
            return movies.Sum(p => p.MoviePrice);
        }
    }

    class Program
    {
        //模型資料陣列,這裡為了簡化資料庫資料獲取,用靜態陣列替代
        private static Movie[] movies = {
            new Movie {MovieID = 1, MovieName = "猩猿崛起II", MovieCost = 80000000.00M, MoviePrice = 80.00M},
            new Movie {MovieID = 2, MovieName = "生化危機", MovieCost = 5000000.00M, MoviePrice = 50.00M},
            new Movie {MovieID = 3, MovieName = "大話西遊", MovieCost = 3000000.00M, MoviePrice = 35.00M},
            new Movie {MovieID = 4, MovieName = "葉問II", MovieCost = 58000000.00M, MoviePrice = 80.00M},
            new Movie {MovieID = 5, MovieName = "魔戒", MovieCost = 180000000.00M, MoviePrice = 60.00M},
            new Movie {MovieID = 6, MovieName = "哈利波特", MovieCost = 170000000.00M, MoviePrice = 60.00M},
            new Movie {MovieID = 7, MovieName = "高考1977", MovieCost = 9000000.00M, MoviePrice = 60.00M},
            new Movie {MovieID = 8, MovieName = "縱橫天下", MovieCost = 3150000.00M, MoviePrice = 40.00M},
            new Movie {MovieID = 9, MovieName = "醉拳", MovieCost = 2000000.00M, MoviePrice = 40.00M}
        };

        static void Main(string[] args)
        {
            MoviesCalculator moviesCalculator = new MoviesCalculator();
            Console.WriteLine(moviesCalculator.CalculateMovies(movies));
            Console.ReadKey(true);
        }
    }
}

這次,多了一個MoviesCalculator工具類,封裝了業務處理邏輯,其中的CalculateMovies方法傳入Movies,計算後輸出結果。

這種方式的邏輯比較清晰了,至少可以到處複用MoviesCalculator工具類,而且邏輯修改也只需要在CalculateMovies方法完成即可。但是,還是有問題:

  1. 下次,我不統計電影了,我統計電視劇了,那moviesCalculator.CalculateMovies(Movies)就不是傳Movies了,程式一旦上規模了,那得改多少處吖?!

  2. 是不是還得生成一個電視劇的處理類?這個電視劇的處理類想過去跟電影的處理類肯定有很多相似之處,那如何複用?

所以,這個方式還是有很大不足。

於是,我們來思考一下第三種方式。

三. 介面、依賴注入

通過介面及依賴注入(DI)等機制的保障,我們可以做得更好。先上程式碼:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace DIApp
{
    //Model類
    public class Movie
    {
        public int MovieID { get; set; }
        public string MovieName { get; set; }
        public decimal MovieCost { get; set; }
        public decimal MoviePrice { set; get; }
    }

    //模型資料類
    public static class Movies
    {
        //模型資料陣列,這裡為了簡化資料庫資料獲取,用靜態陣列替代,實際可以實現get/set方法
        public static Movie[] movies = {
            new Movie {MovieID = 1, MovieName = "猩猿崛起II", MovieCost = 80000000.00M, MoviePrice = 80.00M},
            new Movie {MovieID = 2, MovieName = "生化危機", MovieCost = 5000000.00M, MoviePrice = 50.00M},
            new Movie {MovieID = 3, MovieName = "大話西遊", MovieCost = 3000000.00M, MoviePrice = 35.00M},
            new Movie {MovieID = 4, MovieName = "葉問II", MovieCost = 58000000.00M, MoviePrice = 80.00M},
            new Movie {MovieID = 5, MovieName = "魔戒", MovieCost = 180000000.00M, MoviePrice = 60.00M},
            new Movie {MovieID = 6, MovieName = "哈利波特", MovieCost = 170000000.00M, MoviePrice = 60.00M},
            new Movie {MovieID = 7, MovieName = "高考1977", MovieCost = 9000000.00M, MoviePrice = 60.00M},
            new Movie {MovieID = 8, MovieName = "縱橫天下", MovieCost = 3150000.00M, MoviePrice = 40.00M},
            new Movie {MovieID = 9, MovieName = "醉拳", MovieCost = 2000000.00M, MoviePrice = 40.00M}
        };
        //public IEnumerable<Movie> movies { get; set; }
    }

    //負責Movie計算邏輯的介面實現
    public interface IMoviesCalculator
    {
        decimal CalculateMovies(IEnumerable<Movie> movies);
    }

    //負責Movie計算邏輯的類實現
    public class MoviesCalculator : IMoviesCalculator
    {
        public decimal CalculateMovies(IEnumerable<Movie> movies)
        {
            return movies.Sum(p => p.MoviePrice);
        }
    }

    //Movie導購車類
    public class MoviesCart
    {
        private IMoviesCalculator moviesCalculator;

        public MoviesCart(IMoviesCalculator moviesCalculator)
        {
            this.moviesCalculator = moviesCalculator;
        }

        public decimal CalculateMovies()
        {
            return moviesCalculator.CalculateMovies(Movies.movies);
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            IMoviesCalculator moviesCalculator = new MoviesCalculator();
            MoviesCart moviesCart = new MoviesCart(moviesCalculator);
            Console.WriteLine(moviesCart.CalculateMovies());
            Console.ReadKey(true);
        }
    }
}

這裡我們使用了構造器注入的方式(DI還有設值注入、介面注入等方式)。

public MoviesCart(IMoviesCalculator moviesCalculator)
{
    this.moviesCalculator = moviesCalculator;
}

可以看到,MoviesCart接收的引數是IMoviesCalculator,那麼,任何實現該介面的類,不管是電影、電視劇,或者你還想細分的動畫片、槍戰片、魔幻片,都可以用MoviesCart來處理,這樣,導購車就可以複用了。你可以實現各種XxxxCalculator及其包含的CalculateMovies方法,在導購車裡填入對應的資料,結果就出來了,多好啊!

enter image description here

結果已經很不錯了,但是請等等!還有個問題!那就是: Main裡面的這兩行程式碼:

IMoviesCalculator moviesCalculator = new MoviesCalculator();
MoviesCart moviesCart = new MoviesCart(moviesCalculator);

我們還是得在程式的每個呼叫處例項化具體的XxxxCalculator類!所以,有可能你還得改動多處的程式碼。

那還有辦法繼續改進嗎?

四. 使用依賴注入容器

使用DI容器是非常高效的,.NET、Java都有很多很好的DI容器,我在這裡使用Ninject(Ninject · GitHub)。

程式程式碼如下:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Ninject;

namespace DIAppPro
{
    //Model類
    public class Movie
    {
        public int MovieID { get; set; }
        public string MovieName { get; set; }
        public decimal MovieCost { get; set; }
        public decimal MoviePrice { set; get; }
    }

    //模型資料類
    public static class Movies
    {
        //模型資料陣列,這裡為了簡化資料庫資料獲取,用靜態陣列替代,實際可以實現get/set方法
        public static Movie[] movies = {
            new Movie {MovieID = 1, MovieName = "猩猿崛起II", MovieCost = 80000000.00M, MoviePrice = 80.00M},
            new Movie {MovieID = 2, MovieName = "生化危機", MovieCost = 5000000.00M, MoviePrice = 50.00M},
            new Movie {MovieID = 3, MovieName = "大話西遊", MovieCost = 3000000.00M, MoviePrice = 35.00M},
            new Movie {MovieID = 4, MovieName = "葉問II", MovieCost = 58000000.00M, MoviePrice = 80.00M},
            new Movie {MovieID = 5, MovieName = "魔戒", MovieCost = 180000000.00M, MoviePrice = 60.00M},
            new Movie {MovieID = 6, MovieName = "哈利波特", MovieCost = 170000000.00M, MoviePrice = 60.00M},
            new Movie {MovieID = 7, MovieName = "高考1977", MovieCost = 9000000.00M, MoviePrice = 60.00M},
            new Movie {MovieID = 8, MovieName = "縱橫天下", MovieCost = 3150000.00M, MoviePrice = 40.00M},
            new Movie {MovieID = 9, MovieName = "醉拳", MovieCost = 2000000.00M, MoviePrice = 40.00M}
        };
        //public IEnumerable<Movie> movies { get; set; }
    }

    //負責Movie計算邏輯的介面實現
    public interface IMoviesCalculator
    {
        decimal CalculateMovies(IEnumerable<Movie> movies);
    }

    //負責Movie計算邏輯的類實現
    public class MoviesCalculator : IMoviesCalculator
    {
        public decimal CalculateMovies(IEnumerable<Movie> movies)
        {
            return movies.Sum(p => p.MoviePrice);
        }
    }

    //Movie導購車類
    public class MoviesCart
    {
        private IMoviesCalculator moviesCalculator;

        public MoviesCart(IMoviesCalculator moviesCalculator)
        {
            this.moviesCalculator = moviesCalculator;
        }

        public decimal CalculateMovies()
        {
            return moviesCalculator.CalculateMovies(Movies.movies);
        }
    }

    //Ninject設定靜態類
    public static class NinjectDependencyResolver {
        public static IMoviesCalculator calculator;
        static NinjectDependencyResolver()
        {
            IKernel nKernel = new StandardKernel();
            //介面與具體類例項的繫結
            nKernel.Bind<IMoviesCalculator>().To<MoviesCalculator>();
            calculator = nKernel.Get<IMoviesCalculator>();

        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            MoviesCart moviesCart = new MoviesCart(NinjectDependencyResolver.calculator);
            Console.WriteLine(moviesCart.CalculateMovies());
            Console.ReadKey(true);
        }
    }
}

enter image description here

在這裡,最主要的是引入了DI解析類NinjectDependencyResolver

//介面與具體類例項的繫結
nKernel.Bind<IMoviesCalculator>().To<MoviesCalculator>();
calculator = nKernel.Get<IMoviesCalculator>();

通過Ninject,我們把介面和具體實現類例項繫結在一起,這樣,你要改變類例項時,只要在這一處地方改動就可以了。在Main裡面,你再也不用糾結還有侵入式程式碼的存在了!結果依舊輸出:

505.00

五. 想說的

這裡不是想說程式碼的實現,如果說下去,估計要寫很多。同時,列出這些程式碼也只是為了一步步地說明問題。我最想說的,是程式設計的思路,人人都能寫程式碼,但程式碼好壞是現實存在的。要想馴服爛程式碼,培養程式設計大局觀非常重要,這是所謂工程師和程式設計師的區別,不管是簡單的程式碼還是複雜的程式碼,都應該具備大局觀。

  1. 不要為了方法、設計模式、程式設計正規化而刻意去做,也就是不要過度設計,針對具體情況採取“權衡”方案才更貼切實際,殺雞不用牛刀,但該出手時也要毫不猶豫;

  2. 但從另一個角度來講,掌握好必要的技能,在需要的時候用得上並懂得用,這也非常重要;

  3. 團隊開發,講究的是配合,個人技能固然重要,但現實中始終存在良莠不齊的情況。所以,團隊開發過程中的工程化思維更加重要,要通過培訓、規約、計劃管控、版本控制等等手段,來協調開發。比如,制定開發模式和技術規約,把好的方式固化下來。組織團隊成員統一培訓並掌握良好開發技能,比靠個人摸索的效率要高多了,但是,可惜,這樣的好團隊太少了......

六. 其他

這兩天關於.NET開源及跨平臺的思路真是利好訊息,相關連結如下:

  1. Visual Studio Community 2013下載地址:Visual Studio Community 2013 Downloads

  2. Visual Studio 2015 預覽版下載地址:Visual Studio 2015 Downloads

  3. 相關報導:微軟宣佈.NET開發環境開源 支援三大作業系統-CSDN.NET

enter image description here

相關文章